Skip to content

Commit

Permalink
Add OktaResourceOwnerPasswordCredentials
Browse files Browse the repository at this point in the history
Introduce session_auth parameter to OAuth2ResourceOwnerPasswordCredentials
Do not use authentication by default in Oauth2ResourceOwnerPasswordCredentials
Bump version to 7.0.0
  • Loading branch information
Colin-b committed Apr 27, 2023
1 parent ecb79e1 commit 235e19c
Show file tree
Hide file tree
Showing 7 changed files with 991 additions and 6 deletions.
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
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]

## [7.0.0] - 2023-04-27
### Changed
- `requests_auth.OAuth2ResourceOwnerPasswordCredentials` does not send basic authentication by default.

### Added
- `session_auth` as a parameter of `requests_auth.OAuth2ResourceOwnerPasswordCredentials`. Allowing to provide any kind of optional authentication.
- `requests_auth.OktaResourceOwnerPasswordCredentials` providing Okta resource owner password credentials flow easy setup.
- Explicit support for Python 3.11

## [6.0.0] - 2022-01-11
### Changed
- `requests_auth.oauth2_tokens.TokenMemoryCache.get_token` method now requires arguments to be named.
Expand Down Expand Up @@ -167,7 +176,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Public release

[Unreleased]: https://github.com/Colin-b/requests_auth/compare/v6.0.0...HEAD
[Unreleased]: https://github.com/Colin-b/requests_auth/compare/v7.0.0...HEAD
[7.0.0]: https://github.com/Colin-b/requests_auth/compare/v6.0.0...v7.0.0
[6.0.0]: https://github.com/Colin-b/requests_auth/compare/v5.3.0...v6.0.0
[5.3.0]: https://github.com/Colin-b/requests_auth/compare/v5.2.0...v5.3.0
[5.2.0]: https://github.com/Colin-b/requests_auth/compare/v5.1.0...v5.2.0
Expand Down
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<a href="https://github.com/Colin-b/requests_auth/actions"><img alt="Build status" src="https://github.com/Colin-b/requests_auth/workflows/Release/badge.svg"></a>
<a href="https://github.com/Colin-b/requests_auth/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
<a href="https://github.com/Colin-b/requests_auth/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-275 passed-blue"></a>
<a href="https://github.com/Colin-b/requests_auth/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-305 passed-blue"></a>
<a href="https://pypi.org/project/requests-auth/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/requests_auth"></a>
</p>

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

### Resource Owner Password Credentials flow
### Resource Owner Password Credentials flow

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

