Skip to content

Commit 0b4c157

Browse files
committed
Merge remote-tracking branch 'oauth2cli/dev' into pkce-via-auth-code-flow
2 parents dfbbc66 + decbedc commit 0b4c157

File tree

2 files changed

+34
-4
lines changed

2 files changed

+34
-4
lines changed

msal/oauth2cli/oauth2.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import functools
1616
import random
1717
import string
18+
import hashlib
1819

1920
import requests
2021

@@ -258,6 +259,21 @@ def _stringify(self, sequence):
258259
return sequence # as-is
259260

260261

262+
def _generate_pkce_code_verifier(length=43):
263+
assert 43 <= length <= 128
264+
verifier = "".join( # https://tools.ietf.org/html/rfc7636#section-4.1
265+
random.sample(string.ascii_letters + string.digits + "-._~", length))
266+
code_challenge = (
267+
# https://tools.ietf.org/html/rfc7636#section-4.2
268+
base64.urlsafe_b64encode(hashlib.sha256(verifier.encode("ascii")).digest())
269+
.rstrip(b"=")) # Required by https://tools.ietf.org/html/rfc7636#section-3
270+
return {
271+
"code_verifier": verifier,
272+
"transformation": "S256", # In Python, sha256 is always available
273+
"code_challenge": code_challenge,
274+
}
275+
276+
261277
class Client(BaseClient): # We choose to implement all 4 grants in 1 class
262278
"""This is the main API for oauth2 client.
263279
@@ -401,6 +417,8 @@ def initiate_auth_code_flow(
401417
you can use :func:`~obtain_token_by_auth_code_flow()`
402418
to complete the authentication/authorization.
403419
420+
This method also provides PKCE protection automatically.
421+
404422
:param list scope:
405423
It is a list of case-sensitive strings.
406424
Some ID provider can accept empty string to represent default scope.
@@ -440,14 +458,19 @@ def initiate_auth_code_flow(
440458
# Implicit grant would cause auth response coming back in #fragment,
441459
# but fragment won't reach a web service.
442460
raise ValueError('response_type="token ..." is not allowed')
461+
pkce = _generate_pkce_code_verifier()
443462
flow = { # These data are required by obtain_token_by_auth_code_flow()
444463
"state": state or "".join(random.sample(string.ascii_letters, 16)),
445464
"redirect_uri": redirect_uri,
446465
"scope": scope,
447466
}
448467
auth_uri = self._build_auth_request_uri(
449-
response_type, **dict(flow, **kwargs))
468+
response_type,
469+
code_challenge=pkce["code_challenge"],
470+
code_challenge_method=pkce["transformation"],
471+
**dict(flow, **kwargs))
450472
flow["auth_uri"] = auth_uri
473+
flow["code_verifier"] = pkce["code_verifier"]
451474
return flow
452475

453476
def obtain_token_by_auth_code_flow(
@@ -459,6 +482,8 @@ def obtain_token_by_auth_code_flow(
459482
"""With the auth_response being redirected back,
460483
validate it against auth_code_flow, and then obtain tokens.
461484
485+
Internally, it implements PKCE to mitigate the auth code interception attack.
486+
462487
:param dict auth_code_flow:
463488
The same dict returned by :func:`~initiate_auth_code_flow()`.
464489
:param dict auth_response:
@@ -513,6 +538,10 @@ def authorize(): # A controller in a web app
513538
# It is both unnecessary and harmless, per RFC 6749.
514539
# We use the same scope already used in auth request uri,
515540
# thus token cache can know what scope the tokens are for.
541+
data=dict( # Extract and update the data
542+
kwargs.pop("data", {}),
543+
code_verifier=auth_code_flow["code_verifier"],
544+
),
516545
**kwargs)
517546
if auth_response.get("error"): # It means the first leg encountered error
518547
# Here we do NOT return original auth_response as-is, to prevent a

msal/oauth2cli/oidc.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,11 @@ def initiate_auth_code_flow(
173173
return flow
174174

175175
def obtain_token_by_auth_code_flow(self, auth_code_flow, auth_response, **kwargs):
176-
"""Validate the auth_response being redirected back, and then obtain tokens.
177-
and obtain ID token which can be used for user sign in.
176+
"""Validate the auth_response being redirected back, and then obtain tokens,
177+
including ID token which can be used for user sign in.
178178
179-
It provides nonce protection out-of-the-box.
179+
Internally, it implements nonce to mitigate replay attack.
180+
It also implements PKCE to mitigate the auth code interception attack.
180181
181182
See :func:`oauth2.Client.obtain_token_by_auth_code_flow` in parent class
182183
for descriptions on other parameters and return value.

0 commit comments

Comments
 (0)