Skip to content

Commit 3235c15

Browse files
Breaking: Use httpx generator-based auth-flow protocol in place of internal Clients.
1 parent aec4e38 commit 3235c15

28 files changed

Lines changed: 247 additions & 230 deletions

httpx_auth/_authentication.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import typing
12
from typing import Generator
23

34
import httpx
5+
from httpx import Request, Response
46

57

68
class _MultiAuth(httpx.Auth):
@@ -9,11 +11,32 @@ class _MultiAuth(httpx.Auth):
911
def __init__(self, *authentication_modes):
1012
self.authentication_modes = authentication_modes
1113

12-
def auth_flow(
13-
self, request: httpx.Request
14-
) -> Generator[httpx.Request, httpx.Response, None]:
14+
def sync_auth_flow(
15+
self, request: Request
16+
) -> typing.Generator[Request, Response, None]:
17+
for authentication_mode in self.authentication_modes:
18+
# auth_flow may yield one or more requests, the last of which is the user request with added auth headers
19+
flow = authentication_mode.sync_auth_flow(request)
20+
req = next(flow)
21+
while True:
22+
if req is request:
23+
break
24+
resp = yield req
25+
req = flow.send(resp)
26+
yield request
27+
28+
async def async_auth_flow(
29+
self, request: Request
30+
) -> typing.AsyncGenerator[Request, Response]:
1531
for authentication_mode in self.authentication_modes:
16-
next(authentication_mode.auth_flow(request))
32+
# auth_flow may yield one or more requests, the last of which is the user request with added auth headers
33+
flow = authentication_mode.async_auth_flow(request)
34+
req = await anext(flow)
35+
while True:
36+
if req is request:
37+
break
38+
resp = yield req
39+
req = await flow.asend(resp)
1740
yield request
1841

1942
def __add__(self, other) -> "_MultiAuth":

httpx_auth/_oauth2/authorization_code.py

Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections.abc import Generator
12
from hashlib import sha512
23
from typing import Iterable, Union
34

@@ -52,7 +53,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
5253
:param code_field_name: Field name containing the code. code by default.
5354
:param username: Username in case basic authentication should be used to retrieve token.
5455
:param password: User password in case basic authentication should be used to retrieve token.
55-
:param client: httpx.Client instance that will be used to request the token.
56+
:param headers: Additional headers to set when requesting or refreshing token.
5657
Use it to provide a custom proxying rule for instance.
5758
:param kwargs: all additional authorization parameters that should be put as query parameter
5859
in the authorization URL and as body parameters in the token URL.
@@ -80,7 +81,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
8081
username = kwargs.pop("username", None)
8182
password = kwargs.pop("password", None)
8283
self.auth = (username, password) if username and password else None
83-
self.client = kwargs.pop("client", None)
84+
self.token_headers = kwargs.pop("headers", {})
8485

8586
# As described in https://tools.ietf.org/html/rfc6749#section-4.1.2
8687
code_field_name = kwargs.pop("code_field_name", "code")
@@ -136,7 +137,11 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
136137
self.refresh_token,
137138
)
138139

