From 8f7e7eaa869edbe513af7672f4943a561f87b9c6 Mon Sep 17 00:00:00 2001 From: Raphael Krupinski <10319569-mattesilver@users.noreply.gitlab.com> Date: Wed, 21 Feb 2024 22:39:12 +0100 Subject: [PATCH 1/9] Refactor OAuth flows to use OAuth2 as a base class. --- .../authentication_responses_server.py | 2 +- httpx_auth/_oauth2/authorization_code.py | 14 ++------- httpx_auth/_oauth2/authorization_code_pkce.py | 14 ++------- httpx_auth/_oauth2/client_credentials.py | 14 +++------ httpx_auth/_oauth2/common.py | 31 +++++++++++++++++-- httpx_auth/_oauth2/implicit.py | 19 +++++------- httpx_auth/_oauth2/resource_owner_password.py | 16 +++------- 7 files changed, 51 insertions(+), 59 deletions(-) diff --git a/httpx_auth/_oauth2/authentication_responses_server.py b/httpx_auth/_oauth2/authentication_responses_server.py index 27d2ffb..fef2c80 100644 --- a/httpx_auth/_oauth2/authentication_responses_server.py +++ b/httpx_auth/_oauth2/authentication_responses_server.py @@ -166,7 +166,7 @@ def handle_timeout(self) -> None: raise TimeoutOccurred(self.timeout) -def request_new_grant(grant_details: GrantDetails) -> (str, str): +def request_new_grant(grant_details: GrantDetails) -> tuple[str, str]: """ Ask for a new OAuth2 grant. :return: A tuple (state, grant) diff --git a/httpx_auth/_oauth2/authorization_code.py b/httpx_auth/_oauth2/authorization_code.py index f9df1ab..2f62bcc 100644 --- a/httpx_auth/_oauth2/authorization_code.py +++ b/httpx_auth/_oauth2/authorization_code.py @@ -15,7 +15,7 @@ ) -class OAuth2AuthorizationCode(httpx.Auth, SupportMultiAuth, BrowserAuth): +class OAuth2AuthorizationCode(OAuth2, SupportMultiAuth, BrowserAuth): """ Authorization Code Grant @@ -70,6 +70,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): raise Exception("Token URL is mandatory.") BrowserAuth.__init__(self, kwargs) + OAuth2.__init__(self) self.header_name = kwargs.pop("header_name", None) or "Authorization" self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" @@ -129,17 +130,8 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): self.refresh_data = {"grant_type": "refresh_token"} self.refresh_data.update(kwargs) - def auth_flow( - self, request: httpx.Request - ) -> Generator[httpx.Request, httpx.Response, None]: - token = OAuth2.token_cache.get_token( - self.state, - early_expiry=self.early_expiry, - on_missing_token=self.request_new_token, - on_expired_token=self.refresh_token, - ) + def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) - yield request def request_new_token(self) -> tuple: # Request code diff --git a/httpx_auth/_oauth2/authorization_code_pkce.py b/httpx_auth/_oauth2/authorization_code_pkce.py index 0216c66..f79b645 100644 --- a/httpx_auth/_oauth2/authorization_code_pkce.py +++ b/httpx_auth/_oauth2/authorization_code_pkce.py @@ -16,7 +16,7 @@ ) -class OAuth2AuthorizationCodePKCE(httpx.Auth, SupportMultiAuth, BrowserAuth): +class OAuth2AuthorizationCodePKCE(OAuth2, SupportMultiAuth, BrowserAuth): """ Proof Key for Code Exchange @@ -69,6 +69,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): raise Exception("Token URL is mandatory.") BrowserAuth.__init__(self, kwargs) + OAuth2.__init__(self) self.client = kwargs.pop("client", None) @@ -139,17 +140,8 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): self.refresh_data = {"grant_type": "refresh_token"} self.refresh_data.update(kwargs) - def auth_flow( - self, request: httpx.Request - ) -> Generator[httpx.Request, httpx.Response, None]: - token = OAuth2.token_cache.get_token( - self.state, - early_expiry=self.early_expiry, - on_missing_token=self.request_new_token, - on_expired_token=self.refresh_token, - ) + def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) - yield request def request_new_token(self) -> tuple: # Request code diff --git a/httpx_auth/_oauth2/client_credentials.py b/httpx_auth/_oauth2/client_credentials.py index caa1ab0..7810527 100644 --- a/httpx_auth/_oauth2/client_credentials.py +++ b/httpx_auth/_oauth2/client_credentials.py @@ -10,7 +10,7 @@ ) -class OAuth2ClientCredentials(httpx.Auth, SupportMultiAuth): +class OAuth2ClientCredentials(OAuth2, SupportMultiAuth): """ Client Credentials Grant @@ -49,6 +49,8 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs) if not self.client_secret: raise Exception("client_secret is mandatory.") + super().__init__() + self.header_name = kwargs.pop("header_name", None) or "Authorization" self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" if "{token}" not in self.header_value: @@ -72,16 +74,8 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs) all_parameters_in_url = _add_parameters(self.token_url, self.data) self.state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest() - def auth_flow( - self, request: httpx.Request - ) -> Generator[httpx.Request, httpx.Response, None]: - token = OAuth2.token_cache.get_token( - self.state, - early_expiry=self.early_expiry, - on_missing_token=self.request_new_token, - ) + def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) - yield request def request_new_token(self) -> tuple: client = self.client or httpx.Client() diff --git a/httpx_auth/_oauth2/common.py b/httpx_auth/_oauth2/common.py index a94cde3..8f69a5b 100644 --- a/httpx_auth/_oauth2/common.py +++ b/httpx_auth/_oauth2/common.py @@ -1,4 +1,5 @@ -from typing import Optional +import abc +from typing import Callable, Generator, Optional, Union from urllib.parse import parse_qs, urlsplit, urlunsplit, urlencode import httpx @@ -83,6 +84,32 @@ def request_new_grant_with_post( return token, content.get("expires_in"), content.get("refresh_token") -class OAuth2: +class OAuth2(abc.ABC, httpx.Auth): token_cache = TokenMemoryCache() display = DisplaySettings() + state: Optional[str] = None + early_expiry: float + + refresh_token: Optional[Callable] + + def auth_flow( + self, request: httpx.Request + ) -> Generator[httpx.Request, httpx.Response, None]: + token = OAuth2.token_cache.get_token( + self.state, + early_expiry=self.early_expiry, + on_missing_token=self.request_new_token, + on_expired_token=( + self.refresh_token if "refresh_token" in dir(self) else None + ), + ) + self._update_user_request(request, token) + yield request + + @abc.abstractmethod + def request_new_token(self) -> Union[tuple[str, str], tuple[str, str, int]]: + pass # pragma: no cover + + @abc.abstractmethod + def _update_user_request(self, request: httpx.Request, token: str) -> None: + pass # pragma: no cover diff --git a/httpx_auth/_oauth2/implicit.py b/httpx_auth/_oauth2/implicit.py index 6ec67b1..a52dce0 100644 --- a/httpx_auth/_oauth2/implicit.py +++ b/httpx_auth/_oauth2/implicit.py @@ -1,6 +1,6 @@ import uuid from hashlib import sha512 -from typing import Generator +from typing import Generator, Union import httpx @@ -15,7 +15,7 @@ ) -class OAuth2Implicit(httpx.Auth, SupportMultiAuth, BrowserAuth): +class OAuth2Implicit(OAuth2, SupportMultiAuth, BrowserAuth): """ Implicit Grant @@ -61,6 +61,7 @@ def __init__(self, authorization_url: str, **kwargs): raise Exception("Authorization URL is mandatory.") BrowserAuth.__init__(self, kwargs) + OAuth2.__init__(self) self.header_name = kwargs.pop("header_name", None) or "Authorization" self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" @@ -104,17 +105,11 @@ def __init__(self, authorization_url: str, **kwargs): self.redirect_uri_port, ) - def auth_flow( - self, request: httpx.Request - ) -> Generator[httpx.Request, httpx.Response, None]: - token = OAuth2.token_cache.get_token( - self.state, - early_expiry=self.early_expiry, - on_missing_token=authentication_responses_server.request_new_grant, - grant_details=self.grant_details, - ) + def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) - yield request + + def request_new_token(self) -> tuple[str, str]: + return authentication_responses_server.request_new_grant(self.grant_details) class AzureActiveDirectoryImplicit(OAuth2Implicit): diff --git a/httpx_auth/_oauth2/resource_owner_password.py b/httpx_auth/_oauth2/resource_owner_password.py index 7c38419..d3c97b1 100644 --- a/httpx_auth/_oauth2/resource_owner_password.py +++ b/httpx_auth/_oauth2/resource_owner_password.py @@ -1,5 +1,4 @@ from hashlib import sha512 -from typing import Generator import httpx from httpx_auth._authentication import SupportMultiAuth @@ -10,7 +9,7 @@ ) -class OAuth2ResourceOwnerPasswordCredentials(httpx.Auth, SupportMultiAuth): +class OAuth2ResourceOwnerPasswordCredentials(OAuth2, SupportMultiAuth): """ Resource Owner Password Credentials Grant @@ -42,6 +41,8 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): Use it to provide a custom proxying rule for instance. :param kwargs: all additional authorization parameters that should be put as body parameters in the token URL. """ + super().__init__() + self.token_url = token_url if not self.token_url: raise Exception("Token URL is mandatory.") @@ -85,17 +86,8 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): all_parameters_in_url = _add_parameters(self.token_url, self.data) self.state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest() - def auth_flow( - self, request: httpx.Request - ) -> Generator[httpx.Request, httpx.Response, None]: - token = OAuth2.token_cache.get_token( - self.state, - early_expiry=self.early_expiry, - on_missing_token=self.request_new_token, - on_expired_token=self.refresh_token, - ) + def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) - yield request def request_new_token(self) -> tuple: client = self.client or httpx.Client() From dda1cd51e02672258ac2670499c71cf33b5065cd Mon Sep 17 00:00:00 2001 From: Raphael Krupinski <10319569-mattesilver@users.noreply.gitlab.com> Date: Fri, 23 Feb 2024 12:12:11 +0100 Subject: [PATCH 2/9] Separate OAuth2BaseAuth base class from OAuth2. --- httpx_auth/_oauth2/authorization_code.py | 8 ++++---- httpx_auth/_oauth2/authorization_code_pkce.py | 7 +++---- httpx_auth/_oauth2/client_credentials.py | 6 +++--- httpx_auth/_oauth2/common.py | 5 ++++- httpx_auth/_oauth2/implicit.py | 7 +++---- httpx_auth/_oauth2/resource_owner_password.py | 4 ++-- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/httpx_auth/_oauth2/authorization_code.py b/httpx_auth/_oauth2/authorization_code.py index 2f62bcc..9923cf2 100644 --- a/httpx_auth/_oauth2/authorization_code.py +++ b/httpx_auth/_oauth2/authorization_code.py @@ -1,5 +1,5 @@ from hashlib import sha512 -from typing import Generator, Iterable, Union +from typing import Iterable, Union import httpx @@ -8,14 +8,14 @@ from httpx_auth._oauth2.browser import BrowserAuth from httpx_auth._oauth2.common import ( request_new_grant_with_post, - OAuth2, + OAuth2BaseAuth, _add_parameters, _pop_parameter, _get_query_parameter, ) -class OAuth2AuthorizationCode(OAuth2, SupportMultiAuth, BrowserAuth): +class OAuth2AuthorizationCode(OAuth2BaseAuth, SupportMultiAuth, BrowserAuth): """ Authorization Code Grant @@ -70,7 +70,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): raise Exception("Token URL is mandatory.") BrowserAuth.__init__(self, kwargs) - OAuth2.__init__(self) + OAuth2BaseAuth.__init__(self) self.header_name = kwargs.pop("header_name", None) or "Authorization" self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" diff --git a/httpx_auth/_oauth2/authorization_code_pkce.py b/httpx_auth/_oauth2/authorization_code_pkce.py index f79b645..7697053 100644 --- a/httpx_auth/_oauth2/authorization_code_pkce.py +++ b/httpx_auth/_oauth2/authorization_code_pkce.py @@ -1,7 +1,6 @@ import base64 import os from hashlib import sha256, sha512 -from typing import Generator import httpx @@ -10,13 +9,13 @@ from httpx_auth._oauth2.browser import BrowserAuth from httpx_auth._oauth2.common import ( request_new_grant_with_post, - OAuth2, + OAuth2BaseAuth, _add_parameters, _pop_parameter, ) -class OAuth2AuthorizationCodePKCE(OAuth2, SupportMultiAuth, BrowserAuth): +class OAuth2AuthorizationCodePKCE(OAuth2BaseAuth, SupportMultiAuth, BrowserAuth): """ Proof Key for Code Exchange @@ -69,7 +68,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): raise Exception("Token URL is mandatory.") BrowserAuth.__init__(self, kwargs) - OAuth2.__init__(self) + OAuth2BaseAuth.__init__(self) self.client = kwargs.pop("client", None) diff --git a/httpx_auth/_oauth2/client_credentials.py b/httpx_auth/_oauth2/client_credentials.py index 7810527..92040c7 100644 --- a/httpx_auth/_oauth2/client_credentials.py +++ b/httpx_auth/_oauth2/client_credentials.py @@ -1,16 +1,16 @@ from hashlib import sha512 -from typing import Generator, Union, Iterable +from typing import Union, Iterable import httpx from httpx_auth._authentication import SupportMultiAuth from httpx_auth._oauth2.common import ( - OAuth2, + OAuth2BaseAuth, request_new_grant_with_post, _add_parameters, ) -class OAuth2ClientCredentials(OAuth2, SupportMultiAuth): +class OAuth2ClientCredentials(OAuth2BaseAuth, SupportMultiAuth): """ Client Credentials Grant diff --git a/httpx_auth/_oauth2/common.py b/httpx_auth/_oauth2/common.py index 8f69a5b..08c06de 100644 --- a/httpx_auth/_oauth2/common.py +++ b/httpx_auth/_oauth2/common.py @@ -84,9 +84,12 @@ def request_new_grant_with_post( return token, content.get("expires_in"), content.get("refresh_token") -class OAuth2(abc.ABC, httpx.Auth): +class OAuth2: token_cache = TokenMemoryCache() display = DisplaySettings() + + +class OAuth2BaseAuth(abc.ABC, httpx.Auth): state: Optional[str] = None early_expiry: float diff --git a/httpx_auth/_oauth2/implicit.py b/httpx_auth/_oauth2/implicit.py index a52dce0..608845c 100644 --- a/httpx_auth/_oauth2/implicit.py +++ b/httpx_auth/_oauth2/implicit.py @@ -1,6 +1,5 @@ import uuid from hashlib import sha512 -from typing import Generator, Union import httpx @@ -8,14 +7,14 @@ from httpx_auth._oauth2 import authentication_responses_server from httpx_auth._oauth2.browser import BrowserAuth from httpx_auth._oauth2.common import ( - OAuth2, + OAuth2BaseAuth, _add_parameters, _pop_parameter, _get_query_parameter, ) -class OAuth2Implicit(OAuth2, SupportMultiAuth, BrowserAuth): +class OAuth2Implicit(OAuth2BaseAuth, SupportMultiAuth, BrowserAuth): """ Implicit Grant @@ -61,7 +60,7 @@ def __init__(self, authorization_url: str, **kwargs): raise Exception("Authorization URL is mandatory.") BrowserAuth.__init__(self, kwargs) - OAuth2.__init__(self) + OAuth2BaseAuth.__init__(self) self.header_name = kwargs.pop("header_name", None) or "Authorization" self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" diff --git a/httpx_auth/_oauth2/resource_owner_password.py b/httpx_auth/_oauth2/resource_owner_password.py index d3c97b1..7acd7bc 100644 --- a/httpx_auth/_oauth2/resource_owner_password.py +++ b/httpx_auth/_oauth2/resource_owner_password.py @@ -3,13 +3,13 @@ import httpx from httpx_auth._authentication import SupportMultiAuth from httpx_auth._oauth2.common import ( - OAuth2, + OAuth2BaseAuth, request_new_grant_with_post, _add_parameters, ) -class OAuth2ResourceOwnerPasswordCredentials(OAuth2, SupportMultiAuth): +class OAuth2ResourceOwnerPasswordCredentials(OAuth2BaseAuth, SupportMultiAuth): """ Resource Owner Password Credentials Grant From 4bfb2cd2e5601dade0ce79530e7a7954ff922a7c Mon Sep 17 00:00:00 2001 From: Raphael Krupinski Date: Sat, 24 Feb 2024 20:12:52 +0100 Subject: [PATCH 3/9] Update httpx_auth/_oauth2/resource_owner_password.py Co-authored-by: Colin Bounouar --- httpx_auth/_oauth2/resource_owner_password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpx_auth/_oauth2/resource_owner_password.py b/httpx_auth/_oauth2/resource_owner_password.py index 7acd7bc..d448912 100644 --- a/httpx_auth/_oauth2/resource_owner_password.py +++ b/httpx_auth/_oauth2/resource_owner_password.py @@ -41,7 +41,7 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): Use it to provide a custom proxying rule for instance. :param kwargs: all additional authorization parameters that should be put as body parameters in the token URL. """ - super().__init__() + OAuth2BaseAuth.__init__(self) self.token_url = token_url if not self.token_url: From 427234fed14ac04ebebc2dd674c0e431af864cc0 Mon Sep 17 00:00:00 2001 From: Raphael Krupinski <10319569-mattesilver@users.noreply.gitlab.com> Date: Sat, 24 Feb 2024 20:50:36 +0100 Subject: [PATCH 4/9] Pass state, early_expiry and refresh_token to the __init__ call. --- httpx_auth/_oauth2/authorization_code.py | 9 +++++---- httpx_auth/_oauth2/authorization_code_pkce.py | 9 +++++---- httpx_auth/_oauth2/client_credentials.py | 8 ++++---- httpx_auth/_oauth2/common.py | 14 +++++++------- httpx_auth/_oauth2/implicit.py | 9 +++++---- httpx_auth/_oauth2/resource_owner_password.py | 7 ++++--- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/httpx_auth/_oauth2/authorization_code.py b/httpx_auth/_oauth2/authorization_code.py index 9923cf2..8c7f8ac 100644 --- a/httpx_auth/_oauth2/authorization_code.py +++ b/httpx_auth/_oauth2/authorization_code.py @@ -70,7 +70,6 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): raise Exception("Token URL is mandatory.") BrowserAuth.__init__(self, kwargs) - OAuth2BaseAuth.__init__(self) self.header_name = kwargs.pop("header_name", None) or "Authorization" self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" @@ -78,7 +77,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): raise Exception("header_value parameter must contains {token}.") self.token_field_name = kwargs.pop("token_field_name", None) or "access_token" - self.early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) + early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) username = kwargs.pop("username", None) password = kwargs.pop("password", None) @@ -100,11 +99,11 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): authorization_url_without_nonce, nonce = _pop_parameter( authorization_url_without_nonce, "nonce" ) - self.state = sha512( + state = sha512( authorization_url_without_nonce.encode("unicode_escape") ).hexdigest() custom_code_parameters = { - "state": self.state, + "state": state, "redirect_uri": self.redirect_uri, } if nonce: @@ -130,6 +129,8 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): self.refresh_data = {"grant_type": "refresh_token"} self.refresh_data.update(kwargs) + OAuth2BaseAuth.__init__(self, state, early_expiry, self.refresh_token) + def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) diff --git a/httpx_auth/_oauth2/authorization_code_pkce.py b/httpx_auth/_oauth2/authorization_code_pkce.py index 7697053..e6108c4 100644 --- a/httpx_auth/_oauth2/authorization_code_pkce.py +++ b/httpx_auth/_oauth2/authorization_code_pkce.py @@ -68,7 +68,6 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): raise Exception("Token URL is mandatory.") BrowserAuth.__init__(self, kwargs) - OAuth2BaseAuth.__init__(self) self.client = kwargs.pop("client", None) @@ -78,7 +77,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): raise Exception("header_value parameter must contains {token}.") self.token_field_name = kwargs.pop("token_field_name", None) or "access_token" - self.early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) + early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) # As described in https://tools.ietf.org/html/rfc6749#section-4.1.2 code_field_name = kwargs.pop("code_field_name", "code") @@ -98,11 +97,11 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): authorization_url_without_nonce, nonce = _pop_parameter( authorization_url_without_nonce, "nonce" ) - self.state = sha512( + state = sha512( authorization_url_without_nonce.encode("unicode_escape") ).hexdigest() custom_code_parameters = { - "state": self.state, + "state": state, "redirect_uri": self.redirect_uri, } if nonce: @@ -139,6 +138,8 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): self.refresh_data = {"grant_type": "refresh_token"} self.refresh_data.update(kwargs) + OAuth2BaseAuth.__init__(self, state, early_expiry, self.refresh_token) + def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) diff --git a/httpx_auth/_oauth2/client_credentials.py b/httpx_auth/_oauth2/client_credentials.py index 92040c7..319568c 100644 --- a/httpx_auth/_oauth2/client_credentials.py +++ b/httpx_auth/_oauth2/client_credentials.py @@ -49,15 +49,13 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs) if not self.client_secret: raise Exception("client_secret is mandatory.") - super().__init__() - self.header_name = kwargs.pop("header_name", None) or "Authorization" self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" if "{token}" not in self.header_value: raise Exception("header_value parameter must contains {token}.") self.token_field_name = kwargs.pop("token_field_name", None) or "access_token" - self.early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) + early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) # Time is expressed in seconds self.timeout = int(kwargs.pop("timeout", None) or 60) @@ -72,7 +70,9 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs) self.data.update(kwargs) all_parameters_in_url = _add_parameters(self.token_url, self.data) - self.state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest() + state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest() + + super().__init__(state, early_expiry) def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) diff --git a/httpx_auth/_oauth2/common.py b/httpx_auth/_oauth2/common.py index 08c06de..d73378a 100644 --- a/httpx_auth/_oauth2/common.py +++ b/httpx_auth/_oauth2/common.py @@ -90,10 +90,12 @@ class OAuth2: class OAuth2BaseAuth(abc.ABC, httpx.Auth): - state: Optional[str] = None - early_expiry: float - - refresh_token: Optional[Callable] + def __init__( + self, state: str, early_expiry: float, refresh_token: Optional[Callable] = None + ) -> None: + self.state = state + self.early_expiry = early_expiry + self.refresh_token = refresh_token def auth_flow( self, request: httpx.Request @@ -102,9 +104,7 @@ def auth_flow( self.state, early_expiry=self.early_expiry, on_missing_token=self.request_new_token, - on_expired_token=( - self.refresh_token if "refresh_token" in dir(self) else None - ), + on_expired_token=self.refresh_token, ) self._update_user_request(request, token) yield request diff --git a/httpx_auth/_oauth2/implicit.py b/httpx_auth/_oauth2/implicit.py index 608845c..a6136ac 100644 --- a/httpx_auth/_oauth2/implicit.py +++ b/httpx_auth/_oauth2/implicit.py @@ -60,7 +60,6 @@ def __init__(self, authorization_url: str, **kwargs): raise Exception("Authorization URL is mandatory.") BrowserAuth.__init__(self, kwargs) - OAuth2BaseAuth.__init__(self) self.header_name = kwargs.pop("header_name", None) or "Authorization" self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" @@ -82,7 +81,7 @@ def __init__(self, authorization_url: str, **kwargs): "id_token" if "id_token" == response_type else "access_token" ) - self.early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) + early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) authorization_url_without_nonce = _add_parameters( self.authorization_url, kwargs @@ -90,10 +89,10 @@ def __init__(self, authorization_url: str, **kwargs): authorization_url_without_nonce, nonce = _pop_parameter( authorization_url_without_nonce, "nonce" ) - self.state = sha512( + state = sha512( authorization_url_without_nonce.encode("unicode_escape") ).hexdigest() - custom_parameters = {"state": self.state, "redirect_uri": self.redirect_uri} + custom_parameters = {"state": state, "redirect_uri": self.redirect_uri} if nonce: custom_parameters["nonce"] = nonce grant_url = _add_parameters(authorization_url_without_nonce, custom_parameters) @@ -104,6 +103,8 @@ def __init__(self, authorization_url: str, **kwargs): self.redirect_uri_port, ) + OAuth2BaseAuth.__init__(self, state, early_expiry) + def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) diff --git a/httpx_auth/_oauth2/resource_owner_password.py b/httpx_auth/_oauth2/resource_owner_password.py index d448912..581da05 100644 --- a/httpx_auth/_oauth2/resource_owner_password.py +++ b/httpx_auth/_oauth2/resource_owner_password.py @@ -41,7 +41,6 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): Use it to provide a custom proxying rule for instance. :param kwargs: all additional authorization parameters that should be put as body parameters in the token URL. """ - OAuth2BaseAuth.__init__(self) self.token_url = token_url if not self.token_url: @@ -59,7 +58,7 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): raise Exception("header_value parameter must contains {token}.") self.token_field_name = kwargs.pop("token_field_name", None) or "access_token" - self.early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) + early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) # Time is expressed in seconds self.timeout = int(kwargs.pop("timeout", None) or 60) @@ -84,7 +83,9 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): self.refresh_data.update(kwargs) all_parameters_in_url = _add_parameters(self.token_url, self.data) - self.state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest() + state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest() + + OAuth2BaseAuth.__init__(self, state, early_expiry, self.refresh_token) def _update_user_request(self, request: httpx.Request, token: str) -> None: request.headers[self.header_name] = self.header_value.format(token=token) From 4c35c97dfc7f421d0aa2b5d2ff477c8c5aa35a68 Mon Sep 17 00:00:00 2001 From: Raphael Krupinski <10319569-mattesilver@users.noreply.gitlab.com> Date: Mon, 26 Feb 2024 12:04:28 +0100 Subject: [PATCH 5/9] Pull up validating header_value and setting the requests auth header to the base class --- httpx_auth/_oauth2/authorization_code.py | 18 ++++++++++-------- httpx_auth/_oauth2/authorization_code_pkce.py | 13 +++++-------- httpx_auth/_oauth2/client_credentials.py | 16 ++++++++-------- httpx_auth/_oauth2/common.py | 15 ++++++++++++--- httpx_auth/_oauth2/implicit.py | 17 +++++++++-------- httpx_auth/_oauth2/resource_owner_password.py | 18 ++++++++++-------- 6 files changed, 54 insertions(+), 43 deletions(-) diff --git a/httpx_auth/_oauth2/authorization_code.py b/httpx_auth/_oauth2/authorization_code.py index 8c7f8ac..334a44c 100644 --- a/httpx_auth/_oauth2/authorization_code.py +++ b/httpx_auth/_oauth2/authorization_code.py @@ -71,10 +71,8 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): BrowserAuth.__init__(self, kwargs) - self.header_name = kwargs.pop("header_name", None) or "Authorization" - self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" - if "{token}" not in self.header_value: - raise Exception("header_value parameter must contains {token}.") + header_name = kwargs.pop("header_name", None) or "Authorization" + header_value = kwargs.pop("header_value", None) or "Bearer {token}" self.token_field_name = kwargs.pop("token_field_name", None) or "access_token" early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) @@ -129,10 +127,14 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): self.refresh_data = {"grant_type": "refresh_token"} self.refresh_data.update(kwargs) - OAuth2BaseAuth.__init__(self, state, early_expiry, self.refresh_token) - - def _update_user_request(self, request: httpx.Request, token: str) -> None: - request.headers[self.header_name] = self.header_value.format(token=token) + OAuth2BaseAuth.__init__( + self, + state, + early_expiry, + header_name, + header_value, + self.refresh_token, + ) def request_new_token(self) -> tuple: # Request code diff --git a/httpx_auth/_oauth2/authorization_code_pkce.py b/httpx_auth/_oauth2/authorization_code_pkce.py index e6108c4..314f736 100644 --- a/httpx_auth/_oauth2/authorization_code_pkce.py +++ b/httpx_auth/_oauth2/authorization_code_pkce.py @@ -71,10 +71,8 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): self.client = kwargs.pop("client", None) - self.header_name = kwargs.pop("header_name", None) or "Authorization" - self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" - if "{token}" not in self.header_value: - raise Exception("header_value parameter must contains {token}.") + header_name = kwargs.pop("header_name", None) or "Authorization" + header_value = kwargs.pop("header_value", None) or "Bearer {token}" self.token_field_name = kwargs.pop("token_field_name", None) or "access_token" early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) @@ -138,10 +136,9 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): self.refresh_data = {"grant_type": "refresh_token"} self.refresh_data.update(kwargs) - OAuth2BaseAuth.__init__(self, state, early_expiry, self.refresh_token) - - def _update_user_request(self, request: httpx.Request, token: str) -> None: - request.headers[self.header_name] = self.header_value.format(token=token) + OAuth2BaseAuth.__init__( + self, state, early_expiry, header_name, header_value, self.refresh_token + ) def request_new_token(self) -> tuple: # Request code diff --git a/httpx_auth/_oauth2/client_credentials.py b/httpx_auth/_oauth2/client_credentials.py index 319568c..487b5fd 100644 --- a/httpx_auth/_oauth2/client_credentials.py +++ b/httpx_auth/_oauth2/client_credentials.py @@ -49,10 +49,8 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs) if not self.client_secret: raise Exception("client_secret is mandatory.") - self.header_name = kwargs.pop("header_name", None) or "Authorization" - self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" - if "{token}" not in self.header_value: - raise Exception("header_value parameter must contains {token}.") + header_name = kwargs.pop("header_name", None) or "Authorization" + header_value = kwargs.pop("header_value", None) or "Bearer {token}" self.token_field_name = kwargs.pop("token_field_name", None) or "access_token" early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) @@ -72,10 +70,12 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs) all_parameters_in_url = _add_parameters(self.token_url, self.data) state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest() - super().__init__(state, early_expiry) - - def _update_user_request(self, request: httpx.Request, token: str) -> None: - request.headers[self.header_name] = self.header_value.format(token=token) + super().__init__( + state, + early_expiry, + header_name, + header_value, + ) def request_new_token(self) -> tuple: client = self.client or httpx.Client() diff --git a/httpx_auth/_oauth2/common.py b/httpx_auth/_oauth2/common.py index d73378a..7670c1f 100644 --- a/httpx_auth/_oauth2/common.py +++ b/httpx_auth/_oauth2/common.py @@ -91,10 +91,20 @@ class OAuth2: class OAuth2BaseAuth(abc.ABC, httpx.Auth): def __init__( - self, state: str, early_expiry: float, refresh_token: Optional[Callable] = None + self, + state: str, + early_expiry: float, + header_name: str, + header_value: str, + refresh_token: Optional[Callable] = None, ) -> None: + if "{token}" not in header_value: + raise Exception("header_value parameter must contains {token}.") + self.state = state self.early_expiry = early_expiry + self.header_name = header_name + self.header_value = header_value self.refresh_token = refresh_token def auth_flow( @@ -113,6 +123,5 @@ def auth_flow( def request_new_token(self) -> Union[tuple[str, str], tuple[str, str, int]]: pass # pragma: no cover - @abc.abstractmethod def _update_user_request(self, request: httpx.Request, token: str) -> None: - pass # pragma: no cover + request.headers[self.header_name] = self.header_value.format(token=token) diff --git a/httpx_auth/_oauth2/implicit.py b/httpx_auth/_oauth2/implicit.py index a6136ac..3ebc986 100644 --- a/httpx_auth/_oauth2/implicit.py +++ b/httpx_auth/_oauth2/implicit.py @@ -61,10 +61,8 @@ def __init__(self, authorization_url: str, **kwargs): BrowserAuth.__init__(self, kwargs) - self.header_name = kwargs.pop("header_name", None) or "Authorization" - self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" - if "{token}" not in self.header_value: - raise Exception("header_value parameter must contains {token}.") + header_name = kwargs.pop("header_name", None) or "Authorization" + header_value = kwargs.pop("header_value", None) or "Bearer {token}" response_type = _get_query_parameter(self.authorization_url, "response_type") if response_type: @@ -103,10 +101,13 @@ def __init__(self, authorization_url: str, **kwargs): self.redirect_uri_port, ) - OAuth2BaseAuth.__init__(self, state, early_expiry) - - def _update_user_request(self, request: httpx.Request, token: str) -> None: - request.headers[self.header_name] = self.header_value.format(token=token) + OAuth2BaseAuth.__init__( + self, + state, + early_expiry, + header_name, + header_value, + ) def request_new_token(self) -> tuple[str, str]: return authentication_responses_server.request_new_grant(self.grant_details) diff --git a/httpx_auth/_oauth2/resource_owner_password.py b/httpx_auth/_oauth2/resource_owner_password.py index 581da05..2be25bf 100644 --- a/httpx_auth/_oauth2/resource_owner_password.py +++ b/httpx_auth/_oauth2/resource_owner_password.py @@ -52,10 +52,8 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): if not self.password: raise Exception("Password is mandatory.") - self.header_name = kwargs.pop("header_name", None) or "Authorization" - self.header_value = kwargs.pop("header_value", None) or "Bearer {token}" - if "{token}" not in self.header_value: - raise Exception("header_value parameter must contains {token}.") + header_name = kwargs.pop("header_name", None) or "Authorization" + header_value = kwargs.pop("header_value", None) or "Bearer {token}" self.token_field_name = kwargs.pop("token_field_name", None) or "access_token" early_expiry = float(kwargs.pop("early_expiry", None) or 30.0) @@ -85,10 +83,14 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): all_parameters_in_url = _add_parameters(self.token_url, self.data) state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest() - OAuth2BaseAuth.__init__(self, state, early_expiry, self.refresh_token) - - def _update_user_request(self, request: httpx.Request, token: str) -> None: - request.headers[self.header_name] = self.header_value.format(token=token) + OAuth2BaseAuth.__init__( + self, + state, + early_expiry, + header_name, + header_value, + self.refresh_token, + ) def request_new_token(self) -> tuple: client = self.client or httpx.Client() From da386b32f61133ce4b9b06db964bca632c004bd0 Mon Sep 17 00:00:00 2001 From: Raphael Krupinski <10319569-mattesilver@users.noreply.gitlab.com> Date: Tue, 27 Feb 2024 09:40:33 +0100 Subject: [PATCH 6/9] Breaking: remove on_missing_token_kwargs from TokenMemoryCache.get_token --- httpx_auth/_oauth2/tokens.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/httpx_auth/_oauth2/tokens.py b/httpx_auth/_oauth2/tokens.py index 41c7d44..0e8085a 100644 --- a/httpx_auth/_oauth2/tokens.py +++ b/httpx_auth/_oauth2/tokens.py @@ -114,7 +114,6 @@ def get_token( early_expiry: float = 30.0, on_missing_token=None, on_expired_token=None, - **on_missing_token_kwargs, ) -> str: """ Return the bearer token. @@ -126,7 +125,6 @@ def get_token( expired 30 seconds before real expiry by default. :param on_missing_token: function to call when token is expired or missing (returning token and expiry tuple) :param on_expired_token: function to call to refresh the token when it is expired - :param on_missing_token_kwargs: arguments of the on_missing_token function (key-value arguments) :return: the token :raise AuthenticationFailed: in case token cannot be retrieved. """ @@ -171,7 +169,7 @@ def get_token( logger.debug("Token cannot be found in cache.") if on_missing_token is not None: with self._forbid_concurrent_missing_token_function_call: - new_token = on_missing_token(**on_missing_token_kwargs) + new_token = on_missing_token() if len(new_token) == 2: # Bearer token state, token = new_token self._add_bearer_token(state, token) From 164a47fc915b4869d318a718c94aeeffab88ab54 Mon Sep 17 00:00:00 2001 From: Colin Bounouar Date: Tue, 27 Feb 2024 10:52:13 +0100 Subject: [PATCH 7/9] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5fd574..2427d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- `httpx_auth.JsonTokenFileCache` and `httpx_auth.TokenMemoryCache` `get_token` method does not handles kwargs anymore, the `on_missing_token` callable does not expect any arguments anymore. ## [0.21.0] - 2024-02-19 ### Added From 19db1eb281004db207fa07dbb8594b6b95190377 Mon Sep 17 00:00:00 2001 From: Colin-b Date: Sat, 2 Mar 2024 13:01:01 +0100 Subject: [PATCH 8/9] Bump httpx to latest version --- CHANGELOG.md | 5 +++-- pyproject.toml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2427d8c..ddbb0cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -### Fixed -- `httpx_auth.JsonTokenFileCache` and `httpx_auth.TokenMemoryCache` `get_token` method does not handles kwargs anymore, the `on_missing_token` callable does not expect any arguments anymore. +### Changed +- Requires [`httpx`](https://www.python-httpx.org)==0.27.\* +- `httpx_auth.JsonTokenFileCache` and `httpx_auth.TokenMemoryCache` `get_token` method does not handle kwargs anymore, the `on_missing_token` callable does not expect any arguments anymore. ## [0.21.0] - 2024-02-19 ### Added diff --git a/pyproject.toml b/pyproject.toml index d38316a..8b35b7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers=[ "Topic :: Software Development :: Build Tools", ] dependencies = [ - "httpx==0.26.*", + "httpx==0.27.*", ] dynamic = ["version"] @@ -45,7 +45,7 @@ testing = [ # Used to generate test tokens "pyjwt==2.*", # Used to mock httpx - "pytest_httpx==0.29.*", + "pytest_httpx==0.30.*", # Used to mock date and time "time-machine==2.*", # Used to check coverage From b9d19d63c1eba2aaa098812a391cbaa60d98479a Mon Sep 17 00:00:00 2001 From: Colin-b Date: Sat, 2 Mar 2024 13:02:14 +0100 Subject: [PATCH 9/9] Release version 0.22.0 today --- CHANGELOG.md | 5 ++++- httpx_auth/version.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddbb0cb..67a100a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [0.22.0] - 2024-03-02 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.27.\* - `httpx_auth.JsonTokenFileCache` and `httpx_auth.TokenMemoryCache` `get_token` method does not handle kwargs anymore, the `on_missing_token` callable does not expect any arguments anymore. @@ -248,7 +250,8 @@ Note that a few changes were made: ### Added - Placeholder for port of requests_auth to httpx -[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.21.0...HEAD +[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.22.0...HEAD +[0.22.0]: https://github.com/Colin-b/httpx_auth/compare/v0.21.0...v0.22.0 [0.21.0]: https://github.com/Colin-b/httpx_auth/compare/v0.20.0...v0.21.0 [0.20.0]: https://github.com/Colin-b/httpx_auth/compare/v0.19.0...v0.20.0 [0.19.0]: https://github.com/Colin-b/httpx_auth/compare/v0.18.0...v0.19.0 diff --git a/httpx_auth/version.py b/httpx_auth/version.py index 3ee084a..c27ed71 100644 --- a/httpx_auth/version.py +++ b/httpx_auth/version.py @@ -3,4 +3,4 @@ # Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0) # Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0) # Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9) -__version__ = "0.21.0" +__version__ = "0.22.0"