Skip to content

Commit

Permalink
Add early_expiry to OAuth2AuthorizationCodePKCE
Browse files Browse the repository at this point in the history
  • Loading branch information
Colin-b committed Jan 11, 2022
1 parent 84aed09 commit 004626a
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `requests_auth.OktaClientCredentials` contains a new `early_expiry` parameter allowing to tweak the number of seconds before actual token expiry where the token will be considered as already expired. Default to 30s.
- `requests_auth.OAuth2AuthorizationCode` contains a new `early_expiry` parameter allowing to tweak the number of seconds before actual token expiry where the token will be considered as already expired. Default to 30s.
- `requests_auth.OktaAuthorizationCode` contains a new `early_expiry` parameter allowing to tweak the number of seconds before actual token expiry where the token will be considered as already expired. Default to 30s.
- `requests_auth.OAuth2AuthorizationCodePKCE` contains a new `early_expiry` parameter allowing to tweak the number of seconds before actual token expiry where the token will be considered as already expired. Default to 30s.

### Removed
- `requests_auth.oauth2_tokens.is_expired` is not available anymore.
Expand Down
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,21 +157,22 @@ requests.get('https://www.example.com', auth=OAuth2AuthorizationCodePKCE('https:

#### Parameters

| Name | Description | Mandatory | Default value |
|:------------------------|:---------------------------|:----------|:--------------|
| `authorization_url` | OAuth 2 authorization URL. | Mandatory | |
| `token_url` | OAuth 2 token URL. | Mandatory | |
| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:<redirect_uri_port>/<redirect_uri_endpoint>. | Optional | '' |
| `redirect_uri_port` | The port on which the server listening for the OAuth 2 code will be started. | Optional | 5000 |
| `timeout` | Maximum amount of seconds to wait for a code or a token to be received once requested. | Optional | 60 |
| `success_display_time` | In case a code is successfully received, this is the maximum amount of milliseconds the success page will be displayed in your browser. | Optional | 1 |
| `failure_display_time` | In case received code is not valid, this is the maximum amount of milliseconds the failure page will be displayed in your browser. | Optional | 5000 |
| `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} |
| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | code |
| `token_field_name` | Field name containing the token. | Optional | access_token |
| `code_field_name` | Field name containing the code. | Optional | code |
| `session` | `requests.Session` instance that will be used to request the token. Use it to provide a custom proxying rule for instance. | Optional | |
| Name | Description | Mandatory | Default value |
|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:---------------|
| `authorization_url` | OAuth 2 authorization URL. | Mandatory | |
| `token_url` | OAuth 2 token URL. | Mandatory | |
| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:<redirect_uri_port>/<redirect_uri_endpoint>. | Optional | '' |
| `redirect_uri_port` | The port on which the server listening for the OAuth 2 code will be started. | Optional | 5000 |
| `timeout` | Maximum amount of seconds to wait for a code or a token to be received once requested. | Optional | 60 |
| `success_display_time` | In case a code is successfully received, this is the maximum amount of milliseconds the success page will be displayed in your browser. | Optional | 1 |
| `failure_display_time` | In case received code is not valid, this is the maximum amount of milliseconds the failure page will be displayed in your browser. | Optional | 5000 |
| `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} |
| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | code |
| `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 |
| `code_field_name` | Field name containing the code. | Optional | code |
| `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 query parameter in the authorization URL and as body parameters in the token URL.

Expand Down
5 changes: 5 additions & 0 deletions requests_auth/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,9 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
:param response_type: Value of the response_type query parameter if not already provided in authorization URL.
code 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 code_field_name: Field name containing the code. code by default.
:param session: requests.Session instance that will be used to request the token.
Use it to provide a custom proxying rule for instance.
Expand Down Expand Up @@ -559,6 +562,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)

# As described in https://tools.ietf.org/html/rfc6749#section-4.1.2
code_field_name = kwargs.pop("code_field_name", "code")
Expand Down Expand Up @@ -624,6 +628,7 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
def __call__(self, r):
token = OAuth2.token_cache.get_token(
key=self.state,
early_expiry=self.early_expiry,
on_missing_token=self.request_new_token,
on_expired_token=self.refresh_token,
)
Expand Down
63 changes: 63 additions & 0 deletions tests/test_oauth2_authorization_code_pkce.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,69 @@ def test_oauth2_pkce_flow_get_code_is_sent_in_authorization_header_by_default(
)


def test_oauth2_pkce_flow_token_is_expired_after_30_seconds_by_default(
token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
):
monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
auth = requests_auth.OAuth2AuthorizationCodePKCE(
"http://provide_code", "http://provide_access_token"
)
tab = browser_mock.add_response(
opened_url="http://provide_code?response_type=code&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&code_challenge=5C_ph_KZ3DstYUc965SiqmKAA-ShvKF4Ut7daKd3fjc&code_challenge_method=S256",
reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de",
)
# Add a token that expires in 29 seconds, so should be considered as expired when issuing the request
token_cache._add_token(
key="163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de",
token="2YotnFZFEjr1zCsicMWpAA",
expiry=requests_auth.oauth2_tokens._to_expiry(expires_in=29),
)
# Meaning a new one will be requested
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"
)
assert (
get_request(responses, "http://provide_access_token/").body
== "code_verifier=MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA"
)
tab.assert_success(
"You are now authenticated on 163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de. You may close this tab."
)


def test_oauth2_client_credentials_flow_token_custom_expiry(
token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
):
monkeypatch.setattr(requests_auth.authentication.os, "urandom", lambda x: b"1" * 63)
auth = requests_auth.OAuth2AuthorizationCodePKCE(
"http://provide_code",
"http://provide_access_token",
early_expiry=28,
)
# Add a token that expires in 29 seconds, so should be considered as not expired when issuing the request
token_cache._add_token(
key="163f0455b3e9cad3ca04254e5a0169553100d3aa0756c7964d897da316a695ffed5b4f46ef305094fd0a88cfe4b55ff257652015e4aa8f87b97513dba440f8de",
token="2YotnFZFEjr1zCsicMWpAA",
expiry=requests_auth.oauth2_tokens._to_expiry(expires_in=29),
)
assert (
get_header(responses, auth).get("Authorization")
== "Bearer 2YotnFZFEjr1zCsicMWpAA"
)


def test_expires_in_sent_as_str(
token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock
):
Expand Down

0 comments on commit 004626a

Please sign in to comment.