Skip to content

Commit 53e063c

Browse files
authored
Merge pull request #61 from Colin-b/develop
Release 0.17.0
2 parents e1fa635 + 60bb9a7 commit 53e063c

7 files changed

+821
-18
lines changed

CHANGELOG.md

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
88

9+
## [0.17.0] - 2023-04-26
10+
### Changed
11+
- `httpx_auth.OAuth2ResourceOwnerPasswordCredentials` does not send basic authentication by default.
12+
13+
### Added
14+
- `client_auth` as a parameter of `httpx_auth.OAuth2ResourceOwnerPasswordCredentials`. Allowing to provide any kind of optional authentication.
15+
- `httpx_auth.OktaResourceOwnerPasswordCredentials` providing Okta resource owner password credentials flow easy setup.
16+
917
## [0.16.0] - 2023-04-25
1018
### Changed
1119
- Requires [`httpx`](https://www.python-httpx.org)==0.24.\*
@@ -170,7 +178,8 @@ Note that a few changes were made:
170178
### Added
171179
- Placeholder for port of requests_auth to httpx
172180

173-
[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.16.0...HEAD
181+
[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.17.0...HEAD
182+
[0.17.0]: https://github.com/Colin-b/httpx_auth/compare/v0.16.0...v0.17.0
174183
[0.16.0]: https://github.com/Colin-b/httpx_auth/compare/v0.15.0...v0.16.0
175184
[0.15.0]: https://github.com/Colin-b/httpx_auth/compare/v0.14.1...v0.15.0
176185
[0.14.1]: https://github.com/Colin-b/httpx_auth/compare/v0.14.0...v0.14.1

README.md

+57-14
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<a href="https://github.com/Colin-b/httpx_auth/actions"><img alt="Build status" src="https://github.com/Colin-b/httpx_auth/workflows/Release/badge.svg"></a>
66
<a href="https://github.com/Colin-b/httpx_auth/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
77
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
8-
<a href="https://github.com/Colin-b/httpx_auth/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-307 passed-blue"></a>
8+
<a href="https://github.com/Colin-b/httpx_auth/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-335 passed-blue"></a>
99
<a href="https://pypi.org/project/httpx-auth/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/httpx_auth"></a>
1010
</p>
1111

@@ -282,7 +282,7 @@ Usual extra parameters are:
282282
| `client_secret` | If client is not authenticated with the authorization server |
283283
| `nonce` | Refer to [OpenID ID Token specifications][3] for more details |
284284

285-
### Resource Owner Password Credentials flow
285+
### Resource Owner Password Credentials flow
286286

287287
Resource Owner Password Credentials Grant is implemented following [rfc6749](https://tools.ietf.org/html/rfc6749#section-4.3).
288288

@@ -298,21 +298,64 @@ with httpx.Client() as client:
298298

299299
#### Parameters
300300

301-
| Name | Description | Mandatory | Default value |
302-
|:-------------------|:---------------------------------------------|:----------|:--------------|
303-
| `token_url` | OAuth 2 token URL. | Mandatory | |
304-
| `username` | Resource owner user name. | Mandatory | |
305-
| `password` | Resource owner password. | Mandatory | |
306-
| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 |
307-
| `header_name` | Name of the header field used to send token. | Optional | Authorization |
308-
| `header_value` | Format used to send the token value. "{token}" must be present as it will be replaced by the actual token. | Optional | Bearer {token} |
309-
| `scope` | Scope parameter sent to token URL as body. Can also be a list of scopes. | Optional | |
310-
| `token_field_name` | Field name containing the token. | Optional | access_token |
311-
| `early_expiry` | Number of seconds before actual token expiry where token will be considered as expired. Used to ensure token will not expire between the time of retrieval and the time the request reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry. | Optional | 30.0 |
312-
| `client` | `httpx.Client` instance that will be used to request the token. Use it to provide a custom proxying rule for instance. | Optional | |
301+
| Name | Description | Mandatory | Default value |
302+
|:---------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------|:--------------|
303+
| `token_url` | OAuth 2 token URL. | Mandatory | |
304+
| `username` | Resource owner user name. | Mandatory | |
305+
| `password` | Resource owner password. | Mandatory | |
306+
| `client_auth` | Client authentication if the client type is confidential or the client was issued client credentials (or assigned other authentication requirements). Can be a tuple or any httpx authentication class instance. | Optional | |
307+
| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 |
308+
| `header_name` | Name of the header field used to send token. | Optional | Authorization |
309+
| `header_value` | Format used to send the token value. "{token}" must be present as it will be replaced by the actual token. | Optional | Bearer {token} |
310+
| `scope` | Scope parameter sent to token URL as body. Can also be a list of scopes. | Optional | |
311+
| `token_field_name` | Field name containing the token. | Optional | access_token |
312+
| `early_expiry` | Number of seconds before actual token expiry where token will be considered as expired. Used to ensure token will not expire between the time of retrieval and the time the request reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry. | Optional | 30.0 |
313+
| `client` | `httpx.Client` instance that will be used to request the token. Use it to provide a custom proxying rule for instance. | Optional | |
313314

314315
Any other parameter will be put as body parameter in the token URL.
315316

317+
#### Common providers
318+
319+
Most of [OAuth2](https://oauth.net/2/) Resource Owner Password Credentials providers are supported.
320+
321+
If the one you are looking for is not yet supported, feel free to [ask for its implementation](https://github.com/Colin-b/httpx_auth/issues/new).
322+
323+
##### Okta (OAuth2 Resource Owner Password Credentials)
324+
325+
[Okta Resource Owner Password Credentials](https://developer.okta.com/docs/guides/implement-grant-type/ropassword/main/) providing access tokens is supported.
326+
327+
Use `httpx_auth.OktaResourceOwnerPasswordCredentials` to configure this kind of authentication.
328+
329+
```python
330+
import httpx
331+
from httpx_auth import OktaResourceOwnerPasswordCredentials
332+
333+
334+
okta = OktaResourceOwnerPasswordCredentials(instance='testserver.okta-emea.com', username='user name', password='user password', client_id='54239d18-c68c-4c47-8bdd-ce71ea1d50cd', client_secret="0c5MB")
335+
with httpx.Client() as client:
336+
client.get('https://www.example.com', auth=okta)
337+
```
338+
339+
###### Parameters
340+
341+
| Name | Description | Mandatory | Default value |
342+
|:------------------------|:---------------------------|:----------|:--------------|
343+
| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | |
344+
| `username` | Resource owner user name. | Mandatory | |
345+
| `password` | Resource owner password. | Mandatory | |
346+
| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | |
347+
| `client_secret` | Resource owner password. | Mandatory | |
348+
| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 |
349+
| `header_name` | Name of the header field used to send token. | Optional | Authorization |
350+
| `header_value` | Format used to send the token value. "{token}" must be present as it will be replaced by the actual token. | Optional | Bearer {token} |
351+
| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | openid |
352+
| `token_field_name` | Field name containing the token. | Optional | access_token |
353+
| `early_expiry` | Number of seconds before actual token expiry where token will be considered as expired. Used to ensure token will not expire between the time of retrieval and the time the request reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry. | Optional | 30.0 |
354+
| `client` | `httpx.Client` instance that will be used to request the token. Use it to provide a custom proxying rule for instance. | Optional | |
355+
356+
Any other parameter will be put as body parameters in the token URL.
357+
358+
316359
### Client Credentials flow
317360

318361
Client Credentials Grant is implemented following [rfc6749](https://tools.ietf.org/html/rfc6749#section-4.4).

httpx_auth/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
OAuth2ClientCredentials,
1616
OktaClientCredentials,
1717
OAuth2ResourceOwnerPasswordCredentials,
18+
OktaResourceOwnerPasswordCredentials,
1819
WakaTimeAuthorizationCode,
1920
)
2021
from httpx_auth.oauth2_tokens import JsonTokenFileCache
@@ -47,6 +48,7 @@
4748
"OAuth2ClientCredentials",
4849
"OktaClientCredentials",
4950
"OAuth2ResourceOwnerPasswordCredentials",
51+
"OktaResourceOwnerPasswordCredentials",
5052
"WakaTimeAuthorizationCode",
5153
"JsonTokenFileCache",
5254
"AWS4Auth",

httpx_auth/authentication.py

+64-1
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs):
149149
:param token_url: OAuth 2 token URL.
150150
:param username: Resource owner user name.
151151
:param password: Resource owner password.
152+
:param client_auth: Client authentication if the client type is confidential
153+
or the client was issued client credentials (or assigned other authentication requirements).
154+
Can be a tuple or any httpx authentication class instance.
152155
:param timeout: Maximum amount of seconds to wait for a token to be received once requested.
153156
Wait for 1 minute by default.
154157
:param header_name: Name of the header field used to send token.
@@ -186,6 +189,7 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs):
186189
# Time is expressed in seconds
187190
self.timeout = int(kwargs.pop("timeout", None) or 60)
188191
self.client = kwargs.pop("client", None)
192+
self.client_auth = kwargs.pop("client_auth", None)
189193

190194
# As described in https://tools.ietf.org/html/rfc6749#section-4.3.2
191195
self.data = {
@@ -228,7 +232,8 @@ def request_new_token(self) -> tuple:
228232
return (self.state, token, expires_in) if expires_in else (self.state, token)
229233

230234
def _configure_client(self, client: httpx.Client):
231-
client.auth = (self.username, self.password)
235+
if self.client_auth:
236+
client.auth = self.client_auth
232237
client.timeout = self.timeout
233238

234239

@@ -1223,6 +1228,64 @@ def __init__(self, instance: str, client_id: str, client_secret: str, **kwargs):
12231228
)
12241229

12251230

1231+
class OktaResourceOwnerPasswordCredentials(OAuth2ResourceOwnerPasswordCredentials):
1232+
"""
1233+
Describes an Okta (OAuth 2) resource owner password credentials (also called password) flow requests authentication.
1234+
"""
1235+
1236+
def __init__(
1237+
self,
1238+
instance: str,
1239+
username: str,
1240+
password: str,
1241+
client_id: str,
1242+
client_secret: str,
1243+
**kwargs,
1244+
):
1245+
"""
1246+
:param instance: Okta instance (like "testserver.okta-emea.com")
1247+
:param username: Resource owner user name.
1248+
:param password: Resource owner password.
1249+
:param client_id: Okta Application Identifier (formatted as an Universal Unique Identifier)
1250+
:param client_secret: Resource owner password.
1251+
:param authorization_server: Okta authorization server
1252+
default by default.
1253+
:param timeout: Maximum amount of seconds to wait for a token to be received once requested.
1254+
Wait for 1 minute by default.
1255+
:param header_name: Name of the header field used to send token.
1256+
Token will be sent in Authorization header field by default.
1257+
:param header_value: Format used to send the token value.
1258+
"{token}" must be present as it will be replaced by the actual token.
1259+
Token will be sent as "Bearer {token}" by default.
1260+
:param scope: Scope parameter sent to token URL as body. Can also be a list of scopes.
1261+
Request 'openid' by default.
1262+
:param token_field_name: Field name containing the token. access_token by default.
1263+
:param early_expiry: Number of seconds before actual token expiry where token will be considered as expired.
1264+
Default to 30 seconds to ensure token will not expire between the time of retrieval and the time the request
1265+
reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry.
1266+
:param client: httpx.Client instance that will be used to request the token.
1267+
Use it to provide a custom proxying rule for instance.
1268+
:param kwargs: all additional authorization parameters that should be put as body parameters in the token URL.
1269+
"""
1270+
if not instance:
1271+
raise Exception("Instance is mandatory.")
1272+
if not client_id:
1273+
raise Exception("Client ID is mandatory.")
1274+
if not client_secret:
1275+
raise Exception("Client secret is mandatory.")
1276+
authorization_server = kwargs.pop("authorization_server", None) or "default"
1277+
scopes = kwargs.pop("scope", "openid")
1278+
kwargs["scope"] = " ".join(scopes) if isinstance(scopes, list) else scopes
1279+
OAuth2ResourceOwnerPasswordCredentials.__init__(
1280+
self,
1281+
f"https://{instance}/oauth2/{authorization_server}/v1/token",
1282+
username=username,
1283+
password=password,
1284+
client_auth=(client_id, client_secret),
1285+
**kwargs,
1286+
)
1287+
1288+
12261289
class HeaderApiKey(httpx.Auth, SupportMultiAuth):
12271290
"""Describes an API Key requests authentication."""
12281291

httpx_auth/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
44
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
55
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
6-
__version__ = "0.16.0"
6+
__version__ = "0.17.0"

tests/test_oauth2_resource_owner_password.py

+56
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,62 @@ def test_oauth2_password_credentials_flow_token_is_sent_in_authorization_header_
9696
)
9797

9898

99+
def test_oauth2_password_credentials_flow_does_not_authenticate_by_default(
100+
token_cache, httpx_mock: HTTPXMock
101+
):
102+
auth = httpx_auth.OAuth2ResourceOwnerPasswordCredentials(
103+
"https://provide_access_token", username="test_user", password="test_pwd"
104+
)
105+
httpx_mock.add_response(
106+
method="POST",
107+
url="https://provide_access_token",
108+
json={
109+
"access_token": "2YotnFZFEjr1zCsicMWpAA",
110+
"token_type": "example",
111+
"expires_in": 3600,
112+
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
113+
"example_parameter": "example_value",
114+
},
115+
match_content=b"grant_type=password&username=test_user&password=test_pwd",
116+
)
117+
assert (
118+
get_header(httpx_mock, auth).get("Authorization")
119+
== "Bearer 2YotnFZFEjr1zCsicMWpAA"
120+
)
121+
assert (
122+
"Authorization"
123+
not in httpx_mock.get_request(url="https://provide_access_token").headers
124+
)
125+
126+
127+
def test_oauth2_password_credentials_flow_authentication(
128+
token_cache, httpx_mock: HTTPXMock
129+
):
130+
auth = httpx_auth.OAuth2ResourceOwnerPasswordCredentials(
131+
"https://provide_access_token",
132+
username="test_user",
133+
password="test_pwd",
134+
client_auth=("test_user2", "test_pwd2"),
135+
)
136+
httpx_mock.add_response(
137+
method="POST",
138+
url="https://provide_access_token",
139+
json={
140+
"access_token": "2YotnFZFEjr1zCsicMWpAA",
141+
"token_type": "example",
142+
"expires_in": 3600,
143+
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
144+
"example_parameter": "example_value",
145+
},
146+
match_headers={"Authorization": "Basic dGVzdF91c2VyMjp0ZXN0X3B3ZDI="},
147+
match_content=b"grant_type=password&username=test_user&password=test_pwd",
148+
)
149+
assert (
150+
get_header(httpx_mock, auth).get("Authorization")
151+
== "Bearer 2YotnFZFEjr1zCsicMWpAA"
152+
)
153+
154+
99155
def test_oauth2_password_credentials_flow_token_is_expired_after_30_seconds_by_default(
100156
token_cache, httpx_mock: HTTPXMock
101157
):

0 commit comments

Comments
 (0)