139-
def request_new_token(self) -> tuple:
140+
def request_new_token(
141+
self,
142+
) -> Generator[
143+
httpx.Request, httpx.Response, Union[tuple[str, str], tuple[str, str, int]]
144+
]:
140145
# Request code
141146
state, code = authentication_responses_server.request_new_grant(
142147
self.code_grant_details
@@ -145,46 +150,30 @@ def request_new_token(self) -> tuple:
145150
# As described in https://tools.ietf.org/html/rfc6749#section-4.1.3
146151
self.token_data["code"] = code
147152

148-
client = self.client or httpx.Client()
149-
self._configure_client(client)
150-
try:
151-
# As described in https://tools.ietf.org/html/rfc6749#section-4.1.4
152-
token, expires_in, refresh_token = request_new_grant_with_post(
153-
self.token_url, self.token_data, self.token_field_name, client
154-
)
155-
finally:
156-
# Close client only if it was created by this module
157-
if self.client is None:
158-
client.close()
153+
# As described in https://tools.ietf.org/html/rfc6749#section-4.1.4
154+
token, expires_in, refresh_token = yield from request_new_grant_with_post(
155+
self.token_url, self.token_data, self.token_field_name, self.token_headers
156+
)
159157
# Handle both Access and Bearer tokens
160158
return (
161159
(self.state, token, expires_in, refresh_token)
162160
if expires_in
163161
else (self.state, token)
164162
)
165163

166-
def refresh_token(self, refresh_token: str) -> tuple:
167-
client = self.client or httpx.Client()
168-
self._configure_client(client)
169-
try:
170-
# As described in https://tools.ietf.org/html/rfc6749#section-6
171-
self.refresh_data["refresh_token"] = refresh_token
172-
token, expires_in, refresh_token = request_new_grant_with_post(
173-
self.token_url,
174-
self.refresh_data,
175-
self.token_field_name,
176-
client,
177-
)
178-
finally:
179-
# Close client only if it was created by this module
180-
if self.client is None:
181-
client.close()
164+
def refresh_token(
165+
self, refresh_token: str
166+
) -> Generator[httpx.Request, httpx.Response, tuple[str, str, int, str]]:
167+
# As described in https://tools.ietf.org/html/rfc6749#section-6
168+
self.refresh_data["refresh_token"] = refresh_token
169+
token, expires_in, refresh_token = yield from request_new_grant_with_post(
170+
self.token_url,
171+
self.refresh_data,
172+
self.token_field_name,
173+
self.token_headers,
174+
)
182175
return self.state, token, expires_in, refresh_token
183176

184-
def _configure_client(self, client: httpx.Client):
185-
client.auth = self.auth
186-
client.timeout = self.timeout
187-
188177

189178
class OktaAuthorizationCode(OAuth2AuthorizationCode):
190179
"""
@@ -220,8 +209,7 @@ def __init__(self, instance: str, client_id: str, **kwargs):
220209
:param header_value: Format used to send the token value.
221210
"{token}" must be present as it will be replaced by the actual token.
222211
Token will be sent as "Bearer {token}" by default.
223-
:param client: httpx.Client instance that will be used to request the token.
224-
Use it to provide a custom proxying rule for instance.
212+
:param headers: Additional headers to set when requesting or refreshing token.
225213
:param kwargs: all additional authorization parameters that should be put as query parameter
226214
in the authorization URL.
227215
Usual parameters are:
@@ -276,8 +264,7 @@ def __init__(
276264
:param header_value: Format used to send the token value.
277265
"{token}" must be present as it will be replaced by the actual token.
278266
Token will be sent as "Bearer {token}" by default.
279-
:param client: httpx.Client instance that will be used to request the token.
280-
Use it to provide a custom proxying rule for instance.
267+
:param headers: Additional headers to set when requesting or refreshing token.
281268
:param kwargs: all additional authorization parameters that should be put as query parameter
282269
in the authorization URL.
283270
"""

httpx_auth/_oauth2/authorization_code_pkce.py

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import base64
22
import os
3+
from collections.abc import Generator
34
from hashlib import sha256, sha512
5+
from typing import Union
46

57
import httpx
68

@@ -50,7 +52,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
5052
Default to 30 seconds to ensure token will not expire between the time of retrieval and the time the request
5153
reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry.
5254
:param code_field_name: Field name containing the code. code by default.
53-
:param client: httpx.Client instance that will be used to request the token.
55+
:param headers: Additional headers to set when requesting or refreshing token.
5456
Use it to provide a custom proxying rule for instance.
5557
:param kwargs: all additional authorization parameters that should be put as query parameter
5658
in the authorization URL and as body parameters in the token URL.
@@ -69,7 +71,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
6971

7072
BrowserAuth.__init__(self, kwargs)
7173

72-
self.client = kwargs.pop("client", None)
74+
self.token_headers = kwargs.pop("headers", {})
7375

7476
header_name = kwargs.pop("header_name", None) or "Authorization"
7577
header_value = kwargs.pop("header_value", None) or "Bearer {token}"
@@ -140,7 +142,11 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
140142
self, state, early_expiry, header_name, header_value, self.refresh_token
141143
)
142144

