36
36
from authlib .jose .errors import InvalidClaimError , JoseError , MissingClaimError
37
37
from authlib .oauth2 .auth import ClientAuth
38
38
from authlib .oauth2 .rfc6749 .parameters import prepare_grant_uri
39
+ from authlib .oauth2 .rfc7636 .challenge import create_s256_code_challenge
39
40
from authlib .oidc .core import CodeIDToken , UserInfo
40
41
from authlib .oidc .discovery import OpenIDProviderMetadata , get_well_known_url
41
42
from jinja2 import Environment , Template
@@ -475,6 +476,16 @@ def _validate_metadata(self, m: OpenIDProviderMetadata) -> None:
475
476
)
476
477
)
477
478
479
+ # If PKCE support is advertised ensure the wanted method is available.
480
+ if m .get ("code_challenge_methods_supported" ) is not None :
481
+ m .validate_code_challenge_methods_supported ()
482
+ if "S256" not in m ["code_challenge_methods_supported" ]:
483
+ raise ValueError (
484
+ '"S256" not in "code_challenge_methods_supported" ({supported!r})' .format (
485
+ supported = m ["code_challenge_methods_supported" ],
486
+ )
487
+ )
488
+
478
489
if m .get ("response_types_supported" ) is not None :
479
490
m .validate_response_types_supported ()
480
491
@@ -602,6 +613,11 @@ async def _load_metadata(self) -> OpenIDProviderMetadata:
602
613
if self ._config .jwks_uri :
603
614
metadata ["jwks_uri" ] = self ._config .jwks_uri
604
615
616
+ if self ._config .pkce_method == "always" :
617
+ metadata ["code_challenge_methods_supported" ] = ["S256" ]
618
+ elif self ._config .pkce_method == "never" :
619
+ metadata .pop ("code_challenge_methods_supported" , None )
620
+
605
621
self ._validate_metadata (metadata )
606
622
607
623
return metadata
@@ -653,7 +669,7 @@ async def _load_jwks(self) -> JWKS:
653
669
654
670
return jwk_set
655
671
656
- async def _exchange_code (self , code : str ) -> Token :
672
+ async def _exchange_code (self , code : str , code_verifier : str ) -> Token :
657
673
"""Exchange an authorization code for a token.
658
674
659
675
This calls the ``token_endpoint`` with the authorization code we
@@ -666,6 +682,7 @@ async def _exchange_code(self, code: str) -> Token:
666
682
667
683
Args:
668
684
code: The authorization code we got from the callback.
685
+ code_verifier: The PKCE code verifier to send, blank if unused.
669
686
670
687
Returns:
671
688
A dict containing various tokens.
@@ -696,6 +713,8 @@ async def _exchange_code(self, code: str) -> Token:
696
713
"code" : code ,
697
714
"redirect_uri" : self ._callback_url ,
698
715
}
716
+ if code_verifier :
717
+ args ["code_verifier" ] = code_verifier
699
718
body = urlencode (args , True )
700
719
701
720
# Fill the body/headers with credentials
@@ -914,11 +933,14 @@ async def handle_redirect_request(
914
933
- ``scope``: the list of scopes set in ``oidc_config.scopes``
915
934
- ``state``: a random string
916
935
- ``nonce``: a random string
936
+ - ``code_challenge``: a RFC7636 code challenge (if PKCE is supported)
917
937
918
- In addition generating a redirect URL, we are setting a cookie with
919
- a signed macaroon token containing the state, the nonce and the
920
- client_redirect_url params. Those are then checked when the client
921
- comes back from the provider.
938
+ In addition to generating a redirect URL, we are setting a cookie with
939
+ a signed macaroon token containing the state, the nonce, the
940
+ client_redirect_url, and (optionally) the code_verifier params. The state,
941
+ nonce, and client_redirect_url are then checked when the client comes back
942
+ from the provider. The code_verifier is passed back to the server during
943
+ the token exchange and compared to the code_challenge sent in this request.
922
944
923
945
Args:
924
946
request: the incoming request from the browser.
@@ -935,17 +957,33 @@ async def handle_redirect_request(
935
957
936
958
state = generate_token ()
937
959
nonce = generate_token ()
960
+ code_verifier = ""
938
961
939
962
if not client_redirect_url :
940
963
client_redirect_url = b""
941
964
965
+ metadata = await self .load_metadata ()
966
+
967
+ # Automatically enable PKCE if it is supported.
968
+ extra_grant_values = {}
969
+ if metadata .get ("code_challenge_methods_supported" ):
970
+ code_verifier = generate_token (48 )
971
+
972
+ # Note that we verified the server supports S256 earlier (in
973
+ # OidcProvider._validate_metadata).
974
+ extra_grant_values = {
975
+ "code_challenge_method" : "S256" ,
976
+ "code_challenge" : create_s256_code_challenge (code_verifier ),
977
+ }
978
+
942
979
cookie = self ._macaroon_generaton .generate_oidc_session_token (
943
980
state = state ,
944
981
session_data = OidcSessionData (
945
982
idp_id = self .idp_id ,
946
983
nonce = nonce ,
947
984
client_redirect_url = client_redirect_url .decode (),
948
985
ui_auth_session_id = ui_auth_session_id or "" ,
986
+ code_verifier = code_verifier ,
949
987
),
950
988
)
951
989
@@ -966,7 +1004,6 @@ async def handle_redirect_request(
966
1004
)
967
1005
)
968
1006
969
- metadata = await self .load_metadata ()
970
1007
authorization_endpoint = metadata .get ("authorization_endpoint" )
971
1008
return prepare_grant_uri (
972
1009
authorization_endpoint ,
@@ -976,6 +1013,7 @@ async def handle_redirect_request(
976
1013
scope = self ._scopes ,
977
1014
state = state ,
978
1015
nonce = nonce ,
1016
+ ** extra_grant_values ,
979
1017
)
980
1018
981
1019
async def handle_oidc_callback (
@@ -1003,7 +1041,9 @@ async def handle_oidc_callback(
1003
1041
# Exchange the code with the provider
1004
1042
try :
1005
1043
logger .debug ("Exchanging OAuth2 code for a token" )
1006
- token = await self ._exchange_code (code )
1044
+ token = await self ._exchange_code (
1045
+ code , code_verifier = session_data .code_verifier
1046
+ )
1007
1047
except OidcError as e :
1008
1048
logger .warning ("Could not exchange OAuth2 code: %s" , e )
1009
1049
self ._sso_handler .render_error (request , e .error , e .error_description )
0 commit comments