66from urllib .parse import parse_qsl
77from urllib .parse import urlparse
88
9+ import nacl .hash
10+ from nacl .encoding import URLSafeBase64Encoder
911from jwkest import jws
1012from oic import rndstr
1113from oic .exception import MessageException
1214from oic .oic import PREFERENCE2PROVIDER
1315from oic .oic import scope2claims
14- from oic .oic .message import AccessTokenRequest
1516from oic .oic .message import AccessTokenResponse
16- from oic .oic .message import AuthorizationRequest
1717from oic .oic .message import AuthorizationResponse
1818from oic .oic .message import EndSessionRequest
1919from oic .oic .message import EndSessionResponse
2424from oic .oic .message import RegistrationRequest
2525from oic .oic .message import RegistrationResponse
2626
27+ from .message import AuthorizationRequest
28+ from .message import AccessTokenRequest
2729from .access_token import extract_bearer_token_from_http_request
2830from .client_authentication import verify_client_authentication
2931from .exceptions import AuthorizationError
@@ -328,6 +330,49 @@ def handle_token_request(self, request_body, # type: str
328330 raise InvalidTokenRequest ('grant_type \' {}\' unknown' .format (token_request ['grant_type' ]), token_request ,
329331 oauth_error = 'unsupported_grant_type' )
330332
333+ def _compute_code_challenge (self , code_verifier ):
334+ """
335+ Given a code verifier compute the code_challenge. This code_challenge is computed as defined (https://datatracker.ietf.org/doc/html/rfc7636#section-4.2):
336+
337+ code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))).
338+
339+ This shows that the SHA256 of the ascii encoded code_verifier is URLSafe base64 encoded. We have adjusted the encoding to the ISO_8859_1 encoding,
340+ conform to the AppAuth SDK for Android and IOS. Moreover, we remove the base64 padding (=).
341+
342+ :param code_verifier: the code verifier to transform to the Code Challenge
343+ """
344+ verifier_hash = nacl .hash .sha256 (code_verifier .encode ('ISO_8859_1' ), encoder = URLSafeBase64Encoder )
345+ return verifier_hash .decode ().replace ('=' , '' )
346+
347+ def _PKCE_verify (self , token_request , authentication_request ):
348+ """
349+ Verify that the given code_verifier complies with the initially supplied code_challenge.
350+
351+ Only supports the SHA256 code challenge method, plaintext is regarded as unsafe.
352+
353+ :param cc_cm: the initially supplied Code Challenge Code challenge Method dictionary
354+ :param code_verifier: the code_verfier to check against the code challenge.
355+ :returns: whether the code_verifier is what was expected given the cc_cm
356+ """
357+ code_challenge_method = authentication_request ['code_challenge_method' ]
358+ if code_challenge_method == 'plain' :
359+ return authentication_request ['code_challenge' ] == token_request ['code_verifier' ]
360+
361+ code_challenge = self ._compute_code_challenge (token_request ['code_verifier' ])
362+ return code_challenge == authentication_request ['code_challenge' ]
363+
364+ def _verify_code_exchange_req (self , token_request , authentication_request ):
365+ if token_request ['client_id' ] != authentication_request ['client_id' ]:
366+ logger .info ('Authorization code \' %s\' belonging to \' %s\' was used by \' %s\' ' ,
367+ token_request ['code' ], authentication_request ['client_id' ], token_request ['client_id' ])
368+ raise InvalidAuthorizationCode ('{} unknown' .format (token_request ['code' ]))
369+ if token_request ['redirect_uri' ] != authentication_request ['redirect_uri' ]:
370+ raise InvalidTokenRequest ('Invalid redirect_uri: {} != {}' .format (token_request ['redirect_uri' ],
371+ authentication_request ['redirect_uri' ]),
372+ token_request )
373+ if not self ._PKCE_verify (token_request , authentication_request ):
374+ raise InvalidTokenRequest ('Unexpected Code Verifier: {}' .format (authentication_request ['code_challenge' ]))
375+
331376 def _do_code_exchange (self , request , # type: Dict[str, str]
332377 extra_id_token_claims = None
333378 # type: Optional[Union[Mapping[str, Union[str, List[str]]], Callable[[str, str], Mapping[str, Union[str, List[str]]]]]
@@ -351,14 +396,7 @@ def _do_code_exchange(self, request, # type: Dict[str, str]
351396
352397 authentication_request = self .authz_state .get_authorization_request_for_code (token_request ['code' ])
353398
354- if token_request ['client_id' ] != authentication_request ['client_id' ]:
355- logger .info ('Authorization code \' %s\' belonging to \' %s\' was used by \' %s\' ' ,
356- token_request ['code' ], authentication_request ['client_id' ], token_request ['client_id' ])
357- raise InvalidAuthorizationCode ('{} unknown' .format (token_request ['code' ]))
358- if token_request ['redirect_uri' ] != authentication_request ['redirect_uri' ]:
359- raise InvalidTokenRequest ('Invalid redirect_uri: {} != {}' .format (token_request ['redirect_uri' ],
360- authentication_request ['redirect_uri' ]),
361- token_request )
399+ self ._verify_code_exchange_req (token_request , authentication_request )
362400
363401 sub = self .authz_state .get_subject_identifier_for_code (token_request ['code' ])
364402 user_id = self .authz_state .get_user_id_for_subject_identifier (sub )
@@ -393,7 +431,7 @@ def _do_token_refresh(self, request):
393431 Handles a token request for refreshing an access token (grant_type=refresh_token).
394432 :param request: parsed http request parameters
395433 :return: a token response containing a new Access Token and possibly a new Refresh Token
396- :raise InvalidTokenRequest : if the token request is invalid
434+ :raise InvalidTtoken_requestokenRequest : if the token request is invalid
397435 """
398436 token_request = RefreshAccessTokenRequest ().from_dict (request )
399437 try :
0 commit comments