143-
def request_new_token(self) -> tuple:
145+
def request_new_token(
146+
self,
147+
) -> Generator[
148+
httpx.Request, httpx.Response, Union[tuple[str, str, int, str], tuple[str, str]]
149+
]:
144150
# Request code
145151
state, code = authentication_responses_server.request_new_grant(
146152
self.code_grant_details
@@ -149,45 +155,30 @@ def request_new_token(self) -> tuple:
149155
# As described in https://tools.ietf.org/html/rfc6749#section-4.1.3
150156
self.token_data["code"] = code
151157

152-
client = self.client or httpx.Client()
153-
self._configure_client(client)
154-
try:
155-
# As described in https://tools.ietf.org/html/rfc6749#section-4.1.4
156-
token, expires_in, refresh_token = request_new_grant_with_post(
157-
self.token_url, self.token_data, self.token_field_name, client
158-
)
159-
finally:
160-
# Close client only if it was created by this module
161-
if self.client is None:
162-
client.close()
158+
# As described in https://tools.ietf.org/html/rfc6749#section-4.1.4
159+
token, expires_in, refresh_token = yield from request_new_grant_with_post(
160+
self.token_url, self.token_data, self.token_field_name, self.token_headers
161+
)
163162
# Handle both Access and Bearer tokens
164163
return (
165164
(self.state, token, expires_in, refresh_token)
166165
if expires_in
167166
else (self.state, token)
168167
)
169168

170-
def refresh_token(self, refresh_token: str) -> tuple:
171-
client = self.client or httpx.Client()
172-
self._configure_client(client)
173-
try:
174-
# As described in https://tools.ietf.org/html/rfc6749#section-6
175-
self.refresh_data["refresh_token"] = refresh_token
176-
token, expires_in, refresh_token = request_new_grant_with_post(
177-
self.token_url,
178-
self.refresh_data,
179-
self.token_field_name,
180-
client,
181-
)
182-
finally:
183-
# Close client only if it was created by this module
184-
if self.client is None:
185-
client.close()
169+
def refresh_token(
170+
self, refresh_token: str
171+
) -> Generator[httpx.Request, httpx.Response, tuple[str, str, int, str]]:
172+
# As described in https://tools.ietf.org/html/rfc6749#section-6
173+
self.refresh_data["refresh_token"] = refresh_token
174+
token, expires_in, refresh_token = yield from request_new_grant_with_post(
175+
self.token_url,
176+
self.refresh_data,
177+
self.token_field_name,
178+
self.token_headers,
179+
)
186180
return self.state, token, expires_in, refresh_token
187181

188-
def _configure_client(self, client: httpx.Client):
189-
client.timeout = self.timeout
190-
191182
@staticmethod
192183
def generate_code_verifier() -> bytes:
193184
"""
@@ -256,8 +247,7 @@ def __init__(self, instance: str, client_id: str, **kwargs):
256247
:param header_value: Format used to send the token value.
257248
"{token}" must be present as it will be replaced by the actual token.
258249
Token will be sent as "Bearer {token}" by default.
259-
:param client: httpx.Client instance that will be used to request the token.
260-
Use it to provide a custom proxying rule for instance.
250+
:param headers: Additional headers to set when requesting or refreshing token.
261251
:param kwargs: all additional authorization parameters that should be put as query parameter
262252
in the authorization URL and as body parameters in the token URL.
263253
Usual parameters are:

httpx_auth/_oauth2/client_credentials.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs)
3535
:param early_expiry: Number of seconds before actual token expiry where token will be considered as expired.
3636
Default to 30 seconds to ensure token will not expire between the time of retrieval and the time the request
3737
reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry.
38-
:param client: httpx.Client instance that will be used to request the token.
39-
Use it to provide a custom proxying rule for instance.
38+
:param headers: Additional headers to set when requesting or refreshing token.
4039
:param kwargs: all additional authorization parameters that should be put as query parameter in the token URL.
4140
"""
4241
self.token_url = token_url
@@ -58,7 +57,7 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs)
5857
# Time is expressed in seconds
5958
self.timeout = int(kwargs.pop("timeout", None) or 60)
6059

61-
self.client = kwargs.pop("client", None)
60+
self.token_headers = kwargs.pop("headers", {})
6261

6362
# As described in https://tools.ietf.org/html/rfc6749#section-4.4.2
6463
self.data = {"grant_type": "client_credentials"}
@@ -78,24 +77,13 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs)
7877
)
7978