Expand All @@ -256,6 +256,7 @@ requests.get('https://www.example.com', auth=OAuth2ResourceOwnerPasswordCredenti
| `token_url` | OAuth 2 token URL. | Mandatory | |
| `username` | Resource owner user name. | Mandatory | |
| `password` | Resource owner password. | Mandatory | |
| `session_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 requests authentication class instance. | Optional | |
| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 |
| `header_name` | Name of the header field used to send token. | Optional | Authorization |
| `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} |
Expand All @@ -266,6 +267,46 @@ requests.get('https://www.example.com', auth=OAuth2ResourceOwnerPasswordCredenti

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

#### Common providers

Most of [OAuth2](https://oauth.net/2/) Resource Owner Password Credentials providers are supported.

If the one you are looking for is not yet supported, feel free to [ask for its implementation](https://github.com/Colin-b/requests_auth/issues/new).

##### Okta (OAuth2 Resource Owner Password Credentials)

[Okta Resource Owner Password Credentials](https://developer.okta.com/docs/guides/implement-grant-type/ropassword/main/) providing access tokens is supported.

Use `requests_auth.OktaResourceOwnerPasswordCredentials` to configure this kind of authentication.

```python
import requests
from requests_auth import OktaResourceOwnerPasswordCredentials


okta = OktaResourceOwnerPasswordCredentials(instance='testserver.okta-emea.com', username='user name', password='user password', client_id='54239d18-c68c-4c47-8bdd-ce71ea1d50cd', client_secret="0c5MB")
requests.get('https://www.example.com', auth=okta)
```

###### Parameters

| Name | Description | Mandatory | Default value |
|:------------------------|:---------------------------|:----------|:--------------|
| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | |
| `username` | Resource owner user name. | Mandatory | |
| `password` | Resource owner password. | Mandatory | |
| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | |
| `client_secret` | Resource owner password. | Mandatory | |
| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 |
| `header_name` | Name of the header field used to send token. | Optional | Authorization |
| `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} |
| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | openid |
| `token_field_name` | Field name containing the token. | Optional | access_token |
| `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 |
| `session` | `requests.Session` instance that will be used to request the token. Use it to provide a custom proxying rule for instance. | Optional | |

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

### Client Credentials flow

Client Credentials Grant is implemented following [rfc6749](https://tools.ietf.org/html/rfc6749#section-4.4).
Expand Down
1 change: 1 addition & 0 deletions requests_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
OAuth2ClientCredentials,
OktaClientCredentials,
OAuth2ResourceOwnerPasswordCredentials,
OktaResourceOwnerPasswordCredentials,
)
from requests_auth.oauth2_tokens import JsonTokenFileCache
from requests_auth.errors import (
Expand Down
65 changes: 64 additions & 1 deletion requests_auth/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs):
:param token_url: OAuth 2 token URL.
:param username: Resource owner user name.
:param password: Resource owner password.
:param session_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 requests authentication class instance.
:param timeout: Maximum amount of seconds to wait for a token to be received once requested.
Wait for 1 minute by default.
:param header_name: Name of the header field used to send token.
Expand Down Expand Up @@ -177,7 +180,9 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs):
# Time is expressed in seconds
self.timeout = int(kwargs.pop("timeout", None) or 60)
self.session = kwargs.pop("session", None) or requests.Session()
self.session.auth = (self.username, self.password)
session_auth = kwargs.pop("session_auth", None)
if session_auth:
self.session.auth = session_auth

# As described in https://tools.ietf.org/html/rfc6749#section-4.3.2
self.data = {
Expand Down Expand Up @@ -1187,6 +1192,64 @@ def __init__(self, instance: str, client_id: str, client_secret: str, **kwargs):
)


class OktaResourceOwnerPasswordCredentials(OAuth2ResourceOwnerPasswordCredentials):
"""
Describes an Okta (OAuth 2) resource owner password credentials (also called password) flow requests authentication.
"""

def __init__(
self,
instance: str,
username: str,
password: str,
client_id: str,
client_secret: str,
**kwargs,
):
"""
:param instance: Okta instance (like "testserver.okta-emea.com")
:param username: Resource owner user name.
:param password: Resource owner password.
:param client_id: Okta Application Identifier (formatted as an Universal Unique Identifier)
:param client_secret: Resource owner password.
:param authorization_server: Okta authorization server
default by default.
:param timeout: Maximum amount of seconds to wait for a token to be received once requested.
Wait for 1 minute by default.
:param header_name: Name of the header field used to send token.
Token will be sent in Authorization header field by default.
:param header_value: Format used to send the token value.
"{token}" must be present as it will be replaced by the actual token.
Token will be sent as "Bearer {token}" by default.
:param scope: Scope parameter sent to token URL as body. Can also be a list of scopes.
Request 'openid' by default.
:param token_field_name: Field name containing the token. access_token by default.
:param early_expiry: Number of seconds before actual token expiry where token will be considered as expired.
Default to 30 seconds 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.
:param session: requests.Session instance that will be used to request the token.
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.
"""
if not instance:
raise Exception("Instance is mandatory.")
if not client_id:
raise Exception("Client ID is mandatory.")
if not client_secret:
raise Exception("Client secret is mandatory.")
authorization_server = kwargs.pop("authorization_server", None) or "default"
scopes = kwargs.pop("scope", "openid")
kwargs["scope"] = " ".join(scopes) if isinstance(scopes, list) else scopes
OAuth2ResourceOwnerPasswordCredentials.__init__(
self,
f"https://{instance}/oauth2/{authorization_server}/v1/token",
username=username,
password=password,
session_auth=(client_id, client_secret),
**kwargs,
)


class HeaderApiKey(requests.auth.AuthBase, SupportMultiAuth):
"""Describes an API Key requests authentication."""

Expand Down
2 changes: 1 addition & 1 deletion requests_auth/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = "6.0.0"
__version__ = "7.0.0"
61 changes: 61 additions & 0 deletions tests/test_oauth2_resource_owner_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,67 @@ def test_oauth2_password_credentials_flow_token_is_sent_in_authorization_header_
)


def test_oauth2_password_credentials_flow_does_not_authenticate_by_default(
token_cache, responses: RequestsMock
):
auth = requests_auth.OAuth2ResourceOwnerPasswordCredentials(
"http://provide_access_token", username="test_user", password="test_pwd"
)
responses.add(
responses.POST,
"http://provide_access_token",
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter": "example_value",
},
)
assert (
get_header(responses, auth).get("Authorization")
== "Bearer 2YotnFZFEjr1zCsicMWpAA"
)
token_request = get_request(responses, "http://provide_access_token/")
assert (
token_request.body == "grant_type=password&username=test_user&password=test_pwd"
)
assert "Authorization" not in token_request.headers


def test_oauth2_password_credentials_flow_authentication(
token_cache, responses: RequestsMock
):
auth = requests_auth.OAuth2ResourceOwnerPasswordCredentials(
"http://provide_access_token",
username="test_user",
password="test_pwd",
session_auth=("test_user2", "test_pwd2"),
)
responses.add(
responses.POST,
"http://provide_access_token",
json={
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter": "example_value",
},
)
assert (
get_header(responses, auth).get("Authorization")
== "Bearer 2YotnFZFEjr1zCsicMWpAA"
)
token_request = get_request(responses, "http://provide_access_token/")
assert (
token_request.body == "grant_type=password&username=test_user&password=test_pwd"
)
assert (
"Basic dGVzdF91c2VyMjp0ZXN0X3B3ZDI=" == token_request.headers["Authorization"]
)


def test_oauth2_password_credentials_flow_token_is_expired_after_30_seconds_by_default(
token_cache, responses: RequestsMock
):
Expand Down
Loading

0 comments on commit 235e19c

Please sign in to comment.