From 64b730a2a97250232714f4cf74fb9bbe502e301a Mon Sep 17 00:00:00 2001 From: Tim Hallmann Date: Wed, 25 Sep 2024 16:00:28 +0200 Subject: [PATCH 1/3] Implement state verification in code flow --- .../flows/authorization_code_flow/__init__.py | 13 ++++++++++++- .../flows/authorization_code_flow/client.py | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/simple_openid_connect/flows/authorization_code_flow/__init__.py b/src/simple_openid_connect/flows/authorization_code_flow/__init__.py index e225738..13a109e 100644 --- a/src/simple_openid_connect/flows/authorization_code_flow/__init__.py +++ b/src/simple_openid_connect/flows/authorization_code_flow/__init__.py @@ -21,7 +21,7 @@ TokenRequest, TokenSuccessResponse, ) -from simple_openid_connect.exceptions import AuthenticationFailedError +from simple_openid_connect.exceptions import AuthenticationFailedError, ValidationError logger = logging.getLogger(__name__) @@ -31,6 +31,7 @@ def start_authentication( scope: str, client_id: str, redirect_uri: str, + state: Optional[str] = None, code_challenge: Optional[str] = None, code_challenge_method: Optional[str] = None, ) -> str: @@ -38,6 +39,8 @@ def start_authentication( Start the authentication process by constructing an appropriate :class:`AuthenticationRequest`, serializing it and returning a which the end user now needs to visit. + :param state: The state intended to prevent Cross-Site Request Forgery. + :returns: A URL to which the user agent should be redirected """ request = AuthenticationRequest( @@ -45,6 +48,7 @@ def start_authentication( client_id=client_id, redirect_uri=redirect_uri, response_type="code", + state=state, code_challenge=code_challenge, code_challenge_method=code_challenge_method, ) @@ -56,6 +60,7 @@ def handle_authentication_result( token_endpoint: str, client_authentication: ClientAuthenticationMethod, redirect_uri: Union[Literal["auto"], str] = "auto", + state: Optional[str] = None, code_verifier: Optional[str] = None, code_challenge: Optional[str] = None, code_challenge_method: Optional[str] = None, @@ -70,8 +75,10 @@ def handle_authentication_result( :param client_authentication: A way for the client to authenticate itself :param redirect_uri: The `redirect_uri` that was specified during the authentication initiation. If the special value `auto` is used, it is assumed that `current_url` is the that callback and it is stripped of query parameters and fragments to reproduce the originally supplied one. + :param state: The `state` that was specified during the authentication initiation. :raises AuthenticationFailedError: If the current url indicates an authentication failure that prevents an access token from being retrieved. + :raises ValidationError: If the returned state does not match the given state. :returns: The result of the token exchange """ @@ -92,6 +99,10 @@ def handle_authentication_result( ) auth_response_msg = AuthenticationSuccessResponse.parse_url(str(current_furl)) + + if state != auth_response_msg.state: + raise ValidationError("Returned state does not match given state.") + return exchange_code_for_tokens( token_endpoint=token_endpoint, authentication_response=auth_response_msg, diff --git a/src/simple_openid_connect/flows/authorization_code_flow/client.py b/src/simple_openid_connect/flows/authorization_code_flow/client.py index 8d197fb..d86086c 100644 --- a/src/simple_openid_connect/flows/authorization_code_flow/client.py +++ b/src/simple_openid_connect/flows/authorization_code_flow/client.py @@ -29,6 +29,7 @@ def __init__(self, base_client: "OpenidClient"): def start_authentication( self, + state: Optional[str] = None, code_challenge: Optional[str] = None, code_challenge_method: Optional[str] = None, ) -> str: @@ -36,6 +37,7 @@ def start_authentication( Start the authentication process by constructing an appropriate :class:`AuthenticationRequest`, serializing it and returning a which the end user now needs to visit. + :param state: The state intended to prevent Cross-Site Request Forgery. :param code_challenge: The code challenge intended for use with Proof Key for Code Exchange (PKCE) [RFC7636]. :param code_challenge_method: The code challenge method intended for use with Proof Key for Code Exchange (PKCE) [RFC7636], typically "S256" or "plain". @@ -55,6 +57,7 @@ def start_authentication( self._base_client.scope, self._base_client.client_auth.client_id, redirect_uri.tostr(), + state=state, code_challenge=code_challenge, code_challenge_method=code_challenge_method, ) @@ -63,6 +66,7 @@ def handle_authentication_result( self, current_url: str, additional_redirect_args: Optional[Mapping[str, str]] = None, + state: Optional[str] = None, code_verifier: Optional[str] = None, code_challenge: Optional[str] = None, code_challenge_method: Optional[str] = None, @@ -74,6 +78,7 @@ 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 state: The `state` that was specified during the authentication initiation. :param code_verifier: The code verifier intended for use with Proof Key for Code Exchange (PKCE) [RFC7636]. :param code_challenge: The code challenge intended for use with Proof Key for Code Exchange (PKCE) [RFC7636]. :param code_challenge_method: The code challenge method intended for use with Proof Key for Code Exchange (PKCE) [RFC7636], typically "S256" or "plain". @@ -103,6 +108,7 @@ 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(), + state=state, code_verifier=code_verifier, code_challenge=code_challenge, code_challenge_method=code_challenge_method, From 288f7033e6cb5b53a6e0675aa323394be5882344 Mon Sep 17 00:00:00 2001 From: Tim Hallmann Date: Wed, 25 Sep 2024 16:15:19 +0200 Subject: [PATCH 2/3] Allow to pass a prompt through the client --- .../flows/authorization_code_flow/__init__.py | 4 ++++ .../flows/authorization_code_flow/client.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/simple_openid_connect/flows/authorization_code_flow/__init__.py b/src/simple_openid_connect/flows/authorization_code_flow/__init__.py index 13a109e..7b34036 100644 --- a/src/simple_openid_connect/flows/authorization_code_flow/__init__.py +++ b/src/simple_openid_connect/flows/authorization_code_flow/__init__.py @@ -32,6 +32,7 @@ def start_authentication( client_id: str, redirect_uri: str, state: Optional[str] = None, + prompt: Optional[list[str]] = None, code_challenge: Optional[str] = None, code_challenge_method: Optional[str] = None, ) -> str: @@ -40,6 +41,8 @@ def start_authentication( returning a which the end user now needs to visit. :param state: The state intended to prevent Cross-Site Request Forgery. + :param prompt: Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. + The defined values are: "none", "login", "consent" and "select_account", multiple may be given as a list. :returns: A URL to which the user agent should be redirected """ @@ -49,6 +52,7 @@ def start_authentication( redirect_uri=redirect_uri, response_type="code", state=state, + prompt=prompt, code_challenge=code_challenge, code_challenge_method=code_challenge_method, ) diff --git a/src/simple_openid_connect/flows/authorization_code_flow/client.py b/src/simple_openid_connect/flows/authorization_code_flow/client.py index d86086c..ff45867 100644 --- a/src/simple_openid_connect/flows/authorization_code_flow/client.py +++ b/src/simple_openid_connect/flows/authorization_code_flow/client.py @@ -30,6 +30,7 @@ def __init__(self, base_client: "OpenidClient"): def start_authentication( self, state: Optional[str] = None, + prompt: Optional[list[str]] = None, code_challenge: Optional[str] = None, code_challenge_method: Optional[str] = None, ) -> str: @@ -38,6 +39,8 @@ def start_authentication( returning a which the end user now needs to visit. :param state: The state intended to prevent Cross-Site Request Forgery. + :param prompt: Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. + The defined values are: "none", "login", "consent" and "select_account", multiple may be given as a list. :param code_challenge: The code challenge intended for use with Proof Key for Code Exchange (PKCE) [RFC7636]. :param code_challenge_method: The code challenge method intended for use with Proof Key for Code Exchange (PKCE) [RFC7636], typically "S256" or "plain". @@ -58,6 +61,7 @@ def start_authentication( self._base_client.client_auth.client_id, redirect_uri.tostr(), state=state, + prompt=prompt, code_challenge=code_challenge, code_challenge_method=code_challenge_method, ) From cdef7621d60d20c2ffa9e4cac70758d6496c19bb Mon Sep 17 00:00:00 2001 From: Tim Hallmann Date: Wed, 25 Sep 2024 16:46:32 +0200 Subject: [PATCH 3/3] Allow to pass a nonce through the client --- .../flows/authorization_code_flow/__init__.py | 3 +++ .../flows/authorization_code_flow/client.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/simple_openid_connect/flows/authorization_code_flow/__init__.py b/src/simple_openid_connect/flows/authorization_code_flow/__init__.py index 7b34036..c618484 100644 --- a/src/simple_openid_connect/flows/authorization_code_flow/__init__.py +++ b/src/simple_openid_connect/flows/authorization_code_flow/__init__.py @@ -32,6 +32,7 @@ def start_authentication( client_id: str, redirect_uri: str, state: Optional[str] = None, + nonce: Optional[str] = None, prompt: Optional[list[str]] = None, code_challenge: Optional[str] = None, code_challenge_method: Optional[str] = None, @@ -41,6 +42,7 @@ def start_authentication( returning a which the end user now needs to visit. :param state: The state intended to prevent Cross-Site Request Forgery. + :param nonce: String value used to associate a Client session with an ID Token, and to mitigate replay attacks. :param prompt: Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are: "none", "login", "consent" and "select_account", multiple may be given as a list. @@ -52,6 +54,7 @@ def start_authentication( redirect_uri=redirect_uri, response_type="code", state=state, + nonce=nonce, prompt=prompt, code_challenge=code_challenge, code_challenge_method=code_challenge_method, diff --git a/src/simple_openid_connect/flows/authorization_code_flow/client.py b/src/simple_openid_connect/flows/authorization_code_flow/client.py index ff45867..167d265 100644 --- a/src/simple_openid_connect/flows/authorization_code_flow/client.py +++ b/src/simple_openid_connect/flows/authorization_code_flow/client.py @@ -30,6 +30,7 @@ def __init__(self, base_client: "OpenidClient"): def start_authentication( self, state: Optional[str] = None, + nonce: Optional[str] = None, prompt: Optional[list[str]] = None, code_challenge: Optional[str] = None, code_challenge_method: Optional[str] = None, @@ -39,6 +40,7 @@ def start_authentication( returning a which the end user now needs to visit. :param state: The state intended to prevent Cross-Site Request Forgery. + :param nonce: String value used to associate a Client session with an ID Token, and to mitigate replay attacks. :param prompt: Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are: "none", "login", "consent" and "select_account", multiple may be given as a list. :param code_challenge: The code challenge intended for use with Proof Key for Code Exchange (PKCE) [RFC7636]. @@ -61,6 +63,7 @@ def start_authentication( self._base_client.client_auth.client_id, redirect_uri.tostr(), state=state, + nonce=nonce, prompt=prompt, code_challenge=code_challenge, code_challenge_method=code_challenge_method,