diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c9449c7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: Release + +on: + push: + branches: + - master + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install .[testing] + - name: Test with pytest + run: | + pytest --cov=requests_auth --cov-fail-under=100 --cov-report=term-missing + - name: Create packages + run: | + python -m pip install wheel + python setup.py sdist bdist_wheel + - name: Publish packages + run: | + python -m pip install twine + python -m twine upload dist/* --skip-existing --username __token__ --password ${{ secrets.pypi_password }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a776b51 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Test + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install .[testing] + - name: Test with pytest + run: | + pytest --cov=requests_auth --cov-fail-under=100 --cov-report=term-missing \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d1b54d3..db67e1b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 21.12b0 hooks: - id: black \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4da6c8d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: python -python: - - "3.6" - - "3.7" - - "3.8" - - "3.9-dev" -install: - - pip install .[testing] -script: - - pytest --cov=requests_auth --cov-fail-under=100 -deploy: - provider: pypi - username: __token__ - edge: true - distributions: "sdist bdist_wheel" - skip_existing: true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 527de60..96ccb8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [6.0.0] - 2022-01-11 +### Changed +- `requests_auth.oauth2_tokens.TokenMemoryCache.get_token` method now requires arguments to be named. +- `requests_auth.oauth2_tokens.TokenMemoryCache.get_token` method `on_missing_token` arguments must now be named (switched from args to kwargs). +- `requests_auth.JsonTokenFileCache.get_token` method now requires arguments to be named. +- `requests_auth.JsonTokenFileCache.get_token` method `on_missing_token` arguments must now be named (switched from args to kwargs). +- `requests_auth.testing` now requires `pyjwt==2.*`. + +### Added +- `requests_auth.oauth2_tokens.TokenMemoryCache.get_token` now allows to specify a custom `early_expiry` in seconds (default to 30). +- `requests_auth.JsonTokenFileCache.get_token` now allows to specify a custom `early_expiry` in seconds (default to 30). +- `requests_auth.OAuth2ResourceOwnerPasswordCredentials` 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.OAuth2ClientCredentials` 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.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. +- `requests_auth.OktaAuthorizationCodePKCE` 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.OAuth2Implicit` 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.AzureActiveDirectoryImplicit` 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.AzureActiveDirectoryImplicitIdToken` 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.OktaImplicit` 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.OktaImplicitIdToken` 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. +- `requests_auth.oauth2_tokens.decode_base64` is not available anymore. +- `requests_auth.oauth2_tokens.TokenMemoryCache.add_bearer_token` is not available anymore. +- `requests_auth.oauth2_tokens.TokenMemoryCache.add_access_token` is not available anymore. + +### Fixed +- OAuth2 token will now be considered as expired 30 seconds before actual expiry. To ensure it is still valid when received by the actual server. + ## [5.3.0] - 2021-06-06 ### Added - Support for refresh tokens in the Resource Owner Password Credentials flow and Authorization Code (with and without PKCE) flows (Thanks to [Stijn Caerts](https://github.com/StijnCaerts)). @@ -134,7 +167,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/v5.3.0...HEAD +[Unreleased]: https://github.com/Colin-b/requests_auth/compare/v6.0.0...HEAD +[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 [5.1.0]: https://github.com/Colin-b/requests_auth/compare/v5.0.2...v5.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8bcbc6a..49bfd27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,13 +48,9 @@ Before creating an issue please make sure that it was not already reported. * To add the [pre-commit](https://pre-commit.com) hook, after the installation run: **pre-commit install** 6) Add at least one [`pytest`](http://doc.pytest.org/en/latest/index.html) test case. * Unless it is an internal refactoring request or a documentation update. -7) Increment [version number](https://semver.org) and add related [changelog entry](https://keepachangelog.com/en/1.0.0/). +7) Add related [changelog entry](https://keepachangelog.com/en/1.0.0/) in the `Unreleased` section. * Unless it is a documentation update. -##### Changelog entry - -Once the changelog entry is added, please don't forget to also add the link to the proper tag at the end of the changelog. - #### Enter pull request 1) Go to the *Pull requests* tab and click on the *New pull request* button. diff --git a/LICENSE b/LICENSE index 17be259..ab867f9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Colin Bounouar +Copyright (c) 2022 Colin Bounouar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a1f13a8..be7cd23 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@

pypi version -Build status -Coverage +Build status +Coverage Code style: black -Number of tests +Number of tests Number of downloads

@@ -63,23 +63,24 @@ requests.get('https://www.example.com', auth=OAuth2AuthorizationCode('https://ww #### 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:/. | 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 | -| `username` | User name in case basic authentication should be used to retrieve token. | Optional | | -| `password` | User password in case basic authentication should be used to retrieve token. | Optional | | -| `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:/. | 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 | +| `username` | User name in case basic authentication should be used to retrieve token. | Optional | | +| `password` | User password in case basic authentication should be used to retrieve token. | Optional | | +| `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. @@ -114,23 +115,24 @@ requests.get('https://www.example.com', auth=okta) ###### Parameters -| Name | Description | Mandatory | Default value | -|:------------------------|:---------------------------|:----------|:--------------| -| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | -| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | -| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | token | -| `token_field_name` | Field name containing the token. | Optional | access_token | -| `nonce` | Refer to [OpenID ID Token specifications][3] for more details. | Optional | Newly generated Universal Unique Identifier. | -| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | openid | -| `authorization_server` | Okta authorization server. | Optional | 'default' | -| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | -| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | -| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | -| `success_display_time` | In case a token 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 token 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} | -| `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 | +|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:---------------------------------------------| +| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | +| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | +| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | token | +| `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 | +| `nonce` | Refer to [OpenID ID Token specifications][3] for more details. | Optional | Newly generated Universal Unique Identifier. | +| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | openid | +| `authorization_server` | Okta authorization server. | Optional | 'default' | +| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | +| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | +| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | +| `success_display_time` | In case a token 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 token 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} | +| `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. @@ -155,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:/. | 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:/. | 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. @@ -204,24 +207,25 @@ requests.get('https://www.example.com', auth=okta) ###### Parameters -| Name | Description | Mandatory | Default value | -|:------------------------|:---------------------------|:----------|:--------------| -| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | -| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | -| `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 | -| `nonce` | Refer to [OpenID ID Token specifications][3] for more details. | Optional | Newly generated Universal Unique Identifier. | -| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | openid | -| `authorization_server` | Okta authorization server. | Optional | 'default' | -| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | -| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | -| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | -| `success_display_time` | In case a token 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 token 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} | -| `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 | +|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:---------------------------------------------| +| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | +| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | +| `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 | +| `nonce` | Refer to [OpenID ID Token specifications][3] for more details. | Optional | Newly generated Universal Unique Identifier. | +| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | openid | +| `authorization_server` | Okta authorization server. | Optional | 'default' | +| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | +| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | +| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | +| `success_display_time` | In case a token 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 token 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} | +| `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. @@ -247,17 +251,18 @@ requests.get('https://www.example.com', auth=OAuth2ResourceOwnerPasswordCredenti #### Parameters -| Name | Description | Mandatory | Default value | -|:-------------------|:---------------------------------------------|:----------|:--------------| -| `token_url` | OAuth 2 token URL. | Mandatory | | -| `username` | Resource owner user name. | Mandatory | | -| `password` | 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 to token URL as body. Can also be a list of scopes. | Optional | | -| `token_field_name` | Field name containing the token. | Optional | access_token | -| `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 | +|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:---------------| +| `token_url` | OAuth 2 token URL. | Mandatory | | +| `username` | Resource owner user name. | Mandatory | | +| `password` | 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 to token URL as body. Can also be a list of scopes. | Optional | | +| `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 parameter in the token URL. @@ -276,17 +281,18 @@ requests.get('https://www.example.com', auth=OAuth2ClientCredentials('https://ww #### Parameters -| Name | Description | Mandatory | Default value | -|:-------------------|:---------------------------------------------|:----------|:--------------| -| `token_url` | OAuth 2 token URL. | Mandatory | | -| `client_id` | Resource owner user name. | 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 to token URL as body. Can also be a list of scopes. | Optional | | -| `token_field_name` | Field name containing the token. | Optional | access_token | -| `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 | +|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:---------------| +| `token_url` | OAuth 2 token URL. | Mandatory | | +| `client_id` | Resource owner user name. | 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 to token URL as body. Can also be a list of scopes. | Optional | | +| `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 parameter in the token URL. @@ -313,18 +319,19 @@ requests.get('https://www.example.com', auth=okta) ###### Parameters -| Name | Description | Mandatory | Default value | -|:------------------------|:---------------------------|:----------|:--------------| -| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | -| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | -| `client_secret` | Resource owner password. | Mandatory | | -| `authorization_server` | Okta authorization server. | Optional | 'default' | -| `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 | -| `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 | +|:-----------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:---------------| +| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | +| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | +| `client_secret` | Resource owner password. | Mandatory | | +| `authorization_server` | Okta authorization server. | Optional | 'default' | +| `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 query parameter in the token URL. @@ -343,18 +350,19 @@ requests.get('https://www.example.com', auth=OAuth2Implicit('https://www.authori #### Parameters -| Name | Description | Mandatory | Default value | -|:------------------------|:---------------------------|:----------|:--------------| -| `authorization_url` | OAuth 2 authorization URL. | Mandatory | | -| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | token | -| `token_field_name` | Field name containing the token. | Optional | id_token if response_type is id_token, otherwise access_token | -| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | -| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | -| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | -| `success_display_time` | In case a token 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 token 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} | +| Name | Description | Mandatory | Default value | +|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------|:--------------------------------------------------------------| +| `authorization_url` | OAuth 2 authorization URL. | Mandatory | | +| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | token | +| `token_field_name` | Field name containing the token. | Optional | id_token if response_type is id_token, otherwise 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 | +| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | +| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | +| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | +| `success_display_time` | In case a token 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 token 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} | Any other parameter will be put as query parameter in the authorization URL. @@ -391,20 +399,21 @@ You can retrieve Microsoft Azure Active Directory application information thanks ###### Parameters -| Name | Description | Mandatory | Default value | -|:------------------------|:---------------------------|:----------|:--------------| -| `tenant_id` | Microsoft Tenant Identifier (formatted as an Universal Unique Identifier). | Mandatory | | -| `client_id` | Microsoft Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | -| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | token | -| `token_field_name` | Field name containing the token. | Optional | access_token | -| `nonce` | Refer to [OpenID ID Token specifications][3] for more details | Optional | Newly generated Universal Unique Identifier. | -| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | -| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | -| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | -| `success_display_time` | In case a token 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 token 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} | +| Name | Description | Mandatory | Default value | +|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:---------------------------------------------| +| `tenant_id` | Microsoft Tenant Identifier (formatted as an Universal Unique Identifier). | Mandatory | | +| `client_id` | Microsoft Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | +| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | token | +| `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 | +| `nonce` | Refer to [OpenID ID Token specifications][3] for more details | Optional | Newly generated Universal Unique Identifier. | +| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | +| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | +| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | +| `success_display_time` | In case a token 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 token 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} | Any other parameter will be put as query parameter in the authorization URL. @@ -433,20 +442,21 @@ You can retrieve Microsoft Azure Active Directory application information thanks ###### Parameters -| Name | Description | Mandatory | Default value | -|:------------------------|:---------------------------|:----------|:--------------| -| `tenant_id` | Microsoft Tenant Identifier (formatted as an Universal Unique Identifier). | Mandatory | | -| `client_id` | Microsoft Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | -| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | id_token | -| `token_field_name` | Field name containing the token. | Optional | id_token | -| `nonce` | Refer to [OpenID ID Token specifications][3] for more details | Optional | Newly generated Universal Unique Identifier. | -| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | -| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | -| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | -| `success_display_time` | In case a token 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 token 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} | +| Name | Description | Mandatory | Default value | +|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------|:---------------------------------------------| +| `tenant_id` | Microsoft Tenant Identifier (formatted as an Universal Unique Identifier). | Mandatory | | +| `client_id` | Microsoft Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | +| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | id_token | +| `token_field_name` | Field name containing the token. | Optional | id_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 | +| `nonce` | Refer to [OpenID ID Token specifications][3] for more details | Optional | Newly generated Universal Unique Identifier. | +| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | +| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | +| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | +| `success_display_time` | In case a token 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 token 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} | Any other parameter will be put as query parameter in the authorization URL. @@ -473,22 +483,23 @@ requests.get('https://www.example.com', auth=okta) ###### Parameters -| Name | Description | Mandatory | Default value | -|:------------------------|:---------------------------|:----------|:--------------| -| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | -| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | -| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | token | -| `token_field_name` | Field name containing the token. | Optional | access_token | -| `nonce` | Refer to [OpenID ID Token specifications][3] for more details. | Optional | Newly generated Universal Unique Identifier. | -| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | ['openid', 'profile', 'email'] | -| `authorization_server` | Okta authorization server. | Optional | 'default' | -| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | -| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | -| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | -| `success_display_time` | In case a token 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 token 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} | +| Name | Description | Mandatory | Default value | +|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:---------------------------------------------| +| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | +| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | +| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | token | +| `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 | +| `nonce` | Refer to [OpenID ID Token specifications][3] for more details. | Optional | Newly generated Universal Unique Identifier. | +| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | ['openid', 'profile', 'email'] | +| `authorization_server` | Okta authorization server. | Optional | 'default' | +| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | +| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | +| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | +| `success_display_time` | In case a token 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 token 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} | Any other parameter will be put as query parameter in the authorization URL. @@ -515,22 +526,23 @@ requests.get('https://www.example.com', auth=okta) ###### Parameters -| Name | Description | Mandatory | Default value | -|:------------------------|:---------------------------|:----------|:--------------| -| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | -| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | -| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | id_token | -| `token_field_name` | Field name containing the token. | Optional | id_token | -| `nonce` | Refer to [OpenID ID Token specifications][3] for more details. | Optional | Newly generated Universal Unique Identifier. | -| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | ['openid', 'profile', 'email'] | -| `authorization_server` | Okta authorization server. | Optional | 'default' | -| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | -| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | -| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | -| `success_display_time` | In case a token 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 token 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} | +| Name | Description | Mandatory | Default value | +|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:---------------------------------------------| +| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | | +| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | | +| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | id_token | +| `token_field_name` | Field name containing the token. | Optional | id_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 | +| `nonce` | Refer to [OpenID ID Token specifications][3] for more details. | Optional | Newly generated Universal Unique Identifier. | +| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | ['openid', 'profile', 'email'] | +| `authorization_server` | Okta authorization server. | Optional | 'default' | +| `redirect_uri_endpoint` | Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. | Optional | '' | +| `redirect_uri_port` | The port on which the server listening for the OAuth 2 token will be started. | Optional | 5000 | +| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 | +| `success_display_time` | In case a token 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 token 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} | Any other parameter will be put as query parameter in the authorization URL. diff --git a/requests_auth/authentication.py b/requests_auth/authentication.py index 25f0af7..45fa999 100644 --- a/requests_auth/authentication.py +++ b/requests_auth/authentication.py @@ -74,7 +74,7 @@ def request_new_grant_with_post( token = content.get(grant_name) if not token: raise GrantNotProvided(grant_name, content) - return token, content.get("expires_in"), content.get('refresh_token') + return token, content.get("expires_in"), content.get("refresh_token") class OAuth2: @@ -149,6 +149,9 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): 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. Not sent 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. @@ -169,6 +172,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) # Time is expressed in seconds self.timeout = int(kwargs.pop("timeout", None) or 60) @@ -187,9 +191,7 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs): self.data.update(kwargs) # As described in https://tools.ietf.org/html/rfc6749#section-6 - self.refresh_data = { - "grant_type": "refresh_token" - } + self.refresh_data = {"grant_type": "refresh_token"} if scope: self.refresh_data["scope"] = self.data["scope"] self.refresh_data.update(kwargs) @@ -200,6 +202,7 @@ def __init__(self, token_url: str, username: str, password: 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, ) @@ -216,17 +219,21 @@ def request_new_token(self): self.session, ) # Handle both Access and Bearer tokens - return (self.state, token, expires_in, refresh_token) if expires_in else (self.state, token) + return ( + (self.state, token, expires_in, refresh_token) + if expires_in + else (self.state, token) + ) def refresh_token(self, refresh_token: str): # As described in https://tools.ietf.org/html/rfc6749#section-6 - self.refresh_data['refresh_token'] = refresh_token + self.refresh_data["refresh_token"] = refresh_token token, expires_in, refresh_token = request_new_grant_with_post( self.token_url, self.refresh_data, self.token_field_name, self.timeout, - self.session + self.session, ) return self.state, token, expires_in, refresh_token @@ -253,6 +260,9 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs) 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. Not sent 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 query parameter in the token URL. @@ -273,6 +283,7 @@ def __init__(self, token_url: str, client_id: str, client_secret: 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) # Time is expressed in seconds self.timeout = int(kwargs.pop("timeout", None) or 60) @@ -292,7 +303,11 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs) self.state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest() def __call__(self, r): - token = OAuth2.token_cache.get_token(self.state, self.request_new_token) + token = OAuth2.token_cache.get_token( + key=self.state, + early_expiry=self.early_expiry, + on_missing_token=self.request_new_token, + ) r.headers[self.header_name] = self.header_value.format(token=token) return r @@ -345,6 +360,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 username: User name in case basic authentication should be used to retrieve token. :param password: User password in case basic authentication should be used to retrieve token. @@ -373,6 +391,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) username = kwargs.pop("username", None) password = kwargs.pop("password", None) @@ -424,16 +443,15 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): self.token_data.update(kwargs) # As described in https://tools.ietf.org/html/rfc6749#section-6 - self.refresh_data = { - "grant_type": "refresh_token" - } + self.refresh_data = {"grant_type": "refresh_token"} self.refresh_data.update(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 + on_expired_token=self.refresh_token, ) r.headers[self.header_name] = self.header_value.format(token=token) return r @@ -455,17 +473,21 @@ def request_new_token(self): self.session, ) # Handle both Access and Bearer tokens - return (self.state, token, expires_in, refresh_token) if expires_in else (self.state, token) + return ( + (self.state, token, expires_in, refresh_token) + if expires_in + else (self.state, token) + ) def refresh_token(self, refresh_token: str): # As described in https://tools.ietf.org/html/rfc6749#section-6 - self.refresh_data['refresh_token'] = refresh_token + self.refresh_data["refresh_token"] = refresh_token token, expires_in, refresh_token = request_new_grant_with_post( self.token_url, self.refresh_data, self.token_field_name, self.timeout, - self.session + self.session, ) return self.state, token, expires_in, refresh_token @@ -508,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. @@ -537,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") @@ -596,16 +622,15 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs): self.token_data.update(kwargs) # As described in https://tools.ietf.org/html/rfc6749#section-6 - self.refresh_data = { - "grant_type": "refresh_token" - } + self.refresh_data = {"grant_type": "refresh_token"} self.refresh_data.update(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 + on_expired_token=self.refresh_token, ) r.headers[self.header_name] = self.header_value.format(token=token) return r @@ -627,21 +652,24 @@ def request_new_token(self) -> tuple: self.session, ) # Handle both Access and Bearer tokens - return (self.state, token, expires_in, refresh_token) if expires_in else (self.state, token) + return ( + (self.state, token, expires_in, refresh_token) + if expires_in + else (self.state, token) + ) def refresh_token(self, refresh_token: str): # As described in https://tools.ietf.org/html/rfc6749#section-6 - self.refresh_data['refresh_token'] = refresh_token + self.refresh_data["refresh_token"] = refresh_token token, expires_in, refresh_token = request_new_grant_with_post( self.token_url, self.refresh_data, self.token_field_name, self.timeout, - self.session + self.session, ) return self.state, token, expires_in, refresh_token - @staticmethod def generate_code_verifier() -> bytes: """ @@ -694,6 +722,9 @@ def __init__(self, authorization_url: str, **kwargs): token by default. :param token_field_name: Name of the expected field containing the token. id_token by default if response_type is id_token, else access_token. + :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 redirect_uri_endpoint: Custom endpoint that will be used as redirect_uri the following way: http://localhost:/. Default value is to redirect on / (root). :param redirect_uri_port: The port on which the server listening for the OAuth 2 token will be started. @@ -744,6 +775,8 @@ 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) + authorization_url_without_nonce = _add_parameters( self.authorization_url, kwargs ) @@ -768,9 +801,10 @@ def __init__(self, authorization_url: str, **kwargs): def __call__(self, r): token = OAuth2.token_cache.get_token( - self.state, - oauth2_authentication_responses_server.request_new_grant, - self.grant_details, + key=self.state, + early_expiry=self.early_expiry, + on_missing_token=oauth2_authentication_responses_server.request_new_grant, + grant_details=self.grant_details, ) r.headers[self.header_name] = self.header_value.format(token=token) return r @@ -790,6 +824,9 @@ def __init__(self, tenant_id: str, client_id: str, **kwargs): token by default. :param token_field_name: Name of the expected field 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 nonce: Refer to http://openid.net/specs/openid-connect-core-1_0.html#IDToken for more details (formatted as an Universal Unique Identifier - UUID). Use a newly generated UUID by default. :param redirect_uri_endpoint: Custom endpoint that will be used as redirect_uri the following way: @@ -837,6 +874,9 @@ def __init__(self, tenant_id: str, client_id: str, **kwargs): id_token by default. :param token_field_name: Name of the expected field containing the token. id_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 nonce: Refer to http://openid.net/specs/openid-connect-core-1_0.html#IDToken for more details (formatted as an Universal Unique Identifier - UUID). Use a newly generated UUID by default. :param redirect_uri_endpoint: Custom endpoint that will be used as redirect_uri the following way: @@ -887,6 +927,9 @@ def __init__(self, instance: str, client_id: str, **kwargs): token by default. :param token_field_name: Name of the expected field 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 nonce: Refer to http://openid.net/specs/openid-connect-core-1_0.html#IDToken for more details (formatted as an Universal Unique Identifier - UUID). Use a newly generated UUID by default. :param authorization_server: Okta authorization server. @@ -940,6 +983,9 @@ def __init__(self, instance: str, client_id: str, **kwargs): id_token by default. :param token_field_name: Name of the expected field containing the token. id_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 nonce: Refer to http://openid.net/specs/openid-connect-core-1_0.html#IDToken for more details (formatted as an Universal Unique Identifier - UUID). Use a newly generated UUID by default. :param authorization_server: Okta authorization server @@ -995,6 +1041,9 @@ def __init__(self, instance: str, client_id: str, **kwargs): token by default. :param token_field_name: Name of the expected field 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 nonce: Refer to http://openid.net/specs/openid-connect-core-1_0.html#IDToken for more details (formatted as an Universal Unique Identifier - UUID). Use a newly generated UUID by default. :param authorization_server: Okta authorization server @@ -1050,6 +1099,9 @@ def __init__(self, instance: str, client_id: str, **kwargs): code by default. :param token_field_name: Name of the expected field 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 nonce: Refer to http://openid.net/specs/openid-connect-core-1_0.html#IDToken for more details (formatted as an Universal Unique Identifier - UUID). Use a newly generated UUID by default. @@ -1116,6 +1168,9 @@ def __init__(self, instance: str, client_id: str, client_secret: str, **kwargs): :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 query parameter in the token URL. diff --git a/requests_auth/oauth2_tokens.py b/requests_auth/oauth2_tokens.py index b131dcf..6ac3ca4 100644 --- a/requests_auth/oauth2_tokens.py +++ b/requests_auth/oauth2_tokens.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) -def decode_base64(base64_encoded_string: str) -> str: +def _decode_base64(base64_encoded_string: str) -> str: """ Decode base64, padding being optional. @@ -22,8 +22,18 @@ def decode_base64(base64_encoded_string: str) -> str: return base64.b64decode(base64_encoded_string).decode("unicode_escape") -def is_expired(expiry: float) -> bool: - return datetime.datetime.utcfromtimestamp(expiry) < datetime.datetime.utcnow() +def _is_expired(expiry: float, early_expiry: float) -> bool: + return ( + datetime.datetime.utcfromtimestamp(expiry - early_expiry) + < datetime.datetime.utcnow() + ) + + +def _to_expiry(expires_in: Union[int, str]) -> float: + expiry = datetime.datetime.utcnow().replace( + tzinfo=datetime.timezone.utc + ) + datetime.timedelta(seconds=int(expires_in)) + return expiry.timestamp() class TokenMemoryCache: @@ -36,7 +46,7 @@ def __init__(self): self.forbid_concurrent_cache_access = threading.Lock() self.forbid_concurrent_missing_token_function_call = threading.Lock() - def add_bearer_token(self, key: str, token: str): + def _add_bearer_token(self, key: str, token: str): """ Set the bearer token and save it :param key: key identifier of the token @@ -48,14 +58,20 @@ def add_bearer_token(self, key: str, token: str): raise InvalidToken(token) header, body, other = token.split(".") - body = json.loads(decode_base64(body)) + body = json.loads(_decode_base64(body)) expiry = body.get("exp") if not expiry: raise TokenExpiryNotProvided(expiry) self._add_token(key, token, expiry) - def add_access_token(self, key: str, token: str, expires_in: int, refresh_token: str = None): + def _add_access_token( + self, + key: str, + token: str, + expires_in: Union[int, str], + refresh_token: str = None, + ): """ Set the bearer token and save it :param key: key identifier of the token @@ -64,12 +80,11 @@ def add_access_token(self, key: str, token: str, expires_in: int, refresh_token: :param refresh_token: refresh token value :raise InvalidToken: In case token is invalid. """ - expiry = datetime.datetime.utcnow().replace( - tzinfo=datetime.timezone.utc - ) + datetime.timedelta(seconds=int(expires_in)) - self._add_token(key, token, expiry.timestamp(), refresh_token) + self._add_token(key, token, _to_expiry(expires_in), refresh_token) - def _add_token(self, key: str, token: str, expiry: float, refresh_token: str = None): + def _add_token( + self, key: str, token: str, expiry: float, refresh_token: str = None + ): """ Set the bearer token and save it :param key: key identifier of the token @@ -84,13 +99,26 @@ def _add_token(self, key: str, token: str, expiry: float, refresh_token: str = N f'Inserting token expiring on {datetime.datetime.utcfromtimestamp(expiry)} (UTC) with "{key}" key: {token}' ) - def get_token(self, key: str, on_missing_token=None, *on_missing_token_args, on_expired_token=None) -> str: + def get_token( + self, + key: str, + *, + early_expiry: float = 30.0, + on_missing_token=None, + on_expired_token=None, + **on_missing_token_kwargs, + ) -> str: """ Return the bearer token. :param key: key identifier of the token + :param early_expiry: As the time between the token extraction from cache and the token reception on server side + might not higher than one second, on slow networks, token might be expired when received by the actual server, + even if still valid when fetched. + This is the number of seconds to subtract to the actual token expiry. Token will be considered as + 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_missing_token_args: arguments of the function :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. """ @@ -104,7 +132,7 @@ def get_token(self, key: str, on_missing_token=None, *on_missing_token_args, on_ bearer, expiry = token else: bearer, expiry, refresh_token = token - if is_expired(expiry): + if _is_expired(expiry, early_expiry): logger.debug(f'Authentication token with "{key}" key is expired.') del self.tokens[key] else: @@ -116,13 +144,17 @@ def get_token(self, key: str, on_missing_token=None, *on_missing_token_args, on_ if refresh_token is not None and on_expired_token is not None: try: with self.forbid_concurrent_missing_token_function_call: - state, token, expires_in, refresh_token = on_expired_token(refresh_token) - self.add_access_token(state, token, expires_in, refresh_token) + state, token, expires_in, refresh_token = on_expired_token( + refresh_token + ) + self._add_access_token(state, token, expires_in, refresh_token) logger.debug(f"Refreshed token with key {key}.") with self.forbid_concurrent_cache_access: if state in self.tokens: bearer, expiry, refresh_token = self.tokens[state] - logger.debug(f"Using newly refreshed token, expiring on {datetime.datetime.utcfromtimestamp(expiry)} (UTC).") + logger.debug( + f"Using newly refreshed token, expiring on {datetime.datetime.utcfromtimestamp(expiry)} (UTC)." + ) return bearer except (InvalidGrantRequest, GrantNotProvided): logger.debug(f"Failed to refresh token.") @@ -130,16 +162,16 @@ def get_token(self, key: str, on_missing_token=None, *on_missing_token_args, on_ 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_args) + new_token = on_missing_token(**on_missing_token_kwargs) if len(new_token) == 2: # Bearer token state, token = new_token - self.add_bearer_token(state, token) + self._add_bearer_token(state, token) elif len(new_token) == 3: # Access token state, token, expires_in = new_token - self.add_access_token(state, token, expires_in) + self._add_access_token(state, token, expires_in) else: # Access token and Refresh token state, token, expires_in, refresh_token = new_token - self.add_access_token(state, token, expires_in, refresh_token) + self._add_access_token(state, token, expires_in, refresh_token) if key != state: logger.warning( f"Using a token received on another key than expected. Expecting {key} but was {state}." diff --git a/requests_auth/testing.py b/requests_auth/testing.py index 9f2ef31..fb5e844 100644 --- a/requests_auth/testing.py +++ b/requests_auth/testing.py @@ -15,7 +15,7 @@ def create_token(expiry: Optional[datetime.datetime]) -> str: token = ( jwt.encode({"exp": expiry}, "secret") if expiry else jwt.encode({}, "secret") ) - return token.decode("unicode_escape") + return token @pytest.fixture diff --git a/requests_auth/version.py b/requests_auth/version.py index e374e26..16c0cc7 100644 --- a/requests_auth/version.py +++ b/requests_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__ = "5.3.0" +__version__ = "6.0.0" diff --git a/setup.py b/setup.py index cd38c22..555e7b9 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Build Tools", ], keywords=[ @@ -50,11 +51,11 @@ extras_require={ "testing": [ # Used to generate test tokens - "pyjwt==1.*", + "pyjwt==2.*", # Used to mock responses to requests - "pytest-responses==0.4.*", + "pytest-responses==0.5.*", # Used to check coverage - "pytest-cov==2.*", + "pytest-cov==3.*", ] }, python_requires=">=3.6", diff --git a/tests/test_json_token_file_cache.py b/tests/test_json_token_file_cache.py index bcb54bb..a5234d0 100644 --- a/tests/test_json_token_file_cache.py +++ b/tests/test_json_token_file_cache.py @@ -15,12 +15,12 @@ def token_cache(request): def test_add_bearer_tokens(token_cache): expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - token1 = jwt.encode({"exp": expiry_in_1_hour}, "secret").decode("unicode_escape") - token_cache.add_bearer_token("key1", token1) + token1 = jwt.encode({"exp": expiry_in_1_hour}, "secret") + token_cache._add_bearer_token("key1", token1) expiry_in_2_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=2) - token2 = jwt.encode({"exp": expiry_in_2_hour}, "secret").decode("unicode_escape") - token_cache.add_bearer_token("key2", token2) + token2 = jwt.encode({"exp": expiry_in_2_hour}, "secret") + token_cache._add_bearer_token("key2", token2) # Assert that tokens can be retrieved properly even after other token were inserted assert token_cache.get_token("key1") == token1 @@ -33,12 +33,12 @@ def test_add_bearer_tokens(token_cache): def test_save_bearer_tokens(token_cache, request): expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - token1 = jwt.encode({"exp": expiry_in_1_hour}, "secret").decode("unicode_escape") - token_cache.add_bearer_token("key1", token1) + token1 = jwt.encode({"exp": expiry_in_1_hour}, "secret") + token_cache._add_bearer_token("key1", token1) expiry_in_2_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=2) - token2 = jwt.encode({"exp": expiry_in_2_hour}, "secret").decode("unicode_escape") - token_cache.add_bearer_token("key2", token2) + token2 = jwt.encode({"exp": expiry_in_2_hour}, "secret") + token_cache._add_bearer_token("key2", token2) same_cache = requests_auth.JsonTokenFileCache(request.node.name + ".cache") assert same_cache.get_token("key1") == token1 @@ -52,10 +52,10 @@ def failing_dump(*args): monkeypatch.setattr(requests_auth.oauth2_tokens.json, "dump", failing_dump) expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - token1 = jwt.encode({"exp": expiry_in_1_hour}, "secret").decode("unicode_escape") + token1 = jwt.encode({"exp": expiry_in_1_hour}, "secret") # Assert that the exception is not thrown - token_cache.add_bearer_token("key1", token1) + token_cache._add_bearer_token("key1", token1) same_cache = requests_auth.JsonTokenFileCache(request.node.name + ".cache") with pytest.raises(requests_auth.AuthenticationFailed) as exception_info: @@ -70,18 +70,23 @@ def test_missing_token(token_cache): def test_missing_token_function(token_cache): expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - token = jwt.encode({"exp": expiry_in_1_hour}, "secret").decode("unicode_escape") - retrieved_token = token_cache.get_token("key1", lambda: ("key1", token)) + token = jwt.encode({"exp": expiry_in_1_hour}, "secret") + retrieved_token = token_cache.get_token( + "key1", on_missing_token=lambda: ("key1", token) + ) assert retrieved_token == token def test_token_without_refresh_token(token_cache): expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1) # add token without refresh token - token = jwt.encode({"exp": expiry_in_1_hour}, "secret").decode("unicode_escape") - token_cache.tokens['key1'] = token, expiry_in_1_hour.replace(tzinfo=datetime.timezone.utc).timestamp() + token = jwt.encode({"exp": expiry_in_1_hour}, "secret") + token_cache.tokens["key1"] = ( + token, + expiry_in_1_hour.replace(tzinfo=datetime.timezone.utc).timestamp(), + ) token_cache._save_tokens() # try to retrieve it retrieved_token = token_cache.get_token("key1") - assert token == retrieved_token \ No newline at end of file + assert token == retrieved_token diff --git a/tests/test_oauth2_authorization_code.py b/tests/test_oauth2_authorization_code.py index 8678055..58c399d 100644 --- a/tests/test_oauth2_authorization_code.py +++ b/tests/test_oauth2_authorization_code.py @@ -1,4 +1,5 @@ -from responses import RequestsMock, urlencoded_params_matcher +from responses import RequestsMock +from responses.matchers import urlencoded_params_matcher import pytest import requests @@ -79,6 +80,67 @@ def test_oauth2_authorization_code_flow_get_code_is_sent_in_authorization_header ) +def test_oauth2_authorization_code_flow_token_is_expired_after_30_seconds_by_default( + token_cache, responses: RequestsMock, browser_mock: BrowserMock +): + auth = requests_auth.OAuth2AuthorizationCode( + "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", + 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 + == "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, browser_mock: BrowserMock +): + auth = requests_auth.OAuth2AuthorizationCode( + "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_refresh_token(token_cache, responses: RequestsMock, browser_mock: BrowserMock): auth = requests_auth.OAuth2AuthorizationCode( "http://provide_code", "http://provide_access_token" @@ -98,21 +160,23 @@ def test_refresh_token(token_cache, responses: RequestsMock, browser_mock: Brows "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({ - "grant_type": "authorization_code", - "redirect_uri": "http://localhost:5000/", - "response_type": "code", - "code": "SplxlOBeZQQYbYS6WxSbIA" - }) - ] + urlencoded_params_matcher( + { + "grant_type": "authorization_code", + "redirect_uri": "http://localhost:5000/", + "response_type": "code", + "code": "SplxlOBeZQQYbYS6WxSbIA", + } + ) + ], ) assert ( - get_header(responses, auth).get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + get_header(responses, auth).get("Authorization") + == "Bearer 2YotnFZFEjr1zCsicMWpAA" ) assert ( - get_request(responses, "http://provide_access_token/").body - == "grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA" + get_request(responses, "http://provide_access_token/").body + == "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." @@ -130,22 +194,26 @@ def test_refresh_token(token_cache, responses: RequestsMock, browser_mock: Brows }, match=[ urlencoded_params_matcher( - {"grant_type": "refresh_token", "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", "response_type": "code"}) - ] + { + "grant_type": "refresh_token", + "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", + "response_type": "code", + } + ) + ], ) response = requests.get("http://authorized_only", auth=auth) + assert response.request.headers.get("Authorization") == "Bearer rVR7Syg5bjZtZYjbZIW" assert ( - response.request.headers.get("Authorization") - == "Bearer rVR7Syg5bjZtZYjbZIW" - ) - assert ( - get_request(responses, "http://provide_access_token/").body - == "grant_type=refresh_token&response_type=code&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA" + get_request(responses, "http://provide_access_token/").body + == "grant_type=refresh_token&response_type=code&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA" ) -def test_refresh_token_invalid(token_cache, responses: RequestsMock, browser_mock: BrowserMock): +def test_refresh_token_invalid( + token_cache, responses: RequestsMock, browser_mock: BrowserMock +): auth = requests_auth.OAuth2AuthorizationCode( "http://provide_code", "http://provide_access_token" ) @@ -164,21 +232,23 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock, browser_moc "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({ - "grant_type": "authorization_code", - "redirect_uri": "http://localhost:5000/", - "response_type": "code", - "code": "SplxlOBeZQQYbYS6WxSbIA" - }) - ] + urlencoded_params_matcher( + { + "grant_type": "authorization_code", + "redirect_uri": "http://localhost:5000/", + "response_type": "code", + "code": "SplxlOBeZQQYbYS6WxSbIA", + } + ) + ], ) assert ( - get_header(responses, auth).get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + get_header(responses, auth).get("Authorization") + == "Bearer 2YotnFZFEjr1zCsicMWpAA" ) assert ( - get_request(responses, "http://provide_access_token/").body - == "grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA" + get_request(responses, "http://provide_access_token/").body + == "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." @@ -191,8 +261,13 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock, browser_moc status=400, match=[ urlencoded_params_matcher( - {"grant_type": "refresh_token", "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", "response_type": "code"}) - ] + { + "grant_type": "refresh_token", + "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", + "response_type": "code", + } + ) + ], ) # initialize tab again because a thread can only be started once @@ -204,8 +279,7 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock, browser_moc # if refreshing the token fails, fallback to requesting a new token response = requests.get("http://authorized_only", auth=auth) assert ( - response.request.headers.get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + response.request.headers.get("Authorization") == "Bearer 2YotnFZFEjr1zCsicMWpAA" ) tab.assert_success( @@ -213,7 +287,9 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock, browser_moc ) -def test_refresh_token_access_token_not_expired(token_cache, responses: RequestsMock, browser_mock: BrowserMock): +def test_refresh_token_access_token_not_expired( + token_cache, responses: RequestsMock, browser_mock: BrowserMock +): auth = requests_auth.OAuth2AuthorizationCode( "http://provide_code", "http://provide_access_token" ) @@ -232,21 +308,23 @@ def test_refresh_token_access_token_not_expired(token_cache, responses: Requests "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({ - "grant_type": "authorization_code", - "redirect_uri": "http://localhost:5000/", - "response_type": "code", - "code": "SplxlOBeZQQYbYS6WxSbIA" - }) - ] + urlencoded_params_matcher( + { + "grant_type": "authorization_code", + "redirect_uri": "http://localhost:5000/", + "response_type": "code", + "code": "SplxlOBeZQQYbYS6WxSbIA", + } + ) + ], ) assert ( - get_header(responses, auth).get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + get_header(responses, auth).get("Authorization") + == "Bearer 2YotnFZFEjr1zCsicMWpAA" ) assert ( - get_request(responses, "http://provide_access_token/").body - == "grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA" + get_request(responses, "http://provide_access_token/").body + == "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." @@ -254,7 +332,10 @@ def test_refresh_token_access_token_not_expired(token_cache, responses: Requests # expect Bearer token to remain the same response = requests.get("http://authorized_only", auth=auth) - assert (response.request.headers.get("Authorization") == "Bearer 2YotnFZFEjr1zCsicMWpAA") + assert ( + response.request.headers.get("Authorization") == "Bearer 2YotnFZFEjr1zCsicMWpAA" + ) + def test_empty_token_is_invalid( token_cache, responses: RequestsMock, browser_mock: BrowserMock diff --git a/tests/test_oauth2_authorization_code_okta.py b/tests/test_oauth2_authorization_code_okta.py index c1ceba1..c1551c4 100644 --- a/tests/test_oauth2_authorization_code_okta.py +++ b/tests/test_oauth2_authorization_code_okta.py @@ -49,7 +49,7 @@ def test_oauth2_authorization_code_flow_uses_provided_session( ) -def test_oauth2_authorization_code_flow_get_code_is_sent_in_authorization_header_by_default( +def test_okta_authorization_code_flow_get_code_is_sent_in_authorization_header_by_default( token_cache, responses: RequestsMock, browser_mock: BrowserMock ): auth = requests_auth.OktaAuthorizationCode( @@ -85,6 +85,69 @@ def test_oauth2_authorization_code_flow_get_code_is_sent_in_authorization_header ) +def test_okta_authorization_code_flow_token_is_expired_after_30_seconds_by_default( + token_cache, responses: RequestsMock, browser_mock: BrowserMock +): + auth = requests_auth.OktaAuthorizationCode( + "testserver.okta-emea.com", "54239d18-c68c-4c47-8bdd-ce71ea1d50cd" + ) + tab = browser_mock.add_response( + opened_url="https://testserver.okta-emea.com/oauth2/default/v1/authorize?client_id=54239d18-c68c-4c47-8bdd-ce71ea1d50cd&scope=openid&response_type=code&state=5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F", + reply_url="http://localhost:5000#code=SplxlOBeZQQYbYS6WxSbIA&state=5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b", + ) + # Add a token that expires in 29 seconds, so should be considered as expired when issuing the request + token_cache._add_token( + key="5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b", + token="2YotnFZFEjr1zCsicMWpAA", + expiry=requests_auth.oauth2_tokens._to_expiry(expires_in=29), + ) + # Meaning a new one will be requested + responses.add( + responses.POST, + "https://testserver.okta-emea.com/oauth2/default/v1/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, "https://testserver.okta-emea.com/oauth2/default/v1/token" + ).body + == "grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&client_id=54239d18-c68c-4c47-8bdd-ce71ea1d50cd&scope=openid&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA" + ) + tab.assert_success( + "You are now authenticated on 5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b. You may close this tab." + ) + + +def test_okta_authorization_code_flow_token_custom_expiry( + token_cache, responses: RequestsMock, browser_mock: BrowserMock +): + auth = requests_auth.OktaAuthorizationCode( + "testserver.okta-emea.com", + "54239d18-c68c-4c47-8bdd-ce71ea1d50cd", + 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="5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b", + token="2YotnFZFEjr1zCsicMWpAA", + expiry=requests_auth.oauth2_tokens._to_expiry(expires_in=29), + ) + assert ( + get_header(responses, auth).get("Authorization") + == "Bearer 2YotnFZFEjr1zCsicMWpAA" + ) + + def test_empty_token_is_invalid( token_cache, responses: RequestsMock, browser_mock: BrowserMock ): diff --git a/tests/test_oauth2_authorization_code_pkce.py b/tests/test_oauth2_authorization_code_pkce.py index 1d99ba2..0bcdc7f 100644 --- a/tests/test_oauth2_authorization_code_pkce.py +++ b/tests/test_oauth2_authorization_code_pkce.py @@ -1,4 +1,5 @@ -from responses import RequestsMock, urlencoded_params_matcher +from responses import RequestsMock +from responses.matchers import urlencoded_params_matcher import pytest import requests @@ -81,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 ): @@ -116,7 +180,9 @@ def test_expires_in_sent_as_str( ) -def test_refresh_token(token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock): +def test_refresh_token( + 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" @@ -136,14 +202,16 @@ def test_refresh_token(token_cache, responses: RequestsMock, monkeypatch, browse "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({ - "grant_type": "authorization_code", - "code_verifier": "MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx", - "redirect_uri": "http://localhost:5000/", - "response_type": "code", - "code": "SplxlOBeZQQYbYS6WxSbIA" - }) - ] + urlencoded_params_matcher( + { + "grant_type": "authorization_code", + "code_verifier": "MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx", + "redirect_uri": "http://localhost:5000/", + "response_type": "code", + "code": "SplxlOBeZQQYbYS6WxSbIA", + } + ) + ], ) assert ( get_header(responses, auth).get("Authorization") @@ -168,22 +236,27 @@ def test_refresh_token(token_cache, responses: RequestsMock, monkeypatch, browse "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({"grant_type": "refresh_token", "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", "response_type": "code"}) - ] + urlencoded_params_matcher( + { + "grant_type": "refresh_token", + "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", + "response_type": "code", + } + ) + ], ) response = requests.get("http://authorized_only", auth=auth) + assert response.request.headers.get("Authorization") == "Bearer rVR7Syg5bjZtZYjbZIW" assert ( - response.request.headers.get("Authorization") - == "Bearer rVR7Syg5bjZtZYjbZIW" - ) - assert ( - get_request(responses, "http://provide_access_token/").body - == "grant_type=refresh_token&response_type=code&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA" + get_request(responses, "http://provide_access_token/").body + == "grant_type=refresh_token&response_type=code&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA" ) -def test_refresh_token_invalid(token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock): +def test_refresh_token_invalid( + 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" @@ -203,22 +276,24 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock, monkeypatch "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({ - "grant_type": "authorization_code", - "code_verifier": "MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx", - "redirect_uri": "http://localhost:5000/", - "response_type": "code", - "code": "SplxlOBeZQQYbYS6WxSbIA" - }) - ] + urlencoded_params_matcher( + { + "grant_type": "authorization_code", + "code_verifier": "MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx", + "redirect_uri": "http://localhost:5000/", + "response_type": "code", + "code": "SplxlOBeZQQYbYS6WxSbIA", + } + ) + ], ) assert ( - get_header(responses, auth).get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + 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" + 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." @@ -230,8 +305,14 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock, monkeypatch json={"error": "invalid_request"}, status=400, match=[ - urlencoded_params_matcher({"grant_type": "refresh_token", "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", "response_type": "code"}) - ] + urlencoded_params_matcher( + { + "grant_type": "refresh_token", + "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", + "response_type": "code", + } + ) + ], ) # initialize tab again because a thread can only be started once @@ -243,8 +324,7 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock, monkeypatch # if refreshing the token fails, fallback to requesting a new token response = requests.get("http://authorized_only", auth=auth) assert ( - response.request.headers.get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + response.request.headers.get("Authorization") == "Bearer 2YotnFZFEjr1zCsicMWpAA" ) tab.assert_success( @@ -252,7 +332,9 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock, monkeypatch ) -def test_refresh_token_access_token_not_expired(token_cache, responses: RequestsMock, monkeypatch, browser_mock: BrowserMock): +def test_refresh_token_access_token_not_expired( + 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" @@ -272,22 +354,24 @@ def test_refresh_token_access_token_not_expired(token_cache, responses: Requests "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({ - "grant_type": "authorization_code", - "code_verifier": "MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx", - "redirect_uri": "http://localhost:5000/", - "response_type": "code", - "code": "SplxlOBeZQQYbYS6WxSbIA" - }) - ] + urlencoded_params_matcher( + { + "grant_type": "authorization_code", + "code_verifier": "MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx", + "redirect_uri": "http://localhost:5000/", + "response_type": "code", + "code": "SplxlOBeZQQYbYS6WxSbIA", + } + ) + ], ) assert ( - get_header(responses, auth).get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + 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" + 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." @@ -295,7 +379,9 @@ def test_refresh_token_access_token_not_expired(token_cache, responses: Requests # expect Bearer token to remain the same response = requests.get("http://authorized_only", auth=auth) - assert (response.request.headers.get("Authorization") == "Bearer 2YotnFZFEjr1zCsicMWpAA") + assert ( + response.request.headers.get("Authorization") == "Bearer 2YotnFZFEjr1zCsicMWpAA" + ) def test_nonce_is_sent_if_provided_in_authorization_url( diff --git a/tests/test_oauth2_authorization_code_pkce_okta.py b/tests/test_oauth2_authorization_code_pkce_okta.py index fe26349..b3af18c 100644 --- a/tests/test_oauth2_authorization_code_pkce_okta.py +++ b/tests/test_oauth2_authorization_code_pkce_okta.py @@ -87,6 +87,71 @@ def test_oauth2_pkce_flow_get_code_is_sent_in_authorization_header_by_default( ) +def test_okta_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.OktaAuthorizationCodePKCE( + "testserver.okta-emea.com", "54239d18-c68c-4c47-8bdd-ce71ea1d50cd" + ) + tab = browser_mock.add_response( + opened_url="https://testserver.okta-emea.com/oauth2/default/v1/authorize?client_id=54239d18-c68c-4c47-8bdd-ce71ea1d50cd&scope=openid&response_type=code&state=5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b&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=5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b", + ) + # Add a token that expires in 29 seconds, so should be considered as expired when issuing the request + token_cache._add_token( + key="5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b", + token="2YotnFZFEjr1zCsicMWpAA", + expiry=requests_auth.oauth2_tokens._to_expiry(expires_in=29), + ) + # Meaning a new one will be requested + responses.add( + responses.POST, + "https://testserver.okta-emea.com/oauth2/default/v1/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, "https://testserver.okta-emea.com/oauth2/default/v1/token" + ).body + == "code_verifier=MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEx&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F&client_id=54239d18-c68c-4c47-8bdd-ce71ea1d50cd&scope=openid&response_type=code&code=SplxlOBeZQQYbYS6WxSbIA" + ) + tab.assert_success( + "You are now authenticated on 5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b. You may close this tab." + ) + + +def test_okta_pkce_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.OktaAuthorizationCodePKCE( + "testserver.okta-emea.com", + "54239d18-c68c-4c47-8bdd-ce71ea1d50cd", + 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="5264d11c8b268ccf911ce564ca42fd75cea68c4a3c1ec3ac1ab20243891ab7cd5250ad4c2d002017c6e8ac2ba34954293baa5e0e4fd00bb9ffd4a39c45f1960b", + 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 ): diff --git a/tests/test_oauth2_client_credential.py b/tests/test_oauth2_client_credential.py index 3570dbf..54a94d9 100644 --- a/tests/test_oauth2_client_credential.py +++ b/tests/test_oauth2_client_credential.py @@ -60,6 +60,57 @@ def test_oauth2_client_credentials_flow_token_is_sent_in_authorization_header_by ) +def test_oauth2_client_credentials_flow_token_is_expired_after_30_seconds_by_default( + token_cache, responses: RequestsMock +): + auth = requests_auth.OAuth2ClientCredentials( + "http://provide_access_token", client_id="test_user", client_secret="test_pwd" + ) + # Add a token that expires in 29 seconds, so should be considered as expired when issuing the request + token_cache._add_token( + key="a8a1c17ded24b3710524306819084310b08f97e151c79f4f1979202c541f3e8506c93176f7ee816bfcd2b2f6de9c5c3e16aaff220f1ad8f08d31ee086e8618da", + 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" + ) + + +def test_oauth2_client_credentials_flow_token_custom_expiry( + token_cache, responses: RequestsMock +): + auth = requests_auth.OAuth2ClientCredentials( + "http://provide_access_token", + client_id="test_user", + client_secret="test_pwd", + 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="a8a1c17ded24b3710524306819084310b08f97e151c79f4f1979202c541f3e8506c93176f7ee816bfcd2b2f6de9c5c3e16aaff220f1ad8f08d31ee086e8618da", + 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): auth = requests_auth.OAuth2ClientCredentials( "http://provide_access_token", client_id="test_user", client_secret="test_pwd" diff --git a/tests/test_oauth2_client_credential_okta.py b/tests/test_oauth2_client_credential_okta.py index b55cbff..3594ea4 100644 --- a/tests/test_oauth2_client_credential_okta.py +++ b/tests/test_oauth2_client_credential_okta.py @@ -56,6 +56,57 @@ def test_okta_client_credentials_flow_token_is_sent_in_authorization_header_by_d ) +def test_okta_client_credentials_flow_token_is_expired_after_30_seconds_by_default( + token_cache, responses: RequestsMock +): + auth = requests_auth.OktaClientCredentials( + "test_okta", client_id="test_user", client_secret="test_pwd" + ) + # Add a token that expires in 29 seconds, so should be considered as expired when issuing the request + token_cache._add_token( + key="f0d25aa4e496c6615328e776bb981dabe53fa77768a0a58eaf6d54215c598d80e57ffc7926fd96ec6a6a872942cb684a473e36233b593fb760d3eb6dc22ae550", + token="2YotnFZFEjr1zCsicMWpAA", + expiry=requests_auth.oauth2_tokens._to_expiry(expires_in=29), + ) + # Meaning a new one will be requested + responses.add( + responses.POST, + "https://test_okta/oauth2/default/v1/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" + ) + + +def test_okta_client_credentials_flow_token_custom_expiry( + token_cache, responses: RequestsMock +): + auth = requests_auth.OktaClientCredentials( + "test_okta", + client_id="test_user", + client_secret="test_pwd", + 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="f0d25aa4e496c6615328e776bb981dabe53fa77768a0a58eaf6d54215c598d80e57ffc7926fd96ec6a6a872942cb684a473e36233b593fb760d3eb6dc22ae550", + 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): auth = requests_auth.OktaClientCredentials( "test_okta", client_id="test_user", client_secret="test_pwd" diff --git a/tests/test_oauth2_implicit.py b/tests/test_oauth2_implicit.py index c8d690c..9fb5330 100644 --- a/tests/test_oauth2_implicit.py +++ b/tests/test_oauth2_implicit.py @@ -131,6 +131,46 @@ def test_oauth2_implicit_flow_post_token_is_sent_in_authorization_header_by_defa ) +def test_oauth2_implicit_flow_token_is_expired_after_30_seconds_by_default( + token_cache, responses: RequestsMock, browser_mock: BrowserMock +): + auth = requests_auth.OAuth2Implicit("http://provide_token") + # Add a token that expires in 29 seconds, so should be considered as expired when issuing the request + expiry_in_29_seconds = datetime.datetime.utcnow() + datetime.timedelta(seconds=29) + token_cache._add_token( + key="42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521", + token=create_token(expiry_in_29_seconds), + expiry=requests_auth.oauth2_tokens._to_expiry(expires_in=29), + ) + # Meaning a new one will be requested + expiry_in_1_hour = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + token = create_token(expiry_in_1_hour) + tab = browser_mock.add_response( + opened_url="http://provide_token?response_type=token&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2F", + reply_url="http://localhost:5000", + data=f"access_token={token}&state=42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521", + ) + assert get_header(responses, auth).get("Authorization") == f"Bearer {token}" + tab.assert_success( + "You are now authenticated on 42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521. You may close this tab." + ) + + +def test_oauth2_implicit_flow_token_custom_expiry( + token_cache, responses: RequestsMock, browser_mock: BrowserMock +): + auth = requests_auth.OAuth2Implicit("http://provide_token", early_expiry=28) + # Add a token that expires in 29 seconds, so should be considered as not expired when issuing the request + expiry_in_29_seconds = datetime.datetime.utcnow() + datetime.timedelta(seconds=29) + token_cache._add_token( + key="42a85b271b7a652ca3cc4c398cfd3f01b9ad36bf9c945ba823b023e8f8b95c4638576a0e3dcc96838b838bec33ec6c0ee2609d62ed82480b3b8114ca494c0521", + token=create_token(expiry_in_29_seconds), + expiry=requests_auth.oauth2_tokens._to_expiry(expires_in=29), + ) + token = create_token(expiry_in_29_seconds) + assert get_header(responses, auth).get("Authorization") == f"Bearer {token}" + + def test_browser_opening_failure(token_cache, responses: RequestsMock, monkeypatch): import requests_auth.oauth2_authentication_responses_server diff --git a/tests/test_oauth2_resource_owner_password.py b/tests/test_oauth2_resource_owner_password.py index 9314b62..0b6d22e 100644 --- a/tests/test_oauth2_resource_owner_password.py +++ b/tests/test_oauth2_resource_owner_password.py @@ -1,4 +1,5 @@ -from responses import RequestsMock, urlencoded_params_matcher +from responses import RequestsMock +from responses.matchers import urlencoded_params_matcher import pytest import requests @@ -65,6 +66,61 @@ def test_oauth2_password_credentials_flow_token_is_sent_in_authorization_header_ ) +def test_oauth2_password_credentials_flow_token_is_expired_after_30_seconds_by_default( + token_cache, responses: RequestsMock +): + auth = requests_auth.OAuth2ResourceOwnerPasswordCredentials( + "http://provide_access_token", username="test_user", password="test_pwd" + ) + # Add a token that expires in 29 seconds, so should be considered as expired when issuing the request + token_cache._add_token( + key="db2be9203dd2718c7285319dde1270056808482fbf7fffa6a9362d092d1cf799b393dd15140ea13e4d76d1603e56390a6222ff7063736a1b686d317706b2c001", + 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 + == "grant_type=password&username=test_user&password=test_pwd" + ) + + +def test_oauth2_password_credentials_flow_token_custom_expiry( + token_cache, responses: RequestsMock +): + auth = requests_auth.OAuth2ResourceOwnerPasswordCredentials( + "http://provide_access_token", + username="test_user", + password="test_pwd", + 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="db2be9203dd2718c7285319dde1270056808482fbf7fffa6a9362d092d1cf799b393dd15140ea13e4d76d1603e56390a6222ff7063736a1b686d317706b2c001", + 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): auth = requests_auth.OAuth2ResourceOwnerPasswordCredentials( "http://provide_access_token", username="test_user", password="test_pwd" @@ -106,8 +162,14 @@ def test_refresh_token(token_cache, responses: RequestsMock): "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({"grant_type": "password", "username": "test_user", "password": "test_pwd"}) - ] + urlencoded_params_matcher( + { + "grant_type": "password", + "username": "test_user", + "password": "test_pwd", + } + ) + ], ) assert ( @@ -131,15 +193,17 @@ def test_refresh_token(token_cache, responses: RequestsMock): "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({"grant_type": "refresh_token", "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"}) - ] + urlencoded_params_matcher( + { + "grant_type": "refresh_token", + "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", + } + ) + ], ) response = requests.get("http://authorized_only", auth=auth) - assert ( - response.request.headers.get("Authorization") - == "Bearer rVR7Syg5bjZtZYjbZIW" - ) + assert response.request.headers.get("Authorization") == "Bearer rVR7Syg5bjZtZYjbZIW" assert ( get_request(responses, "http://provide_access_token/").body == "grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA" @@ -162,17 +226,23 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock): "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({"grant_type": "password", "username": "test_user", "password": "test_pwd"}) - ] + urlencoded_params_matcher( + { + "grant_type": "password", + "username": "test_user", + "password": "test_pwd", + } + ) + ], ) assert ( - get_header(responses, auth).get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + get_header(responses, auth).get("Authorization") + == "Bearer 2YotnFZFEjr1zCsicMWpAA" ) assert ( - get_request(responses, "http://provide_access_token/").body - == "grant_type=password&username=test_user&password=test_pwd" + get_request(responses, "http://provide_access_token/").body + == "grant_type=password&username=test_user&password=test_pwd" ) # response for refresh token grant @@ -182,15 +252,19 @@ def test_refresh_token_invalid(token_cache, responses: RequestsMock): json={"error": "invalid_request"}, status=400, match=[ - urlencoded_params_matcher({"grant_type": "refresh_token", "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"}) - ] + urlencoded_params_matcher( + { + "grant_type": "refresh_token", + "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", + } + ) + ], ) # if refreshing the token fails, fallback to requesting a new token response = requests.get("http://authorized_only", auth=auth) assert ( - response.request.headers.get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + response.request.headers.get("Authorization") == "Bearer 2YotnFZFEjr1zCsicMWpAA" ) @@ -210,22 +284,30 @@ def test_refresh_token_access_token_not_expired(token_cache, responses: Requests "example_parameter": "example_value", }, match=[ - urlencoded_params_matcher({"grant_type": "password", "username": "test_user", "password": "test_pwd"}) - ] + urlencoded_params_matcher( + { + "grant_type": "password", + "username": "test_user", + "password": "test_pwd", + } + ) + ], ) assert ( - get_header(responses, auth).get("Authorization") - == "Bearer 2YotnFZFEjr1zCsicMWpAA" + get_header(responses, auth).get("Authorization") + == "Bearer 2YotnFZFEjr1zCsicMWpAA" ) assert ( - get_request(responses, "http://provide_access_token/").body - == "grant_type=password&username=test_user&password=test_pwd" + get_request(responses, "http://provide_access_token/").body + == "grant_type=password&username=test_user&password=test_pwd" ) # expect Bearer token to remain the same response = requests.get("http://authorized_only", auth=auth) - assert (response.request.headers.get("Authorization") == "Bearer 2YotnFZFEjr1zCsicMWpAA") + assert ( + response.request.headers.get("Authorization") == "Bearer 2YotnFZFEjr1zCsicMWpAA" + ) def test_scope_is_sent_as_is_when_provided_as_str(token_cache, responses: RequestsMock):