From 8bfcd479ccfad563bf0c68e6dfdecf0c66b67880 Mon Sep 17 00:00:00 2001 From: Zac Pullar-Strecker Date: Mon, 27 Jan 2020 13:49:47 +1300 Subject: [PATCH] Correctly verify PKCE secret in token endpoint Before this change the PKCE secret would be verified only if it was sent by the client. This defeats the point of PKCE as a malicious actor which intercepted the code returned from the authorization endpoint would be able to send a request to the token endpoint without the code_verifier. This only affects public clients and is subject to the preconditions described by: https://tools.ietf.org/html/rfc7636#section-1 --- oidc_provider/lib/endpoints/token.py | 5 ++++- .../tests/cases/test_token_endpoint.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index c2d3987f..73a29937 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -86,7 +86,10 @@ def validate_params(self): raise TokenError('invalid_grant') # Validate PKCE parameters. - if self.params['code_verifier']: + if self.code.code_challenge: + if self.params['code_verifier'] is None: + raise TokenError('invalid_grant') + if self.code.code_challenge_method == 'S256': new_code_challenge = urlsafe_b64encode( hashlib.sha256(self.params['code_verifier'].encode('ascii')).digest() diff --git a/oidc_provider/tests/cases/test_token_endpoint.py b/oidc_provider/tests/cases/test_token_endpoint.py index b092fe97..a11b1c69 100644 --- a/oidc_provider/tests/cases/test_token_endpoint.py +++ b/oidc_provider/tests/cases/test_token_endpoint.py @@ -826,6 +826,25 @@ def test_pkce_parameters(self): json.loads(response.content.decode('utf-8')) + def test_pkce_missing_code_verifier(self): + """ + Test that a request to the token endpoint without the PKCE parameter + fails when PKCE was used during the authorization request. + """ + + code = create_code(user=self.user, client=self.client, + scope=['openid', 'email'], nonce=FAKE_NONCE, is_authentication=True, + code_challenge=FAKE_CODE_CHALLENGE, code_challenge_method='S256') + code.save() + + post_data = self._auth_code_post_data(code=code.code) + + assert 'code_verifier' not in post_data + + response = self._post_request(post_data) + + assert json.loads(response.content.decode('utf-8')).get('error') == 'invalid_grant' + @override_settings(OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE=False) def test_client_credentials_grant_type(self): fake_scopes_list = ['scopeone', 'scopetwo', INTROSPECTION_SCOPE]