8079
def request_new_token(self) -> tuple:
81-
client = self.client or httpx.Client()
82-
self._configure_client(client)
83-
try:
84-
# As described in https://tools.ietf.org/html/rfc6749#section-4.4.3
85-
token, expires_in, _ = request_new_grant_with_post(
86-
self.token_url, self.data, self.token_field_name, client
87-
)
88-
finally:
89-
# Close client only if it was created by this module
90-
if self.client is None:
91-
client.close()
80+
# As described in https://tools.ietf.org/html/rfc6749#section-4.4.3
81+
token, expires_in, _ = yield from request_new_grant_with_post(
82+
self.token_url, self.data, self.token_field_name, self.token_headers
83+
)
9284
# Handle both Access and Bearer tokens
9385
return (self.state, token, expires_in) if expires_in else (self.state, token)
9486

95-
def _configure_client(self, client: httpx.Client):
96-
client.auth = (self.client_id, self.client_secret)
97-
client.timeout = self.timeout
98-
9987

10088
class OktaClientCredentials(OAuth2ClientCredentials):
10189
"""
@@ -131,7 +119,7 @@ def __init__(
131119
:param early_expiry: Number of seconds before actual token expiry where token will be considered as expired.
132120
Default to 30 seconds to ensure token will not expire between the time of retrieval and the time the request
133121
reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry.
134-
:param client: httpx.Client instance that will be used to request the token.
122+
:param headers: Additional headers to set when requesting or refreshing token.
135123
Use it to provide a custom proxying rule for instance.
136124
:param kwargs: all additional authorization parameters that should be put as query parameter in the token URL.
137125
"""

httpx_auth/_oauth2/common.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
from collections.abc import Mapping
23
from typing import Callable, Generator, Optional, Union
34
from urllib.parse import parse_qs, urlsplit, urlunsplit, urlencode
45

@@ -69,9 +70,9 @@ def _content_from_response(response: httpx.Response) -> dict:
6970

7071

7172
def request_new_grant_with_post(
72-
url: str, data, grant_name: str, client: httpx.Client
73-
) -> (str, int, str):
74-
response = client.post(url, data=data)
73+
url: str, data, grant_name: str, headers: Mapping[str, str]
74+
) -> Generator[httpx.Request, httpx.Response, tuple[str, int, str]]:
75+
response = yield httpx.Request("post", url, data=data, headers=headers)
7576

7677
if response.is_error:
7778
# As described in https://tools.ietf.org/html/rfc6749#section-5.2
@@ -106,11 +107,12 @@ def __init__(
106107
self.header_name = header_name
107108
self.header_value = header_value
108109
self.refresh_token = refresh_token
110+
self.requires_response_body = True
109111

110112
def auth_flow(
111113
self, request: httpx.Request
112114
) -> Generator[httpx.Request, httpx.Response, None]:
113-
token = OAuth2.token_cache.get_token(
115+
token = yield from OAuth2.token_cache.get_token(
114116
self.state,
115117
early_expiry=self.early_expiry,
116118
on_missing_token=self.request_new_token,
@@ -120,7 +122,11 @@ def auth_flow(
120122
yield request
121123

122124
@abc.abstractmethod
123-
def request_new_token(self) -> Union[tuple[str, str], tuple[str, str, int]]:
125+
def request_new_token(
126+
self,
127+
) -> Generator[
128+
httpx.Request, httpx.Response, Union[tuple[str, str], tuple[str, str, int]]
129+
]:
124130
pass # pragma: no cover
125131

126132
def _update_user_request(self, request: httpx.Request, token: str) -> None:

httpx_auth/_oauth2/implicit.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import uuid
2+
from collections.abc import Generator
23
from hashlib import sha512
34

45
import httpx
@@ -109,7 +110,12 @@ def __init__(self, authorization_url: str, **kwargs):
109110
header_value,
110111
)
111112

112-
def request_new_token(self) -> tuple[str, str]:
113+
def request_new_token(
114+
self,
115+
) -> Generator[httpx.Request, httpx.Response, tuple[str, str]]:
116+
# make this function an empty generator
117+
yield from ()
118+
113119
return authentication_responses_server.request_new_grant(self.grant_details)
114120

115121

0 commit comments

Comments
 (0)