Skip to content

Commit

Permalink
Allow to use PKCE
Browse files Browse the repository at this point in the history
Proof Key for Code Exchange
Defined by RFC7636
https://www.rfc-editor.org/rfc/rfc7636
  • Loading branch information
sbrunner committed Aug 21, 2024
1 parent 8cabeef commit b95ebee
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 3 deletions.
17 changes: 17 additions & 0 deletions src/simple_openid_connect/data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Datatypes and models for various OpenID messages
"""
import code
import enum
import logging
import time
Expand Down Expand Up @@ -440,6 +441,12 @@ class AuthenticationRequest(OpenidBaseModel):
acr_values: Optional[List[str]] = None
"OPTIONAL. Requested Authentication Context Class Reference values Space-separated string that specifies the acr values that the Authorization Server is being requested to use for processing this Authentication Request, with the values appearing in order of preference The Authentication Context Class satisfied by the authentication performed is returned as the acr Claim Value, as specified in Section 2 The acr Claim is requested as a Voluntary Claim by this parameter."

code_challenge: Optional[str] = None
"Optional. Code Challenge Code challenge used in the code challenge. This parameter is REQUIRED if code_challenge_method is present."

code_challenge_method: Optional[str] = None
"OPTIONAL. Code Challenge Method Code challenge method used in the code challenge. If the code_challenge is present, this parameter is REQUIRED."


class AuthenticationSuccessResponse(OpenidBaseModel):
"""
Expand Down Expand Up @@ -575,6 +582,16 @@ class TokenRequest(OpenidBaseModel):
scope: Optional[str] = None
"REQUIRED, if grant type is 'password'. The scope requested by the application"

code_verifier: Optional[str] = None
"OPTIONAL. Code Verifier. This parameter is REQUIRED when using Proof Key for Code Exchange (PKCE) [RFC7636] with the authorization code grant type."

code_challenge: Optional[str] = None
"OPTIONAL. Code Challenge. This parameter is REQUIRED when using Proof Key for Code Exchange (PKCE) [RFC7636] with the authorization code grant type."

code_challenge_method: Optional[str] = None
"OPTIONAL. Code Challenge Method. This parameter is REQUIRED when using Proof Key for Code Exchange (PKCE) [RFC7636] with the authorization code grant type."


@model_validator(mode="before")
@classmethod
def _validate_required_based_on_grant_type(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""
import copy
import logging
from typing import Literal, Union
from typing import Literal, Union, Optional

import requests
from furl import furl
Expand All @@ -27,7 +27,7 @@


def start_authentication(
authorization_endpoint: str, scope: str, client_id: str, redirect_uri: str
authorization_endpoint: str, scope: str, client_id: str, redirect_uri: str, code_challenge: Optional[str]=None, code_challenge_method: Optional[str]=None
) -> str:
"""
Start the authentication process by constructing an appropriate :class:`AuthenticationRequest`, serializing it and
Expand All @@ -40,6 +40,8 @@ def start_authentication(
client_id=client_id,
redirect_uri=redirect_uri,
response_type="code",
code_challenge=code_challenge,
code_challenge_method=code_challenge_method
)
return request.encode_url(authorization_endpoint)

Expand All @@ -49,6 +51,9 @@ def handle_authentication_result(
token_endpoint: str,
client_authentication: ClientAuthenticationMethod,
redirect_uri: Union[Literal["auto"], str] = "auto",
code_verifier: Optional[str]=None,
code_challenge: Optional[str]=None,
code_challenge_method: Optional[str]=None
) -> Union[TokenSuccessResponse, TokenErrorResponse]:
"""
Handle an authentication result that is communicated to the RP in form of the user agents current url after having started an authentication process via :func:`start_authentication`.
Expand Down Expand Up @@ -87,6 +92,9 @@ def handle_authentication_result(
authentication_response=auth_response_msg,
redirect_uri=redirect_uri,
client_authentication=client_authentication,
code_verifier=code_verifier,
code_challenge=code_challenge,
code_challenge_method=code_challenge_method
)


Expand All @@ -95,6 +103,9 @@ def exchange_code_for_tokens(
authentication_response: AuthenticationSuccessResponse,
redirect_uri: str,
client_authentication: ClientAuthenticationMethod,
code_verifier: Optional[str]=None,
code_challenge: Optional[str]=None,
code_challenge_method: Optional[str]=None
) -> Union[TokenSuccessResponse, TokenErrorResponse]:
"""
Exchange a received code for access, refresh and id tokens.
Expand All @@ -116,6 +127,9 @@ def exchange_code_for_tokens(
redirect_uri=redirect_uri,
client_id=client_authentication.client_id,
grant_type="authorization_code",
code_verifier=code_verifier,
code_challenge=code_challenge,
code_challenge_method=code_challenge_method,
)
response = requests.post(
token_endpoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ class AuthorizationCodeFlowClient:
def __init__(self, base_client: "OpenidClient"):
self._base_client = base_client

def start_authentication(self) -> str:
def start_authentication(self, code_challenge: Optional[str] = None, code_challenge_method: Optional[str] = None
) -> str:
"""
Start the authentication process by constructing an appropriate :class:`AuthenticationRequest`, serializing it and
returning a which the end user now needs to visit.
:param code_challenge: The code challenge that was used in the authentication request.
:param code_challenge_method: The method that was used to generate the code challenge. typically "S256" or "plain".
:raises ImpossibleOperationError: If the client has no redirect_uri configured and therefore cannot perform this operation.
:returns: A URL to which the user agent should be redirected
Expand All @@ -48,12 +52,17 @@ def start_authentication(self) -> str:
self._base_client.scope,
self._base_client.client_auth.client_id,
redirect_uri.tostr(),
code_challenge=code_challenge,
code_challenge_method=code_challenge_method
)

def handle_authentication_result(
self,
current_url: str,
additional_redirect_args: Optional[Mapping[str, str]] = None,
code_verifier: Optional[str]=None,
code_challenge: Optional[str]=None,
code_challenge_method: Optional[str]=None,
) -> Union[TokenSuccessResponse, TokenErrorResponse]:
"""
Handle an authentication result that is communicated to the RP in form of the user agents current url after having started an authentication process via :func:`start_authentication`.
Expand All @@ -62,6 +71,9 @@ def handle_authentication_result(
The authentication result should be encoded into this url by the authorization server.
:param additional_redirect_args: Additional URL parameters that were added to the redirect uri.
They are probably still present in `current_url` but since they could be of any shape, no attempt is made here to automatically reconstruct them.
:param code_verifier: The code verifier that was used to generate the code challenge.
:param code_challenge: The code challenge that was used in the authentication request.
:param code_challenge_method: The method that was used to generate the code challenge. typically "S256" or "plain".
:raises AuthenticationFailedError: If the current url indicates an authentication failure that prevents an access token from being retrieved.
:raises UnsupportedByProviderError: If the provider only supports implicit flow and has no token endpoint.
Expand All @@ -88,6 +100,9 @@ def handle_authentication_result(
token_endpoint=self._base_client.provider_config.token_endpoint,
client_authentication=self._base_client.client_auth,
redirect_uri=redirect_uri.tostr(),
code_verifier=code_verifier,
code_challenge=code_challenge,
code_challenge_method=code_challenge_method
)

def exchange_code_for_tokens(
Expand Down

0 comments on commit b95ebee

Please sign in to comment.