pax_global_header00006660000000000000000000000064146622653420014524gustar00rootroot0000000000000052 comment=d7db2c33226983648b91e3ec0d9cf2e43dc480d4 authlib-1.3.2/000077500000000000000000000000001466226534200131575ustar00rootroot00000000000000authlib-1.3.2/.codeclimate.yml000066400000000000000000000006701466226534200162340ustar00rootroot00000000000000version: "2" checks: argument-count: enabled: false method-complexity: enabled: true config: threshold: 10 method-lines: enabled: true config: threshold: 35 method-count: enabled: false file-lines: config: threshold: 460 return-statements: config: threshold: 6 similar-code: enabled: false identical-code: enabled: true config: threshold: 100 authlib-1.3.2/.codecov.yml000066400000000000000000000001711466226534200154010ustar00rootroot00000000000000coverage: status: patch: false changes: false project: default: target: '80' comment: false authlib-1.3.2/.flake8000066400000000000000000000001111466226534200143230ustar00rootroot00000000000000[flake8] exclude = tests/* max-line-length = 100 max-complexity = 10 authlib-1.3.2/.github/000077500000000000000000000000001466226534200145175ustar00rootroot00000000000000authlib-1.3.2/.github/CODE_OF_CONDUCT.md000066400000000000000000000026171466226534200173240ustar00rootroot00000000000000# Contributor Code of Conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) authlib-1.3.2/.github/FUNDING.yml000066400000000000000000000003741466226534200163400ustar00rootroot00000000000000# These are supported funding model platforms github: [lepture] patreon: lepture open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: pypi/Authlib custom: https://lepture.com/donate authlib-1.3.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001466226534200167025ustar00rootroot00000000000000authlib-1.3.2/.github/ISSUE_TEMPLATE/ask-for-help.md000066400000000000000000000003501466226534200215120ustar00rootroot00000000000000--- name: Ask for Help about: I need help for my project .... title: '' labels: '' assignees: '' --- This issue tracker is used for bug report, please don't ask for help here. Instead, use StackOverflow with a tag of **Authlib**. authlib-1.3.2/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000010111466226534200213650ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: lepture --- **Describe the bug** A clear and concise description of what the bug is. **Error Stacks** ``` put error stacks here ``` **To Reproduce** A minimal example to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. **Environment:** - OS: - Python Version: - Authlib Version: **Additional context** Add any other context about the problem here. authlib-1.3.2/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011271466226534200224300ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. authlib-1.3.2/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000011161466226534200203170ustar00rootroot00000000000000> DO NOT SEND ANY SECURITY FIX HERE. Please read "Security Reporting" section > on README. **What kind of change does this PR introduce?** (check at least one) - [ ] Bugfix - [ ] Feature - [ ] Code style update - [ ] Refactor - [ ] Other, please describe: **Does this PR introduce a breaking change?** (check one) - [ ] Yes - [ ] No If yes, please describe the impact and migration path for existing applications: (If no, please delete the above question and this text message.) --- - [ ] You consent that the copyright of your pull request source code belongs to Authlib's author. authlib-1.3.2/.github/SECURITY.md000066400000000000000000000007471466226534200163200ustar00rootroot00000000000000# Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 1.1.x | :white_check_mark: | | 0.15.x | :white_check_mark: | | < 0.15 | :x: | ## Reporting a Vulnerability If you found security bugs, please **do not send a public issue or patch**. You can send me email at . Or, you can use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. authlib-1.3.2/.github/workflows/000077500000000000000000000000001466226534200165545ustar00rootroot00000000000000authlib-1.3.2/.github/workflows/codeql-analysis.yml000066400000000000000000000020661466226534200223730ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: python - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 authlib-1.3.2/.github/workflows/pypi.yml000066400000000000000000000020451466226534200202610ustar00rootroot00000000000000name: Release to PyPI permissions: contents: write on: push: tags: - "1.*" env: FORCE_COLOR: '1' jobs: build: name: build dist files runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.9 - name: install build run: python -m pip install --upgrade build - name: build dist run: python -m build - uses: actions/upload-artifact@v3 with: name: artifacts path: dist/* if-no-files-found: error publish: environment: name: pypi-release url: https://pypi.org/project/Authlib/ permissions: id-token: write name: release to pypi needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v3 with: name: artifacts path: dist - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: skip-existing: true password: ${{ secrets.PYPI_API_TOKEN }} authlib-1.3.2/.github/workflows/python.yml000066400000000000000000000024541466226534200206250ustar00rootroot00000000000000name: tests on: push: branches-ignore: - 'wip-*' paths-ignore: - 'docs/**' pull_request: branches-ignore: - 'wip-*' paths-ignore: - 'docs/**' env: FORCE_COLOR: '1' jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 3 matrix: python: - version: "3.8" - version: "3.9" - version: "3.10" - version: "3.11" - version: "3.12" - version: "pypy3.8" - version: "pypy3.9" - version: "pypy3.10" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python.version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python.version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox coverage - name: Test with tox ${{ matrix.python.toxenv }} env: TOXENV: py,jose,clients,flask,django run: tox - name: Report coverage run: | coverage combine coverage report coverage xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml flags: unittests name: GitHub authlib-1.3.2/.gitignore000066400000000000000000000002541466226534200151500ustar00rootroot00000000000000*.pyc *.pyo *.egg-info *.swp __pycache__ build develop-eggs dist eggs parts .DS_Store .installed.cfg docs/_build htmlcov/ venv/ .tox .coverage* .pytest_cache/ *.egg .idea/ authlib-1.3.2/.readthedocs.yaml000066400000000000000000000003071466226534200164060ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: configuration: docs/conf.py python: install: - requirements: docs/requirements.txt - method: pip path: . authlib-1.3.2/BACKERS.md000066400000000000000000000071741466226534200145640ustar00rootroot00000000000000# Sponsors and Backers Many thanks to these awesome sponsors and backers. [Support Me via GitHub Sponsors](https://github.com/users/lepture/sponsorship) ## Sponsors
If you want to quickly add secure token-based authentication to Python projects, feel free to check Auth0's Python SDK and free plan at auth0.com/overview.
For quickly implementing token-based authentication, feel free to check Authing's Python SDK.
Kraken is the world's leading customer & culture platform for energy, water & broadband. Licensing enquiries at Kraken.tech.
Sentry
Sentry
Indeed
Indeed
Around
Around
## Awesome Backers
Aveline
Evilham
Aveline
Aveline
Callam
Callam
Krishna Kumar
Krishna Kumar
Junnplus
Jun
Malik Piara
Malik Piara
Alan
Alan
Alan
Jeff Heaton
Alan
Birk Jernström
Yaal Coop
Yaal Coop
authlib-1.3.2/COMMERCIAL-LICENSE000066400000000000000000000216331466226534200156020ustar00rootroot00000000000000============================== Authlib - Terms and conditions ============================== 1. **Preamble**: This Agreement governs the relationship between "YOU" (hereinafter: Licensee) and Hsiaoming Yang (hereinafter: Licensor). This Agreement sets the terms, rights, restrictions and obligations on using Authlib (hereinafter: The Software) created and owned by Licensor, as detailed herein 2. **License Grant**: Licensor hereby grants Licensee a Personal, Non-assignable & non-transferable, Commercial, Royalty free, Including the rights to create but not distribute derivative works, Non-exclusive license, all with accordance with the terms set forth and other legal restrictions set forth in 3rd party software used while running Software. 2.1. **Limited**: Licensee may use Software for the purpose of: 2.1.1. Running Software on Licensee’s Website[s] and Server[s]; 2.1.2. Modify Software to suit Licensee’s needs and specifications. 2.2. **Non Assignable & Non-Transferable**: Licensee may not assign or transfer his rights and duties under this license. 2.3. **Commercial, Royalty Free**: Licensee may use Software for any purpose, including paid-services, without any royalties 2.4. **Including the Right to Create Derivative Works**: Licensee may create derivative works based on Software, including amending Software’s source code, modifying it, integrating it into a larger work or removing portions of Software, as long as no distribution of the derivative works is made 3. **Term & Termination**: The Term of this license shall be until terminated. Licensor may terminate this Agreement, including Licensee’s license in the case where Licensee: 3.1. became insolvent or otherwise entered into any liquidation process; 3.2. exported The Software to any jurisdiction where licensor may not enforce his rights under this agreements in; 3.3. Licensee was in breach of any of this license's terms and conditions and such breach was not cured, immediately upon notification; 3.4. Licensee in breach of any of the terms of clause 2 to this license; 3.5. Licensee otherwise entered into any arrangement which caused Licensor to be unable to enforce his rights under this License. 4. **Payment**: In consideration of the License granted under clause 2, Licensee shall pay Licensor a fee, via Credit-Card, PayPal or any other mean which Licensor may deem adequate. Failure to perform payment shall construe as material breach of this Agreement. 5. **Upgrades, Updates and Fixes**: Licensor may provide Licensee, from time to time, with Upgrades, Updates or Fixes, as detailed herein and according to his sole discretion. Licensee hereby warrants to keep The Software up-to-date and install all relevant updates and fixes. Licensor shall provide any update or Fix free of charge; however, nothing in this Agreement shall require Licensor to provide Updates or Fixes. 5.1. **Upgrades**: for the purpose of this license, an Upgrade shall be a material amendment in The Software, which contains new features and or major performance improvements and shall be marked as a new version number. For example, The Software under version 1.X.X, an upgrade shall commence under number 2.0.0. 5.2. **Updates**: for the purpose of this license, an update shall be a minor amendment in The Software, which may contain new features or minor improvements and shall be marked as a new sub-version number. For example, The Software under version 1.1.X, an upgrade shall commence under number 1.2.0. 5.3. **Fix**: for the purpose of this license, a fix shall be a minor amendment in The Software, intended to remove bugs or alter minor features which impair the The Software's functionality. A fix shall be marked as a new sub-sub-version number. For example, Software under version 1.1.1, an upgrade shall commence under number 1.1.2. 6. **Support**: Software is provided under an AS-IS basis and without any support, updates or maintenance. Nothing in this Agreement shall require Licensor to provide Licensee with support or fixes to any bug, failure, mis-performance or other defect in The Software. 6.1. **Bug Notification**: Licensee may provide Licensor of details regarding any bug, defect or failure in The Software promptly and with no delay from such event; Licensee shall comply with Licensor's request for information regarding bugs, defects or failures and furnish him with information, screenshots and try to reproduce such bugs, defects or failures. 6.2. **Feature Request**: Licensee may request additional features in Software, provided, however, that (i) Licensee shall waive any claim or right in such feature should feature be developed by Licensor; (ii) Licensee shall be prohibited from developing the feature, or disclose such feature request, or feature, to any 3rd party directly competing with Licensor or any 3rd party which may be, following the development of such feature, in direct competition with Licensor; (iii) Licensee warrants that feature does not infringe any 3rd party patent, trademark, trade-secret or any other intellectual property right; and (iv) Licensee developed, envisioned or created the feature solely by himself. 7. **Liability**: To the extent permitted under Law, The Software is provided under an AS-IS basis. Licensor shall never, and without any limit, be liable for any damage, cost, expense or any other payment incurred by Licensee as a result of Software’s actions, failure, bugs and/or any other interaction between The Software and Licensee’s end-equipment, computers, other software or any 3rd party, end-equipment, computer or services. Moreover, Licensor shall never be liable for any defect in source code written by Licensee when relying on The Software or using The Software’s source code. 8. **Warranty**: 8.1. **Intellectual Property**: Licensor hereby warrants that The Software does not violate or infringe any 3rd party claims in regards to intellectual property, patents and/or trademarks and that to the best of its knowledge no legal action has been taken against it for any infringement or violation of any 3rd party intellectual property rights. 8.2. **No-Warranty**: The Software is provided without any warranty; Licensor hereby disclaims any warranty that The Software shall be error free, without defects or code which may cause damage to Licensee’s computers or to Licensee, and that Software shall be functional. Licensee shall be solely liable to any damage, defect or loss incurred as a result of operating software and undertake the risks contained in running The Software on License’s Server[s] and Website[s]. 8.3. **Prior Inspection**: Licensee hereby states that he inspected The Software thoroughly and found it satisfactory and adequate to his needs, that it does not interfere with his regular operation and that it does meet the standards and scope of his computer systems and architecture. Licensee found that The Software interacts with his development, website and server environment and that it does not infringe any of End User License Agreement of any software Licensee may use in performing his services. Licensee hereby waives any claims regarding The Software's incompatibility, performance, results and features, and warrants that he inspected the The Software. 9. **No Refunds**: Licensee warrants that he inspected The Software according to above clauses and that it is adequate to his needs. Accordingly, as The Software is intangible goods, Licensee shall not be, ever, entitled to any refund, rebate, compensation or restitution for any reason whatsoever, even if The Software contains material flaws. 10. **Indemnification**: Licensee hereby warrants to hold Licensor harmless and indemnify Licensor for any lawsuit brought against it in regards to Licensee’s use of The Software in means that violate, breach or otherwise circumvent this license, Licensor's intellectual property rights or Licensor's title in The Software. Licensor shall promptly notify Licensee in case of such legal action and request Licensee’s consent prior to any settlement in relation to such lawsuit or claim. 11. **Governing Law, Jurisdiction**: Licensee hereby agrees not to initiate class-action lawsuits against Licensor in relation to this license and to compensate Licensor for any legal fees, cost or attorney fees should any claim brought by Licensee against Licensor be denied, in part or in full. authlib-1.3.2/LICENSE000066400000000000000000000027521466226534200141720ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2017, Hsiaoming Yang All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. authlib-1.3.2/MANIFEST.in000066400000000000000000000000601466226534200147110ustar00rootroot00000000000000include README.rst include LICENSE prune tests* authlib-1.3.2/Makefile000066400000000000000000000010401466226534200146120ustar00rootroot00000000000000.PHONY: tests clean clean-pyc clean-build docs build build: @python3 -m build clean: clean-build clean-pyc clean-docs clean-tox tests: @TOXENV=py,flask,django,coverage tox clean-build: @rm -fr build/ @rm -fr dist/ @rm -fr *.egg @rm -fr *.egg-info clean-pyc: @find . -name '*.pyc' -exec rm -f {} + @find . -name '*.pyo' -exec rm -f {} + @find . -name '*~' -exec rm -f {} + @find . -name '__pycache__' -exec rm -fr {} + clean-docs: @rm -fr docs/_build clean-tox: @rm -rf .tox/ build-docs: @sphinx-build docs build/_html -a authlib-1.3.2/README.md000066400000000000000000000175001466226534200144410ustar00rootroot00000000000000 # Authlib Build Status Coverage Status PyPI Version Maintainability Follow Twitter The ultimate Python library in building OAuth and OpenID Connect servers. JWS, JWK, JWA, JWT are included. Authlib is compatible with Python3.6+. **[Migrating from `authlib.jose` to `joserfc`](https://jose.authlib.org/en/dev/migrations/authlib/)** ## Sponsors
Kraken is the world's leading customer & culture platform for energy, water & broadband. Licensing enquiries at Kraken.tech.
A blogging and podcast hosting platform with minimal design but powerful features. Host your blog and Podcast with Typlog.com.
[**Fund Authlib to access additional features**](https://docs.authlib.org/en/latest/community/funding.html) ## Features Generic, spec-compliant implementation to build clients and providers: - [The OAuth 1.0 Protocol](https://docs.authlib.org/en/latest/basic/oauth1.html) - [RFC5849: The OAuth 1.0 Protocol](https://docs.authlib.org/en/latest/specs/rfc5849.html) - [The OAuth 2.0 Authorization Framework](https://docs.authlib.org/en/latest/basic/oauth2.html) - [RFC6749: The OAuth 2.0 Authorization Framework](https://docs.authlib.org/en/latest/specs/rfc6749.html) - [RFC6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://docs.authlib.org/en/latest/specs/rfc6750.html) - [RFC7009: OAuth 2.0 Token Revocation](https://docs.authlib.org/en/latest/specs/rfc7009.html) - [RFC7523: JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://docs.authlib.org/en/latest/specs/rfc7523.html) - [RFC7591: OAuth 2.0 Dynamic Client Registration Protocol](https://docs.authlib.org/en/latest/specs/rfc7591.html) - [RFC7592: OAuth 2.0 Dynamic Client Registration Management Protocol](https://docs.authlib.org/en/latest/specs/rfc7592.html) - [RFC7636: Proof Key for Code Exchange by OAuth Public Clients](https://docs.authlib.org/en/latest/specs/rfc7636.html) - [RFC7662: OAuth 2.0 Token Introspection](https://docs.authlib.org/en/latest/specs/rfc7662.html) - [RFC8414: OAuth 2.0 Authorization Server Metadata](https://docs.authlib.org/en/latest/specs/rfc8414.html) - [RFC8628: OAuth 2.0 Device Authorization Grant](https://docs.authlib.org/en/latest/specs/rfc8628.html) - [RFC9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://docs.authlib.org/en/latest/specs/rfc9068.html) - [Javascript Object Signing and Encryption](https://docs.authlib.org/en/latest/jose/index.html) - [RFC7515: JSON Web Signature](https://docs.authlib.org/en/latest/jose/jws.html) - [RFC7516: JSON Web Encryption](https://docs.authlib.org/en/latest/jose/jwe.html) - [RFC7517: JSON Web Key](https://docs.authlib.org/en/latest/jose/jwk.html) - [RFC7518: JSON Web Algorithms](https://docs.authlib.org/en/latest/specs/rfc7518.html) - [RFC7519: JSON Web Token](https://docs.authlib.org/en/latest/jose/jwt.html) - [RFC7638: JSON Web Key (JWK) Thumbprint](https://docs.authlib.org/en/latest/specs/rfc7638.html) - [ ] RFC7797: JSON Web Signature (JWS) Unencoded Payload Option - [RFC8037: ECDH in JWS and JWE](https://docs.authlib.org/en/latest/specs/rfc8037.html) - [ ] draft-madden-jose-ecdh-1pu-04: Public Key Authenticated Encryption for JOSE: ECDH-1PU - [OpenID Connect 1.0](https://docs.authlib.org/en/latest/specs/oidc.html) - [x] OpenID Connect Core 1.0 - [x] OpenID Connect Discovery 1.0 Connect third party OAuth providers with Authlib built-in client integrations: - Requests - [OAuth1Session](https://docs.authlib.org/en/latest/client/requests.html#requests-oauth-1-0) - [OAuth2Session](https://docs.authlib.org/en/latest/client/requests.html#requests-oauth-2-0) - [OpenID Connect](https://docs.authlib.org/en/latest/client/requests.html#requests-openid-connect) - [AssertionSession](https://docs.authlib.org/en/latest/client/requests.html#requests-service-account) - HTTPX - [AsyncOAuth1Client](https://docs.authlib.org/en/latest/client/httpx.html#httpx-oauth-1-0) - [AsyncOAuth2Client](https://docs.authlib.org/en/latest/client/httpx.html#httpx-oauth-2-0) - [OpenID Connect](https://docs.authlib.org/en/latest/client/httpx.html#httpx-oauth-2-0) - [AsyncAssertionClient](https://docs.authlib.org/en/latest/client/httpx.html#async-service-account) - [Flask OAuth Client](https://docs.authlib.org/en/latest/client/flask.html) - [Django OAuth Client](https://docs.authlib.org/en/latest/client/django.html) - [Starlette OAuth Client](https://docs.authlib.org/en/latest/client/starlette.html) - [FastAPI OAuth Client](https://docs.authlib.org/en/latest/client/fastapi.html) Build your own OAuth 1.0, OAuth 2.0, and OpenID Connect providers: - Flask - [Flask OAuth 1.0 Provider](https://docs.authlib.org/en/latest/flask/1/) - [Flask OAuth 2.0 Provider](https://docs.authlib.org/en/latest/flask/2/) - [Flask OpenID Connect 1.0 Provider](https://docs.authlib.org/en/latest/flask/2/openid-connect.html) - Django - [Django OAuth 1.0 Provider](https://docs.authlib.org/en/latest/django/1/) - [Django OAuth 2.0 Provider](https://docs.authlib.org/en/latest/django/2/) - [Django OpenID Connect 1.0 Provider](https://docs.authlib.org/en/latest/django/2/openid-connect.html) ## Useful Links 1. Homepage: . 2. Documentation: . 3. Purchase Commercial License: . 4. Blog: . 5. Twitter: . 6. StackOverflow: . 7. Other Repositories: . 8. Subscribe Tidelift: [https://tidelift.com/subscription/pkg/pypi-authlib](https://tidelift.com/subscription/pkg/pypi-authlib?utm_source=pypi-authlib&utm_medium=referral&utm_campaign=links). ## Security Reporting If you found security bugs, please do not send a public issue or patch. You can send me email at . Attachment with patch is welcome. My PGP Key fingerprint is: ``` 72F8 E895 A70C EBDF 4F2A DFE0 7E55 E3E0 118B 2B4C ``` Or, you can use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ## License Authlib offers two licenses: 1. BSD (LICENSE) 2. COMMERCIAL-LICENSE Companies can purchase a commercial license at [Authlib Plans](https://authlib.org/plans). **If your company is creating a closed source OAuth provider, it is strongly suggested that your company purchasing a commercial license.** ## Support If you need any help, you can always ask questions on StackOverflow with a tag of "Authlib". DO NOT ASK HELP IN GITHUB ISSUES. We also provide commercial consulting and supports. You can find more information at . authlib-1.3.2/README.rst000066400000000000000000000043301466226534200146460ustar00rootroot00000000000000Authlib ======= The ultimate Python library in building OAuth and OpenID Connect servers. JWS, JWK, JWA, JWT are included. Useful Links ------------ 1. Homepage: https://authlib.org/ 2. Documentation: https://docs.authlib.org/ 3. Purchase Commercial License: https://authlib.org/plans 4. Blog: https://blog.authlib.org/ 5. More Repositories: https://github.com/authlib 6. Twitter: https://twitter.com/authlib 7. Donate: https://www.patreon.com/lepture Specifications -------------- - RFC5849: The OAuth 1.0 Protocol - RFC6749: The OAuth 2.0 Authorization Framework - RFC6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage - RFC7009: OAuth 2.0 Token Revocation - RFC7515: JSON Web Signature - RFC7516: JSON Web Encryption - RFC7517: JSON Web Key - RFC7518: JSON Web Algorithms - RFC7519: JSON Web Token - RFC7521: Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants - RFC7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants - RFC7591: OAuth 2.0 Dynamic Client Registration Protocol - RFC7636: Proof Key for Code Exchange by OAuth Public Clients - RFC7638: JSON Web Key (JWK) Thumbprint - RFC7662: OAuth 2.0 Token Introspection - RFC8037: CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in JSON Object Signing and Encryption (JOSE) - RFC8414: OAuth 2.0 Authorization Server Metadata - RFC8628: OAuth 2.0 Device Authorization Grant - OpenID Connect 1.0 - OpenID Connect Discovery 1.0 - draft-madden-jose-ecdh-1pu-04: Public Key Authenticated Encryption for JOSE: ECDH-1PU Implementations --------------- - Requests OAuth 1 Session - Requests OAuth 2 Session - Requests Assertion Session - HTTPX OAuth 1 Session - HTTPX OAuth 2 Session - HTTPX Assertion Session - Flask OAuth 1/2 Client - Django OAuth 1/2 Client - Starlette OAuth 1/2 Client - Flask OAuth 1.0 Server - Flask OAuth 2.0 Server - Flask OpenID Connect 1.0 - Django OAuth 1.0 Server - Django OAuth 2.0 Server - Django OpenID Connect 1.0 License ------- Authlib is licensed under BSD. Please see LICENSE for licensing details. If this license does not fit your company, consider to purchase a commercial license. Find more information on `Authlib Plans`_. .. _`Authlib Plans`: https://authlib.org/plans authlib-1.3.2/authlib/000077500000000000000000000000001466226534200146075ustar00rootroot00000000000000authlib-1.3.2/authlib/__init__.py000066400000000000000000000007341466226534200167240ustar00rootroot00000000000000""" authlib ~~~~~~~ The ultimate Python library in building OAuth 1.0, OAuth 2.0 and OpenID Connect clients and providers. It covers from low level specification implementation to high level framework integrations. :copyright: (c) 2017 by Hsiaoming Yang. :license: BSD, see LICENSE for more details. """ from .consts import version, homepage, author __version__ = version __homepage__ = homepage __author__ = author __license__ = 'BSD-3-Clause' authlib-1.3.2/authlib/common/000077500000000000000000000000001466226534200160775ustar00rootroot00000000000000authlib-1.3.2/authlib/common/__init__.py000066400000000000000000000000001466226534200201760ustar00rootroot00000000000000authlib-1.3.2/authlib/common/encoding.py000066400000000000000000000030121466226534200202330ustar00rootroot00000000000000import json import base64 import struct def to_bytes(x, charset='utf-8', errors='strict'): if x is None: return None if isinstance(x, bytes): return x if isinstance(x, str): return x.encode(charset, errors) if isinstance(x, (int, float)): return str(x).encode(charset, errors) return bytes(x) def to_unicode(x, charset='utf-8', errors='strict'): if x is None or isinstance(x, str): return x if isinstance(x, bytes): return x.decode(charset, errors) return str(x) def to_native(x, encoding='ascii'): if isinstance(x, str): return x return x.decode(encoding) def json_loads(s): return json.loads(s) def json_dumps(data, ensure_ascii=False): return json.dumps(data, ensure_ascii=ensure_ascii, separators=(',', ':')) def urlsafe_b64decode(s): s += b'=' * (-len(s) % 4) return base64.urlsafe_b64decode(s) def urlsafe_b64encode(s): return base64.urlsafe_b64encode(s).rstrip(b'=') def base64_to_int(s): data = urlsafe_b64decode(to_bytes(s, charset='ascii')) buf = struct.unpack('%sB' % len(data), data) return int(''.join(["%02x" % byte for byte in buf]), 16) def int_to_base64(num): if num < 0: raise ValueError('Must be a positive integer') s = num.to_bytes((num.bit_length() + 7) // 8, 'big', signed=False) return to_unicode(urlsafe_b64encode(s)) def json_b64encode(text): if isinstance(text, dict): text = json_dumps(text) return urlsafe_b64encode(to_bytes(text)) authlib-1.3.2/authlib/common/errors.py000066400000000000000000000032241466226534200177660ustar00rootroot00000000000000from authlib.consts import default_json_headers class AuthlibBaseError(Exception): """Base Exception for all errors in Authlib.""" #: short-string error code error = None #: long-string to describe this error description = '' #: web page that describes this error uri = None def __init__(self, error=None, description=None, uri=None): if error is not None: self.error = error if description is not None: self.description = description if uri is not None: self.uri = uri message = f'{self.error}: {self.description}' super().__init__(message) def __repr__(self): return f'<{self.__class__.__name__} "{self.error}">' class AuthlibHTTPError(AuthlibBaseError): #: HTTP status code status_code = 400 def __init__(self, error=None, description=None, uri=None, status_code=None): super().__init__(error, description, uri) if status_code is not None: self.status_code = status_code def get_error_description(self): return self.description def get_body(self): error = [('error', self.error)] if self.description: error.append(('error_description', self.description)) if self.uri: error.append(('error_uri', self.uri)) return error def get_headers(self): return default_json_headers[:] def __call__(self, uri=None): self.uri = uri body = dict(self.get_body()) headers = self.get_headers() return self.status_code, body, headers class ContinueIteration(AuthlibBaseError): pass authlib-1.3.2/authlib/common/security.py000066400000000000000000000007551466226534200203270ustar00rootroot00000000000000import os import string import random UNICODE_ASCII_CHARACTER_SET = string.ascii_letters + string.digits def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET): rand = random.SystemRandom() return ''.join(rand.choice(chars) for _ in range(length)) def is_secure_transport(uri): """Check if the uri is over ssl.""" if os.getenv('AUTHLIB_INSECURE_TRANSPORT'): return True uri = uri.lower() return uri.startswith(('https://', 'http://localhost:')) authlib-1.3.2/authlib/common/urls.py000066400000000000000000000106251466226534200174420ustar00rootroot00000000000000""" authlib.util.urls ~~~~~~~~~~~~~~~~~ Wrapper functions for URL encoding and decoding. """ import re from urllib.parse import quote as _quote from urllib.parse import unquote as _unquote from urllib.parse import urlencode as _urlencode import urllib.parse as urlparse from .encoding import to_unicode, to_bytes always_safe = ( 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' '0123456789_.-' ) urlencoded = set(always_safe) | set('=&;:%+~,*@!()/?') INVALID_HEX_PATTERN = re.compile(r'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]') def url_encode(params): encoded = [] for k, v in params: encoded.append((to_bytes(k), to_bytes(v))) return to_unicode(_urlencode(encoded)) def url_decode(query): """Decode a query string in x-www-form-urlencoded format into a sequence of two-element tuples. Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce correct formatting of the query string by validation. If validation fails a ValueError will be raised. urllib.parse_qsl will only raise errors if any of name-value pairs omits the equals sign. """ # Check if query contains invalid characters if query and not set(query) <= urlencoded: error = ("Error trying to decode a non urlencoded string. " "Found invalid characters: %s " "in the string: '%s'. " "Please ensure the request/response body is " "x-www-form-urlencoded.") raise ValueError(error % (set(query) - urlencoded, query)) # Check for correctly hex encoded values using a regular expression # All encoded values begin with % followed by two hex characters # correct = %00, %A0, %0A, %FF # invalid = %G0, %5H, %PO if INVALID_HEX_PATTERN.search(query): raise ValueError('Invalid hex encoding in query string.') # We encode to utf-8 prior to parsing because parse_qsl behaves # differently on unicode input in python 2 and 3. # Python 2.7 # >>> urlparse.parse_qsl(u'%E5%95%A6%E5%95%A6') # u'\xe5\x95\xa6\xe5\x95\xa6' # Python 2.7, non unicode input gives the same # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6') # '\xe5\x95\xa6\xe5\x95\xa6' # but now we can decode it to unicode # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6').decode('utf-8') # u'\u5566\u5566' # Python 3.3 however # >>> urllib.parse.parse_qsl(u'%E5%95%A6%E5%95%A6') # u'\u5566\u5566' # We want to allow queries such as "c2" whereas urlparse.parse_qsl # with the strict_parsing flag will not. params = urlparse.parse_qsl(query, keep_blank_values=True) # unicode all the things decoded = [] for k, v in params: decoded.append((to_unicode(k), to_unicode(v))) return decoded def add_params_to_qs(query, params): """Extend a query with a list of two-tuples.""" if isinstance(params, dict): params = params.items() qs = urlparse.parse_qsl(query, keep_blank_values=True) qs.extend(params) return url_encode(qs) def add_params_to_uri(uri, params, fragment=False): """Add a list of two-tuples to the uri query components.""" sch, net, path, par, query, fra = urlparse.urlparse(uri) if fragment: fra = add_params_to_qs(fra, params) else: query = add_params_to_qs(query, params) return urlparse.urlunparse((sch, net, path, par, query, fra)) def quote(s, safe=b'/'): return to_unicode(_quote(to_bytes(s), safe)) def unquote(s): return to_unicode(_unquote(s)) def quote_url(s): return quote(s, b'~@#$&()*!+=:;,.?/\'') def extract_params(raw): """Extract parameters and return them as a list of 2-tuples. Will successfully extract parameters from urlencoded query strings, dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an empty list of parameters. Any other input will result in a return value of None. """ if isinstance(raw, (list, tuple)): try: raw = dict(raw) except (TypeError, ValueError): return None if isinstance(raw, dict): params = [] for k, v in raw.items(): params.append((to_unicode(k), to_unicode(v))) return params if not raw: return None try: return url_decode(raw) except ValueError: return None def is_valid_url(url): parsed = urlparse.urlparse(url) return parsed.scheme and parsed.hostname authlib-1.3.2/authlib/consts.py000066400000000000000000000004541466226534200164750ustar00rootroot00000000000000name = 'Authlib' version = '1.3.2' author = 'Hsiaoming Yang ' homepage = 'https://authlib.org/' default_user_agent = f'{name}/{version} (+{homepage})' default_json_headers = [ ('Content-Type', 'application/json'), ('Cache-Control', 'no-store'), ('Pragma', 'no-cache'), ] authlib-1.3.2/authlib/deprecate.py000066400000000000000000000007411466226534200171170ustar00rootroot00000000000000import warnings class AuthlibDeprecationWarning(DeprecationWarning): pass warnings.simplefilter('always', AuthlibDeprecationWarning) def deprecate(message, version=None, link_uid=None, link_file=None): if version: message += f'\nIt will be compatible before version {version}.' if link_uid and link_file: message += f'\nRead more ' warnings.warn(AuthlibDeprecationWarning(message), stacklevel=2) authlib-1.3.2/authlib/integrations/000077500000000000000000000000001466226534200173155ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/__init__.py000066400000000000000000000000001466226534200214140ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/base_client/000077500000000000000000000000001466226534200215655ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/base_client/__init__.py000066400000000000000000000012151466226534200236750ustar00rootroot00000000000000from .registry import BaseOAuth from .sync_app import BaseApp, OAuth1Mixin, OAuth2Mixin from .sync_openid import OpenIDMixin from .framework_integration import FrameworkIntegration from .errors import ( OAuthError, MissingRequestTokenError, MissingTokenError, TokenExpiredError, InvalidTokenError, UnsupportedTokenTypeError, MismatchingStateError, ) __all__ = [ 'BaseOAuth', 'BaseApp', 'OAuth1Mixin', 'OAuth2Mixin', 'OpenIDMixin', 'FrameworkIntegration', 'OAuthError', 'MissingRequestTokenError', 'MissingTokenError', 'TokenExpiredError', 'InvalidTokenError', 'UnsupportedTokenTypeError', 'MismatchingStateError', ] authlib-1.3.2/authlib/integrations/base_client/async_app.py000066400000000000000000000133271466226534200241220ustar00rootroot00000000000000import time import logging from authlib.common.urls import urlparse from .errors import ( MissingRequestTokenError, MissingTokenError, ) from .sync_app import OAuth1Base, OAuth2Base log = logging.getLogger(__name__) __all__ = ['AsyncOAuth1Mixin', 'AsyncOAuth2Mixin'] class AsyncOAuth1Mixin(OAuth1Base): async def request(self, method, url, token=None, **kwargs): async with self._get_oauth_client() as session: return await _http_request(self, session, method, url, token, kwargs) async def create_authorization_url(self, redirect_uri=None, **kwargs): """Generate the authorization url and state for HTTP redirect. :param redirect_uri: Callback or redirect URI for authorization. :param kwargs: Extra parameters to include. :return: dict """ if not self.authorize_url: raise RuntimeError('Missing "authorize_url" value') if self.authorize_params: kwargs.update(self.authorize_params) async with self._get_oauth_client() as client: client.redirect_uri = redirect_uri params = {} if self.request_token_params: params.update(self.request_token_params) request_token = await client.fetch_request_token(self.request_token_url, **params) log.debug(f'Fetch request token: {request_token!r}') url = client.create_authorization_url(self.authorize_url, **kwargs) state = request_token['oauth_token'] return {'url': url, 'request_token': request_token, 'state': state} async def fetch_access_token(self, request_token=None, **kwargs): """Fetch access token in one step. :param request_token: A previous request token for OAuth 1. :param kwargs: Extra parameters to fetch access token. :return: A token dict. """ async with self._get_oauth_client() as client: if request_token is None: raise MissingRequestTokenError() # merge request token with verifier token = {} token.update(request_token) token.update(kwargs) client.token = token params = self.access_token_params or {} token = await client.fetch_access_token(self.access_token_url, **params) return token class AsyncOAuth2Mixin(OAuth2Base): async def _on_update_token(self, token, refresh_token=None, access_token=None): if self._update_token: await self._update_token( token, refresh_token=refresh_token, access_token=access_token, ) async def load_server_metadata(self): if self._server_metadata_url and '_loaded_at' not in self.server_metadata: async with self.client_cls(**self.client_kwargs) as client: resp = await client.request('GET', self._server_metadata_url, withhold_token=True) resp.raise_for_status() metadata = resp.json() metadata['_loaded_at'] = time.time() self.server_metadata.update(metadata) return self.server_metadata async def request(self, method, url, token=None, **kwargs): metadata = await self.load_server_metadata() async with self._get_oauth_client(**metadata) as session: return await _http_request(self, session, method, url, token, kwargs) async def create_authorization_url(self, redirect_uri=None, **kwargs): """Generate the authorization url and state for HTTP redirect. :param redirect_uri: Callback or redirect URI for authorization. :param kwargs: Extra parameters to include. :return: dict """ metadata = await self.load_server_metadata() authorization_endpoint = self.authorize_url or metadata.get('authorization_endpoint') if not authorization_endpoint: raise RuntimeError('Missing "authorize_url" value') if self.authorize_params: kwargs.update(self.authorize_params) async with self._get_oauth_client(**metadata) as client: client.redirect_uri = redirect_uri return self._create_oauth2_authorization_url( client, authorization_endpoint, **kwargs) async def fetch_access_token(self, redirect_uri=None, **kwargs): """Fetch access token in the final step. :param redirect_uri: Callback or Redirect URI that is used in previous :meth:`authorize_redirect`. :param kwargs: Extra parameters to fetch access token. :return: A token dict. """ metadata = await self.load_server_metadata() token_endpoint = self.access_token_url or metadata.get('token_endpoint') async with self._get_oauth_client(**metadata) as client: if redirect_uri is not None: client.redirect_uri = redirect_uri params = {} if self.access_token_params: params.update(self.access_token_params) params.update(kwargs) token = await client.fetch_token(token_endpoint, **params) return token async def _http_request(ctx, session, method, url, token, kwargs): request = kwargs.pop('request', None) withhold_token = kwargs.get('withhold_token') if ctx.api_base_url and not url.startswith(('https://', 'http://')): url = urlparse.urljoin(ctx.api_base_url, url) if withhold_token: return await session.request(method, url, **kwargs) if token is None and ctx._fetch_token and request: token = await ctx._fetch_token(request) if token is None: raise MissingTokenError() session.token = token return await session.request(method, url, **kwargs) authlib-1.3.2/authlib/integrations/base_client/async_openid.py000066400000000000000000000053631466226534200246210ustar00rootroot00000000000000from authlib.jose import JsonWebToken, JsonWebKey from authlib.oidc.core import UserInfo, CodeIDToken, ImplicitIDToken __all__ = ['AsyncOpenIDMixin'] class AsyncOpenIDMixin: async def fetch_jwk_set(self, force=False): metadata = await self.load_server_metadata() jwk_set = metadata.get('jwks') if jwk_set and not force: return jwk_set uri = metadata.get('jwks_uri') if not uri: raise RuntimeError('Missing "jwks_uri" in metadata') async with self.client_cls(**self.client_kwargs) as client: resp = await client.request('GET', uri, withhold_token=True) resp.raise_for_status() jwk_set = resp.json() self.server_metadata['jwks'] = jwk_set return jwk_set async def userinfo(self, **kwargs): """Fetch user info from ``userinfo_endpoint``.""" metadata = await self.load_server_metadata() resp = await self.get(metadata['userinfo_endpoint'], **kwargs) resp.raise_for_status() data = resp.json() return UserInfo(data) async def parse_id_token(self, token, nonce, claims_options=None): """Return an instance of UserInfo from token's ``id_token``.""" claims_params = dict( nonce=nonce, client_id=self.client_id, ) if 'access_token' in token: claims_params['access_token'] = token['access_token'] claims_cls = CodeIDToken else: claims_cls = ImplicitIDToken metadata = await self.load_server_metadata() if claims_options is None and 'issuer' in metadata: claims_options = {'iss': {'values': [metadata['issuer']]}} alg_values = metadata.get('id_token_signing_alg_values_supported') if not alg_values: alg_values = ['RS256'] jwt = JsonWebToken(alg_values) jwk_set = await self.fetch_jwk_set() try: claims = jwt.decode( token['id_token'], key=JsonWebKey.import_key_set(jwk_set), claims_cls=claims_cls, claims_options=claims_options, claims_params=claims_params, ) except ValueError: jwk_set = await self.fetch_jwk_set(force=True) claims = jwt.decode( token['id_token'], key=JsonWebKey.import_key_set(jwk_set), claims_cls=claims_cls, claims_options=claims_options, claims_params=claims_params, ) # https://github.com/lepture/authlib/issues/259 if claims.get('nonce_supported') is False: claims.params['nonce'] = None claims.validate(leeway=120) return UserInfo(claims) authlib-1.3.2/authlib/integrations/base_client/errors.py000066400000000000000000000011701466226534200234520ustar00rootroot00000000000000from authlib.common.errors import AuthlibBaseError class OAuthError(AuthlibBaseError): error = 'oauth_error' class MissingRequestTokenError(OAuthError): error = 'missing_request_token' class MissingTokenError(OAuthError): error = 'missing_token' class TokenExpiredError(OAuthError): error = 'token_expired' class InvalidTokenError(OAuthError): error = 'token_invalid' class UnsupportedTokenTypeError(OAuthError): error = 'unsupported_token_type' class MismatchingStateError(OAuthError): error = 'mismatching_state' description = 'CSRF Warning! State not equal in request and response.' authlib-1.3.2/authlib/integrations/base_client/framework_integration.py000066400000000000000000000035171466226534200265450ustar00rootroot00000000000000import json import time class FrameworkIntegration: expires_in = 3600 def __init__(self, name, cache=None): self.name = name self.cache = cache def _get_cache_data(self, key): value = self.cache.get(key) if not value: return None try: return json.loads(value) except (TypeError, ValueError): return None def _clear_session_state(self, session): now = time.time() for key in dict(session): if '_authlib_' in key: # TODO: remove in future session.pop(key) elif key.startswith('_state_'): value = session[key] exp = value.get('exp') if not exp or exp < now: session.pop(key) def get_state_data(self, session, state): key = f'_state_{self.name}_{state}' if self.cache: value = self._get_cache_data(key) else: value = session.get(key) if value: return value.get('data') return None def set_state_data(self, session, state, data): key = f'_state_{self.name}_{state}' if self.cache: self.cache.set(key, json.dumps({'data': data}), self.expires_in) else: now = time.time() session[key] = {'data': data, 'exp': now + self.expires_in} def clear_state_data(self, session, state): key = f'_state_{self.name}_{state}' if self.cache: self.cache.delete(key) else: session.pop(key, None) self._clear_session_state(session) def update_token(self, token, refresh_token=None, access_token=None): raise NotImplementedError() @staticmethod def load_config(oauth, name, params): raise NotImplementedError() authlib-1.3.2/authlib/integrations/base_client/registry.py000066400000000000000000000102611466226534200240070ustar00rootroot00000000000000import functools from .framework_integration import FrameworkIntegration __all__ = ['BaseOAuth'] OAUTH_CLIENT_PARAMS = ( 'client_id', 'client_secret', 'request_token_url', 'request_token_params', 'access_token_url', 'access_token_params', 'refresh_token_url', 'refresh_token_params', 'authorize_url', 'authorize_params', 'api_base_url', 'client_kwargs', 'server_metadata_url', ) class BaseOAuth: """Registry for oauth clients. Create an instance for registry:: oauth = OAuth() """ oauth1_client_cls = None oauth2_client_cls = None framework_integration_cls = FrameworkIntegration def __init__(self, cache=None, fetch_token=None, update_token=None): self._registry = {} self._clients = {} self.cache = cache self.fetch_token = fetch_token self.update_token = update_token def create_client(self, name): """Create or get the given named OAuth client. For instance, the OAuth registry has ``.register`` a twitter client, developers may access the client with:: client = oauth.create_client('twitter') :param: name: Name of the remote application :return: OAuth remote app """ if name in self._clients: return self._clients[name] if name not in self._registry: return None overwrite, config = self._registry[name] client_cls = config.pop('client_cls', None) if client_cls and client_cls.OAUTH_APP_CONFIG: kwargs = client_cls.OAUTH_APP_CONFIG kwargs.update(config) else: kwargs = config kwargs = self.generate_client_kwargs(name, overwrite, **kwargs) framework = self.framework_integration_cls(name, self.cache) if client_cls: client = client_cls(framework, name, **kwargs) elif kwargs.get('request_token_url'): client = self.oauth1_client_cls(framework, name, **kwargs) else: client = self.oauth2_client_cls(framework, name, **kwargs) self._clients[name] = client return client def register(self, name, overwrite=False, **kwargs): """Registers a new remote application. :param name: Name of the remote application. :param overwrite: Overwrite existing config with framework settings. :param kwargs: Parameters for :class:`RemoteApp`. Find parameters for the given remote app class. When a remote app is registered, it can be accessed with *named* attribute:: oauth.register('twitter', client_id='', ...) oauth.twitter.get('timeline') """ self._registry[name] = (overwrite, kwargs) return self.create_client(name) def generate_client_kwargs(self, name, overwrite, **kwargs): fetch_token = kwargs.pop('fetch_token', None) update_token = kwargs.pop('update_token', None) config = self.load_config(name, OAUTH_CLIENT_PARAMS) if config: kwargs = _config_client(config, kwargs, overwrite) if not fetch_token and self.fetch_token: fetch_token = functools.partial(self.fetch_token, name) kwargs['fetch_token'] = fetch_token if not kwargs.get('request_token_url'): if not update_token and self.update_token: update_token = functools.partial(self.update_token, name) kwargs['update_token'] = update_token return kwargs def load_config(self, name, params): return self.framework_integration_cls.load_config(self, name, params) def __getattr__(self, key): try: return object.__getattribute__(self, key) except AttributeError: if key in self._registry: return self.create_client(key) raise AttributeError('No such client: %s' % key) def _config_client(config, kwargs, overwrite): for k in OAUTH_CLIENT_PARAMS: v = config.get(k, None) if k not in kwargs: kwargs[k] = v elif overwrite and v: if isinstance(kwargs[k], dict): kwargs[k].update(v) else: kwargs[k] = v return kwargs authlib-1.3.2/authlib/integrations/base_client/sync_app.py000066400000000000000000000304021466226534200237520ustar00rootroot00000000000000import time import logging from authlib.common.urls import urlparse from authlib.consts import default_user_agent from authlib.common.security import generate_token from .errors import ( MismatchingStateError, MissingRequestTokenError, MissingTokenError, ) log = logging.getLogger(__name__) class BaseApp: client_cls = None OAUTH_APP_CONFIG = None def request(self, method, url, token=None, **kwargs): raise NotImplementedError() def get(self, url, **kwargs): """Invoke GET http request. If ``api_base_url`` configured, shortcut is available:: client.get('users/lepture') """ return self.request('GET', url, **kwargs) def post(self, url, **kwargs): """Invoke POST http request. If ``api_base_url`` configured, shortcut is available:: client.post('timeline', json={'text': 'Hi'}) """ return self.request('POST', url, **kwargs) def patch(self, url, **kwargs): """Invoke PATCH http request. If ``api_base_url`` configured, shortcut is available:: client.patch('profile', json={'name': 'Hsiaoming Yang'}) """ return self.request('PATCH', url, **kwargs) def put(self, url, **kwargs): """Invoke PUT http request. If ``api_base_url`` configured, shortcut is available:: client.put('profile', json={'name': 'Hsiaoming Yang'}) """ return self.request('PUT', url, **kwargs) def delete(self, url, **kwargs): """Invoke DELETE http request. If ``api_base_url`` configured, shortcut is available:: client.delete('posts/123') """ return self.request('DELETE', url, **kwargs) class _RequestMixin: def _get_requested_token(self, request): if self._fetch_token and request: return self._fetch_token(request) def _send_token_request(self, session, method, url, token, kwargs): request = kwargs.pop('request', None) withhold_token = kwargs.get('withhold_token') if self.api_base_url and not url.startswith(('https://', 'http://')): url = urlparse.urljoin(self.api_base_url, url) if withhold_token: return session.request(method, url, **kwargs) if token is None: token = self._get_requested_token(request) if token is None: raise MissingTokenError() session.token = token return session.request(method, url, **kwargs) class OAuth1Base: client_cls = None def __init__( self, framework, name=None, fetch_token=None, client_id=None, client_secret=None, request_token_url=None, request_token_params=None, access_token_url=None, access_token_params=None, authorize_url=None, authorize_params=None, api_base_url=None, client_kwargs=None, user_agent=None, **kwargs): self.framework = framework self.name = name self.client_id = client_id self.client_secret = client_secret self.request_token_url = request_token_url self.request_token_params = request_token_params self.access_token_url = access_token_url self.access_token_params = access_token_params self.authorize_url = authorize_url self.authorize_params = authorize_params self.api_base_url = api_base_url self.client_kwargs = client_kwargs or {} self._fetch_token = fetch_token self._user_agent = user_agent or default_user_agent self._kwargs = kwargs def _get_oauth_client(self): session = self.client_cls(self.client_id, self.client_secret, **self.client_kwargs) session.headers['User-Agent'] = self._user_agent return session class OAuth1Mixin(_RequestMixin, OAuth1Base): def request(self, method, url, token=None, **kwargs): with self._get_oauth_client() as session: return self._send_token_request(session, method, url, token, kwargs) def create_authorization_url(self, redirect_uri=None, **kwargs): """Generate the authorization url and state for HTTP redirect. :param redirect_uri: Callback or redirect URI for authorization. :param kwargs: Extra parameters to include. :return: dict """ if not self.authorize_url: raise RuntimeError('Missing "authorize_url" value') if self.authorize_params: kwargs.update(self.authorize_params) with self._get_oauth_client() as client: client.redirect_uri = redirect_uri params = self.request_token_params or {} request_token = client.fetch_request_token(self.request_token_url, **params) log.debug(f'Fetch request token: {request_token!r}') url = client.create_authorization_url(self.authorize_url, **kwargs) state = request_token['oauth_token'] return {'url': url, 'request_token': request_token, 'state': state} def fetch_access_token(self, request_token=None, **kwargs): """Fetch access token in one step. :param request_token: A previous request token for OAuth 1. :param kwargs: Extra parameters to fetch access token. :return: A token dict. """ with self._get_oauth_client() as client: if request_token is None: raise MissingRequestTokenError() # merge request token with verifier token = {} token.update(request_token) token.update(kwargs) client.token = token params = self.access_token_params or {} token = client.fetch_access_token(self.access_token_url, **params) return token class OAuth2Base: client_cls = None def __init__( self, framework, name=None, fetch_token=None, update_token=None, client_id=None, client_secret=None, access_token_url=None, access_token_params=None, authorize_url=None, authorize_params=None, api_base_url=None, client_kwargs=None, server_metadata_url=None, compliance_fix=None, client_auth_methods=None, user_agent=None, **kwargs): self.framework = framework self.name = name self.client_id = client_id self.client_secret = client_secret self.access_token_url = access_token_url self.access_token_params = access_token_params self.authorize_url = authorize_url self.authorize_params = authorize_params self.api_base_url = api_base_url self.client_kwargs = client_kwargs or {} self.compliance_fix = compliance_fix self.client_auth_methods = client_auth_methods self._fetch_token = fetch_token self._update_token = update_token self._user_agent = user_agent or default_user_agent self._server_metadata_url = server_metadata_url self.server_metadata = kwargs def _on_update_token(self, token, refresh_token=None, access_token=None): raise NotImplementedError() def _get_oauth_client(self, **metadata): client_kwargs = {} client_kwargs.update(self.client_kwargs) client_kwargs.update(metadata) if self.authorize_url: client_kwargs['authorization_endpoint'] = self.authorize_url if self.access_token_url: client_kwargs['token_endpoint'] = self.access_token_url session = self.client_cls( client_id=self.client_id, client_secret=self.client_secret, update_token=self._on_update_token, **client_kwargs ) if self.client_auth_methods: for f in self.client_auth_methods: session.register_client_auth_method(f) if self.compliance_fix: self.compliance_fix(session) session.headers['User-Agent'] = self._user_agent return session @staticmethod def _format_state_params(state_data, params): if state_data is None: raise MismatchingStateError() code_verifier = state_data.get('code_verifier') if code_verifier: params['code_verifier'] = code_verifier redirect_uri = state_data.get('redirect_uri') if redirect_uri: params['redirect_uri'] = redirect_uri return params @staticmethod def _create_oauth2_authorization_url(client, authorization_endpoint, **kwargs): rv = {} if client.code_challenge_method: code_verifier = kwargs.get('code_verifier') if not code_verifier: code_verifier = generate_token(48) kwargs['code_verifier'] = code_verifier rv['code_verifier'] = code_verifier log.debug(f'Using code_verifier: {code_verifier!r}') scope = kwargs.get('scope', client.scope) scope = ( (scope if isinstance(scope, (list, tuple)) else scope.split()) if scope else None ) if scope and "openid" in scope: # this is an OpenID Connect service nonce = kwargs.get('nonce') if not nonce: nonce = generate_token(20) kwargs['nonce'] = nonce rv['nonce'] = nonce url, state = client.create_authorization_url( authorization_endpoint, **kwargs) rv['url'] = url rv['state'] = state return rv class OAuth2Mixin(_RequestMixin, OAuth2Base): def _on_update_token(self, token, refresh_token=None, access_token=None): if callable(self._update_token): self._update_token( token, refresh_token=refresh_token, access_token=access_token, ) self.framework.update_token( token, refresh_token=refresh_token, access_token=access_token, ) def request(self, method, url, token=None, **kwargs): metadata = self.load_server_metadata() with self._get_oauth_client(**metadata) as session: return self._send_token_request(session, method, url, token, kwargs) def load_server_metadata(self): if self._server_metadata_url and '_loaded_at' not in self.server_metadata: with self.client_cls(**self.client_kwargs) as session: resp = session.request('GET', self._server_metadata_url, withhold_token=True) resp.raise_for_status() metadata = resp.json() metadata['_loaded_at'] = time.time() self.server_metadata.update(metadata) return self.server_metadata def create_authorization_url(self, redirect_uri=None, **kwargs): """Generate the authorization url and state for HTTP redirect. :param redirect_uri: Callback or redirect URI for authorization. :param kwargs: Extra parameters to include. :return: dict """ metadata = self.load_server_metadata() authorization_endpoint = self.authorize_url or metadata.get('authorization_endpoint') if not authorization_endpoint: raise RuntimeError('Missing "authorize_url" value') if self.authorize_params: kwargs.update(self.authorize_params) with self._get_oauth_client(**metadata) as client: if redirect_uri is not None: client.redirect_uri = redirect_uri return self._create_oauth2_authorization_url( client, authorization_endpoint, **kwargs) def fetch_access_token(self, redirect_uri=None, **kwargs): """Fetch access token in the final step. :param redirect_uri: Callback or Redirect URI that is used in previous :meth:`authorize_redirect`. :param kwargs: Extra parameters to fetch access token. :return: A token dict. """ metadata = self.load_server_metadata() token_endpoint = self.access_token_url or metadata.get('token_endpoint') with self._get_oauth_client(**metadata) as client: if redirect_uri is not None: client.redirect_uri = redirect_uri params = {} if self.access_token_params: params.update(self.access_token_params) params.update(kwargs) token = client.fetch_token(token_endpoint, **params) return token authlib-1.3.2/authlib/integrations/base_client/sync_openid.py000066400000000000000000000054141466226534200244550ustar00rootroot00000000000000from authlib.jose import jwt, JsonWebToken, JsonWebKey from authlib.oidc.core import UserInfo, CodeIDToken, ImplicitIDToken class OpenIDMixin: def fetch_jwk_set(self, force=False): metadata = self.load_server_metadata() jwk_set = metadata.get('jwks') if jwk_set and not force: return jwk_set uri = metadata.get('jwks_uri') if not uri: raise RuntimeError('Missing "jwks_uri" in metadata') with self.client_cls(**self.client_kwargs) as session: resp = session.request('GET', uri, withhold_token=True) resp.raise_for_status() jwk_set = resp.json() self.server_metadata['jwks'] = jwk_set return jwk_set def userinfo(self, **kwargs): """Fetch user info from ``userinfo_endpoint``.""" metadata = self.load_server_metadata() resp = self.get(metadata['userinfo_endpoint'], **kwargs) resp.raise_for_status() data = resp.json() return UserInfo(data) def parse_id_token(self, token, nonce, claims_options=None, leeway=120): """Return an instance of UserInfo from token's ``id_token``.""" if 'id_token' not in token: return None load_key = self.create_load_key() claims_params = dict( nonce=nonce, client_id=self.client_id, ) if 'access_token' in token: claims_params['access_token'] = token['access_token'] claims_cls = CodeIDToken else: claims_cls = ImplicitIDToken metadata = self.load_server_metadata() if claims_options is None and 'issuer' in metadata: claims_options = {'iss': {'values': [metadata['issuer']]}} alg_values = metadata.get('id_token_signing_alg_values_supported') if alg_values: _jwt = JsonWebToken(alg_values) else: _jwt = jwt claims = _jwt.decode( token['id_token'], key=load_key, claims_cls=claims_cls, claims_options=claims_options, claims_params=claims_params, ) # https://github.com/lepture/authlib/issues/259 if claims.get('nonce_supported') is False: claims.params['nonce'] = None claims.validate(leeway=leeway) return UserInfo(claims) def create_load_key(self): def load_key(header, _): jwk_set = JsonWebKey.import_key_set(self.fetch_jwk_set()) try: return jwk_set.find_by_kid(header.get('kid')) except ValueError: # re-try with new jwk set jwk_set = JsonWebKey.import_key_set(self.fetch_jwk_set(force=True)) return jwk_set.find_by_kid(header.get('kid')) return load_key authlib-1.3.2/authlib/integrations/django_client/000077500000000000000000000000001466226534200221155ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/django_client/__init__.py000066400000000000000000000007121466226534200242260ustar00rootroot00000000000000# flake8: noqa from .integration import DjangoIntegration, token_update from .apps import DjangoOAuth1App, DjangoOAuth2App from ..base_client import BaseOAuth, OAuthError class OAuth(BaseOAuth): oauth1_client_cls = DjangoOAuth1App oauth2_client_cls = DjangoOAuth2App framework_integration_cls = DjangoIntegration __all__ = [ 'OAuth', 'DjangoOAuth1App', 'DjangoOAuth2App', 'DjangoIntegration', 'token_update', 'OAuthError', ] authlib-1.3.2/authlib/integrations/django_client/apps.py000066400000000000000000000064061466226534200234400ustar00rootroot00000000000000from django.http import HttpResponseRedirect from ..requests_client import OAuth1Session, OAuth2Session from ..base_client import ( BaseApp, OAuthError, OAuth1Mixin, OAuth2Mixin, OpenIDMixin, ) class DjangoAppMixin: def save_authorize_data(self, request, **kwargs): state = kwargs.pop('state', None) if state: self.framework.set_state_data(request.session, state, kwargs) else: raise RuntimeError('Missing state value') def authorize_redirect(self, request, redirect_uri=None, **kwargs): """Create a HTTP Redirect for Authorization Endpoint. :param request: HTTP request instance from Django view. :param redirect_uri: Callback or redirect URI for authorization. :param kwargs: Extra parameters to include. :return: A HTTP redirect response. """ rv = self.create_authorization_url(redirect_uri, **kwargs) self.save_authorize_data(request, redirect_uri=redirect_uri, **rv) return HttpResponseRedirect(rv['url']) class DjangoOAuth1App(DjangoAppMixin, OAuth1Mixin, BaseApp): client_cls = OAuth1Session def authorize_access_token(self, request, **kwargs): """Fetch access token in one step. :param request: HTTP request instance from Django view. :return: A token dict. """ params = request.GET.dict() state = params.get('oauth_token') if not state: raise OAuthError(description='Missing "oauth_token" parameter') data = self.framework.get_state_data(request.session, state) if not data: raise OAuthError(description='Missing "request_token" in temporary data') params['request_token'] = data['request_token'] params.update(kwargs) self.framework.clear_state_data(request.session, state) return self.fetch_access_token(**params) class DjangoOAuth2App(DjangoAppMixin, OAuth2Mixin, OpenIDMixin, BaseApp): client_cls = OAuth2Session def authorize_access_token(self, request, **kwargs): """Fetch access token in one step. :param request: HTTP request instance from Django view. :return: A token dict. """ if request.method == 'GET': error = request.GET.get('error') if error: description = request.GET.get('error_description') raise OAuthError(error=error, description=description) params = { 'code': request.GET.get('code'), 'state': request.GET.get('state'), } else: params = { 'code': request.POST.get('code'), 'state': request.POST.get('state'), } claims_options = kwargs.pop('claims_options', None) state_data = self.framework.get_state_data(request.session, params.get('state')) self.framework.clear_state_data(request.session, params.get('state')) params = self._format_state_params(state_data, params) token = self.fetch_access_token(**params, **kwargs) if 'id_token' in token and 'nonce' in state_data: userinfo = self.parse_id_token(token, nonce=state_data['nonce'], claims_options=claims_options) token['userinfo'] = userinfo return token authlib-1.3.2/authlib/integrations/django_client/integration.py000066400000000000000000000012121466226534200250060ustar00rootroot00000000000000from django.conf import settings from django.dispatch import Signal from ..base_client import FrameworkIntegration token_update = Signal() class DjangoIntegration(FrameworkIntegration): def update_token(self, token, refresh_token=None, access_token=None): token_update.send( sender=self.__class__, name=self.name, token=token, refresh_token=refresh_token, access_token=access_token, ) @staticmethod def load_config(oauth, name, params): config = getattr(settings, 'AUTHLIB_OAUTH_CLIENTS', None) if config: return config.get(name) authlib-1.3.2/authlib/integrations/django_oauth1/000077500000000000000000000000001466226534200220405ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/django_oauth1/__init__.py000066400000000000000000000003351466226534200241520ustar00rootroot00000000000000# flake8: noqa from .authorization_server import ( BaseServer, CacheAuthorizationServer ) from .resource_protector import ResourceProtector __all__ = ['BaseServer', 'CacheAuthorizationServer', 'ResourceProtector'] authlib-1.3.2/authlib/integrations/django_oauth1/authorization_server.py000066400000000000000000000106701466226534200267040ustar00rootroot00000000000000import logging from authlib.oauth1 import ( OAuth1Request, AuthorizationServer as _AuthorizationServer, ) from authlib.oauth1 import TemporaryCredential from authlib.common.security import generate_token from authlib.common.urls import url_encode from django.core.cache import cache from django.conf import settings from django.http import HttpResponse from .nonce import exists_nonce_in_cache log = logging.getLogger(__name__) class BaseServer(_AuthorizationServer): def __init__(self, client_model, token_model, token_generator=None): self.client_model = client_model self.token_model = token_model if token_generator is None: def token_generator(): return { 'oauth_token': generate_token(42), 'oauth_token_secret': generate_token(48) } self.token_generator = token_generator self._config = getattr(settings, 'AUTHLIB_OAUTH1_PROVIDER', {}) self._nonce_expires_in = self._config.get('nonce_expires_in', 86400) methods = self._config.get('signature_methods') if methods: self.SUPPORTED_SIGNATURE_METHODS = methods def get_client_by_id(self, client_id): try: return self.client_model.objects.get(client_id=client_id) except self.client_model.DoesNotExist: return None def exists_nonce(self, nonce, request): return exists_nonce_in_cache(nonce, request, self._nonce_expires_in) def create_token_credential(self, request): temporary_credential = request.credential token = self.token_generator() item = self.token_model( oauth_token=token['oauth_token'], oauth_token_secret=token['oauth_token_secret'], user_id=temporary_credential.get_user_id(), client_id=temporary_credential.get_client_id() ) item.save() return item def check_authorization_request(self, request): req = self.create_oauth1_request(request) self.validate_authorization_request(req) return req def create_oauth1_request(self, request): if request.method == 'POST': body = request.POST.dict() else: body = None url = request.build_absolute_uri() return OAuth1Request(request.method, url, body, request.headers) def handle_response(self, status_code, payload, headers): resp = HttpResponse(url_encode(payload), status=status_code) for k, v in headers: resp[k] = v return resp class CacheAuthorizationServer(BaseServer): def __init__(self, client_model, token_model, token_generator=None): super().__init__( client_model, token_model, token_generator) self._temporary_expires_in = self._config.get( 'temporary_credential_expires_in', 86400) self._temporary_credential_key_prefix = self._config.get( 'temporary_credential_key_prefix', 'temporary_credential:') def create_temporary_credential(self, request): key_prefix = self._temporary_credential_key_prefix token = self.token_generator() client_id = request.client_id redirect_uri = request.redirect_uri key = key_prefix + token['oauth_token'] token['client_id'] = client_id if redirect_uri: token['oauth_callback'] = redirect_uri cache.set(key, token, timeout=self._temporary_expires_in) return TemporaryCredential(token) def get_temporary_credential(self, request): if not request.token: return None key_prefix = self._temporary_credential_key_prefix key = key_prefix + request.token value = cache.get(key) if value: return TemporaryCredential(value) def delete_temporary_credential(self, request): if request.token: key_prefix = self._temporary_credential_key_prefix key = key_prefix + request.token cache.delete(key) def create_authorization_verifier(self, request): key_prefix = self._temporary_credential_key_prefix verifier = generate_token(36) credential = request.credential user = request.user key = key_prefix + credential.get_oauth_token() credential['oauth_verifier'] = verifier credential['user_id'] = user.pk cache.set(key, credential, timeout=self._temporary_expires_in) return verifier authlib-1.3.2/authlib/integrations/django_oauth1/nonce.py000066400000000000000000000006141466226534200235150ustar00rootroot00000000000000from django.core.cache import cache def exists_nonce_in_cache(nonce, request, timeout): key_prefix = 'nonce:' timestamp = request.timestamp client_id = request.client_id token = request.token key = f'{key_prefix}{nonce}-{timestamp}-{client_id}' if token: key = f'{key}-{token}' rv = bool(cache.get(key)) cache.set(key, 1, timeout=timeout) return rv authlib-1.3.2/authlib/integrations/django_oauth1/resource_protector.py000066400000000000000000000044471466226534200263530ustar00rootroot00000000000000import functools from authlib.oauth1.errors import OAuth1Error from authlib.oauth1 import ResourceProtector as _ResourceProtector from django.http import JsonResponse from django.conf import settings from .nonce import exists_nonce_in_cache class ResourceProtector(_ResourceProtector): def __init__(self, client_model, token_model): self.client_model = client_model self.token_model = token_model config = getattr(settings, 'AUTHLIB_OAUTH1_PROVIDER', {}) methods = config.get('signature_methods', []) if methods and isinstance(methods, (list, tuple)): self.SUPPORTED_SIGNATURE_METHODS = methods self._nonce_expires_in = config.get('nonce_expires_in', 86400) def get_client_by_id(self, client_id): try: return self.client_model.objects.get(client_id=client_id) except self.client_model.DoesNotExist: return None def get_token_credential(self, request): try: return self.token_model.objects.get( client_id=request.client_id, oauth_token=request.token ) except self.token_model.DoesNotExist: return None def exists_nonce(self, nonce, request): return exists_nonce_in_cache(nonce, request, self._nonce_expires_in) def acquire_credential(self, request): if request.method in ['POST', 'PUT']: body = request.POST.dict() else: body = None url = request.build_absolute_uri() req = self.validate_request(request.method, url, body, request.headers) return req.credential def __call__(self, realm=None): def wrapper(f): @functools.wraps(f) def decorated(request, *args, **kwargs): try: credential = self.acquire_credential(request) request.oauth1_credential = credential except OAuth1Error as error: body = dict(error.get_body()) resp = JsonResponse(body, status=error.status_code) resp['Cache-Control'] = 'no-store' resp['Pragma'] = 'no-cache' return resp return f(request, *args, **kwargs) return decorated return wrapper authlib-1.3.2/authlib/integrations/django_oauth2/000077500000000000000000000000001466226534200220415ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/django_oauth2/__init__.py000066400000000000000000000004261466226534200241540ustar00rootroot00000000000000# flake8: noqa from .authorization_server import AuthorizationServer from .resource_protector import ResourceProtector, BearerTokenValidator from .endpoints import RevocationEndpoint from .signals import ( client_authenticated, token_authenticated, token_revoked ) authlib-1.3.2/authlib/integrations/django_oauth2/authorization_server.py000066400000000000000000000104441466226534200267040ustar00rootroot00000000000000from django.http import HttpResponse from django.utils.module_loading import import_string from django.conf import settings from authlib.oauth2 import ( AuthorizationServer as _AuthorizationServer, ) from authlib.oauth2.rfc6750 import BearerTokenGenerator from authlib.common.security import generate_token as _generate_token from authlib.common.encoding import json_dumps from .requests import DjangoOAuth2Request, DjangoJsonRequest from .signals import client_authenticated, token_revoked class AuthorizationServer(_AuthorizationServer): """Django implementation of :class:`authlib.oauth2.rfc6749.AuthorizationServer`. Initialize it with client model and token model:: from authlib.integrations.django_oauth2 import AuthorizationServer from your_project.models import OAuth2Client, OAuth2Token server = AuthorizationServer(OAuth2Client, OAuth2Token) """ def __init__(self, client_model, token_model): self.config = getattr(settings, 'AUTHLIB_OAUTH2_PROVIDER', {}) self.client_model = client_model self.token_model = token_model scopes_supported = self.config.get('scopes_supported') super().__init__(scopes_supported=scopes_supported) # add default token generator self.register_token_generator('default', self.create_bearer_token_generator()) def query_client(self, client_id): """Default method for ``AuthorizationServer.query_client``. Developers MAY rewrite this function to meet their own needs. """ try: return self.client_model.objects.get(client_id=client_id) except self.client_model.DoesNotExist: return None def save_token(self, token, request): """Default method for ``AuthorizationServer.save_token``. Developers MAY rewrite this function to meet their own needs. """ client = request.client if request.user: user_id = request.user.pk else: user_id = client.user_id item = self.token_model( client_id=client.client_id, user_id=user_id, **token ) item.save() return item def create_oauth2_request(self, request): return DjangoOAuth2Request(request) def create_json_request(self, request): return DjangoJsonRequest(request) def handle_response(self, status_code, payload, headers): if isinstance(payload, dict): payload = json_dumps(payload) resp = HttpResponse(payload, status=status_code) for k, v in headers: resp[k] = v return resp def send_signal(self, name, *args, **kwargs): if name == 'after_authenticate_client': client_authenticated.send(sender=self.__class__, *args, **kwargs) elif name == 'after_revoke_token': token_revoked.send(sender=self.__class__, *args, **kwargs) def create_bearer_token_generator(self): """Default method to create BearerToken generator.""" conf = self.config.get('access_token_generator', True) access_token_generator = create_token_generator(conf, 42) conf = self.config.get('refresh_token_generator', False) refresh_token_generator = create_token_generator(conf, 48) conf = self.config.get('token_expires_in') expires_generator = create_token_expires_in_generator(conf) return BearerTokenGenerator( access_token_generator=access_token_generator, refresh_token_generator=refresh_token_generator, expires_generator=expires_generator, ) def create_token_generator(token_generator_conf, length=42): if callable(token_generator_conf): return token_generator_conf if isinstance(token_generator_conf, str): return import_string(token_generator_conf) elif token_generator_conf is True: def token_generator(*args, **kwargs): return _generate_token(length) return token_generator def create_token_expires_in_generator(expires_in_conf=None): data = {} data.update(BearerTokenGenerator.GRANT_TYPES_EXPIRES_IN) if expires_in_conf: data.update(expires_in_conf) def expires_in(client, grant_type): return data.get(grant_type, BearerTokenGenerator.DEFAULT_EXPIRES_IN) return expires_in authlib-1.3.2/authlib/integrations/django_oauth2/endpoints.py000066400000000000000000000034751466226534200244270ustar00rootroot00000000000000from authlib.oauth2.rfc7009 import RevocationEndpoint as _RevocationEndpoint class RevocationEndpoint(_RevocationEndpoint): """The revocation endpoint for OAuth authorization servers allows clients to notify the authorization server that a previously obtained refresh or access token is no longer needed. Register it into authorization server, and create token endpoint response for token revocation:: from django.views.decorators.http import require_http_methods # see register into authorization server instance server.register_endpoint(RevocationEndpoint) @require_http_methods(["POST"]) def revoke_token(request): return server.create_endpoint_response( RevocationEndpoint.ENDPOINT_NAME, request ) """ def query_token(self, token, token_type_hint): """Query requested token from database.""" token_model = self.server.token_model if token_type_hint == 'access_token': rv = _query_access_token(token_model, token) elif token_type_hint == 'refresh_token': rv = _query_refresh_token(token_model, token) else: rv = _query_access_token(token_model, token) if not rv: rv = _query_refresh_token(token_model, token) return rv def revoke_token(self, token, request): """Mark the give token as revoked.""" token.revoked = True token.save() def _query_access_token(token_model, token): try: return token_model.objects.get(access_token=token) except token_model.DoesNotExist: return None def _query_refresh_token(token_model, token): try: return token_model.objects.get(refresh_token=token) except token_model.DoesNotExist: return None authlib-1.3.2/authlib/integrations/django_oauth2/requests.py000066400000000000000000000024511466226534200242700ustar00rootroot00000000000000from collections import defaultdict from django.http import HttpRequest from django.utils.functional import cached_property from authlib.common.encoding import json_loads from authlib.oauth2.rfc6749 import OAuth2Request, JsonRequest class DjangoOAuth2Request(OAuth2Request): def __init__(self, request: HttpRequest): super().__init__(request.method, request.build_absolute_uri(), None, request.headers) self._request = request @property def args(self): return self._request.GET @property def form(self): return self._request.POST @cached_property def data(self): data = {} data.update(self._request.GET.dict()) data.update(self._request.POST.dict()) return data @cached_property def datalist(self): values = defaultdict(list) for k in self.args: values[k].extend(self.args.getlist(k)) for k in self.form: values[k].extend(self.form.getlist(k)) return values class DjangoJsonRequest(JsonRequest): def __init__(self, request: HttpRequest): super().__init__(request.method, request.build_absolute_uri(), None, request.headers) self._request = request @cached_property def data(self): return json_loads(self._request.body) authlib-1.3.2/authlib/integrations/django_oauth2/resource_protector.py000066400000000000000000000050431466226534200263450ustar00rootroot00000000000000import functools from django.http import JsonResponse from authlib.oauth2 import ( OAuth2Error, ResourceProtector as _ResourceProtector, ) from authlib.oauth2.rfc6749 import ( MissingAuthorizationError, ) from authlib.oauth2.rfc6750 import ( BearerTokenValidator as _BearerTokenValidator ) from .requests import DjangoJsonRequest from .signals import token_authenticated class ResourceProtector(_ResourceProtector): def acquire_token(self, request, scopes=None, **kwargs): """A method to acquire current valid token with the given scope. :param request: Django HTTP request instance :param scopes: a list of scope values :return: token object """ req = DjangoJsonRequest(request) # backward compatibility kwargs['scopes'] = scopes for claim in kwargs: if isinstance(kwargs[claim], str): kwargs[claim] = [kwargs[claim]] token = self.validate_request(request=req, **kwargs) token_authenticated.send(sender=self.__class__, token=token) return token def __call__(self, scopes=None, optional=False, **kwargs): claims = kwargs # backward compatibility claims['scopes'] = scopes def wrapper(f): @functools.wraps(f) def decorated(request, *args, **kwargs): try: token = self.acquire_token(request, **claims) request.oauth_token = token except MissingAuthorizationError as error: if optional: request.oauth_token = None return f(request, *args, **kwargs) return return_error_response(error) except OAuth2Error as error: return return_error_response(error) return f(request, *args, **kwargs) return decorated return wrapper class BearerTokenValidator(_BearerTokenValidator): def __init__(self, token_model, realm=None, **extra_attributes): self.token_model = token_model super().__init__(realm, **extra_attributes) def authenticate_token(self, token_string): try: return self.token_model.objects.get(access_token=token_string) except self.token_model.DoesNotExist: return None def return_error_response(error): body = dict(error.get_body()) resp = JsonResponse(body, status=error.status_code) headers = error.get_headers() for k, v in headers: resp[k] = v return resp authlib-1.3.2/authlib/integrations/django_oauth2/signals.py000066400000000000000000000003541466226534200240550ustar00rootroot00000000000000from django.dispatch import Signal #: signal when client is authenticated client_authenticated = Signal() #: signal when token is revoked token_revoked = Signal() #: signal when token is authenticated token_authenticated = Signal() authlib-1.3.2/authlib/integrations/flask_client/000077500000000000000000000000001466226534200217535ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/flask_client/__init__.py000066400000000000000000000032151466226534200240650ustar00rootroot00000000000000from werkzeug.local import LocalProxy from .integration import FlaskIntegration, token_update from .apps import FlaskOAuth1App, FlaskOAuth2App from ..base_client import BaseOAuth, OAuthError class OAuth(BaseOAuth): oauth1_client_cls = FlaskOAuth1App oauth2_client_cls = FlaskOAuth2App framework_integration_cls = FlaskIntegration def __init__(self, app=None, cache=None, fetch_token=None, update_token=None): super().__init__( cache=cache, fetch_token=fetch_token, update_token=update_token) self.app = app if app: self.init_app(app) def init_app(self, app, cache=None, fetch_token=None, update_token=None): """Initialize lazy for Flask app. This is usually used for Flask application factory pattern. """ self.app = app if cache is not None: self.cache = cache if fetch_token: self.fetch_token = fetch_token if update_token: self.update_token = update_token app.extensions = getattr(app, 'extensions', {}) app.extensions['authlib.integrations.flask_client'] = self def create_client(self, name): if not self.app: raise RuntimeError('OAuth is not init with Flask app.') return super().create_client(name) def register(self, name, overwrite=False, **kwargs): self._registry[name] = (overwrite, kwargs) if self.app: return self.create_client(name) return LocalProxy(lambda: self.create_client(name)) __all__ = [ 'OAuth', 'FlaskIntegration', 'FlaskOAuth1App', 'FlaskOAuth2App', 'token_update', 'OAuthError', ] authlib-1.3.2/authlib/integrations/flask_client/apps.py000066400000000000000000000070371466226534200232770ustar00rootroot00000000000000from flask import g, redirect, request, session from ..requests_client import OAuth1Session, OAuth2Session from ..base_client import ( BaseApp, OAuthError, OAuth1Mixin, OAuth2Mixin, OpenIDMixin, ) class FlaskAppMixin: @property def token(self): attr = f'_oauth_token_{self.name}' token = g.get(attr) if token: return token if self._fetch_token: token = self._fetch_token() self.token = token return token @token.setter def token(self, token): attr = f'_oauth_token_{self.name}' setattr(g, attr, token) def _get_requested_token(self, *args, **kwargs): return self.token def save_authorize_data(self, **kwargs): state = kwargs.pop('state', None) if state: self.framework.set_state_data(session, state, kwargs) else: raise RuntimeError('Missing state value') def authorize_redirect(self, redirect_uri=None, **kwargs): """Create a HTTP Redirect for Authorization Endpoint. :param redirect_uri: Callback or redirect URI for authorization. :param kwargs: Extra parameters to include. :return: A HTTP redirect response. """ rv = self.create_authorization_url(redirect_uri, **kwargs) self.save_authorize_data(redirect_uri=redirect_uri, **rv) return redirect(rv['url']) class FlaskOAuth1App(FlaskAppMixin, OAuth1Mixin, BaseApp): client_cls = OAuth1Session def authorize_access_token(self, **kwargs): """Fetch access token in one step. :return: A token dict. """ params = request.args.to_dict(flat=True) state = params.get('oauth_token') if not state: raise OAuthError(description='Missing "oauth_token" parameter') data = self.framework.get_state_data(session, state) if not data: raise OAuthError(description='Missing "request_token" in temporary data') params['request_token'] = data['request_token'] params.update(kwargs) self.framework.clear_state_data(session, state) token = self.fetch_access_token(**params) self.token = token return token class FlaskOAuth2App(FlaskAppMixin, OAuth2Mixin, OpenIDMixin, BaseApp): client_cls = OAuth2Session def authorize_access_token(self, **kwargs): """Fetch access token in one step. :return: A token dict. """ if request.method == 'GET': error = request.args.get('error') if error: description = request.args.get('error_description') raise OAuthError(error=error, description=description) params = { 'code': request.args.get('code'), 'state': request.args.get('state'), } else: params = { 'code': request.form.get('code'), 'state': request.form.get('state'), } claims_options = kwargs.pop('claims_options', None) state_data = self.framework.get_state_data(session, params.get('state')) self.framework.clear_state_data(session, params.get('state')) params = self._format_state_params(state_data, params) token = self.fetch_access_token(**params, **kwargs) self.token = token if 'id_token' in token and 'nonce' in state_data: userinfo = self.parse_id_token(token, nonce=state_data['nonce'], claims_options=claims_options) token['userinfo'] = userinfo return token authlib-1.3.2/authlib/integrations/flask_client/integration.py000066400000000000000000000014451466226534200246540ustar00rootroot00000000000000from flask import current_app from flask.signals import Namespace from ..base_client import FrameworkIntegration _signal = Namespace() #: signal when token is updated token_update = _signal.signal('token_update') class FlaskIntegration(FrameworkIntegration): def update_token(self, token, refresh_token=None, access_token=None): token_update.send( current_app, name=self.name, token=token, refresh_token=refresh_token, access_token=access_token, ) @staticmethod def load_config(oauth, name, params): rv = {} for k in params: conf_key = f'{name}_{k}'.upper() v = oauth.app.config.get(conf_key, None) if v is not None: rv[k] = v return rv authlib-1.3.2/authlib/integrations/flask_oauth1/000077500000000000000000000000001466226534200216765ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/flask_oauth1/__init__.py000066400000000000000000000004041466226534200240050ustar00rootroot00000000000000# flake8: noqa from .authorization_server import AuthorizationServer from .resource_protector import ResourceProtector, current_credential from .cache import ( register_nonce_hooks, register_temporary_credential_hooks, create_exists_nonce_func, ) authlib-1.3.2/authlib/integrations/flask_oauth1/authorization_server.py000066400000000000000000000142331466226534200265410ustar00rootroot00000000000000import logging from werkzeug.utils import import_string from flask import Response from flask import request as flask_req from authlib.oauth1 import ( OAuth1Request, AuthorizationServer as _AuthorizationServer, ) from authlib.common.security import generate_token from authlib.common.urls import url_encode log = logging.getLogger(__name__) class AuthorizationServer(_AuthorizationServer): """Flask implementation of :class:`authlib.rfc5849.AuthorizationServer`. Initialize it with Flask app instance, client model class and cache:: server = AuthorizationServer(app=app, query_client=query_client) # or initialize lazily server = AuthorizationServer() server.init_app(app, query_client=query_client) :param app: A Flask app instance :param query_client: A function to get client by client_id. The client model class MUST implement the methods described by :class:`~authlib.oauth1.rfc5849.ClientMixin`. :param token_generator: A function to generate token """ def __init__(self, app=None, query_client=None, token_generator=None): self.app = app self.query_client = query_client self.token_generator = token_generator self._hooks = { 'exists_nonce': None, 'create_temporary_credential': None, 'get_temporary_credential': None, 'delete_temporary_credential': None, 'create_authorization_verifier': None, 'create_token_credential': None, } if app is not None: self.init_app(app) def init_app(self, app, query_client=None, token_generator=None): if query_client is not None: self.query_client = query_client if token_generator is not None: self.token_generator = token_generator if self.token_generator is None: self.token_generator = self.create_token_generator(app) methods = app.config.get('OAUTH1_SUPPORTED_SIGNATURE_METHODS') if methods and isinstance(methods, (list, tuple)): self.SUPPORTED_SIGNATURE_METHODS = methods self.app = app def register_hook(self, name, func): if name not in self._hooks: raise ValueError('Invalid "name" of hook') self._hooks[name] = func def create_token_generator(self, app): token_generator = app.config.get('OAUTH1_TOKEN_GENERATOR') if isinstance(token_generator, str): token_generator = import_string(token_generator) else: length = app.config.get('OAUTH1_TOKEN_LENGTH', 42) def token_generator(): return generate_token(length) secret_generator = app.config.get('OAUTH1_TOKEN_SECRET_GENERATOR') if isinstance(secret_generator, str): secret_generator = import_string(secret_generator) else: length = app.config.get('OAUTH1_TOKEN_SECRET_LENGTH', 48) def secret_generator(): return generate_token(length) def create_token(): return { 'oauth_token': token_generator(), 'oauth_token_secret': secret_generator() } return create_token def get_client_by_id(self, client_id): return self.query_client(client_id) def exists_nonce(self, nonce, request): func = self._hooks['exists_nonce'] if callable(func): timestamp = request.timestamp client_id = request.client_id token = request.token return func(nonce, timestamp, client_id, token) raise RuntimeError('"exists_nonce" hook is required.') def create_temporary_credential(self, request): func = self._hooks['create_temporary_credential'] if callable(func): token = self.token_generator() return func(token, request.client_id, request.redirect_uri) raise RuntimeError( '"create_temporary_credential" hook is required.' ) def get_temporary_credential(self, request): func = self._hooks['get_temporary_credential'] if callable(func): return func(request.token) raise RuntimeError( '"get_temporary_credential" hook is required.' ) def delete_temporary_credential(self, request): func = self._hooks['delete_temporary_credential'] if callable(func): return func(request.token) raise RuntimeError( '"delete_temporary_credential" hook is required.' ) def create_authorization_verifier(self, request): func = self._hooks['create_authorization_verifier'] if callable(func): verifier = generate_token(36) func(request.credential, request.user, verifier) return verifier raise RuntimeError( '"create_authorization_verifier" hook is required.' ) def create_token_credential(self, request): func = self._hooks['create_token_credential'] if callable(func): temporary_credential = request.credential token = self.token_generator() return func(token, temporary_credential) raise RuntimeError( '"create_token_credential" hook is required.' ) def check_authorization_request(self): req = self.create_oauth1_request(None) self.validate_authorization_request(req) return req def create_authorization_response(self, request=None, grant_user=None): return super()\ .create_authorization_response(request, grant_user) def create_token_response(self, request=None): return super().create_token_response(request) def create_oauth1_request(self, request): if request is None: request = flask_req if request.method in ('POST', 'PUT'): body = request.form.to_dict(flat=True) else: body = None return OAuth1Request(request.method, request.url, body, request.headers) def handle_response(self, status_code, payload, headers): return Response( url_encode(payload), status=status_code, headers=headers ) authlib-1.3.2/authlib/integrations/flask_oauth1/cache.py000066400000000000000000000056641466226534200233260ustar00rootroot00000000000000from authlib.oauth1 import TemporaryCredential def register_temporary_credential_hooks( authorization_server, cache, key_prefix='temporary_credential:'): """Register temporary credential related hooks to authorization server. :param authorization_server: AuthorizationServer instance :param cache: Cache instance :param key_prefix: key prefix for temporary credential """ def create_temporary_credential(token, client_id, redirect_uri): key = key_prefix + token['oauth_token'] token['client_id'] = client_id if redirect_uri: token['oauth_callback'] = redirect_uri cache.set(key, token, timeout=86400) # cache for one day return TemporaryCredential(token) def get_temporary_credential(oauth_token): if not oauth_token: return None key = key_prefix + oauth_token value = cache.get(key) if value: return TemporaryCredential(value) def delete_temporary_credential(oauth_token): if oauth_token: key = key_prefix + oauth_token cache.delete(key) def create_authorization_verifier(credential, grant_user, verifier): key = key_prefix + credential.get_oauth_token() credential['oauth_verifier'] = verifier credential['user_id'] = grant_user.get_user_id() cache.set(key, credential, timeout=86400) return credential authorization_server.register_hook( 'create_temporary_credential', create_temporary_credential) authorization_server.register_hook( 'get_temporary_credential', get_temporary_credential) authorization_server.register_hook( 'delete_temporary_credential', delete_temporary_credential) authorization_server.register_hook( 'create_authorization_verifier', create_authorization_verifier) def create_exists_nonce_func(cache, key_prefix='nonce:', expires=86400): """Create an ``exists_nonce`` function that can be used in hooks and resource protector. :param cache: Cache instance :param key_prefix: key prefix for temporary credential :param expires: Expire time for nonce """ def exists_nonce(nonce, timestamp, client_id, oauth_token): key = f'{key_prefix}{nonce}-{timestamp}-{client_id}' if oauth_token: key = f'{key}-{oauth_token}' rv = cache.has(key) cache.set(key, 1, timeout=expires) return rv return exists_nonce def register_nonce_hooks( authorization_server, cache, key_prefix='nonce:', expires=86400): """Register nonce related hooks to authorization server. :param authorization_server: AuthorizationServer instance :param cache: Cache instance :param key_prefix: key prefix for temporary credential :param expires: Expire time for nonce """ exists_nonce = create_exists_nonce_func(cache, key_prefix, expires) authorization_server.register_hook('exists_nonce', exists_nonce) authlib-1.3.2/authlib/integrations/flask_oauth1/resource_protector.py000066400000000000000000000073351466226534200262100ustar00rootroot00000000000000import functools from flask import g, json, Response from flask import request as _req from werkzeug.local import LocalProxy from authlib.consts import default_json_headers from authlib.oauth1 import ResourceProtector as _ResourceProtector from authlib.oauth1.errors import OAuth1Error class ResourceProtector(_ResourceProtector): """A protecting method for resource servers. Initialize a resource protector with the these method: 1. query_client 2. query_token, 3. exists_nonce Usually, a ``query_client`` method would look like (if using SQLAlchemy):: def query_client(client_id): return Client.query.filter_by(client_id=client_id).first() A ``query_token`` method accept two parameters, ``client_id`` and ``oauth_token``:: def query_token(client_id, oauth_token): return Token.query.filter_by(client_id=client_id, oauth_token=oauth_token).first() And for ``exists_nonce``, if using cache, we have a built-in hook to create this method:: from authlib.integrations.flask_oauth1 import create_exists_nonce_func exists_nonce = create_exists_nonce_func(cache) Then initialize the resource protector with those methods:: require_oauth = ResourceProtector( app, query_client=query_client, query_token=query_token, exists_nonce=exists_nonce, ) """ def __init__(self, app=None, query_client=None, query_token=None, exists_nonce=None): self.query_client = query_client self.query_token = query_token self._exists_nonce = exists_nonce self.app = app if app: self.init_app(app) def init_app(self, app, query_client=None, query_token=None, exists_nonce=None): if query_client is not None: self.query_client = query_client if query_token is not None: self.query_token = query_token if exists_nonce is not None: self._exists_nonce = exists_nonce methods = app.config.get('OAUTH1_SUPPORTED_SIGNATURE_METHODS') if methods and isinstance(methods, (list, tuple)): self.SUPPORTED_SIGNATURE_METHODS = methods self.app = app def get_client_by_id(self, client_id): return self.query_client(client_id) def get_token_credential(self, request): return self.query_token(request.client_id, request.token) def exists_nonce(self, nonce, request): if not self._exists_nonce: raise RuntimeError('"exists_nonce" function is required.') timestamp = request.timestamp client_id = request.client_id token = request.token return self._exists_nonce(nonce, timestamp, client_id, token) def acquire_credential(self): req = self.validate_request( _req.method, _req.url, _req.form.to_dict(flat=True), _req.headers ) g.authlib_server_oauth1_credential = req.credential return req.credential def __call__(self, scope=None): def wrapper(f): @functools.wraps(f) def decorated(*args, **kwargs): try: self.acquire_credential() except OAuth1Error as error: body = dict(error.get_body()) return Response( json.dumps(body), status=error.status_code, headers=default_json_headers, ) return f(*args, **kwargs) return decorated return wrapper def _get_current_credential(): return g.get('authlib_server_oauth1_credential') current_credential = LocalProxy(_get_current_credential) authlib-1.3.2/authlib/integrations/flask_oauth2/000077500000000000000000000000001466226534200216775ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/flask_oauth2/__init__.py000066400000000000000000000003631466226534200240120ustar00rootroot00000000000000# flake8: noqa from .authorization_server import AuthorizationServer from .resource_protector import ( ResourceProtector, current_token, ) from .signals import ( client_authenticated, token_authenticated, token_revoked, ) authlib-1.3.2/authlib/integrations/flask_oauth2/authorization_server.py000066400000000000000000000133431466226534200265430ustar00rootroot00000000000000from werkzeug.utils import import_string from flask import Response, json from flask import request as flask_req from authlib.oauth2 import ( AuthorizationServer as _AuthorizationServer, ) from authlib.oauth2.rfc6750 import BearerTokenGenerator from authlib.common.security import generate_token from .requests import FlaskOAuth2Request, FlaskJsonRequest from .signals import client_authenticated, token_revoked class AuthorizationServer(_AuthorizationServer): """Flask implementation of :class:`authlib.oauth2.rfc6749.AuthorizationServer`. Initialize it with ``query_client``, ``save_token`` methods and Flask app instance:: def query_client(client_id): return Client.query.filter_by(client_id=client_id).first() def save_token(token, request): if request.user: user_id = request.user.id else: user_id = None client = request.client tok = Token( client_id=client.client_id, user_id=user.id, **token ) db.session.add(tok) db.session.commit() server = AuthorizationServer(app, query_client, save_token) # or initialize lazily server = AuthorizationServer() server.init_app(app, query_client, save_token) """ def __init__(self, app=None, query_client=None, save_token=None): super().__init__() self._query_client = query_client self._save_token = save_token self._error_uris = None if app is not None: self.init_app(app) def init_app(self, app, query_client=None, save_token=None): """Initialize later with Flask app instance.""" if query_client is not None: self._query_client = query_client if save_token is not None: self._save_token = save_token self.register_token_generator('default', self.create_bearer_token_generator(app.config)) self.scopes_supported = app.config.get('OAUTH2_SCOPES_SUPPORTED') self._error_uris = app.config.get('OAUTH2_ERROR_URIS') def query_client(self, client_id): return self._query_client(client_id) def save_token(self, token, request): return self._save_token(token, request) def get_error_uri(self, request, error): if self._error_uris: uris = dict(self._error_uris) return uris.get(error.error) def create_oauth2_request(self, request): return FlaskOAuth2Request(flask_req) def create_json_request(self, request): return FlaskJsonRequest(flask_req) def handle_response(self, status_code, payload, headers): if isinstance(payload, dict): payload = json.dumps(payload) return Response(payload, status=status_code, headers=headers) def send_signal(self, name, *args, **kwargs): if name == 'after_authenticate_client': client_authenticated.send(self, *args, **kwargs) elif name == 'after_revoke_token': token_revoked.send(self, *args, **kwargs) def create_bearer_token_generator(self, config): """Create a generator function for generating ``token`` value. This method will create a Bearer Token generator with :class:`authlib.oauth2.rfc6750.BearerToken`. Configurable settings: 1. OAUTH2_ACCESS_TOKEN_GENERATOR: Boolean or import string, default is True. 2. OAUTH2_REFRESH_TOKEN_GENERATOR: Boolean or import string, default is False. 3. OAUTH2_TOKEN_EXPIRES_IN: Dict or import string, default is None. By default, it will not generate ``refresh_token``, which can be turn on by configure ``OAUTH2_REFRESH_TOKEN_GENERATOR``. Here are some examples of the token generator:: OAUTH2_ACCESS_TOKEN_GENERATOR = 'your_project.generators.gen_token' # and in module `your_project.generators`, you can define: def gen_token(client, grant_type, user, scope): # generate token according to these parameters token = create_random_token() return f'{client.id}-{user.id}-{token}' Here is an example of ``OAUTH2_TOKEN_EXPIRES_IN``:: OAUTH2_TOKEN_EXPIRES_IN = { 'authorization_code': 864000, 'urn:ietf:params:oauth:grant-type:jwt-bearer': 3600, } """ conf = config.get('OAUTH2_ACCESS_TOKEN_GENERATOR', True) access_token_generator = create_token_generator(conf, 42) conf = config.get('OAUTH2_REFRESH_TOKEN_GENERATOR', False) refresh_token_generator = create_token_generator(conf, 48) expires_conf = config.get('OAUTH2_TOKEN_EXPIRES_IN') expires_generator = create_token_expires_in_generator(expires_conf) return BearerTokenGenerator( access_token_generator, refresh_token_generator, expires_generator ) def create_token_expires_in_generator(expires_in_conf=None): if isinstance(expires_in_conf, str): return import_string(expires_in_conf) data = {} data.update(BearerTokenGenerator.GRANT_TYPES_EXPIRES_IN) if isinstance(expires_in_conf, dict): data.update(expires_in_conf) def expires_in(client, grant_type): return data.get(grant_type, BearerTokenGenerator.DEFAULT_EXPIRES_IN) return expires_in def create_token_generator(token_generator_conf, length=42): if callable(token_generator_conf): return token_generator_conf if isinstance(token_generator_conf, str): return import_string(token_generator_conf) elif token_generator_conf is True: def token_generator(*args, **kwargs): return generate_token(length) return token_generator authlib-1.3.2/authlib/integrations/flask_oauth2/errors.py000066400000000000000000000021131466226534200235620ustar00rootroot00000000000000import importlib.metadata import werkzeug from werkzeug.exceptions import HTTPException _version = importlib.metadata.version('werkzeug').split('.')[0] if _version in ('0', '1'): class _HTTPException(HTTPException): def __init__(self, code, body, headers, response=None): super().__init__(None, response) self.code = code self.body = body self.headers = headers def get_body(self, environ=None): return self.body def get_headers(self, environ=None): return self.headers else: class _HTTPException(HTTPException): def __init__(self, code, body, headers, response=None): super().__init__(None, response) self.code = code self.body = body self.headers = headers def get_body(self, environ=None, scope=None): return self.body def get_headers(self, environ=None, scope=None): return self.headers def raise_http_exception(status, body, headers): raise _HTTPException(status, body, headers) authlib-1.3.2/authlib/integrations/flask_oauth2/requests.py000066400000000000000000000017761466226534200241370ustar00rootroot00000000000000from collections import defaultdict from functools import cached_property from flask.wrappers import Request from authlib.oauth2.rfc6749 import OAuth2Request, JsonRequest class FlaskOAuth2Request(OAuth2Request): def __init__(self, request: Request): super().__init__(request.method, request.url, None, request.headers) self._request = request @property def args(self): return self._request.args @property def form(self): return self._request.form @property def data(self): return self._request.values @cached_property def datalist(self): values = defaultdict(list) for k in self.data: values[k].extend(self.data.getlist(k)) return values class FlaskJsonRequest(JsonRequest): def __init__(self, request: Request): super().__init__(request.method, request.url, None, request.headers) self._request = request @property def data(self): return self._request.get_json() authlib-1.3.2/authlib/integrations/flask_oauth2/resource_protector.py000066400000000000000000000074031466226534200262050ustar00rootroot00000000000000import functools from contextlib import contextmanager from flask import g, json from flask import request as _req from werkzeug.local import LocalProxy from authlib.oauth2 import ( OAuth2Error, ResourceProtector as _ResourceProtector ) from authlib.oauth2.rfc6749 import ( MissingAuthorizationError, ) from .requests import FlaskJsonRequest from .signals import token_authenticated from .errors import raise_http_exception class ResourceProtector(_ResourceProtector): """A protecting method for resource servers. Creating a ``require_oauth`` decorator easily with ResourceProtector:: from authlib.integrations.flask_oauth2 import ResourceProtector require_oauth = ResourceProtector() # add bearer token validator from authlib.oauth2.rfc6750 import BearerTokenValidator from project.models import Token class MyBearerTokenValidator(BearerTokenValidator): def authenticate_token(self, token_string): return Token.query.filter_by(access_token=token_string).first() require_oauth.register_token_validator(MyBearerTokenValidator()) # protect resource with require_oauth @app.route('/user') @require_oauth(['profile']) def user_profile(): user = User.get(current_token.user_id) return jsonify(user.to_dict()) """ def raise_error_response(self, error): """Raise HTTPException for OAuth2Error. Developers can re-implement this method to customize the error response. :param error: OAuth2Error :raise: HTTPException """ status = error.status_code body = json.dumps(dict(error.get_body())) headers = error.get_headers() raise_http_exception(status, body, headers) def acquire_token(self, scopes=None, **kwargs): """A method to acquire current valid token with the given scope. :param scopes: a list of scope values :return: token object """ request = FlaskJsonRequest(_req) # backward compatibility kwargs['scopes'] = scopes for claim in kwargs: if isinstance(kwargs[claim], str): kwargs[claim] = [kwargs[claim]] token = self.validate_request(request=request, **kwargs) token_authenticated.send(self, token=token) g.authlib_server_oauth2_token = token return token @contextmanager def acquire(self, scopes=None): """The with statement of ``require_oauth``. Instead of using a decorator, you can use a with statement instead:: @app.route('/api/user') def user_api(): with require_oauth.acquire('profile') as token: user = User.get(token.user_id) return jsonify(user.to_dict()) """ try: yield self.acquire_token(scopes) except OAuth2Error as error: self.raise_error_response(error) def __call__(self, scopes=None, optional=False, **kwargs): claims = kwargs # backward compatibility claims['scopes'] = scopes def wrapper(f): @functools.wraps(f) def decorated(*args, **kwargs): try: self.acquire_token(**claims) except MissingAuthorizationError as error: if optional: return f(*args, **kwargs) self.raise_error_response(error) except OAuth2Error as error: self.raise_error_response(error) return f(*args, **kwargs) return decorated return wrapper def _get_current_token(): return g.get('authlib_server_oauth2_token') current_token = LocalProxy(_get_current_token) authlib-1.3.2/authlib/integrations/flask_oauth2/signals.py000066400000000000000000000005251466226534200237130ustar00rootroot00000000000000from flask.signals import Namespace _signal = Namespace() #: signal when client is authenticated client_authenticated = _signal.signal('client_authenticated') #: signal when token is revoked token_revoked = _signal.signal('token_revoked') #: signal when token is authenticated token_authenticated = _signal.signal('token_authenticated') authlib-1.3.2/authlib/integrations/httpx_client/000077500000000000000000000000001466226534200220225ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/httpx_client/__init__.py000066400000000000000000000014441466226534200241360ustar00rootroot00000000000000from authlib.oauth1 import ( SIGNATURE_HMAC_SHA1, SIGNATURE_RSA_SHA1, SIGNATURE_PLAINTEXT, SIGNATURE_TYPE_HEADER, SIGNATURE_TYPE_QUERY, SIGNATURE_TYPE_BODY, ) from .oauth1_client import OAuth1Auth, AsyncOAuth1Client, OAuth1Client from .oauth2_client import ( OAuth2Auth, OAuth2Client, OAuth2ClientAuth, AsyncOAuth2Client, ) from .assertion_client import AssertionClient, AsyncAssertionClient from ..base_client import OAuthError __all__ = [ 'OAuthError', 'OAuth1Auth', 'AsyncOAuth1Client', 'SIGNATURE_HMAC_SHA1', 'SIGNATURE_RSA_SHA1', 'SIGNATURE_PLAINTEXT', 'SIGNATURE_TYPE_HEADER', 'SIGNATURE_TYPE_QUERY', 'SIGNATURE_TYPE_BODY', 'OAuth2Auth', 'OAuth2ClientAuth', 'OAuth2Client', 'AsyncOAuth2Client', 'AssertionClient', 'AsyncAssertionClient', ] authlib-1.3.2/authlib/integrations/httpx_client/assertion_client.py000066400000000000000000000061531466226534200257460ustar00rootroot00000000000000import httpx from httpx import Response, USE_CLIENT_DEFAULT from authlib.oauth2.rfc7521 import AssertionClient as _AssertionClient from authlib.oauth2.rfc7523 import JWTBearerGrant from .utils import extract_client_kwargs from .oauth2_client import OAuth2Auth from ..base_client import OAuthError __all__ = ['AsyncAssertionClient'] class AsyncAssertionClient(_AssertionClient, httpx.AsyncClient): token_auth_class = OAuth2Auth oauth_error_class = OAuthError JWT_BEARER_GRANT_TYPE = JWTBearerGrant.GRANT_TYPE ASSERTION_METHODS = { JWT_BEARER_GRANT_TYPE: JWTBearerGrant.sign, } DEFAULT_GRANT_TYPE = JWT_BEARER_GRANT_TYPE def __init__(self, token_endpoint, issuer, subject, audience=None, grant_type=None, claims=None, token_placement='header', scope=None, **kwargs): client_kwargs = extract_client_kwargs(kwargs) httpx.AsyncClient.__init__(self, **client_kwargs) _AssertionClient.__init__( self, session=None, token_endpoint=token_endpoint, issuer=issuer, subject=subject, audience=audience, grant_type=grant_type, claims=claims, token_placement=token_placement, scope=scope, **kwargs ) async def request(self, method, url, withhold_token=False, auth=USE_CLIENT_DEFAULT, **kwargs) -> Response: """Send request with auto refresh token feature.""" if not withhold_token and auth is USE_CLIENT_DEFAULT: if not self.token or self.token.is_expired(): await self.refresh_token() auth = self.token_auth return await super().request( method, url, auth=auth, **kwargs) async def _refresh_token(self, data): resp = await self.request( 'POST', self.token_endpoint, data=data, withhold_token=True) return self.parse_response_token(resp) class AssertionClient(_AssertionClient, httpx.Client): token_auth_class = OAuth2Auth oauth_error_class = OAuthError JWT_BEARER_GRANT_TYPE = JWTBearerGrant.GRANT_TYPE ASSERTION_METHODS = { JWT_BEARER_GRANT_TYPE: JWTBearerGrant.sign, } DEFAULT_GRANT_TYPE = JWT_BEARER_GRANT_TYPE def __init__(self, token_endpoint, issuer, subject, audience=None, grant_type=None, claims=None, token_placement='header', scope=None, **kwargs): client_kwargs = extract_client_kwargs(kwargs) httpx.Client.__init__(self, **client_kwargs) _AssertionClient.__init__( self, session=self, token_endpoint=token_endpoint, issuer=issuer, subject=subject, audience=audience, grant_type=grant_type, claims=claims, token_placement=token_placement, scope=scope, **kwargs ) def request(self, method, url, withhold_token=False, auth=USE_CLIENT_DEFAULT, **kwargs): """Send request with auto refresh token feature.""" if not withhold_token and auth is USE_CLIENT_DEFAULT: if not self.token or self.token.is_expired(): self.refresh_token() auth = self.token_auth return super().request( method, url, auth=auth, **kwargs) authlib-1.3.2/authlib/integrations/httpx_client/oauth1_client.py000066400000000000000000000077641466226534200251510ustar00rootroot00000000000000import typing import httpx from httpx import Auth, Request, Response from authlib.oauth1 import ( SIGNATURE_HMAC_SHA1, SIGNATURE_TYPE_HEADER, ) from authlib.common.encoding import to_unicode from authlib.oauth1 import ClientAuth from authlib.oauth1.client import OAuth1Client as _OAuth1Client from .utils import build_request, extract_client_kwargs from ..base_client import OAuthError class OAuth1Auth(Auth, ClientAuth): """Signs the httpx request using OAuth 1 (RFC5849)""" requires_request_body = True def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: url, headers, body = self.prepare( request.method, str(request.url), request.headers, request.content) headers['Content-Length'] = str(len(body)) yield build_request(url=url, headers=headers, body=body, initial_request=request) class AsyncOAuth1Client(_OAuth1Client, httpx.AsyncClient): auth_class = OAuth1Auth def __init__(self, client_id, client_secret=None, token=None, token_secret=None, redirect_uri=None, rsa_key=None, verifier=None, signature_method=SIGNATURE_HMAC_SHA1, signature_type=SIGNATURE_TYPE_HEADER, force_include_body=False, **kwargs): _client_kwargs = extract_client_kwargs(kwargs) httpx.AsyncClient.__init__(self, **_client_kwargs) _OAuth1Client.__init__( self, None, client_id=client_id, client_secret=client_secret, token=token, token_secret=token_secret, redirect_uri=redirect_uri, rsa_key=rsa_key, verifier=verifier, signature_method=signature_method, signature_type=signature_type, force_include_body=force_include_body, **kwargs) async def fetch_access_token(self, url, verifier=None, **kwargs): """Method for fetching an access token from the token endpoint. This is the final step in the OAuth 1 workflow. An access token is obtained using all previously obtained credentials, including the verifier from the authorization step. :param url: Access Token endpoint. :param verifier: A verifier string to prove authorization was granted. :param kwargs: Extra parameters to include for fetching access token. :return: A token dict. """ if verifier: self.auth.verifier = verifier if not self.auth.verifier: self.handle_error('missing_verifier', 'Missing "verifier" value') token = await self._fetch_token(url, **kwargs) self.auth.verifier = None return token async def _fetch_token(self, url, **kwargs): resp = await self.post(url, **kwargs) text = await resp.aread() token = self.parse_response_token(resp.status_code, to_unicode(text)) self.token = token return token @staticmethod def handle_error(error_type, error_description): raise OAuthError(error_type, error_description) class OAuth1Client(_OAuth1Client, httpx.Client): auth_class = OAuth1Auth def __init__(self, client_id, client_secret=None, token=None, token_secret=None, redirect_uri=None, rsa_key=None, verifier=None, signature_method=SIGNATURE_HMAC_SHA1, signature_type=SIGNATURE_TYPE_HEADER, force_include_body=False, **kwargs): _client_kwargs = extract_client_kwargs(kwargs) httpx.Client.__init__(self, **_client_kwargs) _OAuth1Client.__init__( self, self, client_id=client_id, client_secret=client_secret, token=token, token_secret=token_secret, redirect_uri=redirect_uri, rsa_key=rsa_key, verifier=verifier, signature_method=signature_method, signature_type=signature_type, force_include_body=force_include_body, **kwargs) @staticmethod def handle_error(error_type, error_description): raise OAuthError(error_type, error_description) authlib-1.3.2/authlib/integrations/httpx_client/oauth2_client.py000066400000000000000000000204431466226534200251370ustar00rootroot00000000000000import typing from contextlib import asynccontextmanager import httpx from httpx import Auth, Request, Response, USE_CLIENT_DEFAULT from anyio import Lock # Import after httpx so import errors refer to httpx from authlib.common.urls import url_decode from authlib.oauth2.client import OAuth2Client as _OAuth2Client from authlib.oauth2.auth import ClientAuth, TokenAuth from .utils import HTTPX_CLIENT_KWARGS, build_request from ..base_client import ( OAuthError, InvalidTokenError, MissingTokenError, UnsupportedTokenTypeError, ) __all__ = [ 'OAuth2Auth', 'OAuth2ClientAuth', 'AsyncOAuth2Client', 'OAuth2Client', ] class OAuth2Auth(Auth, TokenAuth): """Sign requests for OAuth 2.0, currently only bearer token is supported.""" requires_request_body = True def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: try: url, headers, body = self.prepare( str(request.url), request.headers, request.content) headers['Content-Length'] = str(len(body)) yield build_request(url=url, headers=headers, body=body, initial_request=request) except KeyError as error: description = f'Unsupported token_type: {str(error)}' raise UnsupportedTokenTypeError(description=description) class OAuth2ClientAuth(Auth, ClientAuth): requires_request_body = True def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: url, headers, body = self.prepare( request.method, str(request.url), request.headers, request.content) headers['Content-Length'] = str(len(body)) yield build_request(url=url, headers=headers, body=body, initial_request=request) class AsyncOAuth2Client(_OAuth2Client, httpx.AsyncClient): SESSION_REQUEST_PARAMS = HTTPX_CLIENT_KWARGS client_auth_class = OAuth2ClientAuth token_auth_class = OAuth2Auth oauth_error_class = OAuthError def __init__(self, client_id=None, client_secret=None, token_endpoint_auth_method=None, revocation_endpoint_auth_method=None, scope=None, redirect_uri=None, token=None, token_placement='header', update_token=None, leeway=60, **kwargs): # extract httpx.Client kwargs client_kwargs = self._extract_session_request_params(kwargs) httpx.AsyncClient.__init__(self, **client_kwargs) # We use a Lock to synchronize coroutines to prevent # multiple concurrent attempts to refresh the same token self._token_refresh_lock = Lock() _OAuth2Client.__init__( self, session=None, client_id=client_id, client_secret=client_secret, token_endpoint_auth_method=token_endpoint_auth_method, revocation_endpoint_auth_method=revocation_endpoint_auth_method, scope=scope, redirect_uri=redirect_uri, token=token, token_placement=token_placement, update_token=update_token, leeway=leeway, **kwargs ) async def request(self, method, url, withhold_token=False, auth=USE_CLIENT_DEFAULT, **kwargs): if not withhold_token and auth is USE_CLIENT_DEFAULT: if not self.token: raise MissingTokenError() await self.ensure_active_token(self.token) auth = self.token_auth return await super().request( method, url, auth=auth, **kwargs) @asynccontextmanager async def stream(self, method, url, withhold_token=False, auth=USE_CLIENT_DEFAULT, **kwargs): if not withhold_token and auth is USE_CLIENT_DEFAULT: if not self.token: raise MissingTokenError() await self.ensure_active_token(self.token) auth = self.token_auth async with super().stream( method, url, auth=auth, **kwargs) as resp: yield resp async def ensure_active_token(self, token): async with self._token_refresh_lock: if self.token.is_expired(leeway=self.leeway): refresh_token = token.get('refresh_token') url = self.metadata.get('token_endpoint') if refresh_token and url: await self.refresh_token(url, refresh_token=refresh_token) elif self.metadata.get('grant_type') == 'client_credentials': access_token = token['access_token'] new_token = await self.fetch_token(url, grant_type='client_credentials') if self.update_token: await self.update_token(new_token, access_token=access_token) else: raise InvalidTokenError() async def _fetch_token(self, url, body='', headers=None, auth=USE_CLIENT_DEFAULT, method='POST', **kwargs): if method.upper() == 'POST': resp = await self.post( url, data=dict(url_decode(body)), headers=headers, auth=auth, **kwargs) else: if '?' in url: url = '&'.join([url, body]) else: url = '?'.join([url, body]) resp = await self.get(url, headers=headers, auth=auth, **kwargs) for hook in self.compliance_hook['access_token_response']: resp = hook(resp) return self.parse_response_token(resp) async def _refresh_token(self, url, refresh_token=None, body='', headers=None, auth=USE_CLIENT_DEFAULT, **kwargs): resp = await self.post( url, data=dict(url_decode(body)), headers=headers, auth=auth, **kwargs) for hook in self.compliance_hook['refresh_token_response']: resp = hook(resp) token = self.parse_response_token(resp) if 'refresh_token' not in token: self.token['refresh_token'] = refresh_token if self.update_token: await self.update_token(self.token, refresh_token=refresh_token) return self.token def _http_post(self, url, body=None, auth=USE_CLIENT_DEFAULT, headers=None, **kwargs): return self.post( url, data=dict(url_decode(body)), headers=headers, auth=auth, **kwargs) class OAuth2Client(_OAuth2Client, httpx.Client): SESSION_REQUEST_PARAMS = HTTPX_CLIENT_KWARGS client_auth_class = OAuth2ClientAuth token_auth_class = OAuth2Auth oauth_error_class = OAuthError def __init__(self, client_id=None, client_secret=None, token_endpoint_auth_method=None, revocation_endpoint_auth_method=None, scope=None, redirect_uri=None, token=None, token_placement='header', update_token=None, **kwargs): # extract httpx.Client kwargs client_kwargs = self._extract_session_request_params(kwargs) httpx.Client.__init__(self, **client_kwargs) _OAuth2Client.__init__( self, session=self, client_id=client_id, client_secret=client_secret, token_endpoint_auth_method=token_endpoint_auth_method, revocation_endpoint_auth_method=revocation_endpoint_auth_method, scope=scope, redirect_uri=redirect_uri, token=token, token_placement=token_placement, update_token=update_token, **kwargs ) @staticmethod def handle_error(error_type, error_description): raise OAuthError(error_type, error_description) def request(self, method, url, withhold_token=False, auth=USE_CLIENT_DEFAULT, **kwargs): if not withhold_token and auth is USE_CLIENT_DEFAULT: if not self.token: raise MissingTokenError() if not self.ensure_active_token(self.token): raise InvalidTokenError() auth = self.token_auth return super().request( method, url, auth=auth, **kwargs) def stream(self, method, url, withhold_token=False, auth=USE_CLIENT_DEFAULT, **kwargs): if not withhold_token and auth is USE_CLIENT_DEFAULT: if not self.token: raise MissingTokenError() if not self.ensure_active_token(self.token): raise InvalidTokenError() auth = self.token_auth return super().stream( method, url, auth=auth, **kwargs) authlib-1.3.2/authlib/integrations/httpx_client/utils.py000066400000000000000000000015701466226534200235370ustar00rootroot00000000000000from httpx import Request HTTPX_CLIENT_KWARGS = [ 'headers', 'cookies', 'verify', 'cert', 'http1', 'http2', 'proxies', 'timeout', 'follow_redirects', 'limits', 'max_redirects', 'event_hooks', 'base_url', 'transport', 'app', 'trust_env', ] def extract_client_kwargs(kwargs): client_kwargs = {} for k in HTTPX_CLIENT_KWARGS: if k in kwargs: client_kwargs[k] = kwargs.pop(k) return client_kwargs def build_request(url, headers, body, initial_request: Request) -> Request: """Make sure that all the data from initial request is passed to the updated object""" updated_request = Request( method=initial_request.method, url=url, headers=headers, content=body ) if hasattr(initial_request, 'extensions'): updated_request.extensions = initial_request.extensions return updated_request authlib-1.3.2/authlib/integrations/requests_client/000077500000000000000000000000001466226534200225265ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/requests_client/__init__.py000066400000000000000000000012141466226534200246350ustar00rootroot00000000000000from .oauth1_session import OAuth1Session, OAuth1Auth from .oauth2_session import OAuth2Session, OAuth2Auth from .assertion_session import AssertionSession from ..base_client import OAuthError from authlib.oauth1 import ( SIGNATURE_HMAC_SHA1, SIGNATURE_RSA_SHA1, SIGNATURE_PLAINTEXT, SIGNATURE_TYPE_HEADER, SIGNATURE_TYPE_QUERY, SIGNATURE_TYPE_BODY, ) __all__ = [ 'OAuthError', 'OAuth1Session', 'OAuth1Auth', 'SIGNATURE_HMAC_SHA1', 'SIGNATURE_RSA_SHA1', 'SIGNATURE_PLAINTEXT', 'SIGNATURE_TYPE_HEADER', 'SIGNATURE_TYPE_QUERY', 'SIGNATURE_TYPE_BODY', 'OAuth2Session', 'OAuth2Auth', 'AssertionSession', ] authlib-1.3.2/authlib/integrations/requests_client/assertion_session.py000066400000000000000000000035471466226534200266630ustar00rootroot00000000000000from requests import Session from authlib.oauth2.rfc7521 import AssertionClient from authlib.oauth2.rfc7523 import JWTBearerGrant from .oauth2_session import OAuth2Auth from .utils import update_session_configure class AssertionAuth(OAuth2Auth): def ensure_active_token(self): if self.client and (not self.token or self.token.is_expired(self.client.leeway)): return self.client.refresh_token() class AssertionSession(AssertionClient, Session): """Constructs a new Assertion Framework for OAuth 2.0 Authorization Grants per RFC7521_. .. _RFC7521: https://tools.ietf.org/html/rfc7521 """ token_auth_class = AssertionAuth JWT_BEARER_GRANT_TYPE = JWTBearerGrant.GRANT_TYPE ASSERTION_METHODS = { JWT_BEARER_GRANT_TYPE: JWTBearerGrant.sign, } DEFAULT_GRANT_TYPE = JWT_BEARER_GRANT_TYPE def __init__(self, token_endpoint, issuer, subject, audience=None, grant_type=None, claims=None, token_placement='header', scope=None, default_timeout=None, leeway=60, **kwargs): Session.__init__(self) self.default_timeout = default_timeout update_session_configure(self, kwargs) AssertionClient.__init__( self, session=self, token_endpoint=token_endpoint, issuer=issuer, subject=subject, audience=audience, grant_type=grant_type, claims=claims, token_placement=token_placement, scope=scope, leeway=leeway, **kwargs ) def request(self, method, url, withhold_token=False, auth=None, **kwargs): """Send request with auto refresh token feature.""" if self.default_timeout: kwargs.setdefault('timeout', self.default_timeout) if not withhold_token and auth is None: auth = self.token_auth return super().request( method, url, auth=auth, **kwargs) authlib-1.3.2/authlib/integrations/requests_client/oauth1_session.py000066400000000000000000000042411466226534200260450ustar00rootroot00000000000000from requests import Session from requests.auth import AuthBase from authlib.oauth1 import ( SIGNATURE_HMAC_SHA1, SIGNATURE_TYPE_HEADER, ) from authlib.common.encoding import to_native from authlib.oauth1 import ClientAuth from authlib.oauth1.client import OAuth1Client from ..base_client import OAuthError from .utils import update_session_configure class OAuth1Auth(AuthBase, ClientAuth): """Signs the request using OAuth 1 (RFC5849)""" def __call__(self, req): url, headers, body = self.prepare( req.method, req.url, req.headers, req.body) req.url = to_native(url) req.prepare_headers(headers) if body: req.body = body return req class OAuth1Session(OAuth1Client, Session): auth_class = OAuth1Auth def __init__(self, client_id, client_secret=None, token=None, token_secret=None, redirect_uri=None, rsa_key=None, verifier=None, signature_method=SIGNATURE_HMAC_SHA1, signature_type=SIGNATURE_TYPE_HEADER, force_include_body=False, **kwargs): Session.__init__(self) update_session_configure(self, kwargs) OAuth1Client.__init__( self, session=self, client_id=client_id, client_secret=client_secret, token=token, token_secret=token_secret, redirect_uri=redirect_uri, rsa_key=rsa_key, verifier=verifier, signature_method=signature_method, signature_type=signature_type, force_include_body=force_include_body, **kwargs) def rebuild_auth(self, prepared_request, response): """When being redirected we should always strip Authorization header, since nonce may not be reused as per OAuth spec. """ if 'Authorization' in prepared_request.headers: # If we get redirected to a new host, we should strip out # any authentication headers. prepared_request.headers.pop('Authorization', True) prepared_request.prepare_auth(self.auth) @staticmethod def handle_error(error_type, error_description): raise OAuthError(error_type, error_description) authlib-1.3.2/authlib/integrations/requests_client/oauth2_session.py000066400000000000000000000111341466226534200260450ustar00rootroot00000000000000from requests import Session from requests.auth import AuthBase from authlib.oauth2.client import OAuth2Client from authlib.oauth2.auth import ClientAuth, TokenAuth from ..base_client import ( OAuthError, InvalidTokenError, MissingTokenError, UnsupportedTokenTypeError, ) from .utils import update_session_configure __all__ = ['OAuth2Session', 'OAuth2Auth'] class OAuth2Auth(AuthBase, TokenAuth): """Sign requests for OAuth 2.0, currently only bearer token is supported.""" def ensure_active_token(self): if self.client and not self.client.ensure_active_token(self.token): raise InvalidTokenError() def __call__(self, req): self.ensure_active_token() try: req.url, req.headers, req.body = self.prepare( req.url, req.headers, req.body) except KeyError as error: description = f'Unsupported token_type: {str(error)}' raise UnsupportedTokenTypeError(description=description) return req class OAuth2ClientAuth(AuthBase, ClientAuth): """Attaches OAuth Client Authentication to the given Request object. """ def __call__(self, req): req.url, req.headers, req.body = self.prepare( req.method, req.url, req.headers, req.body ) return req class OAuth2Session(OAuth2Client, Session): """Construct a new OAuth 2 client requests session. :param client_id: Client ID, which you get from client registration. :param client_secret: Client Secret, which you get from registration. :param authorization_endpoint: URL of the authorization server's authorization endpoint. :param token_endpoint: URL of the authorization server's token endpoint. :param token_endpoint_auth_method: client authentication method for token endpoint. :param revocation_endpoint: URL of the authorization server's OAuth 2.0 revocation endpoint. :param revocation_endpoint_auth_method: client authentication method for revocation endpoint. :param scope: Scope that you needed to access user resources. :param state: Shared secret to prevent CSRF attack. :param redirect_uri: Redirect URI you registered as callback. :param token: A dict of token attributes such as ``access_token``, ``token_type`` and ``expires_at``. :param token_placement: The place to put token in HTTP request. Available values: "header", "body", "uri". :param update_token: A function for you to update token. It accept a :class:`OAuth2Token` as parameter. :param leeway: Time window in seconds before the actual expiration of the authentication token, that the token is considered expired and will be refreshed. :param default_timeout: If settled, every requests will have a default timeout. """ client_auth_class = OAuth2ClientAuth token_auth_class = OAuth2Auth oauth_error_class = OAuthError SESSION_REQUEST_PARAMS = ( 'allow_redirects', 'timeout', 'cookies', 'files', 'proxies', 'hooks', 'stream', 'verify', 'cert', 'json' ) def __init__(self, client_id=None, client_secret=None, token_endpoint_auth_method=None, revocation_endpoint_auth_method=None, scope=None, state=None, redirect_uri=None, token=None, token_placement='header', update_token=None, leeway=60, default_timeout=None, **kwargs): Session.__init__(self) self.default_timeout = default_timeout update_session_configure(self, kwargs) OAuth2Client.__init__( self, session=self, client_id=client_id, client_secret=client_secret, token_endpoint_auth_method=token_endpoint_auth_method, revocation_endpoint_auth_method=revocation_endpoint_auth_method, scope=scope, state=state, redirect_uri=redirect_uri, token=token, token_placement=token_placement, update_token=update_token, leeway=leeway, **kwargs ) def fetch_access_token(self, url=None, **kwargs): """Alias for fetch_token.""" return self.fetch_token(url, **kwargs) def request(self, method, url, withhold_token=False, auth=None, **kwargs): """Send request with auto refresh token feature (if available).""" if self.default_timeout: kwargs.setdefault('timeout', self.default_timeout) if not withhold_token and auth is None: if not self.token: raise MissingTokenError() auth = self.token_auth return super().request( method, url, auth=auth, **kwargs) authlib-1.3.2/authlib/integrations/requests_client/utils.py000066400000000000000000000004221466226534200242360ustar00rootroot00000000000000REQUESTS_SESSION_KWARGS = [ 'proxies', 'hooks', 'stream', 'verify', 'cert', 'max_redirects', 'trust_env', ] def update_session_configure(session, kwargs): for k in REQUESTS_SESSION_KWARGS: if k in kwargs: setattr(session, k, kwargs.pop(k)) authlib-1.3.2/authlib/integrations/sqla_oauth2/000077500000000000000000000000001466226534200215375ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/sqla_oauth2/__init__.py000066400000000000000000000010441466226534200236470ustar00rootroot00000000000000from .client_mixin import OAuth2ClientMixin from .tokens_mixins import OAuth2AuthorizationCodeMixin, OAuth2TokenMixin from .functions import ( create_query_client_func, create_save_token_func, create_query_token_func, create_revocation_endpoint, create_bearer_token_validator, ) __all__ = [ 'OAuth2ClientMixin', 'OAuth2AuthorizationCodeMixin', 'OAuth2TokenMixin', 'create_query_client_func', 'create_save_token_func', 'create_query_token_func', 'create_revocation_endpoint', 'create_bearer_token_validator', ] authlib-1.3.2/authlib/integrations/sqla_oauth2/client_mixin.py000066400000000000000000000100241466226534200245700ustar00rootroot00000000000000import secrets from sqlalchemy import Column, String, Text, Integer from authlib.common.encoding import json_loads, json_dumps from authlib.oauth2.rfc6749 import ClientMixin from authlib.oauth2.rfc6749 import scope_to_list, list_to_scope class OAuth2ClientMixin(ClientMixin): client_id = Column(String(48), index=True) client_secret = Column(String(120)) client_id_issued_at = Column(Integer, nullable=False, default=0) client_secret_expires_at = Column(Integer, nullable=False, default=0) _client_metadata = Column('client_metadata', Text) @property def client_info(self): """Implementation for Client Info in OAuth 2.0 Dynamic Client Registration Protocol via `Section 3.2.1`_. .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc7591#section-3.2.1 """ return dict( client_id=self.client_id, client_secret=self.client_secret, client_id_issued_at=self.client_id_issued_at, client_secret_expires_at=self.client_secret_expires_at, ) @property def client_metadata(self): if 'client_metadata' in self.__dict__: return self.__dict__['client_metadata'] if self._client_metadata: data = json_loads(self._client_metadata) self.__dict__['client_metadata'] = data return data return {} def set_client_metadata(self, value): self._client_metadata = json_dumps(value) if 'client_metadata' in self.__dict__: del self.__dict__['client_metadata'] @property def redirect_uris(self): return self.client_metadata.get('redirect_uris', []) @property def token_endpoint_auth_method(self): return self.client_metadata.get( 'token_endpoint_auth_method', 'client_secret_basic' ) @property def grant_types(self): return self.client_metadata.get('grant_types', []) @property def response_types(self): return self.client_metadata.get('response_types', []) @property def client_name(self): return self.client_metadata.get('client_name') @property def client_uri(self): return self.client_metadata.get('client_uri') @property def logo_uri(self): return self.client_metadata.get('logo_uri') @property def scope(self): return self.client_metadata.get('scope', '') @property def contacts(self): return self.client_metadata.get('contacts', []) @property def tos_uri(self): return self.client_metadata.get('tos_uri') @property def policy_uri(self): return self.client_metadata.get('policy_uri') @property def jwks_uri(self): return self.client_metadata.get('jwks_uri') @property def jwks(self): return self.client_metadata.get('jwks', []) @property def software_id(self): return self.client_metadata.get('software_id') @property def software_version(self): return self.client_metadata.get('software_version') def get_client_id(self): return self.client_id def get_default_redirect_uri(self): if self.redirect_uris: return self.redirect_uris[0] def get_allowed_scope(self, scope): if not scope: return '' allowed = set(self.scope.split()) scopes = scope_to_list(scope) return list_to_scope([s for s in scopes if s in allowed]) def check_redirect_uri(self, redirect_uri): return redirect_uri in self.redirect_uris def check_client_secret(self, client_secret): return secrets.compare_digest(self.client_secret, client_secret) def check_endpoint_auth_method(self, method, endpoint): if endpoint == 'token': return self.token_endpoint_auth_method == method # TODO return True def check_response_type(self, response_type): return response_type in self.response_types def check_grant_type(self, grant_type): return grant_type in self.grant_types authlib-1.3.2/authlib/integrations/sqla_oauth2/functions.py000066400000000000000000000061731466226534200241300ustar00rootroot00000000000000import time def create_query_client_func(session, client_model): """Create an ``query_client`` function that can be used in authorization server. :param session: SQLAlchemy session :param client_model: Client model class """ def query_client(client_id): q = session.query(client_model) return q.filter_by(client_id=client_id).first() return query_client def create_save_token_func(session, token_model): """Create an ``save_token`` function that can be used in authorization server. :param session: SQLAlchemy session :param token_model: Token model class """ def save_token(token, request): if request.user: user_id = request.user.get_user_id() else: user_id = None client = request.client item = token_model( client_id=client.client_id, user_id=user_id, **token ) session.add(item) session.commit() return save_token def create_query_token_func(session, token_model): """Create an ``query_token`` function for revocation, introspection token endpoints. :param session: SQLAlchemy session :param token_model: Token model class """ def query_token(token, token_type_hint): q = session.query(token_model) if token_type_hint == 'access_token': return q.filter_by(access_token=token).first() elif token_type_hint == 'refresh_token': return q.filter_by(refresh_token=token).first() # without token_type_hint item = q.filter_by(access_token=token).first() if item: return item return q.filter_by(refresh_token=token).first() return query_token def create_revocation_endpoint(session, token_model): """Create a revocation endpoint class with SQLAlchemy session and token model. :param session: SQLAlchemy session :param token_model: Token model class """ from authlib.oauth2.rfc7009 import RevocationEndpoint query_token = create_query_token_func(session, token_model) class _RevocationEndpoint(RevocationEndpoint): def query_token(self, token, token_type_hint): return query_token(token, token_type_hint) def revoke_token(self, token, request): now = int(time.time()) hint = request.form.get('token_type_hint') token.access_token_revoked_at = now if hint != 'access_token': token.refresh_token_revoked_at = now session.add(token) session.commit() return _RevocationEndpoint def create_bearer_token_validator(session, token_model): """Create an bearer token validator class with SQLAlchemy session and token model. :param session: SQLAlchemy session :param token_model: Token model class """ from authlib.oauth2.rfc6750 import BearerTokenValidator class _BearerTokenValidator(BearerTokenValidator): def authenticate_token(self, token_string): q = session.query(token_model) return q.filter_by(access_token=token_string).first() return _BearerTokenValidator authlib-1.3.2/authlib/integrations/sqla_oauth2/tokens_mixins.py000066400000000000000000000037301466226534200250060ustar00rootroot00000000000000import time from sqlalchemy import Column, String, Text, Integer from authlib.oauth2.rfc6749 import ( TokenMixin, AuthorizationCodeMixin, ) class OAuth2AuthorizationCodeMixin(AuthorizationCodeMixin): code = Column(String(120), unique=True, nullable=False) client_id = Column(String(48)) redirect_uri = Column(Text, default='') response_type = Column(Text, default='') scope = Column(Text, default='') nonce = Column(Text) auth_time = Column( Integer, nullable=False, default=lambda: int(time.time()) ) code_challenge = Column(Text) code_challenge_method = Column(String(48)) def is_expired(self): return self.auth_time + 300 < time.time() def get_redirect_uri(self): return self.redirect_uri def get_scope(self): return self.scope def get_auth_time(self): return self.auth_time def get_nonce(self): return self.nonce class OAuth2TokenMixin(TokenMixin): client_id = Column(String(48)) token_type = Column(String(40)) access_token = Column(String(255), unique=True, nullable=False) refresh_token = Column(String(255), index=True) scope = Column(Text, default='') issued_at = Column( Integer, nullable=False, default=lambda: int(time.time()) ) access_token_revoked_at = Column(Integer, nullable=False, default=0) refresh_token_revoked_at = Column(Integer, nullable=False, default=0) expires_in = Column(Integer, nullable=False, default=0) def check_client(self, client): return self.client_id == client.get_client_id() def get_scope(self): return self.scope def get_expires_in(self): return self.expires_in def is_revoked(self): return self.access_token_revoked_at or self.refresh_token_revoked_at def is_expired(self): if not self.expires_in: return False expires_at = self.issued_at + self.expires_in return expires_at < time.time() authlib-1.3.2/authlib/integrations/starlette_client/000077500000000000000000000000001466226534200226625ustar00rootroot00000000000000authlib-1.3.2/authlib/integrations/starlette_client/__init__.py000066400000000000000000000012321466226534200247710ustar00rootroot00000000000000# flake8: noqa from ..base_client import BaseOAuth, OAuthError from .integration import StarletteIntegration from .apps import StarletteOAuth1App, StarletteOAuth2App class OAuth(BaseOAuth): oauth1_client_cls = StarletteOAuth1App oauth2_client_cls = StarletteOAuth2App framework_integration_cls = StarletteIntegration def __init__(self, config=None, cache=None, fetch_token=None, update_token=None): super().__init__( cache=cache, fetch_token=fetch_token, update_token=update_token) self.config = config __all__ = [ 'OAuth', 'OAuthError', 'StarletteIntegration', 'StarletteOAuth1App', 'StarletteOAuth2App', ] authlib-1.3.2/authlib/integrations/starlette_client/apps.py000066400000000000000000000067441466226534200242120ustar00rootroot00000000000000from starlette.datastructures import URL from starlette.responses import RedirectResponse from ..base_client import OAuthError from ..base_client import BaseApp from ..base_client.async_app import AsyncOAuth1Mixin, AsyncOAuth2Mixin from ..base_client.async_openid import AsyncOpenIDMixin from ..httpx_client import AsyncOAuth1Client, AsyncOAuth2Client class StarletteAppMixin: async def save_authorize_data(self, request, **kwargs): state = kwargs.pop('state', None) if state: if self.framework.cache: session = None else: session = request.session await self.framework.set_state_data(session, state, kwargs) else: raise RuntimeError('Missing state value') async def authorize_redirect(self, request, redirect_uri=None, **kwargs): """Create a HTTP Redirect for Authorization Endpoint. :param request: HTTP request instance from Starlette view. :param redirect_uri: Callback or redirect URI for authorization. :param kwargs: Extra parameters to include. :return: A HTTP redirect response. """ # Handle Starlette >= 0.26.0 where redirect_uri may now be a URL and not a string if redirect_uri and isinstance(redirect_uri, URL): redirect_uri = str(redirect_uri) rv = await self.create_authorization_url(redirect_uri, **kwargs) await self.save_authorize_data(request, redirect_uri=redirect_uri, **rv) return RedirectResponse(rv['url'], status_code=302) class StarletteOAuth1App(StarletteAppMixin, AsyncOAuth1Mixin, BaseApp): client_cls = AsyncOAuth1Client async def authorize_access_token(self, request, **kwargs): params = dict(request.query_params) state = params.get('oauth_token') if not state: raise OAuthError(description='Missing "oauth_token" parameter') data = await self.framework.get_state_data(request.session, state) if not data: raise OAuthError(description='Missing "request_token" in temporary data') params['request_token'] = data['request_token'] params.update(kwargs) await self.framework.clear_state_data(request.session, state) return await self.fetch_access_token(**params) class StarletteOAuth2App(StarletteAppMixin, AsyncOAuth2Mixin, AsyncOpenIDMixin, BaseApp): client_cls = AsyncOAuth2Client async def authorize_access_token(self, request, **kwargs): error = request.query_params.get('error') if error: description = request.query_params.get('error_description') raise OAuthError(error=error, description=description) params = { 'code': request.query_params.get('code'), 'state': request.query_params.get('state'), } if self.framework.cache: session = None else: session = request.session claims_options = kwargs.pop('claims_options', None) state_data = await self.framework.get_state_data(session, params.get('state')) await self.framework.clear_state_data(session, params.get('state')) params = self._format_state_params(state_data, params) token = await self.fetch_access_token(**params, **kwargs) if 'id_token' in token and 'nonce' in state_data: userinfo = await self.parse_id_token(token, nonce=state_data['nonce'], claims_options=claims_options) token['userinfo'] = userinfo return token authlib-1.3.2/authlib/integrations/starlette_client/integration.py000066400000000000000000000042371466226534200255650ustar00rootroot00000000000000import json import time from typing import ( Any, Dict, Hashable, Optional, ) from ..base_client import FrameworkIntegration class StarletteIntegration(FrameworkIntegration): async def _get_cache_data(self, key: Hashable): value = await self.cache.get(key) if not value: return None try: return json.loads(value) except (TypeError, ValueError): return None async def get_state_data(self, session: Optional[Dict[str, Any]], state: str) -> Dict[str, Any]: key = f'_state_{self.name}_{state}' if self.cache: value = await self._get_cache_data(key) elif session is not None: value = session.get(key) else: value = None if value: return value.get('data') return None async def set_state_data(self, session: Optional[Dict[str, Any]], state: str, data: Any): key_prefix = f'_state_{self.name}_' key = f'{key_prefix}{state}' if self.cache: await self.cache.set(key, json.dumps({'data': data}), self.expires_in) elif session is not None: # clear old state data to avoid session size growing for old_key in list(session.keys()): if old_key.startswith(key_prefix): session.pop(old_key) now = time.time() session[key] = {'data': data, 'exp': now + self.expires_in} async def clear_state_data(self, session: Optional[Dict[str, Any]], state: str): key = f'_state_{self.name}_{state}' if self.cache: await self.cache.delete(key) elif session is not None: session.pop(key, None) self._clear_session_state(session) def update_token(self, token, refresh_token=None, access_token=None): pass @staticmethod def load_config(oauth, name, params): if not oauth.config: return {} rv = {} for k in params: conf_key = f'{name}_{k}'.upper() v = oauth.config.get(conf_key, default=None) if v is not None: rv[k] = v return rv authlib-1.3.2/authlib/jose/000077500000000000000000000000001466226534200155475ustar00rootroot00000000000000authlib-1.3.2/authlib/jose/__init__.py000066400000000000000000000025671466226534200176720ustar00rootroot00000000000000""" authlib.jose ~~~~~~~~~~~~ JOSE implementation in Authlib. Tracking the status of JOSE specs at https://tools.ietf.org/wg/jose/ """ from .rfc7515 import ( JsonWebSignature, JWSAlgorithm, JWSHeader, JWSObject, ) from .rfc7516 import ( JsonWebEncryption, JWEAlgorithm, JWEEncAlgorithm, JWEZipAlgorithm, ) from .rfc7517 import Key, KeySet, JsonWebKey from .rfc7518 import ( register_jws_rfc7518, register_jwe_rfc7518, ECDHESAlgorithm, OctKey, RSAKey, ECKey, ) from .rfc7519 import JsonWebToken, BaseClaims, JWTClaims from .rfc8037 import OKPKey, register_jws_rfc8037 from .errors import JoseError # register algorithms register_jws_rfc7518(JsonWebSignature) register_jws_rfc8037(JsonWebSignature) register_jwe_rfc7518(JsonWebEncryption) # attach algorithms ECDHESAlgorithm.ALLOWED_KEY_CLS = (ECKey, OKPKey) # register supported keys JsonWebKey.JWK_KEY_CLS = { OctKey.kty: OctKey, RSAKey.kty: RSAKey, ECKey.kty: ECKey, OKPKey.kty: OKPKey, } jwt = JsonWebToken(list(JsonWebSignature.ALGORITHMS_REGISTRY.keys())) __all__ = [ 'JoseError', 'JsonWebSignature', 'JWSAlgorithm', 'JWSHeader', 'JWSObject', 'JsonWebEncryption', 'JWEAlgorithm', 'JWEEncAlgorithm', 'JWEZipAlgorithm', 'JsonWebKey', 'Key', 'KeySet', 'OctKey', 'RSAKey', 'ECKey', 'OKPKey', 'JsonWebToken', 'BaseClaims', 'JWTClaims', 'jwt', ] authlib-1.3.2/authlib/jose/drafts/000077500000000000000000000000001466226534200170325ustar00rootroot00000000000000authlib-1.3.2/authlib/jose/drafts/__init__.py000066400000000000000000000010061466226534200211400ustar00rootroot00000000000000from ._jwe_algorithms import JWE_DRAFT_ALG_ALGORITHMS from ._jwe_enc_cryptography import C20PEncAlgorithm try: from ._jwe_enc_cryptodome import XC20PEncAlgorithm except ImportError: XC20PEncAlgorithm = None def register_jwe_draft(cls): for alg in JWE_DRAFT_ALG_ALGORITHMS: cls.register_algorithm(alg) cls.register_algorithm(C20PEncAlgorithm(256)) # C20P if XC20PEncAlgorithm is not None: cls.register_algorithm(XC20PEncAlgorithm(256)) # XC20P __all__ = ['register_jwe_draft'] authlib-1.3.2/authlib/jose/drafts/_jwe_algorithms.py000066400000000000000000000154051466226534200225660ustar00rootroot00000000000000import struct from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash from authlib.jose.errors import InvalidEncryptionAlgorithmForECDH1PUWithKeyWrappingError from authlib.jose.rfc7516 import JWEAlgorithmWithTagAwareKeyAgreement from authlib.jose.rfc7518 import AESAlgorithm, CBCHS2EncAlgorithm, ECKey, u32be_len_input from authlib.jose.rfc8037 import OKPKey class ECDH1PUAlgorithm(JWEAlgorithmWithTagAwareKeyAgreement): EXTRA_HEADERS = ['epk', 'apu', 'apv', 'skid'] ALLOWED_KEY_CLS = (ECKey, OKPKey) # https://datatracker.ietf.org/doc/html/draft-madden-jose-ecdh-1pu-04 def __init__(self, key_size=None): if key_size is None: self.name = 'ECDH-1PU' self.description = 'ECDH-1PU in the Direct Key Agreement mode' else: self.name = f'ECDH-1PU+A{key_size}KW' self.description = ( 'ECDH-1PU using Concat KDF and CEK wrapped ' 'with A{}KW').format(key_size) self.key_size = key_size self.aeskw = AESAlgorithm(key_size) def prepare_key(self, raw_data): if isinstance(raw_data, self.ALLOWED_KEY_CLS): return raw_data return ECKey.import_key(raw_data) def generate_preset(self, enc_alg, key): epk = self._generate_ephemeral_key(key) h = self._prepare_headers(epk) preset = {'epk': epk, 'header': h} if self.key_size is not None: cek = enc_alg.generate_cek() preset['cek'] = cek return preset def compute_shared_key(self, shared_key_e, shared_key_s): return shared_key_e + shared_key_s def compute_fixed_info(self, headers, bit_size, tag): if tag is None: cctag = b'' else: cctag = u32be_len_input(tag) # AlgorithmID if self.key_size is None: alg_id = u32be_len_input(headers['enc']) else: alg_id = u32be_len_input(headers['alg']) # PartyUInfo apu_info = u32be_len_input(headers.get('apu'), True) # PartyVInfo apv_info = u32be_len_input(headers.get('apv'), True) # SuppPubInfo pub_info = struct.pack('>I', bit_size) + cctag return alg_id + apu_info + apv_info + pub_info def compute_derived_key(self, shared_key, fixed_info, bit_size): ckdf = ConcatKDFHash( algorithm=hashes.SHA256(), length=bit_size // 8, otherinfo=fixed_info, backend=default_backend() ) return ckdf.derive(shared_key) def deliver_at_sender(self, sender_static_key, sender_ephemeral_key, recipient_pubkey, headers, bit_size, tag): shared_key_s = sender_static_key.exchange_shared_key(recipient_pubkey) shared_key_e = sender_ephemeral_key.exchange_shared_key(recipient_pubkey) shared_key = self.compute_shared_key(shared_key_e, shared_key_s) fixed_info = self.compute_fixed_info(headers, bit_size, tag) return self.compute_derived_key(shared_key, fixed_info, bit_size) def deliver_at_recipient(self, recipient_key, sender_static_pubkey, sender_ephemeral_pubkey, headers, bit_size, tag): shared_key_s = recipient_key.exchange_shared_key(sender_static_pubkey) shared_key_e = recipient_key.exchange_shared_key(sender_ephemeral_pubkey) shared_key = self.compute_shared_key(shared_key_e, shared_key_s) fixed_info = self.compute_fixed_info(headers, bit_size, tag) return self.compute_derived_key(shared_key, fixed_info, bit_size) def _generate_ephemeral_key(self, key): return key.generate_key(key['crv'], is_private=True) def _prepare_headers(self, epk): # REQUIRED_JSON_FIELDS contains only public fields pub_epk = {k: epk[k] for k in epk.REQUIRED_JSON_FIELDS} pub_epk['kty'] = epk.kty return {'epk': pub_epk} def generate_keys_and_prepare_headers(self, enc_alg, key, sender_key, preset=None): if not isinstance(enc_alg, CBCHS2EncAlgorithm): raise InvalidEncryptionAlgorithmForECDH1PUWithKeyWrappingError() if preset and 'epk' in preset: epk = preset['epk'] h = {} else: epk = self._generate_ephemeral_key(key) h = self._prepare_headers(epk) if preset and 'cek' in preset: cek = preset['cek'] else: cek = enc_alg.generate_cek() return {'epk': epk, 'cek': cek, 'header': h} def _agree_upon_key_at_sender(self, enc_alg, headers, key, sender_key, epk, tag=None): if self.key_size is None: bit_size = enc_alg.CEK_SIZE else: bit_size = self.key_size public_key = key.get_op_key('wrapKey') return self.deliver_at_sender(sender_key, epk, public_key, headers, bit_size, tag) def _wrap_cek(self, cek, dk): kek = self.aeskw.prepare_key(dk) return self.aeskw.wrap_cek(cek, kek) def agree_upon_key_and_wrap_cek(self, enc_alg, headers, key, sender_key, epk, cek, tag): dk = self._agree_upon_key_at_sender(enc_alg, headers, key, sender_key, epk, tag) return self._wrap_cek(cek, dk) def wrap(self, enc_alg, headers, key, sender_key, preset=None): # In this class this method is used in direct key agreement mode only if self.key_size is not None: raise RuntimeError('Invalid algorithm state detected') if preset and 'epk' in preset: epk = preset['epk'] h = {} else: epk = self._generate_ephemeral_key(key) h = self._prepare_headers(epk) dk = self._agree_upon_key_at_sender(enc_alg, headers, key, sender_key, epk) return {'ek': b'', 'cek': dk, 'header': h} def unwrap(self, enc_alg, ek, headers, key, sender_key, tag=None): if 'epk' not in headers: raise ValueError('Missing "epk" in headers') if self.key_size is None: bit_size = enc_alg.CEK_SIZE else: bit_size = self.key_size sender_pubkey = sender_key.get_op_key('wrapKey') epk = key.import_key(headers['epk']) epk_pubkey = epk.get_op_key('wrapKey') dk = self.deliver_at_recipient(key, sender_pubkey, epk_pubkey, headers, bit_size, tag) if self.key_size is None: return dk kek = self.aeskw.prepare_key(dk) return self.aeskw.unwrap(enc_alg, ek, headers, kek) JWE_DRAFT_ALG_ALGORITHMS = [ ECDH1PUAlgorithm(None), # ECDH-1PU ECDH1PUAlgorithm(128), # ECDH-1PU+A128KW ECDH1PUAlgorithm(192), # ECDH-1PU+A192KW ECDH1PUAlgorithm(256), # ECDH-1PU+A256KW ] def register_jwe_alg_draft(cls): for alg in JWE_DRAFT_ALG_ALGORITHMS: cls.register_algorithm(alg) authlib-1.3.2/authlib/jose/drafts/_jwe_enc_cryptodome.py000066400000000000000000000035041466226534200234240ustar00rootroot00000000000000""" authlib.jose.draft ~~~~~~~~~~~~~~~~~~~~ Content Encryption per `Section 4`_. .. _`Section 4`: https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02#section-4 """ from authlib.jose.rfc7516 import JWEEncAlgorithm from Cryptodome.Cipher import ChaCha20_Poly1305 as Cryptodome_ChaCha20_Poly1305 class XC20PEncAlgorithm(JWEEncAlgorithm): # Use of an IV of size 192 bits is REQUIRED with this algorithm. # https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02#section-4.1 IV_SIZE = 192 def __init__(self, key_size): self.name = 'XC20P' self.description = 'XChaCha20-Poly1305' self.key_size = key_size self.CEK_SIZE = key_size def encrypt(self, msg, aad, iv, key): """Content Encryption with AEAD_XCHACHA20_POLY1305 :param msg: text to be encrypt in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param key: encrypted key in bytes :return: (ciphertext, tag) """ self.check_iv(iv) chacha = Cryptodome_ChaCha20_Poly1305.new(key=key, nonce=iv) chacha.update(aad) ciphertext, tag = chacha.encrypt_and_digest(msg) return ciphertext, tag def decrypt(self, ciphertext, aad, iv, tag, key): """Content Decryption with AEAD_XCHACHA20_POLY1305 :param ciphertext: ciphertext in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param tag: authentication tag in bytes :param key: encrypted key in bytes :return: message """ self.check_iv(iv) chacha = Cryptodome_ChaCha20_Poly1305.new(key=key, nonce=iv) chacha.update(aad) return chacha.decrypt_and_verify(ciphertext, tag) authlib-1.3.2/authlib/jose/drafts/_jwe_enc_cryptography.py000066400000000000000000000033171466226534200237740ustar00rootroot00000000000000""" authlib.jose.draft ~~~~~~~~~~~~~~~~~~~~ Content Encryption per `Section 4`_. .. _`Section 4`: https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02#section-4 """ from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from authlib.jose.rfc7516 import JWEEncAlgorithm class C20PEncAlgorithm(JWEEncAlgorithm): # Use of an IV of size 96 bits is REQUIRED with this algorithm. # https://datatracker.ietf.org/doc/html/draft-amringer-jose-chacha-02#section-4.1 IV_SIZE = 96 def __init__(self, key_size): self.name = 'C20P' self.description = 'ChaCha20-Poly1305' self.key_size = key_size self.CEK_SIZE = key_size def encrypt(self, msg, aad, iv, key): """Content Encryption with AEAD_CHACHA20_POLY1305 :param msg: text to be encrypt in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param key: encrypted key in bytes :return: (ciphertext, tag) """ self.check_iv(iv) chacha = ChaCha20Poly1305(key) ciphertext = chacha.encrypt(iv, msg, aad) return ciphertext[:-16], ciphertext[-16:] def decrypt(self, ciphertext, aad, iv, tag, key): """Content Decryption with AEAD_CHACHA20_POLY1305 :param ciphertext: ciphertext in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param tag: authentication tag in bytes :param key: encrypted key in bytes :return: message """ self.check_iv(iv) chacha = ChaCha20Poly1305(key) return chacha.decrypt(iv, ciphertext + tag, aad) authlib-1.3.2/authlib/jose/errors.py000066400000000000000000000056411466226534200174430ustar00rootroot00000000000000from authlib.common.errors import AuthlibBaseError class JoseError(AuthlibBaseError): pass class DecodeError(JoseError): error = 'decode_error' class MissingAlgorithmError(JoseError): error = 'missing_algorithm' class UnsupportedAlgorithmError(JoseError): error = 'unsupported_algorithm' class BadSignatureError(JoseError): error = 'bad_signature' def __init__(self, result): super().__init__() self.result = result class InvalidHeaderParameterNameError(JoseError): error = 'invalid_header_parameter_name' def __init__(self, name): description = f'Invalid Header Parameter Name: {name}' super().__init__( description=description) class InvalidEncryptionAlgorithmForECDH1PUWithKeyWrappingError(JoseError): error = 'invalid_encryption_algorithm_for_ECDH_1PU_with_key_wrapping' def __init__(self): description = 'In key agreement with key wrapping mode ECDH-1PU algorithm ' \ 'only supports AES_CBC_HMAC_SHA2 family encryption algorithms' super().__init__( description=description) class InvalidAlgorithmForMultipleRecipientsMode(JoseError): error = 'invalid_algorithm_for_multiple_recipients_mode' def __init__(self, alg): description = f'{alg} algorithm cannot be used in multiple recipients mode' super().__init__( description=description) class KeyMismatchError(JoseError): error = 'key_mismatch_error' description = 'Key does not match to any recipient' class MissingEncryptionAlgorithmError(JoseError): error = 'missing_encryption_algorithm' description = 'Missing "enc" in header' class UnsupportedEncryptionAlgorithmError(JoseError): error = 'unsupported_encryption_algorithm' description = 'Unsupported "enc" value in header' class UnsupportedCompressionAlgorithmError(JoseError): error = 'unsupported_compression_algorithm' description = 'Unsupported "zip" value in header' class InvalidUseError(JoseError): error = 'invalid_use' description = 'Key "use" is not valid for your usage' class InvalidClaimError(JoseError): error = 'invalid_claim' def __init__(self, claim): self.claim_name = claim description = f'Invalid claim "{claim}"' super().__init__(description=description) class MissingClaimError(JoseError): error = 'missing_claim' def __init__(self, claim): description = f'Missing "{claim}" claim' super().__init__(description=description) class InsecureClaimError(JoseError): error = 'insecure_claim' def __init__(self, claim): description = f'Insecure claim "{claim}"' super().__init__(description=description) class ExpiredTokenError(JoseError): error = 'expired_token' description = 'The token is expired' class InvalidTokenError(JoseError): error = 'invalid_token' description = 'The token is not valid yet' authlib-1.3.2/authlib/jose/jwk.py000066400000000000000000000007521466226534200167200ustar00rootroot00000000000000from authlib.deprecate import deprecate from .rfc7517 import JsonWebKey def loads(obj, kid=None): deprecate('Please use ``JsonWebKey`` directly.') key_set = JsonWebKey.import_key_set(obj) if key_set: return key_set.find_by_kid(kid) return JsonWebKey.import_key(obj) def dumps(key, kty=None, **params): deprecate('Please use ``JsonWebKey`` directly.') if kty: params['kty'] = kty key = JsonWebKey.import_key(key, params) return dict(key) authlib-1.3.2/authlib/jose/rfc7515/000077500000000000000000000000001466226534200166435ustar00rootroot00000000000000authlib-1.3.2/authlib/jose/rfc7515/__init__.py000066400000000000000000000005501466226534200207540ustar00rootroot00000000000000""" authlib.jose.rfc7515 ~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of JSON Web Signature (JWS). https://tools.ietf.org/html/rfc7515 """ from .jws import JsonWebSignature from .models import JWSAlgorithm, JWSHeader, JWSObject __all__ = [ 'JsonWebSignature', 'JWSAlgorithm', 'JWSHeader', 'JWSObject' ] authlib-1.3.2/authlib/jose/rfc7515/jws.py000066400000000000000000000260061466226534200200240ustar00rootroot00000000000000from authlib.common.encoding import ( to_bytes, to_unicode, urlsafe_b64encode, json_b64encode, ) from authlib.jose.util import ( extract_header, extract_segment, ensure_dict, ) from authlib.jose.errors import ( DecodeError, MissingAlgorithmError, UnsupportedAlgorithmError, BadSignatureError, InvalidHeaderParameterNameError, ) from .models import JWSHeader, JWSObject class JsonWebSignature: #: Registered Header Parameter Names defined by Section 4.1 REGISTERED_HEADER_PARAMETER_NAMES = frozenset([ 'alg', 'jku', 'jwk', 'kid', 'x5u', 'x5c', 'x5t', 'x5t#S256', 'typ', 'cty', 'crit' ]) #: Defined available JWS algorithms in the registry ALGORITHMS_REGISTRY = {} def __init__(self, algorithms=None, private_headers=None): self._private_headers = private_headers self._algorithms = algorithms @classmethod def register_algorithm(cls, algorithm): if not algorithm or algorithm.algorithm_type != 'JWS': raise ValueError( f'Invalid algorithm for JWS, {algorithm!r}') cls.ALGORITHMS_REGISTRY[algorithm.name] = algorithm def serialize_compact(self, protected, payload, key): """Generate a JWS Compact Serialization. The JWS Compact Serialization represents digitally signed or MACed content as a compact, URL-safe string, per `Section 7.1`_. .. code-block:: text BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload) || '.' || BASE64URL(JWS Signature) :param protected: A dict of protected header :param payload: A bytes/string of payload :param key: Private key used to generate signature :return: byte """ jws_header = JWSHeader(protected, None) self._validate_private_headers(protected) algorithm, key = self._prepare_algorithm_key(protected, payload, key) protected_segment = json_b64encode(jws_header.protected) payload_segment = urlsafe_b64encode(to_bytes(payload)) # calculate signature signing_input = b'.'.join([protected_segment, payload_segment]) signature = urlsafe_b64encode(algorithm.sign(signing_input, key)) return b'.'.join([protected_segment, payload_segment, signature]) def deserialize_compact(self, s, key, decode=None): """Exact JWS Compact Serialization, and validate with the given key. If key is not provided, the returned dict will contain the signature, and signing input values. Via `Section 7.1`_. :param s: text of JWS Compact Serialization :param key: key used to verify the signature :param decode: a function to decode payload data :return: JWSObject :raise: BadSignatureError .. _`Section 7.1`: https://tools.ietf.org/html/rfc7515#section-7.1 """ try: s = to_bytes(s) signing_input, signature_segment = s.rsplit(b'.', 1) protected_segment, payload_segment = signing_input.split(b'.', 1) except ValueError: raise DecodeError('Not enough segments') protected = _extract_header(protected_segment) jws_header = JWSHeader(protected, None) payload = _extract_payload(payload_segment) if decode: payload = decode(payload) signature = _extract_signature(signature_segment) rv = JWSObject(jws_header, payload, 'compact') algorithm, key = self._prepare_algorithm_key(jws_header, payload, key) if algorithm.verify(signing_input, signature, key): return rv raise BadSignatureError(rv) def serialize_json(self, header_obj, payload, key): """Generate a JWS JSON Serialization. The JWS JSON Serialization represents digitally signed or MACed content as a JSON object, per `Section 7.2`_. :param header_obj: A dict/list of header :param payload: A string/dict of payload :param key: Private key used to generate signature :return: JWSObject Example ``header_obj`` of JWS JSON Serialization:: { "protected: {"alg": "HS256"}, "header": {"kid": "jose"} } Pass a dict to generate flattened JSON Serialization, pass a list of header dict to generate standard JSON Serialization. """ payload_segment = json_b64encode(payload) def _sign(jws_header): self._validate_private_headers(jws_header) _alg, _key = self._prepare_algorithm_key(jws_header, payload, key) protected_segment = json_b64encode(jws_header.protected) signing_input = b'.'.join([protected_segment, payload_segment]) signature = urlsafe_b64encode(_alg.sign(signing_input, _key)) rv = { 'protected': to_unicode(protected_segment), 'signature': to_unicode(signature) } if jws_header.header is not None: rv['header'] = jws_header.header return rv if isinstance(header_obj, dict): data = _sign(JWSHeader.from_dict(header_obj)) data['payload'] = to_unicode(payload_segment) return data signatures = [_sign(JWSHeader.from_dict(h)) for h in header_obj] return { 'payload': to_unicode(payload_segment), 'signatures': signatures } def deserialize_json(self, obj, key, decode=None): """Exact JWS JSON Serialization, and validate with the given key. If key is not provided, it will return a dict without signature verification. Header will still be validated. Via `Section 7.2`_. :param obj: text of JWS JSON Serialization :param key: key used to verify the signature :param decode: a function to decode payload data :return: JWSObject :raise: BadSignatureError .. _`Section 7.2`: https://tools.ietf.org/html/rfc7515#section-7.2 """ obj = ensure_dict(obj, 'JWS') payload_segment = obj.get('payload') if payload_segment is None: raise DecodeError('Missing "payload" value') payload_segment = to_bytes(payload_segment) payload = _extract_payload(payload_segment) if decode: payload = decode(payload) if 'signatures' not in obj: # flattened JSON JWS jws_header, valid = self._validate_json_jws( payload_segment, payload, obj, key) rv = JWSObject(jws_header, payload, 'flat') if valid: return rv raise BadSignatureError(rv) headers = [] is_valid = True for header_obj in obj['signatures']: jws_header, valid = self._validate_json_jws( payload_segment, payload, header_obj, key) headers.append(jws_header) if not valid: is_valid = False rv = JWSObject(headers, payload, 'json') if is_valid: return rv raise BadSignatureError(rv) def serialize(self, header, payload, key): """Generate a JWS Serialization. It will automatically generate a Compact or JSON Serialization depending on the given header. If a header is in a JSON header format, it will call :meth:`serialize_json`, otherwise it will call :meth:`serialize_compact`. :param header: A dict/list of header :param payload: A string/dict of payload :param key: Private key used to generate signature :return: byte/dict """ if isinstance(header, (list, tuple)): return self.serialize_json(header, payload, key) if 'protected' in header: return self.serialize_json(header, payload, key) return self.serialize_compact(header, payload, key) def deserialize(self, s, key, decode=None): """Deserialize JWS Serialization, both compact and JSON format. It will automatically deserialize depending on the given JWS. :param s: text of JWS Compact/JSON Serialization :param key: key used to verify the signature :param decode: a function to decode payload data :return: dict :raise: BadSignatureError If key is not provided, it will still deserialize the serialization without verification. """ if isinstance(s, dict): return self.deserialize_json(s, key, decode) s = to_bytes(s) if s.startswith(b'{') and s.endswith(b'}'): return self.deserialize_json(s, key, decode) return self.deserialize_compact(s, key, decode) def _prepare_algorithm_key(self, header, payload, key): if 'alg' not in header: raise MissingAlgorithmError() alg = header['alg'] if self._algorithms is not None and alg not in self._algorithms: raise UnsupportedAlgorithmError() if alg not in self.ALGORITHMS_REGISTRY: raise UnsupportedAlgorithmError() algorithm = self.ALGORITHMS_REGISTRY[alg] if callable(key): key = key(header, payload) elif key is None and 'jwk' in header: key = header['jwk'] key = algorithm.prepare_key(key) return algorithm, key def _validate_private_headers(self, header): # only validate private headers when developers set # private headers explicitly if self._private_headers is not None: names = self.REGISTERED_HEADER_PARAMETER_NAMES.copy() names = names.union(self._private_headers) for k in header: if k not in names: raise InvalidHeaderParameterNameError(k) def _validate_json_jws(self, payload_segment, payload, header_obj, key): protected_segment = header_obj.get('protected') if not protected_segment: raise DecodeError('Missing "protected" value') signature_segment = header_obj.get('signature') if not signature_segment: raise DecodeError('Missing "signature" value') protected_segment = to_bytes(protected_segment) protected = _extract_header(protected_segment) header = header_obj.get('header') if header and not isinstance(header, dict): raise DecodeError('Invalid "header" value') jws_header = JWSHeader(protected, header) algorithm, key = self._prepare_algorithm_key(jws_header, payload, key) signing_input = b'.'.join([protected_segment, payload_segment]) signature = _extract_signature(to_bytes(signature_segment)) if algorithm.verify(signing_input, signature, key): return jws_header, True return jws_header, False def _extract_header(header_segment): return extract_header(header_segment, DecodeError) def _extract_signature(signature_segment): return extract_segment(signature_segment, DecodeError, 'signature') def _extract_payload(payload_segment): return extract_segment(payload_segment, DecodeError, 'payload') authlib-1.3.2/authlib/jose/rfc7515/models.py000066400000000000000000000046151466226534200205060ustar00rootroot00000000000000class JWSAlgorithm: """Interface for JWS algorithm. JWA specification (RFC7518) SHOULD implement the algorithms for JWS with this base implementation. """ name = None description = None algorithm_type = 'JWS' algorithm_location = 'alg' def prepare_key(self, raw_data): """Prepare key for signing and verifying signature.""" raise NotImplementedError() def sign(self, msg, key): """Sign the text msg with a private/sign key. :param msg: message bytes to be signed :param key: private key to sign the message :return: bytes """ raise NotImplementedError def verify(self, msg, sig, key): """Verify the signature of text msg with a public/verify key. :param msg: message bytes to be signed :param sig: result signature to be compared :param key: public key to verify the signature :return: boolean """ raise NotImplementedError class JWSHeader(dict): """Header object for JWS. It combine the protected header and unprotected header together. JWSHeader itself is a dict of the combined dict. e.g. >>> protected = {'alg': 'HS256'} >>> header = {'kid': 'a'} >>> jws_header = JWSHeader(protected, header) >>> print(jws_header) {'alg': 'HS256', 'kid': 'a'} >>> jws_header.protected == protected >>> jws_header.header == header :param protected: dict of protected header :param header: dict of unprotected header """ def __init__(self, protected, header): obj = {} if protected: obj.update(protected) if header: obj.update(header) super().__init__(obj) self.protected = protected self.header = header @classmethod def from_dict(cls, obj): if isinstance(obj, cls): return obj return cls(obj.get('protected'), obj.get('header')) class JWSObject(dict): """A dict instance to represent a JWS object.""" def __init__(self, header, payload, type='compact'): super().__init__( header=header, payload=payload, ) self.header = header self.payload = payload self.type = type @property def headers(self): """Alias of ``header`` for JSON typed JWS.""" if self.type == 'json': return self['header'] authlib-1.3.2/authlib/jose/rfc7516/000077500000000000000000000000001466226534200166445ustar00rootroot00000000000000authlib-1.3.2/authlib/jose/rfc7516/__init__.py000066400000000000000000000007211466226534200207550ustar00rootroot00000000000000""" authlib.jose.rfc7516 ~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of JSON Web Encryption (JWE). https://tools.ietf.org/html/rfc7516 """ from .jwe import JsonWebEncryption from .models import JWEAlgorithm, JWEAlgorithmWithTagAwareKeyAgreement, JWEEncAlgorithm, JWEZipAlgorithm __all__ = [ 'JsonWebEncryption', 'JWEAlgorithm', 'JWEAlgorithmWithTagAwareKeyAgreement', 'JWEEncAlgorithm', 'JWEZipAlgorithm' ] authlib-1.3.2/authlib/jose/rfc7516/jwe.py000066400000000000000000000720121466226534200200050ustar00rootroot00000000000000from collections import OrderedDict from copy import deepcopy from authlib.common.encoding import ( to_bytes, urlsafe_b64encode, json_b64encode, to_unicode ) from authlib.jose.rfc7516.models import JWEAlgorithmWithTagAwareKeyAgreement, JWESharedHeader, JWEHeader from authlib.jose.util import ( extract_header, extract_segment, ensure_dict, ) from authlib.jose.errors import ( DecodeError, MissingAlgorithmError, UnsupportedAlgorithmError, MissingEncryptionAlgorithmError, UnsupportedEncryptionAlgorithmError, UnsupportedCompressionAlgorithmError, InvalidHeaderParameterNameError, InvalidAlgorithmForMultipleRecipientsMode, KeyMismatchError, ) class JsonWebEncryption: #: Registered Header Parameter Names defined by Section 4.1 REGISTERED_HEADER_PARAMETER_NAMES = frozenset([ 'alg', 'enc', 'zip', 'jku', 'jwk', 'kid', 'x5u', 'x5c', 'x5t', 'x5t#S256', 'typ', 'cty', 'crit' ]) ALG_REGISTRY = {} ENC_REGISTRY = {} ZIP_REGISTRY = {} def __init__(self, algorithms=None, private_headers=None): self._algorithms = algorithms self._private_headers = private_headers @classmethod def register_algorithm(cls, algorithm): """Register an algorithm for ``alg`` or ``enc`` or ``zip`` of JWE.""" if not algorithm or algorithm.algorithm_type != 'JWE': raise ValueError( f'Invalid algorithm for JWE, {algorithm!r}') if algorithm.algorithm_location == 'alg': cls.ALG_REGISTRY[algorithm.name] = algorithm elif algorithm.algorithm_location == 'enc': cls.ENC_REGISTRY[algorithm.name] = algorithm elif algorithm.algorithm_location == 'zip': cls.ZIP_REGISTRY[algorithm.name] = algorithm def serialize_compact(self, protected, payload, key, sender_key=None): """Generate a JWE Compact Serialization. The JWE Compact Serialization represents encrypted content as a compact, URL-safe string. This string is:: BASE64URL(UTF8(JWE Protected Header)) || '.' || BASE64URL(JWE Encrypted Key) || '.' || BASE64URL(JWE Initialization Vector) || '.' || BASE64URL(JWE Ciphertext) || '.' || BASE64URL(JWE Authentication Tag) Only one recipient is supported by the JWE Compact Serialization and it provides no syntax to represent JWE Shared Unprotected Header, JWE Per-Recipient Unprotected Header, or JWE AAD values. :param protected: A dict of protected header :param payload: Payload (bytes or a value convertible to bytes) :param key: Public key used to encrypt payload :param sender_key: Sender's private key in case JWEAlgorithmWithTagAwareKeyAgreement is used :return: JWE compact serialization as bytes """ # step 1: Prepare algorithms & key alg = self.get_header_alg(protected) enc = self.get_header_enc(protected) zip_alg = self.get_header_zip(protected) self._validate_sender_key(sender_key, alg) self._validate_private_headers(protected, alg) key = prepare_key(alg, protected, key) if sender_key is not None: sender_key = alg.prepare_key(sender_key) # self._post_validate_header(protected, algorithm) # step 2: Generate a random Content Encryption Key (CEK) # use enc_alg.generate_cek() in scope of upcoming .wrap or .generate_keys_and_prepare_headers call # step 3: Encrypt the CEK with the recipient's public key if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement) and alg.key_size is not None: # For a JWE algorithm with tag-aware key agreement in case key agreement with key wrapping mode is used: # Defer key agreement with key wrapping until authentication tag is computed prep = alg.generate_keys_and_prepare_headers(enc, key, sender_key) epk = prep['epk'] cek = prep['cek'] protected.update(prep['header']) else: # In any other case: # Keep the normal steps order defined by RFC 7516 if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement): wrapped = alg.wrap(enc, protected, key, sender_key) else: wrapped = alg.wrap(enc, protected, key) cek = wrapped['cek'] ek = wrapped['ek'] if 'header' in wrapped: protected.update(wrapped['header']) # step 4: Generate a random JWE Initialization Vector iv = enc.generate_iv() # step 5: Let the Additional Authenticated Data encryption parameter # be ASCII(BASE64URL(UTF8(JWE Protected Header))) protected_segment = json_b64encode(protected) aad = to_bytes(protected_segment, 'ascii') # step 6: compress message if required if zip_alg: msg = zip_alg.compress(to_bytes(payload)) else: msg = to_bytes(payload) # step 7: perform encryption ciphertext, tag = enc.encrypt(msg, aad, iv, cek) if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement) and alg.key_size is not None: # For a JWE algorithm with tag-aware key agreement in case key agreement with key wrapping mode is used: # Perform key agreement with key wrapping deferred at step 3 wrapped = alg.agree_upon_key_and_wrap_cek(enc, protected, key, sender_key, epk, cek, tag) ek = wrapped['ek'] # step 8: build resulting message return b'.'.join([ protected_segment, urlsafe_b64encode(ek), urlsafe_b64encode(iv), urlsafe_b64encode(ciphertext), urlsafe_b64encode(tag) ]) def serialize_json(self, header_obj, payload, keys, sender_key=None): """Generate a JWE JSON Serialization (in fully general syntax). The JWE JSON Serialization represents encrypted content as a JSON object. This representation is neither optimized for compactness nor URL safe. The following members are defined for use in top-level JSON objects used for the fully general JWE JSON Serialization syntax: protected The "protected" member MUST be present and contain the value BASE64URL(UTF8(JWE Protected Header)) when the JWE Protected Header value is non-empty; otherwise, it MUST be absent. These Header Parameter values are integrity protected. unprotected The "unprotected" member MUST be present and contain the value JWE Shared Unprotected Header when the JWE Shared Unprotected Header value is non-empty; otherwise, it MUST be absent. This value is represented as an unencoded JSON object, rather than as a string. These Header Parameter values are not integrity protected. iv The "iv" member MUST be present and contain the value BASE64URL(JWE Initialization Vector) when the JWE Initialization Vector value is non-empty; otherwise, it MUST be absent. aad The "aad" member MUST be present and contain the value BASE64URL(JWE AAD)) when the JWE AAD value is non-empty; otherwise, it MUST be absent. A JWE AAD value can be included to supply a base64url-encoded value to be integrity protected but not encrypted. ciphertext The "ciphertext" member MUST be present and contain the value BASE64URL(JWE Ciphertext). tag The "tag" member MUST be present and contain the value BASE64URL(JWE Authentication Tag) when the JWE Authentication Tag value is non-empty; otherwise, it MUST be absent. recipients The "recipients" member value MUST be an array of JSON objects. Each object contains information specific to a single recipient. This member MUST be present with exactly one array element per recipient, even if some or all of the array element values are the empty JSON object "{}" (which can happen when all Header Parameter values are shared between all recipients and when no encrypted key is used, such as when doing Direct Encryption). The following members are defined for use in the JSON objects that are elements of the "recipients" array: header The "header" member MUST be present and contain the value JWE Per- Recipient Unprotected Header when the JWE Per-Recipient Unprotected Header value is non-empty; otherwise, it MUST be absent. This value is represented as an unencoded JSON object, rather than as a string. These Header Parameter values are not integrity protected. encrypted_key The "encrypted_key" member MUST be present and contain the value BASE64URL(JWE Encrypted Key) when the JWE Encrypted Key value is non-empty; otherwise, it MUST be absent. This implementation assumes that "alg" and "enc" header fields are contained in the protected or shared unprotected header. :param header_obj: A dict of headers (in addition optionally contains JWE AAD) :param payload: Payload (bytes or a value convertible to bytes) :param keys: Public keys (or a single public key) used to encrypt payload :param sender_key: Sender's private key in case JWEAlgorithmWithTagAwareKeyAgreement is used :return: JWE JSON serialization (in fully general syntax) as dict Example of `header_obj`:: { "protected": { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll" }, "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ], "aad": b'Authenticate me too.' } """ if not isinstance(keys, list): # single key keys = [keys] if not keys: raise ValueError("No keys have been provided") header_obj = deepcopy(header_obj) shared_header = JWESharedHeader.from_dict(header_obj) recipients = header_obj.get('recipients') if recipients is None: recipients = [{} for _ in keys] for i in range(len(recipients)): if recipients[i] is None: recipients[i] = {} if 'header' not in recipients[i]: recipients[i]['header'] = {} jwe_aad = header_obj.get('aad') if len(keys) != len(recipients): raise ValueError("Count of recipient keys {} does not equal to count of recipients {}" .format(len(keys), len(recipients))) # step 1: Prepare algorithms & key alg = self.get_header_alg(shared_header) enc = self.get_header_enc(shared_header) zip_alg = self.get_header_zip(shared_header) self._validate_sender_key(sender_key, alg) self._validate_private_headers(shared_header, alg) for recipient in recipients: self._validate_private_headers(recipient['header'], alg) for i in range(len(keys)): keys[i] = prepare_key(alg, recipients[i]['header'], keys[i]) if sender_key is not None: sender_key = alg.prepare_key(sender_key) # self._post_validate_header(protected, algorithm) # step 2: Generate a random Content Encryption Key (CEK) # use enc_alg.generate_cek() in scope of upcoming .wrap or .generate_keys_and_prepare_headers call # step 3: Encrypt the CEK with the recipient's public key preset = alg.generate_preset(enc, keys[0]) if 'cek' in preset: cek = preset['cek'] else: cek = None if len(keys) > 1 and cek is None: raise InvalidAlgorithmForMultipleRecipientsMode(alg.name) if 'header' in preset: shared_header.update_protected(preset['header']) if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement) and alg.key_size is not None: # For a JWE algorithm with tag-aware key agreement in case key agreement with key wrapping mode is used: # Defer key agreement with key wrapping until authentication tag is computed epks = [] for i in range(len(keys)): prep = alg.generate_keys_and_prepare_headers(enc, keys[i], sender_key, preset) if cek is None: cek = prep['cek'] epks.append(prep['epk']) recipients[i]['header'].update(prep['header']) else: # In any other case: # Keep the normal steps order defined by RFC 7516 for i in range(len(keys)): if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement): wrapped = alg.wrap(enc, shared_header, keys[i], sender_key, preset) else: wrapped = alg.wrap(enc, shared_header, keys[i], preset) if cek is None: cek = wrapped['cek'] recipients[i]['encrypted_key'] = wrapped['ek'] if 'header' in wrapped: recipients[i]['header'].update(wrapped['header']) # step 4: Generate a random JWE Initialization Vector iv = enc.generate_iv() # step 5: Compute the Encoded Protected Header value # BASE64URL(UTF8(JWE Protected Header)). If the JWE Protected Header # is not present, let this value be the empty string. # Let the Additional Authenticated Data encryption parameter be # ASCII(Encoded Protected Header). However, if a JWE AAD value is # present, instead let the Additional Authenticated Data encryption # parameter be ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)). aad = json_b64encode(shared_header.protected) if shared_header.protected else b'' if jwe_aad is not None: aad += b'.' + urlsafe_b64encode(jwe_aad) aad = to_bytes(aad, 'ascii') # step 6: compress message if required if zip_alg: msg = zip_alg.compress(to_bytes(payload)) else: msg = to_bytes(payload) # step 7: perform encryption ciphertext, tag = enc.encrypt(msg, aad, iv, cek) if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement) and alg.key_size is not None: # For a JWE algorithm with tag-aware key agreement in case key agreement with key wrapping mode is used: # Perform key agreement with key wrapping deferred at step 3 for i in range(len(keys)): wrapped = alg.agree_upon_key_and_wrap_cek(enc, shared_header, keys[i], sender_key, epks[i], cek, tag) recipients[i]['encrypted_key'] = wrapped['ek'] # step 8: build resulting message obj = OrderedDict() if shared_header.protected: obj['protected'] = to_unicode(json_b64encode(shared_header.protected)) if shared_header.unprotected: obj['unprotected'] = shared_header.unprotected for recipient in recipients: if not recipient['header']: del recipient['header'] recipient['encrypted_key'] = to_unicode(urlsafe_b64encode(recipient['encrypted_key'])) for member in set(recipient.keys()): if member not in {'header', 'encrypted_key'}: del recipient[member] obj['recipients'] = recipients if jwe_aad is not None: obj['aad'] = to_unicode(urlsafe_b64encode(jwe_aad)) obj['iv'] = to_unicode(urlsafe_b64encode(iv)) obj['ciphertext'] = to_unicode(urlsafe_b64encode(ciphertext)) obj['tag'] = to_unicode(urlsafe_b64encode(tag)) return obj def serialize(self, header, payload, key, sender_key=None): """Generate a JWE Serialization. It will automatically generate a compact or JSON serialization depending on `header` argument. If `header` is a dict with "protected", "unprotected" and/or "recipients" keys, it will call `serialize_json`, otherwise it will call `serialize_compact`. :param header: A dict of header(s) :param payload: Payload (bytes or a value convertible to bytes) :param key: Public key(s) used to encrypt payload :param sender_key: Sender's private key in case JWEAlgorithmWithTagAwareKeyAgreement is used :return: JWE compact serialization as bytes or JWE JSON serialization as dict """ if 'protected' in header or 'unprotected' in header or 'recipients' in header: return self.serialize_json(header, payload, key, sender_key) return self.serialize_compact(header, payload, key, sender_key) def deserialize_compact(self, s, key, decode=None, sender_key=None): """Extract JWE Compact Serialization. :param s: JWE Compact Serialization as bytes :param key: Private key used to decrypt payload (optionally can be a tuple of kid and essentially key) :param decode: Function to decode payload data :param sender_key: Sender's public key in case JWEAlgorithmWithTagAwareKeyAgreement is used :return: dict with `header` and `payload` keys where `header` value is a dict containing protected header fields """ try: s = to_bytes(s) protected_s, ek_s, iv_s, ciphertext_s, tag_s = s.rsplit(b'.') except ValueError: raise DecodeError('Not enough segments') protected = extract_header(protected_s, DecodeError) ek = extract_segment(ek_s, DecodeError, 'encryption key') iv = extract_segment(iv_s, DecodeError, 'initialization vector') ciphertext = extract_segment(ciphertext_s, DecodeError, 'ciphertext') tag = extract_segment(tag_s, DecodeError, 'authentication tag') alg = self.get_header_alg(protected) enc = self.get_header_enc(protected) zip_alg = self.get_header_zip(protected) self._validate_sender_key(sender_key, alg) self._validate_private_headers(protected, alg) if isinstance(key, tuple) and len(key) == 2: # Ignore separately provided kid, extract essentially key only key = key[1] key = prepare_key(alg, protected, key) if sender_key is not None: sender_key = alg.prepare_key(sender_key) if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement): # For a JWE algorithm with tag-aware key agreement: if alg.key_size is not None: # In case key agreement with key wrapping mode is used: # Provide authentication tag to .unwrap method cek = alg.unwrap(enc, ek, protected, key, sender_key, tag) else: # Otherwise, don't provide authentication tag to .unwrap method cek = alg.unwrap(enc, ek, protected, key, sender_key) else: # For any other JWE algorithm: # Don't provide authentication tag to .unwrap method cek = alg.unwrap(enc, ek, protected, key) aad = to_bytes(protected_s, 'ascii') msg = enc.decrypt(ciphertext, aad, iv, tag, cek) if zip_alg: payload = zip_alg.decompress(to_bytes(msg)) else: payload = msg if decode: payload = decode(payload) return {'header': protected, 'payload': payload} def deserialize_json(self, obj, key, decode=None, sender_key=None): """Extract JWE JSON Serialization. :param obj: JWE JSON Serialization as dict or str :param key: Private key used to decrypt payload (optionally can be a tuple of kid and essentially key) :param decode: Function to decode payload data :param sender_key: Sender's public key in case JWEAlgorithmWithTagAwareKeyAgreement is used :return: dict with `header` and `payload` keys where `header` value is a dict containing `protected`, `unprotected`, `recipients` and/or `aad` keys """ obj = ensure_dict(obj, 'JWE') obj = deepcopy(obj) if 'protected' in obj: protected = extract_header(to_bytes(obj['protected']), DecodeError) else: protected = None unprotected = obj.get('unprotected') recipients = obj['recipients'] for recipient in recipients: if 'header' not in recipient: recipient['header'] = {} recipient['encrypted_key'] = extract_segment( to_bytes(recipient['encrypted_key']), DecodeError, 'encrypted key') if 'aad' in obj: jwe_aad = extract_segment(to_bytes(obj['aad']), DecodeError, 'JWE AAD') else: jwe_aad = None iv = extract_segment(to_bytes(obj['iv']), DecodeError, 'initialization vector') ciphertext = extract_segment(to_bytes(obj['ciphertext']), DecodeError, 'ciphertext') tag = extract_segment(to_bytes(obj['tag']), DecodeError, 'authentication tag') shared_header = JWESharedHeader(protected, unprotected) alg = self.get_header_alg(shared_header) enc = self.get_header_enc(shared_header) zip_alg = self.get_header_zip(shared_header) self._validate_sender_key(sender_key, alg) self._validate_private_headers(shared_header, alg) for recipient in recipients: self._validate_private_headers(recipient['header'], alg) kid = None if isinstance(key, tuple) and len(key) == 2: # Extract separately provided kid and essentially key kid = key[0] key = key[1] key = alg.prepare_key(key) if kid is None: # If kid has not been provided separately, try to get it from key itself kid = key.kid if sender_key is not None: sender_key = alg.prepare_key(sender_key) def _unwrap_with_sender_key_and_tag(ek, header): return alg.unwrap(enc, ek, header, key, sender_key, tag) def _unwrap_with_sender_key_and_without_tag(ek, header): return alg.unwrap(enc, ek, header, key, sender_key) def _unwrap_without_sender_key_and_tag(ek, header): return alg.unwrap(enc, ek, header, key) def _unwrap_for_matching_recipient(unwrap_func): if kid is not None: for recipient in recipients: if recipient['header'].get('kid') == kid: header = JWEHeader(protected, unprotected, recipient['header']) return unwrap_func(recipient['encrypted_key'], header) # Since no explicit match has been found, iterate over all the recipients error = None for recipient in recipients: header = JWEHeader(protected, unprotected, recipient['header']) try: return unwrap_func(recipient['encrypted_key'], header) except Exception as e: error = e else: if error is None: raise KeyMismatchError() else: raise error if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement): # For a JWE algorithm with tag-aware key agreement: if alg.key_size is not None: # In case key agreement with key wrapping mode is used: # Provide authentication tag to .unwrap method cek = _unwrap_for_matching_recipient(_unwrap_with_sender_key_and_tag) else: # Otherwise, don't provide authentication tag to .unwrap method cek = _unwrap_for_matching_recipient(_unwrap_with_sender_key_and_without_tag) else: # For any other JWE algorithm: # Don't provide authentication tag to .unwrap method cek = _unwrap_for_matching_recipient(_unwrap_without_sender_key_and_tag) aad = to_bytes(obj.get('protected', '')) if 'aad' in obj: aad += b'.' + to_bytes(obj['aad']) aad = to_bytes(aad, 'ascii') msg = enc.decrypt(ciphertext, aad, iv, tag, cek) if zip_alg: payload = zip_alg.decompress(to_bytes(msg)) else: payload = msg if decode: payload = decode(payload) for recipient in recipients: if not recipient['header']: del recipient['header'] for member in set(recipient.keys()): if member != 'header': del recipient[member] header = {} if protected: header['protected'] = protected if unprotected: header['unprotected'] = unprotected header['recipients'] = recipients if jwe_aad is not None: header['aad'] = jwe_aad return { 'header': header, 'payload': payload } def deserialize(self, obj, key, decode=None, sender_key=None): """Extract a JWE Serialization. It supports both compact and JSON serialization. :param obj: JWE compact serialization as bytes or JWE JSON serialization as dict or str :param key: Private key used to decrypt payload (optionally can be a tuple of kid and essentially key) :param decode: Function to decode payload data :param sender_key: Sender's public key in case JWEAlgorithmWithTagAwareKeyAgreement is used :return: dict with `header` and `payload` keys """ if isinstance(obj, dict): return self.deserialize_json(obj, key, decode, sender_key) obj = to_bytes(obj) if obj.startswith(b'{') and obj.endswith(b'}'): return self.deserialize_json(obj, key, decode, sender_key) return self.deserialize_compact(obj, key, decode, sender_key) @staticmethod def parse_json(obj): """Parse JWE JSON Serialization. :param obj: JWE JSON Serialization as str or dict :return: Parsed JWE JSON Serialization as dict if `obj` is an str, or `obj` as is if `obj` is already a dict """ return ensure_dict(obj, 'JWE') def get_header_alg(self, header): if 'alg' not in header: raise MissingAlgorithmError() alg = header['alg'] if self._algorithms is not None and alg not in self._algorithms: raise UnsupportedAlgorithmError() if alg not in self.ALG_REGISTRY: raise UnsupportedAlgorithmError() return self.ALG_REGISTRY[alg] def get_header_enc(self, header): if 'enc' not in header: raise MissingEncryptionAlgorithmError() enc = header['enc'] if self._algorithms is not None and enc not in self._algorithms: raise UnsupportedEncryptionAlgorithmError() if enc not in self.ENC_REGISTRY: raise UnsupportedEncryptionAlgorithmError() return self.ENC_REGISTRY[enc] def get_header_zip(self, header): if 'zip' in header: z = header['zip'] if self._algorithms is not None and z not in self._algorithms: raise UnsupportedCompressionAlgorithmError() if z not in self.ZIP_REGISTRY: raise UnsupportedCompressionAlgorithmError() return self.ZIP_REGISTRY[z] def _validate_sender_key(self, sender_key, alg): if isinstance(alg, JWEAlgorithmWithTagAwareKeyAgreement): if sender_key is None: raise ValueError("{} algorithm requires sender_key but passed sender_key value is None" .format(alg.name)) else: if sender_key is not None: raise ValueError("{} algorithm does not use sender_key but passed sender_key value is not None" .format(alg.name)) def _validate_private_headers(self, header, alg): # only validate private headers when developers set # private headers explicitly if self._private_headers is None: return names = self.REGISTERED_HEADER_PARAMETER_NAMES.copy() names = names.union(self._private_headers) if alg.EXTRA_HEADERS: names = names.union(alg.EXTRA_HEADERS) for k in header: if k not in names: raise InvalidHeaderParameterNameError(k) def prepare_key(alg, header, key): if callable(key): key = key(header, None) elif key is None and 'jwk' in header: key = header['jwk'] return alg.prepare_key(key) authlib-1.3.2/authlib/jose/rfc7516/models.py000066400000000000000000000103651466226534200205060ustar00rootroot00000000000000import os from abc import ABCMeta class JWEAlgorithmBase(metaclass=ABCMeta): """Base interface for all JWE algorithms. """ EXTRA_HEADERS = None name = None description = None algorithm_type = 'JWE' algorithm_location = 'alg' def prepare_key(self, raw_data): raise NotImplementedError def generate_preset(self, enc_alg, key): raise NotImplementedError class JWEAlgorithm(JWEAlgorithmBase, metaclass=ABCMeta): """Interface for JWE algorithm conforming to RFC7518. JWA specification (RFC7518) SHOULD implement the algorithms for JWE with this base implementation. """ def wrap(self, enc_alg, headers, key, preset=None): raise NotImplementedError def unwrap(self, enc_alg, ek, headers, key): raise NotImplementedError class JWEAlgorithmWithTagAwareKeyAgreement(JWEAlgorithmBase, metaclass=ABCMeta): """Interface for JWE algorithm with tag-aware key agreement (in key agreement with key wrapping mode). ECDH-1PU is an example of such an algorithm. """ def generate_keys_and_prepare_headers(self, enc_alg, key, sender_key, preset=None): raise NotImplementedError def agree_upon_key_and_wrap_cek(self, enc_alg, headers, key, sender_key, epk, cek, tag): raise NotImplementedError def wrap(self, enc_alg, headers, key, sender_key, preset=None): raise NotImplementedError def unwrap(self, enc_alg, ek, headers, key, sender_key, tag=None): raise NotImplementedError class JWEEncAlgorithm: name = None description = None algorithm_type = 'JWE' algorithm_location = 'enc' IV_SIZE = None CEK_SIZE = None def generate_cek(self): return os.urandom(self.CEK_SIZE // 8) def generate_iv(self): return os.urandom(self.IV_SIZE // 8) def check_iv(self, iv): if len(iv) * 8 != self.IV_SIZE: raise ValueError('Invalid "iv" size') def encrypt(self, msg, aad, iv, key): """Encrypt the given "msg" text. :param msg: text to be encrypt in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param key: encrypted key in bytes :return: (ciphertext, tag) """ raise NotImplementedError def decrypt(self, ciphertext, aad, iv, tag, key): """Decrypt the given cipher text. :param ciphertext: ciphertext in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param tag: authentication tag in bytes :param key: encrypted key in bytes :return: message """ raise NotImplementedError class JWEZipAlgorithm: name = None description = None algorithm_type = 'JWE' algorithm_location = 'zip' def compress(self, s): raise NotImplementedError def decompress(self, s): raise NotImplementedError class JWESharedHeader(dict): """Shared header object for JWE. Combines protected header and shared unprotected header together. """ def __init__(self, protected, unprotected): obj = {} if protected: obj.update(protected) if unprotected: obj.update(unprotected) super().__init__(obj) self.protected = protected if protected else {} self.unprotected = unprotected if unprotected else {} def update_protected(self, addition): self.update(addition) self.protected.update(addition) @classmethod def from_dict(cls, obj): if isinstance(obj, cls): return obj return cls(obj.get('protected'), obj.get('unprotected')) class JWEHeader(dict): """Header object for JWE. Combines protected header, shared unprotected header and specific recipient's unprotected header together. """ def __init__(self, protected, unprotected, header): obj = {} if protected: obj.update(protected) if unprotected: obj.update(unprotected) if header: obj.update(header) super().__init__(obj) self.protected = protected if protected else {} self.unprotected = unprotected if unprotected else {} self.header = header if header else {} authlib-1.3.2/authlib/jose/rfc7517/000077500000000000000000000000001466226534200166455ustar00rootroot00000000000000authlib-1.3.2/authlib/jose/rfc7517/__init__.py000066400000000000000000000006501466226534200207570ustar00rootroot00000000000000""" authlib.jose.rfc7517 ~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of JSON Web Key (JWK). https://tools.ietf.org/html/rfc7517 """ from ._cryptography_key import load_pem_key from .base_key import Key from .asymmetric_key import AsymmetricKey from .key_set import KeySet from .jwk import JsonWebKey __all__ = ['Key', 'AsymmetricKey', 'KeySet', 'JsonWebKey', 'load_pem_key'] authlib-1.3.2/authlib/jose/rfc7517/_cryptography_key.py000066400000000000000000000023511466226534200227620ustar00rootroot00000000000000from cryptography.x509 import load_pem_x509_certificate from cryptography.hazmat.primitives.serialization import ( load_pem_private_key, load_pem_public_key, load_ssh_public_key, ) from cryptography.hazmat.backends import default_backend from authlib.common.encoding import to_bytes def load_pem_key(raw, ssh_type=None, key_type=None, password=None): raw = to_bytes(raw) if ssh_type and raw.startswith(ssh_type): return load_ssh_public_key(raw, backend=default_backend()) if key_type == 'public': return load_pem_public_key(raw, backend=default_backend()) if key_type == 'private' or password is not None: return load_pem_private_key(raw, password=password, backend=default_backend()) if b'PUBLIC' in raw: return load_pem_public_key(raw, backend=default_backend()) if b'PRIVATE' in raw: return load_pem_private_key(raw, password=password, backend=default_backend()) if b'CERTIFICATE' in raw: cert = load_pem_x509_certificate(raw, default_backend()) return cert.public_key() try: return load_pem_private_key(raw, password=password, backend=default_backend()) except ValueError: return load_pem_public_key(raw, backend=default_backend()) authlib-1.3.2/authlib/jose/rfc7517/asymmetric_key.py000066400000000000000000000141251466226534200222470ustar00rootroot00000000000000from authlib.common.encoding import to_bytes from cryptography.hazmat.primitives.serialization import ( Encoding, PrivateFormat, PublicFormat, BestAvailableEncryption, NoEncryption, ) from ._cryptography_key import load_pem_key from .base_key import Key class AsymmetricKey(Key): """This is the base class for a JSON Web Key.""" PUBLIC_KEY_FIELDS = [] PRIVATE_KEY_FIELDS = [] PRIVATE_KEY_CLS = bytes PUBLIC_KEY_CLS = bytes SSH_PUBLIC_PREFIX = b'' def __init__(self, private_key=None, public_key=None, options=None): super().__init__(options) self.private_key = private_key self.public_key = public_key @property def public_only(self): if self.private_key: return False if 'd' in self.tokens: return False return True def get_op_key(self, operation): """Get the raw key for the given key_op. This method will also check if the given key_op is supported by this key. :param operation: key operation value, such as "sign", "encrypt". :return: raw key """ self.check_key_op(operation) if operation in self.PUBLIC_KEY_OPS: return self.get_public_key() return self.get_private_key() def get_public_key(self): if self.public_key: return self.public_key private_key = self.get_private_key() if private_key: return private_key.public_key() return self.public_key def get_private_key(self): if self.private_key: return self.private_key if self.tokens: self.load_raw_key() return self.private_key def load_raw_key(self): if 'd' in self.tokens: self.private_key = self.load_private_key() else: self.public_key = self.load_public_key() def load_dict_key(self): if self.private_key: self._dict_data.update(self.dumps_private_key()) else: self._dict_data.update(self.dumps_public_key()) def dumps_private_key(self): raise NotImplementedError() def dumps_public_key(self): raise NotImplementedError() def load_private_key(self): raise NotImplementedError() def load_public_key(self): raise NotImplementedError() def as_dict(self, is_private=False, **params): """Represent this key as a dict of the JSON Web Key.""" tokens = self.tokens if is_private and 'd' not in tokens: raise ValueError('This is a public key') kid = tokens.get('kid') if 'd' in tokens and not is_private: # filter out private fields tokens = {k: tokens[k] for k in tokens if k in self.PUBLIC_KEY_FIELDS} tokens['kty'] = self.kty if kid: tokens['kid'] = kid if not kid: tokens['kid'] = self.thumbprint() tokens.update(params) return tokens def as_key(self, is_private=False): """Represent this key as raw key.""" if is_private: return self.get_private_key() return self.get_public_key() def as_bytes(self, encoding=None, is_private=False, password=None): """Export key into PEM/DER format bytes. :param encoding: "PEM" or "DER" :param is_private: export private key or public key :param password: encrypt private key with password :return: bytes """ if encoding is None or encoding == 'PEM': encoding = Encoding.PEM elif encoding == 'DER': encoding = Encoding.DER else: raise ValueError(f'Invalid encoding: {encoding!r}') raw_key = self.as_key(is_private) if is_private: if not raw_key: raise ValueError('This is a public key') if password is None: encryption_algorithm = NoEncryption() else: encryption_algorithm = BestAvailableEncryption(to_bytes(password)) return raw_key.private_bytes( encoding=encoding, format=PrivateFormat.PKCS8, encryption_algorithm=encryption_algorithm, ) return raw_key.public_bytes( encoding=encoding, format=PublicFormat.SubjectPublicKeyInfo, ) def as_pem(self, is_private=False, password=None): return self.as_bytes(is_private=is_private, password=password) def as_der(self, is_private=False, password=None): return self.as_bytes(encoding='DER', is_private=is_private, password=password) @classmethod def import_dict_key(cls, raw, options=None): cls.check_required_fields(raw) key = cls(options=options) key._dict_data = raw return key @classmethod def import_key(cls, raw, options=None): if isinstance(raw, cls): if options is not None: raw.options.update(options) return raw if isinstance(raw, cls.PUBLIC_KEY_CLS): key = cls(public_key=raw, options=options) elif isinstance(raw, cls.PRIVATE_KEY_CLS): key = cls(private_key=raw, options=options) elif isinstance(raw, dict): key = cls.import_dict_key(raw, options) else: if options is not None: password = options.pop('password', None) else: password = None raw_key = load_pem_key(raw, cls.SSH_PUBLIC_PREFIX, password=password) if isinstance(raw_key, cls.PUBLIC_KEY_CLS): key = cls(public_key=raw_key, options=options) elif isinstance(raw_key, cls.PRIVATE_KEY_CLS): key = cls(private_key=raw_key, options=options) else: raise ValueError('Invalid data for importing key') return key @classmethod def validate_raw_key(cls, key): return isinstance(key, cls.PUBLIC_KEY_CLS) or isinstance(key, cls.PRIVATE_KEY_CLS) @classmethod def generate_key(cls, crv_or_size, options=None, is_private=False): raise NotImplementedError() authlib-1.3.2/authlib/jose/rfc7517/base_key.py000066400000000000000000000062751466226534200210130ustar00rootroot00000000000000import hashlib from collections import OrderedDict from authlib.common.encoding import ( json_dumps, to_bytes, to_unicode, urlsafe_b64encode, ) from ..errors import InvalidUseError class Key: """This is the base class for a JSON Web Key.""" kty = '_' ALLOWED_PARAMS = [ 'use', 'key_ops', 'alg', 'kid', 'x5u', 'x5c', 'x5t', 'x5t#S256' ] PRIVATE_KEY_OPS = [ 'sign', 'decrypt', 'unwrapKey', ] PUBLIC_KEY_OPS = [ 'verify', 'encrypt', 'wrapKey', ] REQUIRED_JSON_FIELDS = [] def __init__(self, options=None): self.options = options or {} self._dict_data = {} @property def tokens(self): if not self._dict_data: self.load_dict_key() rv = dict(self._dict_data) rv['kty'] = self.kty for k in self.ALLOWED_PARAMS: if k not in rv and k in self.options: rv[k] = self.options[k] return rv @property def kid(self): return self.tokens.get('kid') def keys(self): return self.tokens.keys() def __getitem__(self, item): return self.tokens[item] @property def public_only(self): raise NotImplementedError() def load_raw_key(self): raise NotImplementedError() def load_dict_key(self): raise NotImplementedError() def check_key_op(self, operation): """Check if the given key_op is supported by this key. :param operation: key operation value, such as "sign", "encrypt". :raise: ValueError """ key_ops = self.tokens.get('key_ops') if key_ops is not None and operation not in key_ops: raise ValueError(f'Unsupported key_op "{operation}"') if operation in self.PRIVATE_KEY_OPS and self.public_only: raise ValueError(f'Invalid key_op "{operation}" for public key') use = self.tokens.get('use') if use: if operation in ['sign', 'verify']: if use != 'sig': raise InvalidUseError() elif operation in ['decrypt', 'encrypt', 'wrapKey', 'unwrapKey']: if use != 'enc': raise InvalidUseError() def as_dict(self, is_private=False, **params): raise NotImplementedError() def as_json(self, is_private=False, **params): """Represent this key as a JSON string.""" obj = self.as_dict(is_private, **params) return json_dumps(obj) def thumbprint(self): """Implementation of RFC7638 JSON Web Key (JWK) Thumbprint.""" fields = list(self.REQUIRED_JSON_FIELDS) fields.append('kty') fields.sort() data = OrderedDict() for k in fields: data[k] = self.tokens[k] json_data = json_dumps(data) digest_data = hashlib.sha256(to_bytes(json_data)).digest() return to_unicode(urlsafe_b64encode(digest_data)) @classmethod def check_required_fields(cls, data): for k in cls.REQUIRED_JSON_FIELDS: if k not in data: raise ValueError(f'Missing required field: "{k}"') @classmethod def validate_raw_key(cls, key): raise NotImplementedError() authlib-1.3.2/authlib/jose/rfc7517/jwk.py000066400000000000000000000037721466226534200200230ustar00rootroot00000000000000from authlib.common.encoding import json_loads from .key_set import KeySet from ._cryptography_key import load_pem_key class JsonWebKey: JWK_KEY_CLS = {} @classmethod def generate_key(cls, kty, crv_or_size, options=None, is_private=False): """Generate a Key with the given key type, curve name or bit size. :param kty: string of ``oct``, ``RSA``, ``EC``, ``OKP`` :param crv_or_size: curve name or bit size :param options: a dict of other options for Key :param is_private: create a private key or public key :return: Key instance """ key_cls = cls.JWK_KEY_CLS[kty] return key_cls.generate_key(crv_or_size, options, is_private) @classmethod def import_key(cls, raw, options=None): """Import a Key from bytes, string, PEM or dict. :return: Key instance """ kty = None if options is not None: kty = options.get('kty') if kty is None and isinstance(raw, dict): kty = raw.get('kty') if kty is None: raw_key = load_pem_key(raw) for _kty in cls.JWK_KEY_CLS: key_cls = cls.JWK_KEY_CLS[_kty] if key_cls.validate_raw_key(raw_key): return key_cls.import_key(raw_key, options) key_cls = cls.JWK_KEY_CLS[kty] return key_cls.import_key(raw, options) @classmethod def import_key_set(cls, raw): """Import KeySet from string, dict or a list of keys. :return: KeySet instance """ raw = _transform_raw_key(raw) if isinstance(raw, dict) and 'keys' in raw: keys = raw.get('keys') return KeySet([cls.import_key(k) for k in keys]) raise ValueError('Invalid key set format') def _transform_raw_key(raw): if isinstance(raw, str) and \ raw.startswith('{') and raw.endswith('}'): return json_loads(raw) elif isinstance(raw, (tuple, list)): return {'keys': raw} return raw authlib-1.3.2/authlib/jose/rfc7517/key_set.py000066400000000000000000000015631466226534200206670ustar00rootroot00000000000000from authlib.common.encoding import json_dumps class KeySet: """This class represents a JSON Web Key Set.""" def __init__(self, keys): self.keys = keys def as_dict(self, is_private=False, **params): """Represent this key as a dict of the JSON Web Key Set.""" return {'keys': [k.as_dict(is_private, **params) for k in self.keys]} def as_json(self, is_private=False, **params): """Represent this key set as a JSON string.""" obj = self.as_dict(is_private, **params) return json_dumps(obj) def find_by_kid(self, kid): """Find the key matches the given kid value. :param kid: A string of kid :return: Key instance :raise: ValueError """ for k in self.keys: if k.kid == kid: return k raise ValueError('Invalid JSON Web Key Set') authlib-1.3.2/authlib/jose/rfc7518/000077500000000000000000000000001466226534200166465ustar00rootroot00000000000000authlib-1.3.2/authlib/jose/rfc7518/__init__.py000066400000000000000000000015571466226534200207670ustar00rootroot00000000000000from .oct_key import OctKey from .rsa_key import RSAKey from .ec_key import ECKey from .jws_algs import JWS_ALGORITHMS from .jwe_algs import JWE_ALG_ALGORITHMS, AESAlgorithm, ECDHESAlgorithm, u32be_len_input from .jwe_encs import JWE_ENC_ALGORITHMS, CBCHS2EncAlgorithm from .jwe_zips import DeflateZipAlgorithm def register_jws_rfc7518(cls): for algorithm in JWS_ALGORITHMS: cls.register_algorithm(algorithm) def register_jwe_rfc7518(cls): for algorithm in JWE_ALG_ALGORITHMS: cls.register_algorithm(algorithm) for algorithm in JWE_ENC_ALGORITHMS: cls.register_algorithm(algorithm) cls.register_algorithm(DeflateZipAlgorithm()) __all__ = [ 'register_jws_rfc7518', 'register_jwe_rfc7518', 'OctKey', 'RSAKey', 'ECKey', 'u32be_len_input', 'AESAlgorithm', 'ECDHESAlgorithm', 'CBCHS2EncAlgorithm', ] authlib-1.3.2/authlib/jose/rfc7518/ec_key.py000066400000000000000000000066671466226534200204760ustar00rootroot00000000000000from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.ec import ( EllipticCurvePublicKey, EllipticCurvePrivateKeyWithSerialization, EllipticCurvePrivateNumbers, EllipticCurvePublicNumbers, SECP256R1, SECP384R1, SECP521R1, SECP256K1, ) from cryptography.hazmat.backends import default_backend from authlib.common.encoding import base64_to_int, int_to_base64 from ..rfc7517 import AsymmetricKey class ECKey(AsymmetricKey): """Key class of the ``EC`` key type.""" kty = 'EC' DSS_CURVES = { 'P-256': SECP256R1, 'P-384': SECP384R1, 'P-521': SECP521R1, # https://tools.ietf.org/html/rfc8812#section-3.1 'secp256k1': SECP256K1, } CURVES_DSS = { SECP256R1.name: 'P-256', SECP384R1.name: 'P-384', SECP521R1.name: 'P-521', SECP256K1.name: 'secp256k1', } REQUIRED_JSON_FIELDS = ['crv', 'x', 'y'] PUBLIC_KEY_FIELDS = REQUIRED_JSON_FIELDS PRIVATE_KEY_FIELDS = ['crv', 'd', 'x', 'y'] PUBLIC_KEY_CLS = EllipticCurvePublicKey PRIVATE_KEY_CLS = EllipticCurvePrivateKeyWithSerialization SSH_PUBLIC_PREFIX = b'ecdsa-sha2-' def exchange_shared_key(self, pubkey): # # used in ECDHESAlgorithm private_key = self.get_private_key() if private_key: return private_key.exchange(ec.ECDH(), pubkey) raise ValueError('Invalid key for exchanging shared key') @property def curve_key_size(self): raw_key = self.get_private_key() if not raw_key: raw_key = self.public_key return raw_key.curve.key_size def load_private_key(self): curve = self.DSS_CURVES[self._dict_data['crv']]() public_numbers = EllipticCurvePublicNumbers( base64_to_int(self._dict_data['x']), base64_to_int(self._dict_data['y']), curve, ) private_numbers = EllipticCurvePrivateNumbers( base64_to_int(self.tokens['d']), public_numbers ) return private_numbers.private_key(default_backend()) def load_public_key(self): curve = self.DSS_CURVES[self._dict_data['crv']]() public_numbers = EllipticCurvePublicNumbers( base64_to_int(self._dict_data['x']), base64_to_int(self._dict_data['y']), curve, ) return public_numbers.public_key(default_backend()) def dumps_private_key(self): numbers = self.private_key.private_numbers() return { 'crv': self.CURVES_DSS[self.private_key.curve.name], 'x': int_to_base64(numbers.public_numbers.x), 'y': int_to_base64(numbers.public_numbers.y), 'd': int_to_base64(numbers.private_value), } def dumps_public_key(self): numbers = self.public_key.public_numbers() return { 'crv': self.CURVES_DSS[numbers.curve.name], 'x': int_to_base64(numbers.x), 'y': int_to_base64(numbers.y) } @classmethod def generate_key(cls, crv='P-256', options=None, is_private=False) -> 'ECKey': if crv not in cls.DSS_CURVES: raise ValueError(f'Invalid crv value: "{crv}"') raw_key = ec.generate_private_key( curve=cls.DSS_CURVES[crv](), backend=default_backend(), ) if not is_private: raw_key = raw_key.public_key() return cls.import_key(raw_key, options=options) authlib-1.3.2/authlib/jose/rfc7518/jwe_algs.py000066400000000000000000000260571466226534200210250ustar00rootroot00000000000000import os import struct from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.keywrap import ( aes_key_wrap, aes_key_unwrap ) from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers.algorithms import AES from cryptography.hazmat.primitives.ciphers.modes import GCM from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash from authlib.common.encoding import ( to_bytes, to_native, urlsafe_b64decode, urlsafe_b64encode ) from authlib.jose.rfc7516 import JWEAlgorithm from .rsa_key import RSAKey from .ec_key import ECKey from .oct_key import OctKey class DirectAlgorithm(JWEAlgorithm): name = 'dir' description = 'Direct use of a shared symmetric key' def prepare_key(self, raw_data): return OctKey.import_key(raw_data) def generate_preset(self, enc_alg, key): return {} def wrap(self, enc_alg, headers, key, preset=None): cek = key.get_op_key('encrypt') if len(cek) * 8 != enc_alg.CEK_SIZE: raise ValueError('Invalid "cek" length') return {'ek': b'', 'cek': cek} def unwrap(self, enc_alg, ek, headers, key): cek = key.get_op_key('decrypt') if len(cek) * 8 != enc_alg.CEK_SIZE: raise ValueError('Invalid "cek" length') return cek class RSAAlgorithm(JWEAlgorithm): #: A key of size 2048 bits or larger MUST be used with these algorithms #: RSA1_5, RSA-OAEP, RSA-OAEP-256 key_size = 2048 def __init__(self, name, description, pad_fn): self.name = name self.description = description self.padding = pad_fn def prepare_key(self, raw_data): return RSAKey.import_key(raw_data) def generate_preset(self, enc_alg, key): cek = enc_alg.generate_cek() return {'cek': cek} def wrap(self, enc_alg, headers, key, preset=None): if preset and 'cek' in preset: cek = preset['cek'] else: cek = enc_alg.generate_cek() op_key = key.get_op_key('wrapKey') if op_key.key_size < self.key_size: raise ValueError('A key of size 2048 bits or larger MUST be used') ek = op_key.encrypt(cek, self.padding) return {'ek': ek, 'cek': cek} def unwrap(self, enc_alg, ek, headers, key): # it will raise ValueError if failed op_key = key.get_op_key('unwrapKey') cek = op_key.decrypt(ek, self.padding) if len(cek) * 8 != enc_alg.CEK_SIZE: raise ValueError('Invalid "cek" length') return cek class AESAlgorithm(JWEAlgorithm): def __init__(self, key_size): self.name = f'A{key_size}KW' self.description = f'AES Key Wrap using {key_size}-bit key' self.key_size = key_size def prepare_key(self, raw_data): return OctKey.import_key(raw_data) def generate_preset(self, enc_alg, key): cek = enc_alg.generate_cek() return {'cek': cek} def _check_key(self, key): if len(key) * 8 != self.key_size: raise ValueError( f'A key of size {self.key_size} bits is required.') def wrap_cek(self, cek, key): op_key = key.get_op_key('wrapKey') self._check_key(op_key) ek = aes_key_wrap(op_key, cek, default_backend()) return {'ek': ek, 'cek': cek} def wrap(self, enc_alg, headers, key, preset=None): if preset and 'cek' in preset: cek = preset['cek'] else: cek = enc_alg.generate_cek() return self.wrap_cek(cek, key) def unwrap(self, enc_alg, ek, headers, key): op_key = key.get_op_key('unwrapKey') self._check_key(op_key) cek = aes_key_unwrap(op_key, ek, default_backend()) if len(cek) * 8 != enc_alg.CEK_SIZE: raise ValueError('Invalid "cek" length') return cek class AESGCMAlgorithm(JWEAlgorithm): EXTRA_HEADERS = frozenset(['iv', 'tag']) def __init__(self, key_size): self.name = f'A{key_size}GCMKW' self.description = f'Key wrapping with AES GCM using {key_size}-bit key' self.key_size = key_size def prepare_key(self, raw_data): return OctKey.import_key(raw_data) def generate_preset(self, enc_alg, key): cek = enc_alg.generate_cek() return {'cek': cek} def _check_key(self, key): if len(key) * 8 != self.key_size: raise ValueError( f'A key of size {self.key_size} bits is required.') def wrap(self, enc_alg, headers, key, preset=None): if preset and 'cek' in preset: cek = preset['cek'] else: cek = enc_alg.generate_cek() op_key = key.get_op_key('wrapKey') self._check_key(op_key) #: https://tools.ietf.org/html/rfc7518#section-4.7.1.1 #: The "iv" (initialization vector) Header Parameter value is the #: base64url-encoded representation of the 96-bit IV value iv_size = 96 iv = os.urandom(iv_size // 8) cipher = Cipher(AES(op_key), GCM(iv), backend=default_backend()) enc = cipher.encryptor() ek = enc.update(cek) + enc.finalize() h = { 'iv': to_native(urlsafe_b64encode(iv)), 'tag': to_native(urlsafe_b64encode(enc.tag)) } return {'ek': ek, 'cek': cek, 'header': h} def unwrap(self, enc_alg, ek, headers, key): op_key = key.get_op_key('unwrapKey') self._check_key(op_key) iv = headers.get('iv') if not iv: raise ValueError('Missing "iv" in headers') tag = headers.get('tag') if not tag: raise ValueError('Missing "tag" in headers') iv = urlsafe_b64decode(to_bytes(iv)) tag = urlsafe_b64decode(to_bytes(tag)) cipher = Cipher(AES(op_key), GCM(iv, tag), backend=default_backend()) d = cipher.decryptor() cek = d.update(ek) + d.finalize() if len(cek) * 8 != enc_alg.CEK_SIZE: raise ValueError('Invalid "cek" length') return cek class ECDHESAlgorithm(JWEAlgorithm): EXTRA_HEADERS = ['epk', 'apu', 'apv'] ALLOWED_KEY_CLS = ECKey # https://tools.ietf.org/html/rfc7518#section-4.6 def __init__(self, key_size=None): if key_size is None: self.name = 'ECDH-ES' self.description = 'ECDH-ES in the Direct Key Agreement mode' else: self.name = f'ECDH-ES+A{key_size}KW' self.description = ( 'ECDH-ES using Concat KDF and CEK wrapped ' 'with A{}KW').format(key_size) self.key_size = key_size self.aeskw = AESAlgorithm(key_size) def prepare_key(self, raw_data): if isinstance(raw_data, self.ALLOWED_KEY_CLS): return raw_data return ECKey.import_key(raw_data) def generate_preset(self, enc_alg, key): epk = self._generate_ephemeral_key(key) h = self._prepare_headers(epk) preset = {'epk': epk, 'header': h} if self.key_size is not None: cek = enc_alg.generate_cek() preset['cek'] = cek return preset def compute_fixed_info(self, headers, bit_size): # AlgorithmID if self.key_size is None: alg_id = u32be_len_input(headers['enc']) else: alg_id = u32be_len_input(headers['alg']) # PartyUInfo apu_info = u32be_len_input(headers.get('apu'), True) # PartyVInfo apv_info = u32be_len_input(headers.get('apv'), True) # SuppPubInfo pub_info = struct.pack('>I', bit_size) return alg_id + apu_info + apv_info + pub_info def compute_derived_key(self, shared_key, fixed_info, bit_size): ckdf = ConcatKDFHash( algorithm=hashes.SHA256(), length=bit_size // 8, otherinfo=fixed_info, backend=default_backend() ) return ckdf.derive(shared_key) def deliver(self, key, pubkey, headers, bit_size): shared_key = key.exchange_shared_key(pubkey) fixed_info = self.compute_fixed_info(headers, bit_size) return self.compute_derived_key(shared_key, fixed_info, bit_size) def _generate_ephemeral_key(self, key): return key.generate_key(key['crv'], is_private=True) def _prepare_headers(self, epk): # REQUIRED_JSON_FIELDS contains only public fields pub_epk = {k: epk[k] for k in epk.REQUIRED_JSON_FIELDS} pub_epk['kty'] = epk.kty return {'epk': pub_epk} def wrap(self, enc_alg, headers, key, preset=None): if self.key_size is None: bit_size = enc_alg.CEK_SIZE else: bit_size = self.key_size if preset and 'epk' in preset: epk = preset['epk'] h = {} else: epk = self._generate_ephemeral_key(key) h = self._prepare_headers(epk) public_key = key.get_op_key('wrapKey') dk = self.deliver(epk, public_key, headers, bit_size) if self.key_size is None: return {'ek': b'', 'cek': dk, 'header': h} if preset and 'cek' in preset: preset_for_kw = {'cek': preset['cek']} else: preset_for_kw = None kek = self.aeskw.prepare_key(dk) rv = self.aeskw.wrap(enc_alg, headers, kek, preset_for_kw) rv['header'] = h return rv def unwrap(self, enc_alg, ek, headers, key): if 'epk' not in headers: raise ValueError('Missing "epk" in headers') if self.key_size is None: bit_size = enc_alg.CEK_SIZE else: bit_size = self.key_size epk = key.import_key(headers['epk']) public_key = epk.get_op_key('wrapKey') dk = self.deliver(key, public_key, headers, bit_size) if self.key_size is None: return dk kek = self.aeskw.prepare_key(dk) return self.aeskw.unwrap(enc_alg, ek, headers, kek) def u32be_len_input(s, base64=False): if not s: return b'\x00\x00\x00\x00' if base64: s = urlsafe_b64decode(to_bytes(s)) else: s = to_bytes(s) return struct.pack('>I', len(s)) + s JWE_ALG_ALGORITHMS = [ DirectAlgorithm(), # dir RSAAlgorithm('RSA1_5', 'RSAES-PKCS1-v1_5', padding.PKCS1v15()), RSAAlgorithm( 'RSA-OAEP', 'RSAES OAEP using default parameters', padding.OAEP(padding.MGF1(hashes.SHA1()), hashes.SHA1(), None)), RSAAlgorithm( 'RSA-OAEP-256', 'RSAES OAEP using SHA-256 and MGF1 with SHA-256', padding.OAEP(padding.MGF1(hashes.SHA256()), hashes.SHA256(), None)), AESAlgorithm(128), # A128KW AESAlgorithm(192), # A192KW AESAlgorithm(256), # A256KW AESGCMAlgorithm(128), # A128GCMKW AESGCMAlgorithm(192), # A192GCMKW AESGCMAlgorithm(256), # A256GCMKW ECDHESAlgorithm(None), # ECDH-ES ECDHESAlgorithm(128), # ECDH-ES+A128KW ECDHESAlgorithm(192), # ECDH-ES+A192KW ECDHESAlgorithm(256), # ECDH-ES+A256KW ] # 'PBES2-HS256+A128KW': '', # 'PBES2-HS384+A192KW': '', # 'PBES2-HS512+A256KW': '', authlib-1.3.2/authlib/jose/rfc7518/jwe_encs.py000066400000000000000000000116671466226534200210300ustar00rootroot00000000000000""" authlib.jose.rfc7518 ~~~~~~~~~~~~~~~~~~~~ Cryptographic Algorithms for Cryptographic Algorithms for Content Encryption per `Section 5`_. .. _`Section 5`: https://tools.ietf.org/html/rfc7518#section-5 """ import hmac import hashlib from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers.algorithms import AES from cryptography.hazmat.primitives.ciphers.modes import GCM, CBC from cryptography.hazmat.primitives.padding import PKCS7 from cryptography.exceptions import InvalidTag from ..rfc7516 import JWEEncAlgorithm from .util import encode_int class CBCHS2EncAlgorithm(JWEEncAlgorithm): # The IV used is a 128-bit value generated randomly or # pseudo-randomly for use in the cipher. IV_SIZE = 128 def __init__(self, key_size, hash_type): self.name = f'A{key_size}CBC-HS{hash_type}' tpl = 'AES_{}_CBC_HMAC_SHA_{} authenticated encryption algorithm' self.description = tpl.format(key_size, hash_type) # bit length self.key_size = key_size # byte length self.key_len = key_size // 8 self.CEK_SIZE = key_size * 2 self.hash_alg = getattr(hashlib, f'sha{hash_type}') def _hmac(self, ciphertext, aad, iv, key): al = encode_int(len(aad) * 8, 64) msg = aad + iv + ciphertext + al d = hmac.new(key, msg, self.hash_alg).digest() return d[:self.key_len] def encrypt(self, msg, aad, iv, key): """Key Encryption with AES_CBC_HMAC_SHA2. :param msg: text to be encrypt in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param key: encrypted key in bytes :return: (ciphertext, iv, tag) """ self.check_iv(iv) hkey = key[:self.key_len] ekey = key[self.key_len:] pad = PKCS7(AES.block_size).padder() padded_data = pad.update(msg) + pad.finalize() cipher = Cipher(AES(ekey), CBC(iv), backend=default_backend()) enc = cipher.encryptor() ciphertext = enc.update(padded_data) + enc.finalize() tag = self._hmac(ciphertext, aad, iv, hkey) return ciphertext, tag def decrypt(self, ciphertext, aad, iv, tag, key): """Key Decryption with AES AES_CBC_HMAC_SHA2. :param ciphertext: ciphertext in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param tag: authentication tag in bytes :param key: encrypted key in bytes :return: message """ self.check_iv(iv) hkey = key[:self.key_len] dkey = key[self.key_len:] _tag = self._hmac(ciphertext, aad, iv, hkey) if not hmac.compare_digest(_tag, tag): raise InvalidTag() cipher = Cipher(AES(dkey), CBC(iv), backend=default_backend()) d = cipher.decryptor() data = d.update(ciphertext) + d.finalize() unpad = PKCS7(AES.block_size).unpadder() return unpad.update(data) + unpad.finalize() class GCMEncAlgorithm(JWEEncAlgorithm): # Use of an IV of size 96 bits is REQUIRED with this algorithm. # https://tools.ietf.org/html/rfc7518#section-5.3 IV_SIZE = 96 def __init__(self, key_size): self.name = f'A{key_size}GCM' self.description = f'AES GCM using {key_size}-bit key' self.key_size = key_size self.CEK_SIZE = key_size def encrypt(self, msg, aad, iv, key): """Key Encryption with AES GCM :param msg: text to be encrypt in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param key: encrypted key in bytes :return: (ciphertext, iv, tag) """ self.check_iv(iv) cipher = Cipher(AES(key), GCM(iv), backend=default_backend()) enc = cipher.encryptor() enc.authenticate_additional_data(aad) ciphertext = enc.update(msg) + enc.finalize() return ciphertext, enc.tag def decrypt(self, ciphertext, aad, iv, tag, key): """Key Decryption with AES GCM :param ciphertext: ciphertext in bytes :param aad: additional authenticated data in bytes :param iv: initialization vector in bytes :param tag: authentication tag in bytes :param key: encrypted key in bytes :return: message """ self.check_iv(iv) cipher = Cipher(AES(key), GCM(iv, tag), backend=default_backend()) d = cipher.decryptor() d.authenticate_additional_data(aad) return d.update(ciphertext) + d.finalize() JWE_ENC_ALGORITHMS = [ CBCHS2EncAlgorithm(128, 256), # A128CBC-HS256 CBCHS2EncAlgorithm(192, 384), # A192CBC-HS384 CBCHS2EncAlgorithm(256, 512), # A256CBC-HS512 GCMEncAlgorithm(128), # A128GCM GCMEncAlgorithm(192), # A192GCM GCMEncAlgorithm(256), # A256GCM ] authlib-1.3.2/authlib/jose/rfc7518/jwe_zips.py000066400000000000000000000010611466226534200210500ustar00rootroot00000000000000import zlib from ..rfc7516 import JWEZipAlgorithm, JsonWebEncryption class DeflateZipAlgorithm(JWEZipAlgorithm): name = 'DEF' description = 'DEFLATE' def compress(self, s): """Compress bytes data with DEFLATE algorithm.""" data = zlib.compress(s) # drop gzip headers and tail return data[2:-4] def decompress(self, s): """Decompress DEFLATE bytes data.""" return zlib.decompress(s, -zlib.MAX_WBITS) def register_jwe_rfc7518(): JsonWebEncryption.register_algorithm(DeflateZipAlgorithm()) authlib-1.3.2/authlib/jose/rfc7518/jws_algs.py000066400000000000000000000145351466226534200210410ustar00rootroot00000000000000""" authlib.jose.rfc7518 ~~~~~~~~~~~~~~~~~~~~ "alg" (Algorithm) Header Parameter Values for JWS per `Section 3`_. .. _`Section 3`: https://tools.ietf.org/html/rfc7518#section-3 """ import hmac import hashlib from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric.utils import ( decode_dss_signature, encode_dss_signature ) from cryptography.hazmat.primitives.asymmetric.ec import ECDSA from cryptography.hazmat.primitives.asymmetric import padding from cryptography.exceptions import InvalidSignature from ..rfc7515 import JWSAlgorithm from .oct_key import OctKey from .rsa_key import RSAKey from .ec_key import ECKey from .util import encode_int, decode_int class NoneAlgorithm(JWSAlgorithm): name = 'none' description = 'No digital signature or MAC performed' def prepare_key(self, raw_data): return None def sign(self, msg, key): return b'' def verify(self, msg, sig, key): return False class HMACAlgorithm(JWSAlgorithm): """HMAC using SHA algorithms for JWS. Available algorithms: - HS256: HMAC using SHA-256 - HS384: HMAC using SHA-384 - HS512: HMAC using SHA-512 """ SHA256 = hashlib.sha256 SHA384 = hashlib.sha384 SHA512 = hashlib.sha512 def __init__(self, sha_type): self.name = f'HS{sha_type}' self.description = f'HMAC using SHA-{sha_type}' self.hash_alg = getattr(self, f'SHA{sha_type}') def prepare_key(self, raw_data): return OctKey.import_key(raw_data) def sign(self, msg, key): # it is faster than the one in cryptography op_key = key.get_op_key('sign') return hmac.new(op_key, msg, self.hash_alg).digest() def verify(self, msg, sig, key): op_key = key.get_op_key('verify') v_sig = hmac.new(op_key, msg, self.hash_alg).digest() return hmac.compare_digest(sig, v_sig) class RSAAlgorithm(JWSAlgorithm): """RSA using SHA algorithms for JWS. Available algorithms: - RS256: RSASSA-PKCS1-v1_5 using SHA-256 - RS384: RSASSA-PKCS1-v1_5 using SHA-384 - RS512: RSASSA-PKCS1-v1_5 using SHA-512 """ SHA256 = hashes.SHA256 SHA384 = hashes.SHA384 SHA512 = hashes.SHA512 def __init__(self, sha_type): self.name = f'RS{sha_type}' self.description = f'RSASSA-PKCS1-v1_5 using SHA-{sha_type}' self.hash_alg = getattr(self, f'SHA{sha_type}') self.padding = padding.PKCS1v15() def prepare_key(self, raw_data): return RSAKey.import_key(raw_data) def sign(self, msg, key): op_key = key.get_op_key('sign') return op_key.sign(msg, self.padding, self.hash_alg()) def verify(self, msg, sig, key): op_key = key.get_op_key('verify') try: op_key.verify(sig, msg, self.padding, self.hash_alg()) return True except InvalidSignature: return False class ECAlgorithm(JWSAlgorithm): """ECDSA using SHA algorithms for JWS. Available algorithms: - ES256: ECDSA using P-256 and SHA-256 - ES384: ECDSA using P-384 and SHA-384 - ES512: ECDSA using P-521 and SHA-512 """ SHA256 = hashes.SHA256 SHA384 = hashes.SHA384 SHA512 = hashes.SHA512 def __init__(self, name, curve, sha_type): self.name = name self.curve = curve self.description = f'ECDSA using {self.curve} and SHA-{sha_type}' self.hash_alg = getattr(self, f'SHA{sha_type}') def prepare_key(self, raw_data): key = ECKey.import_key(raw_data) if key['crv'] != self.curve: raise ValueError(f'Key for "{self.name}" not supported, only "{self.curve}" allowed') return key def sign(self, msg, key): op_key = key.get_op_key('sign') der_sig = op_key.sign(msg, ECDSA(self.hash_alg())) r, s = decode_dss_signature(der_sig) size = key.curve_key_size return encode_int(r, size) + encode_int(s, size) def verify(self, msg, sig, key): key_size = key.curve_key_size length = (key_size + 7) // 8 if len(sig) != 2 * length: return False r = decode_int(sig[:length]) s = decode_int(sig[length:]) der_sig = encode_dss_signature(r, s) try: op_key = key.get_op_key('verify') op_key.verify(der_sig, msg, ECDSA(self.hash_alg())) return True except InvalidSignature: return False class RSAPSSAlgorithm(JWSAlgorithm): """RSASSA-PSS using SHA algorithms for JWS. Available algorithms: - PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 - PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 - PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 """ SHA256 = hashes.SHA256 SHA384 = hashes.SHA384 SHA512 = hashes.SHA512 def __init__(self, sha_type): self.name = f'PS{sha_type}' tpl = 'RSASSA-PSS using SHA-{} and MGF1 with SHA-{}' self.description = tpl.format(sha_type, sha_type) self.hash_alg = getattr(self, f'SHA{sha_type}') def prepare_key(self, raw_data): return RSAKey.import_key(raw_data) def sign(self, msg, key): op_key = key.get_op_key('sign') return op_key.sign( msg, padding.PSS( mgf=padding.MGF1(self.hash_alg()), salt_length=self.hash_alg.digest_size ), self.hash_alg() ) def verify(self, msg, sig, key): op_key = key.get_op_key('verify') try: op_key.verify( sig, msg, padding.PSS( mgf=padding.MGF1(self.hash_alg()), salt_length=self.hash_alg.digest_size ), self.hash_alg() ) return True except InvalidSignature: return False JWS_ALGORITHMS = [ NoneAlgorithm(), # none HMACAlgorithm(256), # HS256 HMACAlgorithm(384), # HS384 HMACAlgorithm(512), # HS512 RSAAlgorithm(256), # RS256 RSAAlgorithm(384), # RS384 RSAAlgorithm(512), # RS512 ECAlgorithm('ES256', 'P-256', 256), ECAlgorithm('ES384', 'P-384', 384), ECAlgorithm('ES512', 'P-521', 512), ECAlgorithm('ES256K', 'secp256k1', 256), # defined in RFC8812 RSAPSSAlgorithm(256), # PS256 RSAPSSAlgorithm(384), # PS384 RSAPSSAlgorithm(512), # PS512 ] authlib-1.3.2/authlib/jose/rfc7518/oct_key.py000066400000000000000000000051661466226534200206650ustar00rootroot00000000000000from authlib.common.encoding import ( to_bytes, to_unicode, urlsafe_b64encode, urlsafe_b64decode, ) from authlib.common.security import generate_token from ..rfc7517 import Key POSSIBLE_UNSAFE_KEYS = ( b"-----BEGIN ", b"---- BEGIN ", b"ssh-rsa ", b"ssh-dss ", b"ssh-ed25519 ", b"ecdsa-sha2-", ) class OctKey(Key): """Key class of the ``oct`` key type.""" kty = 'oct' REQUIRED_JSON_FIELDS = ['k'] def __init__(self, raw_key=None, options=None): super().__init__(options) self.raw_key = raw_key @property def public_only(self): return False def get_op_key(self, operation): """Get the raw key for the given key_op. This method will also check if the given key_op is supported by this key. :param operation: key operation value, such as "sign", "encrypt". :return: raw key """ self.check_key_op(operation) if not self.raw_key: self.load_raw_key() return self.raw_key def load_raw_key(self): self.raw_key = urlsafe_b64decode(to_bytes(self.tokens['k'])) def load_dict_key(self): k = to_unicode(urlsafe_b64encode(self.raw_key)) self._dict_data = {'kty': self.kty, 'k': k} def as_dict(self, is_private=False, **params): tokens = self.tokens if 'kid' not in tokens: tokens['kid'] = self.thumbprint() tokens.update(params) return tokens @classmethod def validate_raw_key(cls, key): return isinstance(key, bytes) @classmethod def import_key(cls, raw, options=None): """Import a key from bytes, string, or dict data.""" if isinstance(raw, cls): if options is not None: raw.options.update(options) return raw if isinstance(raw, dict): cls.check_required_fields(raw) key = cls(options=options) key._dict_data = raw else: raw_key = to_bytes(raw) # security check if raw_key.startswith(POSSIBLE_UNSAFE_KEYS): raise ValueError("This key may not be safe to import") key = cls(raw_key=raw_key, options=options) return key @classmethod def generate_key(cls, key_size=256, options=None, is_private=True): """Generate a ``OctKey`` with the given bit size.""" if not is_private: raise ValueError('oct key can not be generated as public') if key_size % 8 != 0: raise ValueError('Invalid bit size for oct key') return cls.import_key(generate_token(key_size // 8), options) authlib-1.3.2/authlib/jose/rfc7518/rsa_key.py000066400000000000000000000101401466226534200206510ustar00rootroot00000000000000from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric.rsa import ( RSAPublicKey, RSAPrivateKeyWithSerialization, RSAPrivateNumbers, RSAPublicNumbers, rsa_recover_prime_factors, rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp ) from cryptography.hazmat.backends import default_backend from authlib.common.encoding import base64_to_int, int_to_base64 from ..rfc7517 import AsymmetricKey class RSAKey(AsymmetricKey): """Key class of the ``RSA`` key type.""" kty = 'RSA' PUBLIC_KEY_CLS = RSAPublicKey PRIVATE_KEY_CLS = RSAPrivateKeyWithSerialization PUBLIC_KEY_FIELDS = ['e', 'n'] PRIVATE_KEY_FIELDS = ['d', 'dp', 'dq', 'e', 'n', 'p', 'q', 'qi'] REQUIRED_JSON_FIELDS = ['e', 'n'] SSH_PUBLIC_PREFIX = b'ssh-rsa' def dumps_private_key(self): numbers = self.private_key.private_numbers() return { 'n': int_to_base64(numbers.public_numbers.n), 'e': int_to_base64(numbers.public_numbers.e), 'd': int_to_base64(numbers.d), 'p': int_to_base64(numbers.p), 'q': int_to_base64(numbers.q), 'dp': int_to_base64(numbers.dmp1), 'dq': int_to_base64(numbers.dmq1), 'qi': int_to_base64(numbers.iqmp) } def dumps_public_key(self): numbers = self.public_key.public_numbers() return { 'n': int_to_base64(numbers.n), 'e': int_to_base64(numbers.e) } def load_private_key(self): obj = self._dict_data if 'oth' in obj: # pragma: no cover # https://tools.ietf.org/html/rfc7518#section-6.3.2.7 raise ValueError('"oth" is not supported yet') public_numbers = RSAPublicNumbers( base64_to_int(obj['e']), base64_to_int(obj['n'])) if has_all_prime_factors(obj): numbers = RSAPrivateNumbers( d=base64_to_int(obj['d']), p=base64_to_int(obj['p']), q=base64_to_int(obj['q']), dmp1=base64_to_int(obj['dp']), dmq1=base64_to_int(obj['dq']), iqmp=base64_to_int(obj['qi']), public_numbers=public_numbers) else: d = base64_to_int(obj['d']) p, q = rsa_recover_prime_factors( public_numbers.n, d, public_numbers.e) numbers = RSAPrivateNumbers( d=d, p=p, q=q, dmp1=rsa_crt_dmp1(d, p), dmq1=rsa_crt_dmq1(d, q), iqmp=rsa_crt_iqmp(p, q), public_numbers=public_numbers) return numbers.private_key(default_backend()) def load_public_key(self): numbers = RSAPublicNumbers( base64_to_int(self._dict_data['e']), base64_to_int(self._dict_data['n']) ) return numbers.public_key(default_backend()) @classmethod def generate_key(cls, key_size=2048, options=None, is_private=False) -> 'RSAKey': if key_size < 512: raise ValueError('key_size must not be less than 512') if key_size % 8 != 0: raise ValueError('Invalid key_size for RSAKey') raw_key = rsa.generate_private_key( public_exponent=65537, key_size=key_size, backend=default_backend(), ) if not is_private: raw_key = raw_key.public_key() return cls.import_key(raw_key, options=options) @classmethod def import_dict_key(cls, raw, options=None): cls.check_required_fields(raw) key = cls(options=options) key._dict_data = raw if 'd' in raw and not has_all_prime_factors(raw): # reload dict key key.load_raw_key() key.load_dict_key() return key def has_all_prime_factors(obj): props = ['p', 'q', 'dp', 'dq', 'qi'] props_found = [prop in obj for prop in props] if all(props_found): return True if any(props_found): raise ValueError( 'RSA key must include all parameters ' 'if any are present besides d') return False authlib-1.3.2/authlib/jose/rfc7518/util.py000066400000000000000000000004111466226534200201710ustar00rootroot00000000000000import binascii def encode_int(num, bits): length = ((bits + 7) // 8) * 2 padded_hex = '%0*x' % (length, num) big_endian = binascii.a2b_hex(padded_hex.encode('ascii')) return big_endian def decode_int(b): return int(binascii.b2a_hex(b), 16) authlib-1.3.2/authlib/jose/rfc7519/000077500000000000000000000000001466226534200166475ustar00rootroot00000000000000authlib-1.3.2/authlib/jose/rfc7519/__init__.py000066400000000000000000000004651466226534200207650ustar00rootroot00000000000000""" authlib.jose.rfc7519 ~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of JSON Web Token (JWT). https://tools.ietf.org/html/rfc7519 """ from .jwt import JsonWebToken from .claims import BaseClaims, JWTClaims __all__ = ['JsonWebToken', 'BaseClaims', 'JWTClaims'] authlib-1.3.2/authlib/jose/rfc7519/claims.py000066400000000000000000000210051466226534200204670ustar00rootroot00000000000000import time from authlib.jose.errors import ( MissingClaimError, InvalidClaimError, ExpiredTokenError, InvalidTokenError, ) class BaseClaims(dict): """Payload claims for JWT, which contains a validate interface. :param payload: the payload dict of JWT :param header: the header dict of JWT :param options: validate options :param params: other params An example on ``options`` parameter, the format is inspired by `OpenID Connect Claims`_:: { "iss": { "essential": True, "values": ["https://example.com", "https://example.org"] }, "sub": { "essential": True "value": "248289761001" }, "jti": { "validate": validate_jti } } .. _`OpenID Connect Claims`: http://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests """ REGISTERED_CLAIMS = [] def __init__(self, payload, header, options=None, params=None): super().__init__(payload) self.header = header self.options = options or {} self.params = params or {} def __getattr__(self, key): try: return object.__getattribute__(self, key) except AttributeError as error: if key in self.REGISTERED_CLAIMS: return self.get(key) raise error def _validate_essential_claims(self): for k in self.options: if self.options[k].get('essential'): if k not in self: raise MissingClaimError(k) elif not self.get(k): raise InvalidClaimError(k) def _validate_claim_value(self, claim_name): option = self.options.get(claim_name) if not option: return value = self.get(claim_name) option_value = option.get('value') if option_value and value != option_value: raise InvalidClaimError(claim_name) option_values = option.get('values') if option_values and value not in option_values: raise InvalidClaimError(claim_name) validate = option.get('validate') if validate and not validate(self, value): raise InvalidClaimError(claim_name) def get_registered_claims(self): rv = {} for k in self.REGISTERED_CLAIMS: if k in self: rv[k] = self[k] return rv class JWTClaims(BaseClaims): REGISTERED_CLAIMS = ['iss', 'sub', 'aud', 'exp', 'nbf', 'iat', 'jti'] def validate(self, now=None, leeway=0): """Validate everything in claims payload.""" self._validate_essential_claims() if now is None: now = int(time.time()) self.validate_iss() self.validate_sub() self.validate_aud() self.validate_exp(now, leeway) self.validate_nbf(now, leeway) self.validate_iat(now, leeway) self.validate_jti() # Validate custom claims for key in self.options.keys(): if key not in self.REGISTERED_CLAIMS: self._validate_claim_value(key) def validate_iss(self): """The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The "iss" value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL. """ self._validate_claim_value('iss') def validate_sub(self): """The "sub" (subject) claim identifies the principal that is the subject of the JWT. The claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The "sub" value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL. """ self._validate_claim_value('sub') def validate_aud(self): """The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case- sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL. """ aud_option = self.options.get('aud') aud = self.get('aud') if not aud_option or not aud: return aud_values = aud_option.get('values') if not aud_values: aud_value = aud_option.get('value') if aud_value: aud_values = [aud_value] if not aud_values: return if isinstance(self['aud'], list): aud_list = self['aud'] else: aud_list = [self['aud']] if not any([v in aud_list for v in aud_values]): raise InvalidClaimError('aud') def validate_exp(self, now, leeway): """The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL. """ if 'exp' in self: exp = self['exp'] if not _validate_numeric_time(exp): raise InvalidClaimError('exp') if exp < (now - leeway): raise ExpiredTokenError() def validate_nbf(self, now, leeway): """The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the "nbf" claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the "nbf" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL. """ if 'nbf' in self: nbf = self['nbf'] if not _validate_numeric_time(nbf): raise InvalidClaimError('nbf') if nbf > (now + leeway): raise InvalidTokenError() def validate_iat(self, now, leeway): """The "iat" (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL. """ if 'iat' in self: iat = self['iat'] if not _validate_numeric_time(iat): raise InvalidClaimError('iat') if iat > (now + leeway): raise InvalidTokenError( description='The token is not valid as it was issued in the future' ) def validate_jti(self): """The "jti" (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The "jti" claim can be used to prevent the JWT from being replayed. The "jti" value is a case- sensitive string. Use of this claim is OPTIONAL. """ self._validate_claim_value('jti') def _validate_numeric_time(s): return isinstance(s, (int, float)) authlib-1.3.2/authlib/jose/rfc7519/jwt.py000066400000000000000000000134761466226534200200400ustar00rootroot00000000000000import re import random import datetime import calendar from authlib.common.encoding import ( to_bytes, to_unicode, json_loads, json_dumps, ) from .claims import JWTClaims from ..errors import DecodeError, InsecureClaimError from ..rfc7515 import JsonWebSignature from ..rfc7516 import JsonWebEncryption from ..rfc7517 import KeySet, Key class JsonWebToken: SENSITIVE_NAMES = ('password', 'token', 'secret', 'secret_key') # Thanks to sentry SensitiveDataFilter SENSITIVE_VALUES = re.compile(r'|'.join([ # http://www.richardsramblings.com/regex/credit-card-numbers/ r'\b(?:3[47]\d|(?:4\d|5[1-5]|65)\d{2}|6011)\d{12}\b', # various private keys r'-----BEGIN[A-Z ]+PRIVATE KEY-----.+-----END[A-Z ]+PRIVATE KEY-----', # social security numbers (US) r'^\b(?!(000|666|9))\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b', ]), re.DOTALL) def __init__(self, algorithms, private_headers=None): self._jws = JsonWebSignature(algorithms, private_headers=private_headers) self._jwe = JsonWebEncryption(algorithms, private_headers=private_headers) def check_sensitive_data(self, payload): """Check if payload contains sensitive information.""" for k in payload: # check claims key name if k in self.SENSITIVE_NAMES: raise InsecureClaimError(k) # check claims values v = payload[k] if isinstance(v, str) and self.SENSITIVE_VALUES.search(v): raise InsecureClaimError(k) def encode(self, header, payload, key, check=True): """Encode a JWT with the given header, payload and key. :param header: A dict of JWS header :param payload: A dict to be encoded :param key: key used to sign the signature :param check: check if sensitive data in payload :return: bytes """ header.setdefault('typ', 'JWT') for k in ['exp', 'iat', 'nbf']: # convert datetime into timestamp claim = payload.get(k) if isinstance(claim, datetime.datetime): payload[k] = calendar.timegm(claim.utctimetuple()) if check: self.check_sensitive_data(payload) key = find_encode_key(key, header) text = to_bytes(json_dumps(payload)) if 'enc' in header: return self._jwe.serialize_compact(header, text, key) else: return self._jws.serialize_compact(header, text, key) def decode(self, s, key, claims_cls=None, claims_options=None, claims_params=None): """Decode the JWT with the given key. This is similar with :meth:`verify`, except that it will raise BadSignatureError when signature doesn't match. :param s: text of JWT :param key: key used to verify the signature :param claims_cls: class to be used for JWT claims :param claims_options: `options` parameters for claims_cls :param claims_params: `params` parameters for claims_cls :return: claims_cls instance :raise: BadSignatureError """ if claims_cls is None: claims_cls = JWTClaims if callable(key): load_key = key else: load_key = create_load_key(prepare_raw_key(key)) s = to_bytes(s) dot_count = s.count(b'.') if dot_count == 2: data = self._jws.deserialize_compact(s, load_key, decode_payload) elif dot_count == 4: data = self._jwe.deserialize_compact(s, load_key, decode_payload) else: raise DecodeError('Invalid input segments length') return claims_cls( data['payload'], data['header'], options=claims_options, params=claims_params, ) def decode_payload(bytes_payload): try: payload = json_loads(to_unicode(bytes_payload)) except ValueError: raise DecodeError('Invalid payload value') if not isinstance(payload, dict): raise DecodeError('Invalid payload type') return payload def prepare_raw_key(raw): if isinstance(raw, KeySet): return raw if isinstance(raw, str) and \ raw.startswith('{') and raw.endswith('}'): raw = json_loads(raw) elif isinstance(raw, (tuple, list)): raw = {'keys': raw} return raw def find_encode_key(key, header): if isinstance(key, KeySet): kid = header.get('kid') if kid: return key.find_by_kid(kid) rv = random.choice(key.keys) # use side effect to add kid value into header header['kid'] = rv.kid return rv if isinstance(key, dict) and 'keys' in key: keys = key['keys'] kid = header.get('kid') for k in keys: if k.get('kid') == kid: return k if not kid: rv = random.choice(keys) header['kid'] = rv['kid'] return rv raise ValueError('Invalid JSON Web Key Set') # append kid into header if isinstance(key, dict) and 'kid' in key: header['kid'] = key['kid'] elif isinstance(key, Key) and key.kid: header['kid'] = key.kid return key def create_load_key(key): def load_key(header, payload): if isinstance(key, KeySet): return key.find_by_kid(header.get('kid')) if isinstance(key, dict) and 'keys' in key: keys = key['keys'] kid = header.get('kid') if kid is not None: # look for the requested key for k in keys: if k.get('kid') == kid: return k else: # use the only key if len(keys) == 1: return keys[0] raise ValueError('Invalid JSON Web Key Set') return key return load_key authlib-1.3.2/authlib/jose/rfc8037/000077500000000000000000000000001466226534200166435ustar00rootroot00000000000000authlib-1.3.2/authlib/jose/rfc8037/__init__.py000066400000000000000000000001671466226534200207600ustar00rootroot00000000000000from .okp_key import OKPKey from .jws_eddsa import register_jws_rfc8037 __all__ = ['register_jws_rfc8037', 'OKPKey'] authlib-1.3.2/authlib/jose/rfc8037/jws_eddsa.py000066400000000000000000000013141466226534200211570ustar00rootroot00000000000000from cryptography.exceptions import InvalidSignature from ..rfc7515 import JWSAlgorithm from .okp_key import OKPKey class EdDSAAlgorithm(JWSAlgorithm): name = 'EdDSA' description = 'Edwards-curve Digital Signature Algorithm for JWS' def prepare_key(self, raw_data): return OKPKey.import_key(raw_data) def sign(self, msg, key): op_key = key.get_op_key('sign') return op_key.sign(msg) def verify(self, msg, sig, key): op_key = key.get_op_key('verify') try: op_key.verify(sig, msg) return True except InvalidSignature: return False def register_jws_rfc8037(cls): cls.register_algorithm(EdDSAAlgorithm()) authlib-1.3.2/authlib/jose/rfc8037/okp_key.py000066400000000000000000000067501466226534200206660ustar00rootroot00000000000000from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PublicKey, Ed25519PrivateKey ) from cryptography.hazmat.primitives.asymmetric.ed448 import ( Ed448PublicKey, Ed448PrivateKey ) from cryptography.hazmat.primitives.asymmetric.x25519 import ( X25519PublicKey, X25519PrivateKey ) from cryptography.hazmat.primitives.asymmetric.x448 import ( X448PublicKey, X448PrivateKey ) from cryptography.hazmat.primitives.serialization import ( Encoding, PublicFormat, PrivateFormat, NoEncryption ) from authlib.common.encoding import ( to_unicode, to_bytes, urlsafe_b64decode, urlsafe_b64encode, ) from ..rfc7517 import AsymmetricKey PUBLIC_KEYS_MAP = { 'Ed25519': Ed25519PublicKey, 'Ed448': Ed448PublicKey, 'X25519': X25519PublicKey, 'X448': X448PublicKey, } PRIVATE_KEYS_MAP = { 'Ed25519': Ed25519PrivateKey, 'Ed448': Ed448PrivateKey, 'X25519': X25519PrivateKey, 'X448': X448PrivateKey, } class OKPKey(AsymmetricKey): """Key class of the ``OKP`` key type.""" kty = 'OKP' REQUIRED_JSON_FIELDS = ['crv', 'x'] PUBLIC_KEY_FIELDS = REQUIRED_JSON_FIELDS PRIVATE_KEY_FIELDS = ['crv', 'd'] PUBLIC_KEY_CLS = tuple(PUBLIC_KEYS_MAP.values()) PRIVATE_KEY_CLS = tuple(PRIVATE_KEYS_MAP.values()) SSH_PUBLIC_PREFIX = b'ssh-ed25519' def exchange_shared_key(self, pubkey): # used in ECDHESAlgorithm private_key = self.get_private_key() if private_key and isinstance(private_key, (X25519PrivateKey, X448PrivateKey)): return private_key.exchange(pubkey) raise ValueError('Invalid key for exchanging shared key') @staticmethod def get_key_curve(key): if isinstance(key, (Ed25519PublicKey, Ed25519PrivateKey)): return 'Ed25519' elif isinstance(key, (Ed448PublicKey, Ed448PrivateKey)): return 'Ed448' elif isinstance(key, (X25519PublicKey, X25519PrivateKey)): return 'X25519' elif isinstance(key, (X448PublicKey, X448PrivateKey)): return 'X448' def load_private_key(self): crv_key = PRIVATE_KEYS_MAP[self._dict_data['crv']] d_bytes = urlsafe_b64decode(to_bytes(self._dict_data['d'])) return crv_key.from_private_bytes(d_bytes) def load_public_key(self): crv_key = PUBLIC_KEYS_MAP[self._dict_data['crv']] x_bytes = urlsafe_b64decode(to_bytes(self._dict_data['x'])) return crv_key.from_public_bytes(x_bytes) def dumps_private_key(self): obj = self.dumps_public_key(self.private_key.public_key()) d_bytes = self.private_key.private_bytes( Encoding.Raw, PrivateFormat.Raw, NoEncryption() ) obj['d'] = to_unicode(urlsafe_b64encode(d_bytes)) return obj def dumps_public_key(self, public_key=None): if public_key is None: public_key = self.public_key x_bytes = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw) return { 'crv': self.get_key_curve(public_key), 'x': to_unicode(urlsafe_b64encode(x_bytes)), } @classmethod def generate_key(cls, crv='Ed25519', options=None, is_private=False) -> 'OKPKey': if crv not in PRIVATE_KEYS_MAP: raise ValueError(f'Invalid crv value: "{crv}"') private_key_cls = PRIVATE_KEYS_MAP[crv] raw_key = private_key_cls.generate() if not is_private: raw_key = raw_key.public_key() return cls.import_key(raw_key, options=options) authlib-1.3.2/authlib/jose/util.py000066400000000000000000000020511466226534200170740ustar00rootroot00000000000000import binascii from authlib.common.encoding import urlsafe_b64decode, json_loads, to_unicode from authlib.jose.errors import DecodeError def extract_header(header_segment, error_cls): header_data = extract_segment(header_segment, error_cls, 'header') try: header = json_loads(header_data.decode('utf-8')) except ValueError as e: raise error_cls(f'Invalid header string: {e}') if not isinstance(header, dict): raise error_cls('Header must be a json object') return header def extract_segment(segment, error_cls, name='payload'): try: return urlsafe_b64decode(segment) except (TypeError, binascii.Error): msg = f'Invalid {name} padding' raise error_cls(msg) def ensure_dict(s, structure_name): if not isinstance(s, dict): try: s = json_loads(to_unicode(s)) except (ValueError, TypeError): raise DecodeError(f'Invalid {structure_name}') if not isinstance(s, dict): raise DecodeError(f'Invalid {structure_name}') return s authlib-1.3.2/authlib/oauth1/000077500000000000000000000000001466226534200160105ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth1/__init__.py000066400000000000000000000013371466226534200201250ustar00rootroot00000000000000from .rfc5849 import ( OAuth1Request, ClientAuth, SIGNATURE_HMAC_SHA1, SIGNATURE_RSA_SHA1, SIGNATURE_PLAINTEXT, SIGNATURE_TYPE_HEADER, SIGNATURE_TYPE_QUERY, SIGNATURE_TYPE_BODY, ClientMixin, TemporaryCredentialMixin, TokenCredentialMixin, TemporaryCredential, AuthorizationServer, ResourceProtector, ) __all__ = [ 'OAuth1Request', 'ClientAuth', 'SIGNATURE_HMAC_SHA1', 'SIGNATURE_RSA_SHA1', 'SIGNATURE_PLAINTEXT', 'SIGNATURE_TYPE_HEADER', 'SIGNATURE_TYPE_QUERY', 'SIGNATURE_TYPE_BODY', 'ClientMixin', 'TemporaryCredentialMixin', 'TokenCredentialMixin', 'TemporaryCredential', 'AuthorizationServer', 'ResourceProtector', ] authlib-1.3.2/authlib/oauth1/client.py000066400000000000000000000145741466226534200176530ustar00rootroot00000000000000from authlib.common.urls import ( url_decode, add_params_to_uri, urlparse, ) from authlib.common.encoding import json_loads from .rfc5849 import ( SIGNATURE_HMAC_SHA1, SIGNATURE_TYPE_HEADER, ClientAuth, ) class OAuth1Client: auth_class = ClientAuth def __init__(self, session, client_id, client_secret=None, token=None, token_secret=None, redirect_uri=None, rsa_key=None, verifier=None, signature_method=SIGNATURE_HMAC_SHA1, signature_type=SIGNATURE_TYPE_HEADER, force_include_body=False, realm=None, **kwargs): if not client_id: raise ValueError('Missing "client_id"') self.session = session self.auth = self.auth_class( client_id, client_secret=client_secret, token=token, token_secret=token_secret, redirect_uri=redirect_uri, signature_method=signature_method, signature_type=signature_type, rsa_key=rsa_key, verifier=verifier, realm=realm, force_include_body=force_include_body ) self._kwargs = kwargs @property def redirect_uri(self): return self.auth.redirect_uri @redirect_uri.setter def redirect_uri(self, uri): self.auth.redirect_uri = uri @property def token(self): return dict( oauth_token=self.auth.token, oauth_token_secret=self.auth.token_secret, oauth_verifier=self.auth.verifier ) @token.setter def token(self, token): """This token setter is designed for an easy integration for OAuthClient. Make sure both OAuth1Session and OAuth2Session have token setters. """ if token is None: self.auth.token = None self.auth.token_secret = None self.auth.verifier = None elif 'oauth_token' in token: self.auth.token = token['oauth_token'] if 'oauth_token_secret' in token: self.auth.token_secret = token['oauth_token_secret'] if 'oauth_verifier' in token: self.auth.verifier = token['oauth_verifier'] else: message = f'oauth_token is missing: {token!r}' self.handle_error('missing_token', message) def create_authorization_url(self, url, request_token=None, **kwargs): """Create an authorization URL by appending request_token and optional kwargs to url. This is the second step in the OAuth 1 workflow. The user should be redirected to this authorization URL, grant access to you, and then be redirected back to you. The redirection back can either be specified during client registration or by supplying a callback URI per request. :param url: The authorization endpoint URL. :param request_token: The previously obtained request token. :param kwargs: Optional parameters to append to the URL. :returns: The authorization URL with new parameters embedded. """ kwargs['oauth_token'] = request_token or self.auth.token if self.auth.redirect_uri: kwargs['oauth_callback'] = self.auth.redirect_uri return add_params_to_uri(url, kwargs.items()) def fetch_request_token(self, url, **kwargs): """Method for fetching an access token from the token endpoint. This is the first step in the OAuth 1 workflow. A request token is obtained by making a signed post request to url. The token is then parsed from the application/x-www-form-urlencoded response and ready to be used to construct an authorization url. :param url: Request Token endpoint. :param kwargs: Extra parameters to include for fetching token. :return: A Request Token dict. """ return self._fetch_token(url, **kwargs) def fetch_access_token(self, url, verifier=None, **kwargs): """Method for fetching an access token from the token endpoint. This is the final step in the OAuth 1 workflow. An access token is obtained using all previously obtained credentials, including the verifier from the authorization step. :param url: Access Token endpoint. :param verifier: A verifier string to prove authorization was granted. :param kwargs: Extra parameters to include for fetching access token. :return: A token dict. """ if verifier: self.auth.verifier = verifier if not self.auth.verifier: self.handle_error('missing_verifier', 'Missing "verifier" value') return self._fetch_token(url, **kwargs) def parse_authorization_response(self, url): """Extract parameters from the post authorization redirect response URL. :param url: The full URL that resulted from the user being redirected back from the OAuth provider to you, the client. :returns: A dict of parameters extracted from the URL. """ token = dict(url_decode(urlparse.urlparse(url).query)) self.token = token return token def _fetch_token(self, url, **kwargs): resp = self.session.post(url, auth=self.auth, **kwargs) token = self.parse_response_token(resp.status_code, resp.text) self.token = token self.auth.verifier = None return token def parse_response_token(self, status_code, text): if status_code >= 400: message = ( "Token request failed with code {}, " "response was '{}'." ).format(status_code, text) self.handle_error('fetch_token_denied', message) try: text = text.strip() if text.startswith('{'): token = json_loads(text) else: token = dict(url_decode(text)) except (TypeError, ValueError) as e: error = ( "Unable to decode token from token response. " "This is commonly caused by an unsuccessful request where" " a non urlencoded error message is returned. " "The decoding error was {}" ).format(e) raise ValueError(error) return token @staticmethod def handle_error(error_type, error_description): raise ValueError(f'{error_type}: {error_description}') authlib-1.3.2/authlib/oauth1/errors.py000066400000000000000000000000561466226534200176770ustar00rootroot00000000000000# flake8: noqa from .rfc5849.errors import * authlib-1.3.2/authlib/oauth1/rfc5849/000077500000000000000000000000001466226534200171145ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth1/rfc5849/__init__.py000066400000000000000000000020141466226534200212220ustar00rootroot00000000000000""" authlib.oauth1.rfc5849 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of The OAuth 1.0 Protocol. https://tools.ietf.org/html/rfc5849 """ from .wrapper import OAuth1Request from .client_auth import ClientAuth from .signature import ( SIGNATURE_HMAC_SHA1, SIGNATURE_RSA_SHA1, SIGNATURE_PLAINTEXT, SIGNATURE_TYPE_HEADER, SIGNATURE_TYPE_QUERY, SIGNATURE_TYPE_BODY, ) from .models import ( ClientMixin, TemporaryCredentialMixin, TokenCredentialMixin, TemporaryCredential, ) from .authorization_server import AuthorizationServer from .resource_protector import ResourceProtector __all__ = [ 'OAuth1Request', 'ClientAuth', 'SIGNATURE_HMAC_SHA1', 'SIGNATURE_RSA_SHA1', 'SIGNATURE_PLAINTEXT', 'SIGNATURE_TYPE_HEADER', 'SIGNATURE_TYPE_QUERY', 'SIGNATURE_TYPE_BODY', 'ClientMixin', 'TemporaryCredentialMixin', 'TokenCredentialMixin', 'TemporaryCredential', 'AuthorizationServer', 'ResourceProtector', ] authlib-1.3.2/authlib/oauth1/rfc5849/authorization_server.py000066400000000000000000000330551466226534200237620ustar00rootroot00000000000000from authlib.common.urls import is_valid_url, add_params_to_uri from .base_server import BaseServer from .errors import ( OAuth1Error, InvalidRequestError, MissingRequiredParameterError, InvalidClientError, InvalidTokenError, AccessDeniedError, MethodNotAllowedError, ) class AuthorizationServer(BaseServer): TOKEN_RESPONSE_HEADER = [ ('Content-Type', 'application/x-www-form-urlencoded'), ('Cache-Control', 'no-store'), ('Pragma', 'no-cache'), ] TEMPORARY_CREDENTIALS_METHOD = 'POST' def _get_client(self, request): client = self.get_client_by_id(request.client_id) request.client = client return client def create_oauth1_request(self, request): raise NotImplementedError() def handle_response(self, status_code, payload, headers): raise NotImplementedError() def handle_error_response(self, error): return self.handle_response( error.status_code, error.get_body(), error.get_headers() ) def validate_temporary_credentials_request(self, request): """Validate HTTP request for temporary credentials.""" # The client obtains a set of temporary credentials from the server by # making an authenticated (Section 3) HTTP "POST" request to the # Temporary Credential Request endpoint (unless the server advertises # another HTTP request method for the client to use). if request.method.upper() != self.TEMPORARY_CREDENTIALS_METHOD: raise MethodNotAllowedError() # REQUIRED parameter if not request.client_id: raise MissingRequiredParameterError('oauth_consumer_key') # REQUIRED parameter oauth_callback = request.redirect_uri if not request.redirect_uri: raise MissingRequiredParameterError('oauth_callback') # An absolute URI or # other means (the parameter value MUST be set to "oob" if oauth_callback != 'oob' and not is_valid_url(oauth_callback): raise InvalidRequestError('Invalid "oauth_callback" value') client = self._get_client(request) if not client: raise InvalidClientError() self.validate_timestamp_and_nonce(request) self.validate_oauth_signature(request) return request def create_temporary_credentials_response(self, request=None): """Validate temporary credentials token request and create response for temporary credentials token. Assume the endpoint of temporary credentials request is ``https://photos.example.net/initiate``: .. code-block:: http POST /initiate HTTP/1.1 Host: photos.example.net Authorization: OAuth realm="Photos", oauth_consumer_key="dpf43f3p2l4k3l03", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131200", oauth_nonce="wIjqoS", oauth_callback="http%3A%2F%2Fprinter.example.com%2Fready", oauth_signature="74KNZJeDHnMBp0EMJ9ZHt%2FXKycU%3D" The server validates the request and replies with a set of temporary credentials in the body of the HTTP response: .. code-block:: http HTTP/1.1 200 OK Content-Type: application/x-www-form-urlencoded oauth_token=hh5s93j4hdidpola&oauth_token_secret=hdhd0244k9j7ao03& oauth_callback_confirmed=true :param request: OAuth1Request instance. :returns: (status_code, body, headers) """ try: request = self.create_oauth1_request(request) self.validate_temporary_credentials_request(request) except OAuth1Error as error: return self.handle_error_response(error) credential = self.create_temporary_credential(request) payload = [ ('oauth_token', credential.get_oauth_token()), ('oauth_token_secret', credential.get_oauth_token_secret()), ('oauth_callback_confirmed', True) ] return self.handle_response(200, payload, self.TOKEN_RESPONSE_HEADER) def validate_authorization_request(self, request): """Validate the request for resource owner authorization.""" if not request.token: raise MissingRequiredParameterError('oauth_token') credential = self.get_temporary_credential(request) if not credential: raise InvalidTokenError() # assign credential for later use request.credential = credential return request def create_authorization_response(self, request, grant_user=None): """Validate authorization request and create authorization response. Assume the endpoint for authorization request is ``https://photos.example.net/authorize``, the client redirects Jane's user-agent to the server's Resource Owner Authorization endpoint to obtain Jane's approval for accessing her private photos:: https://photos.example.net/authorize?oauth_token=hh5s93j4hdidpola The server requests Jane to sign in using her username and password and if successful, asks her to approve granting 'printer.example.com' access to her private photos. Jane approves the request and her user-agent is redirected to the callback URI provided by the client in the previous request (line breaks are for display purposes only):: http://printer.example.com/ready? oauth_token=hh5s93j4hdidpola&oauth_verifier=hfdp7dh39dks9884 :param request: OAuth1Request instance. :param grant_user: if granted, pass the grant user, otherwise None. :returns: (status_code, body, headers) """ request = self.create_oauth1_request(request) # authorize endpoint should try catch this error self.validate_authorization_request(request) temporary_credentials = request.credential redirect_uri = temporary_credentials.get_redirect_uri() if not redirect_uri or redirect_uri == 'oob': client_id = temporary_credentials.get_client_id() client = self.get_client_by_id(client_id) redirect_uri = client.get_default_redirect_uri() if grant_user is None: error = AccessDeniedError() location = add_params_to_uri(redirect_uri, error.get_body()) return self.handle_response(302, '', [('Location', location)]) request.user = grant_user verifier = self.create_authorization_verifier(request) params = [ ('oauth_token', request.token), ('oauth_verifier', verifier) ] location = add_params_to_uri(redirect_uri, params) return self.handle_response(302, '', [('Location', location)]) def validate_token_request(self, request): """Validate request for issuing token.""" if not request.client_id: raise MissingRequiredParameterError('oauth_consumer_key') client = self._get_client(request) if not client: raise InvalidClientError() if not request.token: raise MissingRequiredParameterError('oauth_token') token = self.get_temporary_credential(request) if not token: raise InvalidTokenError() verifier = request.oauth_params.get('oauth_verifier') if not verifier: raise MissingRequiredParameterError('oauth_verifier') if not token.check_verifier(verifier): raise InvalidRequestError('Invalid "oauth_verifier"') request.credential = token self.validate_timestamp_and_nonce(request) self.validate_oauth_signature(request) return request def create_token_response(self, request): """Validate token request and create token response. Assuming the endpoint of token request is ``https://photos.example.net/token``, the callback request informs the client that Jane completed the authorization process. The client then requests a set of token credentials using its temporary credentials (over a secure Transport Layer Security (TLS) channel): .. code-block:: http POST /token HTTP/1.1 Host: photos.example.net Authorization: OAuth realm="Photos", oauth_consumer_key="dpf43f3p2l4k3l03", oauth_token="hh5s93j4hdidpola", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131201", oauth_nonce="walatlh", oauth_verifier="hfdp7dh39dks9884", oauth_signature="gKgrFCywp7rO0OXSjdot%2FIHF7IU%3D" The server validates the request and replies with a set of token credentials in the body of the HTTP response: .. code-block:: http HTTP/1.1 200 OK Content-Type: application/x-www-form-urlencoded oauth_token=nnch734d00sl2jdk&oauth_token_secret=pfkkdhi9sl3r4s00 :param request: OAuth1Request instance. :returns: (status_code, body, headers) """ try: request = self.create_oauth1_request(request) except OAuth1Error as error: return self.handle_error_response(error) try: self.validate_token_request(request) except OAuth1Error as error: self.delete_temporary_credential(request) return self.handle_error_response(error) credential = self.create_token_credential(request) payload = [ ('oauth_token', credential.get_oauth_token()), ('oauth_token_secret', credential.get_oauth_token_secret()), ] self.delete_temporary_credential(request) return self.handle_response(200, payload, self.TOKEN_RESPONSE_HEADER) def create_temporary_credential(self, request): """Generate and save a temporary credential into database or cache. A temporary credential is used for exchanging token credential. This method should be re-implemented:: def create_temporary_credential(self, request): oauth_token = generate_token(36) oauth_token_secret = generate_token(48) temporary_credential = TemporaryCredential( oauth_token=oauth_token, oauth_token_secret=oauth_token_secret, client_id=request.client_id, redirect_uri=request.redirect_uri, ) # if the credential has a save method temporary_credential.save() return temporary_credential :param request: OAuth1Request instance :return: TemporaryCredential instance """ raise NotImplementedError() def get_temporary_credential(self, request): """Get the temporary credential from database or cache. A temporary credential should share the same methods as described in models of ``TemporaryCredentialMixin``:: def get_temporary_credential(self, request): key = 'a-key-prefix:{}'.format(request.token) data = cache.get(key) # TemporaryCredential shares methods from TemporaryCredentialMixin return TemporaryCredential(data) :param request: OAuth1Request instance :return: TemporaryCredential instance """ raise NotImplementedError() def delete_temporary_credential(self, request): """Delete temporary credential from database or cache. For instance, if temporary credential is saved in cache:: def delete_temporary_credential(self, request): key = 'a-key-prefix:{}'.format(request.token) cache.delete(key) :param request: OAuth1Request instance """ raise NotImplementedError() def create_authorization_verifier(self, request): """Create and bind ``oauth_verifier`` to temporary credential. It could be re-implemented in this way:: def create_authorization_verifier(self, request): verifier = generate_token(36) temporary_credential = request.credential user_id = request.user.id temporary_credential.user_id = user_id temporary_credential.oauth_verifier = verifier # if the credential has a save method temporary_credential.save() # remember to return the verifier return verifier :param request: OAuth1Request instance :return: A string of ``oauth_verifier`` """ raise NotImplementedError() def create_token_credential(self, request): """Create and save token credential into database. This method would be re-implemented like this:: def create_token_credential(self, request): oauth_token = generate_token(36) oauth_token_secret = generate_token(48) temporary_credential = request.credential token_credential = TokenCredential( oauth_token=oauth_token, oauth_token_secret=oauth_token_secret, client_id=temporary_credential.get_client_id(), user_id=temporary_credential.get_user_id() ) # if the credential has a save method token_credential.save() return token_credential :param request: OAuth1Request instance :return: TokenCredential instance """ raise NotImplementedError() authlib-1.3.2/authlib/oauth1/rfc5849/base_server.py000066400000000000000000000074111466226534200217710ustar00rootroot00000000000000import time from .signature import ( SIGNATURE_HMAC_SHA1, SIGNATURE_PLAINTEXT, SIGNATURE_RSA_SHA1, ) from .signature import ( verify_hmac_sha1, verify_plaintext, verify_rsa_sha1, ) from .errors import ( InvalidRequestError, MissingRequiredParameterError, UnsupportedSignatureMethodError, InvalidNonceError, InvalidSignatureError, ) class BaseServer: SIGNATURE_METHODS = { SIGNATURE_HMAC_SHA1: verify_hmac_sha1, SIGNATURE_RSA_SHA1: verify_rsa_sha1, SIGNATURE_PLAINTEXT: verify_plaintext, } SUPPORTED_SIGNATURE_METHODS = [SIGNATURE_HMAC_SHA1] EXPIRY_TIME = 300 @classmethod def register_signature_method(cls, name, verify): """Extend signature method verification. :param name: A string to represent signature method. :param verify: A function to verify signature. The ``verify`` method accept ``OAuth1Request`` as parameter:: def verify_custom_method(request): # verify this request, return True or False return True Server.register_signature_method('custom-name', verify_custom_method) """ cls.SIGNATURE_METHODS[name] = verify def validate_timestamp_and_nonce(self, request): """Validate ``oauth_timestamp`` and ``oauth_nonce`` in HTTP request. :param request: OAuth1Request instance """ timestamp = request.oauth_params.get('oauth_timestamp') nonce = request.oauth_params.get('oauth_nonce') if request.signature_method == SIGNATURE_PLAINTEXT: # The parameters MAY be omitted when using the "PLAINTEXT" # signature method if not timestamp and not nonce: return if not timestamp: raise MissingRequiredParameterError('oauth_timestamp') try: # The timestamp value MUST be a positive integer timestamp = int(timestamp) if timestamp < 0: raise InvalidRequestError('Invalid "oauth_timestamp" value') if self.EXPIRY_TIME and time.time() - timestamp > self.EXPIRY_TIME: raise InvalidRequestError('Invalid "oauth_timestamp" value') except (ValueError, TypeError): raise InvalidRequestError('Invalid "oauth_timestamp" value') if not nonce: raise MissingRequiredParameterError('oauth_nonce') if self.exists_nonce(nonce, request): raise InvalidNonceError() def validate_oauth_signature(self, request): """Validate ``oauth_signature`` from HTTP request. :param request: OAuth1Request instance """ method = request.signature_method if not method: raise MissingRequiredParameterError('oauth_signature_method') if method not in self.SUPPORTED_SIGNATURE_METHODS: raise UnsupportedSignatureMethodError() if not request.signature: raise MissingRequiredParameterError('oauth_signature') verify = self.SIGNATURE_METHODS.get(method) if not verify: raise UnsupportedSignatureMethodError() if not verify(request): raise InvalidSignatureError() def get_client_by_id(self, client_id): """Get client instance with the given ``client_id``. :param client_id: A string of client_id :return: Client instance """ raise NotImplementedError() def exists_nonce(self, nonce, request): """The nonce value MUST be unique across all requests with the same timestamp, client credentials, and token combinations. :param nonce: A string value of ``oauth_nonce`` :param request: OAuth1Request instance :return: Boolean """ raise NotImplementedError() authlib-1.3.2/authlib/oauth1/rfc5849/client_auth.py000066400000000000000000000154101466226534200217660ustar00rootroot00000000000000import time import base64 import hashlib from authlib.common.security import generate_token from authlib.common.urls import extract_params from authlib.common.encoding import to_native from .wrapper import OAuth1Request from .signature import ( SIGNATURE_HMAC_SHA1, SIGNATURE_PLAINTEXT, SIGNATURE_RSA_SHA1, SIGNATURE_TYPE_HEADER, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, ) from .signature import ( sign_hmac_sha1, sign_rsa_sha1, sign_plaintext ) from .parameters import ( prepare_form_encoded_body, prepare_headers, prepare_request_uri_query, ) CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' CONTENT_TYPE_MULTI_PART = 'multipart/form-data' class ClientAuth: SIGNATURE_METHODS = { SIGNATURE_HMAC_SHA1: sign_hmac_sha1, SIGNATURE_RSA_SHA1: sign_rsa_sha1, SIGNATURE_PLAINTEXT: sign_plaintext, } @classmethod def register_signature_method(cls, name, sign): """Extend client signature methods. :param name: A string to represent signature method. :param sign: A function to generate signature. The ``sign`` method accept 2 parameters:: def custom_sign_method(client, request): # client is the instance of Client. return 'your-signed-string' Client.register_signature_method('custom-name', custom_sign_method) """ cls.SIGNATURE_METHODS[name] = sign def __init__(self, client_id, client_secret=None, token=None, token_secret=None, redirect_uri=None, rsa_key=None, verifier=None, signature_method=SIGNATURE_HMAC_SHA1, signature_type=SIGNATURE_TYPE_HEADER, realm=None, force_include_body=False): self.client_id = client_id self.client_secret = client_secret self.token = token self.token_secret = token_secret self.redirect_uri = redirect_uri self.signature_method = signature_method self.signature_type = signature_type self.rsa_key = rsa_key self.verifier = verifier self.realm = realm self.force_include_body = force_include_body def get_oauth_signature(self, method, uri, headers, body): """Get an OAuth signature to be used in signing a request To satisfy `section 3.4.1.2`_ item 2, if the request argument's headers dict attribute contains a Host item, its value will replace any netloc part of the request argument's uri attribute value. .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 """ sign = self.SIGNATURE_METHODS.get(self.signature_method) if not sign: raise ValueError('Invalid signature method.') request = OAuth1Request(method, uri, body=body, headers=headers) return sign(self, request) def get_oauth_params(self, nonce, timestamp): oauth_params = [ ('oauth_nonce', nonce), ('oauth_timestamp', timestamp), ('oauth_version', '1.0'), ('oauth_signature_method', self.signature_method), ('oauth_consumer_key', self.client_id), ] if self.token: oauth_params.append(('oauth_token', self.token)) if self.redirect_uri: oauth_params.append(('oauth_callback', self.redirect_uri)) if self.verifier: oauth_params.append(('oauth_verifier', self.verifier)) return oauth_params def _render(self, uri, headers, body, oauth_params): if self.signature_type == SIGNATURE_TYPE_HEADER: headers = prepare_headers(oauth_params, headers, realm=self.realm) elif self.signature_type == SIGNATURE_TYPE_BODY: if CONTENT_TYPE_FORM_URLENCODED in headers.get('Content-Type', ''): decoded_body = extract_params(body) or [] body = prepare_form_encoded_body(oauth_params, decoded_body) headers['Content-Type'] = CONTENT_TYPE_FORM_URLENCODED elif self.signature_type == SIGNATURE_TYPE_QUERY: uri = prepare_request_uri_query(oauth_params, uri) else: raise ValueError('Unknown signature type specified.') return uri, headers, body def sign(self, method, uri, headers, body): """Sign the HTTP request, add OAuth parameters and signature. :param method: HTTP method of the request. :param uri: URI of the HTTP request. :param body: Body payload of the HTTP request. :param headers: Headers of the HTTP request. :return: uri, headers, body """ nonce = generate_nonce() timestamp = generate_timestamp() if body is None: body = b'' # transform int to str timestamp = str(timestamp) if headers is None: headers = {} oauth_params = self.get_oauth_params(nonce, timestamp) # https://datatracker.ietf.org/doc/html/draft-eaton-oauth-bodyhash-00.html # include oauth_body_hash if body and headers.get('Content-Type') != CONTENT_TYPE_FORM_URLENCODED: oauth_body_hash = base64.b64encode(hashlib.sha1(body).digest()) oauth_params.append(('oauth_body_hash', oauth_body_hash.decode('utf-8'))) uri, headers, body = self._render(uri, headers, body, oauth_params) sig = self.get_oauth_signature(method, uri, headers, body) oauth_params.append(('oauth_signature', sig)) uri, headers, body = self._render(uri, headers, body, oauth_params) return uri, headers, body def prepare(self, method, uri, headers, body): """Add OAuth parameters to the request. Parameters may be included from the body if the content-type is urlencoded, if no content type is set, a guess is made. """ content_type = to_native(headers.get('Content-Type', '')) if self.signature_type == SIGNATURE_TYPE_BODY: content_type = CONTENT_TYPE_FORM_URLENCODED elif not content_type and extract_params(body): content_type = CONTENT_TYPE_FORM_URLENCODED if CONTENT_TYPE_FORM_URLENCODED in content_type: headers['Content-Type'] = CONTENT_TYPE_FORM_URLENCODED uri, headers, body = self.sign(method, uri, headers, body) elif self.force_include_body: # To allow custom clients to work on non form encoded bodies. uri, headers, body = self.sign(method, uri, headers, body) else: # Omit body data in the signing of non form-encoded requests uri, headers, _ = self.sign(method, uri, headers, b'') body = b'' return uri, headers, body def generate_nonce(): return generate_token() def generate_timestamp(): return str(int(time.time())) authlib-1.3.2/authlib/oauth1/rfc5849/errors.py000066400000000000000000000043771466226534200210150ustar00rootroot00000000000000""" authlib.oauth1.rfc5849.errors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RFC5849 has no definition on errors. This module is designed by Authlib based on OAuth 1.0a `Section 10`_ with some changes. .. _`Section 10`: https://oauth.net/core/1.0a/#rfc.section.10 """ from authlib.common.errors import AuthlibHTTPError from authlib.common.security import is_secure_transport class OAuth1Error(AuthlibHTTPError): def __init__(self, description=None, uri=None, status_code=None): super().__init__(None, description, uri, status_code) def get_headers(self): """Get a list of headers.""" return [ ('Content-Type', 'application/x-www-form-urlencoded'), ('Cache-Control', 'no-store'), ('Pragma', 'no-cache') ] class InsecureTransportError(OAuth1Error): error = 'insecure_transport' description = 'OAuth 2 MUST utilize https.' @classmethod def check(cls, uri): if not is_secure_transport(uri): raise cls() class InvalidRequestError(OAuth1Error): error = 'invalid_request' class UnsupportedParameterError(OAuth1Error): error = 'unsupported_parameter' class UnsupportedSignatureMethodError(OAuth1Error): error = 'unsupported_signature_method' class MissingRequiredParameterError(OAuth1Error): error = 'missing_required_parameter' def __init__(self, key): description = f'missing "{key}" in parameters' super().__init__(description=description) class DuplicatedOAuthProtocolParameterError(OAuth1Error): error = 'duplicated_oauth_protocol_parameter' class InvalidClientError(OAuth1Error): error = 'invalid_client' status_code = 401 class InvalidTokenError(OAuth1Error): error = 'invalid_token' description = 'Invalid or expired "oauth_token" in parameters' status_code = 401 class InvalidSignatureError(OAuth1Error): error = 'invalid_signature' status_code = 401 class InvalidNonceError(OAuth1Error): error = 'invalid_nonce' status_code = 401 class AccessDeniedError(OAuth1Error): error = 'access_denied' description = 'The resource owner or authorization server denied the request' class MethodNotAllowedError(OAuth1Error): error = 'method_not_allowed' status_code = 405 authlib-1.3.2/authlib/oauth1/rfc5849/models.py000066400000000000000000000065321466226534200207570ustar00rootroot00000000000000class ClientMixin: def get_default_redirect_uri(self): """A method to get client default redirect_uri. For instance, the database table for client has a column called ``default_redirect_uri``:: def get_default_redirect_uri(self): return self.default_redirect_uri :return: A URL string """ raise NotImplementedError() def get_client_secret(self): """A method to return the client_secret of this client. For instance, the database table has a column called ``client_secret``:: def get_client_secret(self): return self.client_secret """ raise NotImplementedError() def get_rsa_public_key(self): """A method to get the RSA public key for RSA-SHA1 signature method. For instance, the value is saved on column ``rsa_public_key``:: def get_rsa_public_key(self): return self.rsa_public_key """ raise NotImplementedError() class TokenCredentialMixin: def get_oauth_token(self): """A method to get the value of ``oauth_token``. For instance, the database table has a column called ``oauth_token``:: def get_oauth_token(self): return self.oauth_token :return: A string """ raise NotImplementedError() def get_oauth_token_secret(self): """A method to get the value of ``oauth_token_secret``. For instance, the database table has a column called ``oauth_token_secret``:: def get_oauth_token_secret(self): return self.oauth_token_secret :return: A string """ raise NotImplementedError() class TemporaryCredentialMixin(TokenCredentialMixin): def get_client_id(self): """A method to get the client_id associated with this credential. For instance, the table in the database has a column ``client_id``:: def get_client_id(self): return self.client_id """ raise NotImplementedError() def get_redirect_uri(self): """A method to get temporary credential's ``oauth_callback``. For instance, the database table for temporary credential has a column called ``oauth_callback``:: def get_redirect_uri(self): return self.oauth_callback :return: A URL string """ raise NotImplementedError() def check_verifier(self, verifier): """A method to check if the given verifier matches this temporary credential. For instance that this temporary credential has recorded the value in database as column ``oauth_verifier``:: def check_verifier(self, verifier): return self.oauth_verifier == verifier :return: Boolean """ raise NotImplementedError() class TemporaryCredential(dict, TemporaryCredentialMixin): def get_client_id(self): return self.get('client_id') def get_user_id(self): return self.get('user_id') def get_redirect_uri(self): return self.get('oauth_callback') def check_verifier(self, verifier): return self.get('oauth_verifier') == verifier def get_oauth_token(self): return self.get('oauth_token') def get_oauth_token_secret(self): return self.get('oauth_token_secret') authlib-1.3.2/authlib/oauth1/rfc5849/parameters.py000066400000000000000000000065771466226534200216500ustar00rootroot00000000000000""" authlib.spec.rfc5849.parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec. .. _`section 3.5`: https://tools.ietf.org/html/rfc5849#section-3.5 """ from authlib.common.urls import urlparse, url_encode, extract_params from .util import escape def prepare_headers(oauth_params, headers=None, realm=None): """**Prepare the Authorization header.** Per `section 3.5.1`_ of the spec. Protocol parameters can be transmitted using the HTTP "Authorization" header field as defined by `RFC2617`_ with the auth-scheme name set to "OAuth" (case insensitive). For example:: Authorization: OAuth realm="Photos", oauth_consumer_key="dpf43f3p2l4k3l03", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131200", oauth_nonce="wIjqoS", oauth_callback="http%3A%2F%2Fprinter.example.com%2Fready", oauth_signature="74KNZJeDHnMBp0EMJ9ZHt%2FXKycU%3D", oauth_version="1.0" .. _`section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1 .. _`RFC2617`: https://tools.ietf.org/html/rfc2617 """ headers = headers or {} # step 1, 2, 3 in Section 3.5.1 header_parameters = ', '.join([ f'{escape(k)}="{escape(v)}"' for k, v in oauth_params if k.startswith('oauth_') ]) # 4. The OPTIONAL "realm" parameter MAY be added and interpreted per # `RFC2617 section 1.2`_. # # .. _`RFC2617 section 1.2`: https://tools.ietf.org/html/rfc2617#section-1.2 if realm: # NOTE: realm should *not* be escaped header_parameters = f'realm="{realm}", ' + header_parameters # the auth-scheme name set to "OAuth" (case insensitive). headers['Authorization'] = f'OAuth {header_parameters}' return headers def _append_params(oauth_params, params): """Append OAuth params to an existing set of parameters. Both params and oauth_params is must be lists of 2-tuples. Per `section 3.5.2`_ and `3.5.3`_ of the spec. .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2 .. _`3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3 """ merged = list(params) merged.extend(oauth_params) # The request URI / entity-body MAY include other request-specific # parameters, in which case, the protocol parameters SHOULD be appended # following the request-specific parameters, properly separated by an "&" # character (ASCII code 38) merged.sort(key=lambda i: i[0].startswith('oauth_')) return merged def prepare_form_encoded_body(oauth_params, body): """Prepare the Form-Encoded Body. Per `section 3.5.2`_ of the spec. .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2 """ # append OAuth params to the existing body return url_encode(_append_params(oauth_params, body)) def prepare_request_uri_query(oauth_params, uri): """Prepare the Request URI Query. Per `section 3.5.3`_ of the spec. .. _`section 3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3 """ # append OAuth params to the existing set of query components sch, net, path, par, query, fra = urlparse.urlparse(uri) query = url_encode( _append_params(oauth_params, extract_params(query) or [])) return urlparse.urlunparse((sch, net, path, par, query, fra)) authlib-1.3.2/authlib/oauth1/rfc5849/resource_protector.py000066400000000000000000000023521466226534200234200ustar00rootroot00000000000000from .base_server import BaseServer from .wrapper import OAuth1Request from .errors import ( MissingRequiredParameterError, InvalidClientError, InvalidTokenError, ) class ResourceProtector(BaseServer): def validate_request(self, method, uri, body, headers): request = OAuth1Request(method, uri, body, headers) if not request.client_id: raise MissingRequiredParameterError('oauth_consumer_key') client = self.get_client_by_id(request.client_id) if not client: raise InvalidClientError() request.client = client if not request.token: raise MissingRequiredParameterError('oauth_token') token = self.get_token_credential(request) if not token: raise InvalidTokenError() request.credential = token self.validate_timestamp_and_nonce(request) self.validate_oauth_signature(request) return request def get_token_credential(self, request): """Fetch the token credential from data store like a database, framework should implement this function. :param request: OAuth1Request instance :return: Token model instance """ raise NotImplementedError() authlib-1.3.2/authlib/oauth1/rfc5849/rsa.py000066400000000000000000000016001466226534200202500ustar00rootroot00000000000000from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import ( load_pem_private_key, load_pem_public_key ) from cryptography.hazmat.primitives.asymmetric import padding from cryptography.exceptions import InvalidSignature from authlib.common.encoding import to_bytes def sign_sha1(msg, rsa_private_key): key = load_pem_private_key( to_bytes(rsa_private_key), password=None, backend=default_backend() ) return key.sign(msg, padding.PKCS1v15(), hashes.SHA1()) def verify_sha1(sig, msg, rsa_public_key): key = load_pem_public_key( to_bytes(rsa_public_key), backend=default_backend() ) try: key.verify(sig, msg, padding.PKCS1v15(), hashes.SHA1()) return True except InvalidSignature: return False authlib-1.3.2/authlib/oauth1/rfc5849/signature.py000066400000000000000000000334531466226534200214770ustar00rootroot00000000000000""" authlib.oauth1.rfc5849.signature ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of `section 3.4`_ of the spec. .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 """ import binascii import hashlib import hmac from authlib.common.urls import urlparse from authlib.common.encoding import to_unicode, to_bytes from .util import escape, unescape SIGNATURE_HMAC_SHA1 = "HMAC-SHA1" SIGNATURE_RSA_SHA1 = "RSA-SHA1" SIGNATURE_PLAINTEXT = "PLAINTEXT" SIGNATURE_TYPE_HEADER = 'HEADER' SIGNATURE_TYPE_QUERY = 'QUERY' SIGNATURE_TYPE_BODY = 'BODY' def construct_base_string(method, uri, params, host=None): """Generate signature base string from request, per `Section 3.4.1`_. For example, the HTTP request:: POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded Authorization: OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2", oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131201", oauth_nonce="7d8f3e4a", oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D" c2&a3=2+q is represented by the following signature base string (line breaks are for display purposes only):: POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q %26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_ key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk 9d7dh3k39sjv7 .. _`Section 3.4.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1 """ # Create base string URI per Section 3.4.1.2 base_string_uri = normalize_base_string_uri(uri, host) # Cleanup parameter sources per Section 3.4.1.3.1 unescaped_params = [] for k, v in params: # The "oauth_signature" parameter MUST be excluded from the signature if k in ('oauth_signature', 'realm'): continue # ensure oauth params are unescaped if k.startswith('oauth_'): v = unescape(v) unescaped_params.append((k, v)) # Normalize parameters per Section 3.4.1.3.2 normalized_params = normalize_parameters(unescaped_params) # construct base string return '&'.join([ escape(method.upper()), escape(base_string_uri), escape(normalized_params), ]) def normalize_base_string_uri(uri, host=None): """Normalize Base String URI per `Section 3.4.1.2`_. For example, the HTTP request:: GET /r%20v/X?id=123 HTTP/1.1 Host: EXAMPLE.COM:80 is represented by the base string URI: "http://example.com/r%20v/X". In another example, the HTTPS request:: GET /?q=1 HTTP/1.1 Host: www.example.net:8080 is represented by the base string URI: "https://www.example.net:8080/". .. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 The host argument overrides the netloc part of the uri argument. """ uri = to_unicode(uri) scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri) # The scheme, authority, and path of the request resource URI `RFC3986` # are included by constructing an "http" or "https" URI representing # the request resource (without the query or fragment) as follows: # # .. _`RFC3986`: https://tools.ietf.org/html/rfc3986 if not scheme or not netloc: raise ValueError('uri must include a scheme and netloc') # Per `RFC 2616 section 5.1.2`_: # # Note that the absolute path cannot be empty; if none is present in # the original URI, it MUST be given as "/" (the server root). # # .. _`RFC 2616 section 5.1.2`: https://tools.ietf.org/html/rfc2616#section-5.1.2 if not path: path = '/' # 1. The scheme and host MUST be in lowercase. scheme = scheme.lower() netloc = netloc.lower() # 2. The host and port values MUST match the content of the HTTP # request "Host" header field. if host is not None: netloc = host.lower() # 3. The port MUST be included if it is not the default port for the # scheme, and MUST be excluded if it is the default. Specifically, # the port MUST be excluded when making an HTTP request `RFC2616`_ # to port 80 or when making an HTTPS request `RFC2818`_ to port 443. # All other non-default port numbers MUST be included. # # .. _`RFC2616`: https://tools.ietf.org/html/rfc2616 # .. _`RFC2818`: https://tools.ietf.org/html/rfc2818 default_ports = ( ('http', '80'), ('https', '443'), ) if ':' in netloc: host, port = netloc.split(':', 1) if (scheme, port) in default_ports: netloc = host return urlparse.urlunparse((scheme, netloc, path, params, '', '')) def normalize_parameters(params): """Normalize parameters per `Section 3.4.1.3.2`_. For example, the list of parameters from the previous section would be normalized as follows: Encoded:: +------------------------+------------------+ | Name | Value | +------------------------+------------------+ | b5 | %3D%253D | | a3 | a | | c%40 | | | a2 | r%20b | | oauth_consumer_key | 9djdj82h48djs9d2 | | oauth_token | kkk9d7dh3k39sjv7 | | oauth_signature_method | HMAC-SHA1 | | oauth_timestamp | 137131201 | | oauth_nonce | 7d8f3e4a | | c2 | | | a3 | 2%20q | +------------------------+------------------+ Sorted:: +------------------------+------------------+ | Name | Value | +------------------------+------------------+ | a2 | r%20b | | a3 | 2%20q | | a3 | a | | b5 | %3D%253D | | c%40 | | | c2 | | | oauth_consumer_key | 9djdj82h48djs9d2 | | oauth_nonce | 7d8f3e4a | | oauth_signature_method | HMAC-SHA1 | | oauth_timestamp | 137131201 | | oauth_token | kkk9d7dh3k39sjv7 | +------------------------+------------------+ Concatenated Pairs:: +-------------------------------------+ | Name=Value | +-------------------------------------+ | a2=r%20b | | a3=2%20q | | a3=a | | b5=%3D%253D | | c%40= | | c2= | | oauth_consumer_key=9djdj82h48djs9d2 | | oauth_nonce=7d8f3e4a | | oauth_signature_method=HMAC-SHA1 | | oauth_timestamp=137131201 | | oauth_token=kkk9d7dh3k39sjv7 | +-------------------------------------+ and concatenated together into a single string (line breaks are for display purposes only):: a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1 &oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7 .. _`Section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 """ # 1. First, the name and value of each parameter are encoded # (`Section 3.6`_). # # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key_values = [(escape(k), escape(v)) for k, v in params] # 2. The parameters are sorted by name, using ascending byte value # ordering. If two or more parameters share the same name, they # are sorted by their value. key_values.sort() # 3. The name of each parameter is concatenated to its corresponding # value using an "=" character (ASCII code 61) as a separator, even # if the value is empty. parameter_parts = [f'{k}={v}' for k, v in key_values] # 4. The sorted name/value pairs are concatenated together into a # single string by using an "&" character (ASCII code 38) as # separator. return '&'.join(parameter_parts) def generate_signature_base_string(request): """Generate signature base string from request.""" host = request.headers.get('Host', None) return construct_base_string( request.method, request.uri, request.params, host) def hmac_sha1_signature(base_string, client_secret, token_secret): """Generate signature via HMAC-SHA1 method, per `Section 3.4.2`_. The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature algorithm as defined in `RFC2104`_:: digest = HMAC-SHA1 (key, text) .. _`RFC2104`: https://tools.ietf.org/html/rfc2104 .. _`Section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2 """ # The HMAC-SHA1 function variables are used in following way: # text is set to the value of the signature base string from # `Section 3.4.1.1`_. # # .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1 text = base_string # key is set to the concatenated values of: # 1. The client shared-secret, after being encoded (`Section 3.6`_). # # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key = escape(client_secret or '') # 2. An "&" character (ASCII code 38), which MUST be included # even when either secret is empty. key += '&' # 3. The token shared-secret, after being encoded (`Section 3.6`_). # # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key += escape(token_secret or '') signature = hmac.new(to_bytes(key), to_bytes(text), hashlib.sha1) # digest is used to set the value of the "oauth_signature" protocol # parameter, after the result octet string is base64-encoded # per `RFC2045, Section 6.8`. # # .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8 sig = binascii.b2a_base64(signature.digest())[:-1] return to_unicode(sig) def rsa_sha1_signature(base_string, rsa_private_key): """Generate signature via RSA-SHA1 method, per `Section 3.4.3`_. The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in `RFC3447, Section 8.2`_ (also known as PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5. To use this method, the client MUST have established client credentials with the server that included its RSA public key (in a manner that is beyond the scope of this specification). .. _`Section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3 .. _`RFC3447, Section 8.2`: https://tools.ietf.org/html/rfc3447#section-8.2 """ from .rsa import sign_sha1 base_string = to_bytes(base_string) s = sign_sha1(to_bytes(base_string), rsa_private_key) sig = binascii.b2a_base64(s)[:-1] return to_unicode(sig) def plaintext_signature(client_secret, token_secret): """Generate signature via PLAINTEXT method, per `Section 3.4.4`_. The "PLAINTEXT" method does not employ a signature algorithm. It MUST be used with a transport-layer mechanism such as TLS or SSL (or sent over a secure channel with equivalent protections). It does not utilize the signature base string or the "oauth_timestamp" and "oauth_nonce" parameters. .. _`Section 3.4.4`: https://tools.ietf.org/html/rfc5849#section-3.4.4 """ # The "oauth_signature" protocol parameter is set to the concatenated # value of: # 1. The client shared-secret, after being encoded (`Section 3.6`_). # # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 signature = escape(client_secret or '') # 2. An "&" character (ASCII code 38), which MUST be included even # when either secret is empty. signature += '&' # 3. The token shared-secret, after being encoded (`Section 3.6`_). # # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 signature += escape(token_secret or '') return signature def sign_hmac_sha1(client, request): """Sign a HMAC-SHA1 signature.""" base_string = generate_signature_base_string(request) return hmac_sha1_signature( base_string, client.client_secret, client.token_secret) def sign_rsa_sha1(client, request): """Sign a RSASSA-PKCS #1 v1.5 base64 encoded signature.""" base_string = generate_signature_base_string(request) return rsa_sha1_signature(base_string, client.rsa_key) def sign_plaintext(client, request): """Sign a PLAINTEXT signature.""" return plaintext_signature(client.client_secret, client.token_secret) def verify_hmac_sha1(request): """Verify a HMAC-SHA1 signature.""" base_string = generate_signature_base_string(request) sig = hmac_sha1_signature( base_string, request.client_secret, request.token_secret) return hmac.compare_digest(sig, request.signature) def verify_rsa_sha1(request): """Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature.""" from .rsa import verify_sha1 base_string = generate_signature_base_string(request) sig = binascii.a2b_base64(to_bytes(request.signature)) return verify_sha1(sig, to_bytes(base_string), request.rsa_public_key) def verify_plaintext(request): """Verify a PLAINTEXT signature.""" sig = plaintext_signature(request.client_secret, request.token_secret) return hmac.compare_digest(sig, request.signature) authlib-1.3.2/authlib/oauth1/rfc5849/util.py000066400000000000000000000002101466226534200204340ustar00rootroot00000000000000from authlib.common.urls import quote, unquote def escape(s): return quote(s, safe=b'~') def unescape(s): return unquote(s) authlib-1.3.2/authlib/oauth1/rfc5849/wrapper.py000066400000000000000000000075511466226534200211560ustar00rootroot00000000000000from urllib.request import parse_keqv_list, parse_http_list from authlib.common.urls import ( urlparse, extract_params, url_decode, ) from .signature import ( SIGNATURE_TYPE_QUERY, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_HEADER ) from .errors import ( InsecureTransportError, DuplicatedOAuthProtocolParameterError ) from .util import unescape class OAuth1Request: def __init__(self, method, uri, body=None, headers=None): InsecureTransportError.check(uri) self.method = method self.uri = uri self.body = body self.headers = headers or {} # states namespaces self.client = None self.credential = None self.user = None self.query = urlparse.urlparse(uri).query self.query_params = url_decode(self.query) self.body_params = extract_params(body) or [] self.auth_params, self.realm = _parse_authorization_header(headers) self.signature_type, self.oauth_params = _parse_oauth_params( self.query_params, self.body_params, self.auth_params) params = [] params.extend(self.query_params) params.extend(self.body_params) params.extend(self.auth_params) self.params = params @property def client_id(self): return self.oauth_params.get('oauth_consumer_key') @property def client_secret(self): if self.client: return self.client.get_client_secret() @property def rsa_public_key(self): if self.client: return self.client.get_rsa_public_key() @property def timestamp(self): return self.oauth_params.get('oauth_timestamp') @property def redirect_uri(self): return self.oauth_params.get('oauth_callback') @property def signature(self): return self.oauth_params.get('oauth_signature') @property def signature_method(self): return self.oauth_params.get('oauth_signature_method') @property def token(self): return self.oauth_params.get('oauth_token') @property def token_secret(self): if self.credential: return self.credential.get_oauth_token_secret() def _filter_oauth(params): for k, v in params: if k.startswith('oauth_'): yield (k, v) def _parse_authorization_header(headers): """Parse an OAuth authorization header into a list of 2-tuples""" authorization_header = headers.get('Authorization') if not authorization_header: return [], None auth_scheme = 'oauth ' if authorization_header.lower().startswith(auth_scheme): items = parse_http_list(authorization_header[len(auth_scheme):]) try: items = parse_keqv_list(items).items() auth_params = [(unescape(k), unescape(v)) for k, v in items] realm = dict(auth_params).get('realm') return auth_params, realm except (IndexError, ValueError): pass raise ValueError('Malformed authorization header') def _parse_oauth_params(query_params, body_params, auth_params): oauth_params_set = [ (SIGNATURE_TYPE_QUERY, list(_filter_oauth(query_params))), (SIGNATURE_TYPE_BODY, list(_filter_oauth(body_params))), (SIGNATURE_TYPE_HEADER, list(_filter_oauth(auth_params))) ] oauth_params_set = [params for params in oauth_params_set if params[1]] if len(oauth_params_set) > 1: found_types = [p[0] for p in oauth_params_set] raise DuplicatedOAuthProtocolParameterError( '"oauth_" params must come from only 1 signature type ' 'but were found in {}'.format(','.join(found_types)) ) if oauth_params_set: signature_type = oauth_params_set[0][0] oauth_params = dict(oauth_params_set[0][1]) else: signature_type = None oauth_params = {} return signature_type, oauth_params authlib-1.3.2/authlib/oauth2/000077500000000000000000000000001466226534200160115ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/__init__.py000066400000000000000000000006471466226534200201310ustar00rootroot00000000000000from .base import OAuth2Error from .auth import ClientAuth, TokenAuth from .client import OAuth2Client from .rfc6749 import ( OAuth2Request, JsonRequest, AuthorizationServer, ClientAuthentication, ResourceProtector, ) __all__ = [ 'OAuth2Error', 'ClientAuth', 'TokenAuth', 'OAuth2Client', 'OAuth2Request', 'JsonRequest', 'AuthorizationServer', 'ClientAuthentication', 'ResourceProtector', ] authlib-1.3.2/authlib/oauth2/auth.py000066400000000000000000000065571466226534200173410ustar00rootroot00000000000000import base64 from authlib.common.urls import add_params_to_qs, add_params_to_uri from authlib.common.encoding import to_bytes, to_native from .rfc6749 import OAuth2Token from .rfc6750 import add_bearer_token def encode_client_secret_basic(client, method, uri, headers, body): text = f'{client.client_id}:{client.client_secret}' auth = to_native(base64.b64encode(to_bytes(text, 'latin1'))) headers['Authorization'] = f'Basic {auth}' return uri, headers, body def encode_client_secret_post(client, method, uri, headers, body): body = add_params_to_qs(body or '', [ ('client_id', client.client_id), ('client_secret', client.client_secret or '') ]) if 'Content-Length' in headers: headers['Content-Length'] = str(len(body)) return uri, headers, body def encode_none(client, method, uri, headers, body): if method == 'GET': uri = add_params_to_uri(uri, [('client_id', client.client_id)]) return uri, headers, body body = add_params_to_qs(body, [('client_id', client.client_id)]) if 'Content-Length' in headers: headers['Content-Length'] = str(len(body)) return uri, headers, body class ClientAuth: """Attaches OAuth Client Information to HTTP requests. :param client_id: Client ID, which you get from client registration. :param client_secret: Client Secret, which you get from registration. :param auth_method: Client auth method for token endpoint. The supported methods for now: * client_secret_basic (default) * client_secret_post * none """ DEFAULT_AUTH_METHODS = { 'client_secret_basic': encode_client_secret_basic, 'client_secret_post': encode_client_secret_post, 'none': encode_none, } def __init__(self, client_id, client_secret, auth_method=None): if auth_method is None: auth_method = 'client_secret_basic' self.client_id = client_id self.client_secret = client_secret if auth_method in self.DEFAULT_AUTH_METHODS: auth_method = self.DEFAULT_AUTH_METHODS[auth_method] self.auth_method = auth_method def prepare(self, method, uri, headers, body): return self.auth_method(self, method, uri, headers, body) class TokenAuth: """Attach token information to HTTP requests. :param token: A dict or OAuth2Token instance of an OAuth 2.0 token :param token_placement: The placement of the token, default is ``header``, available choices: * header (default) * body * uri """ DEFAULT_TOKEN_TYPE = 'bearer' SIGN_METHODS = { 'bearer': add_bearer_token } def __init__(self, token, token_placement='header', client=None): self.token = OAuth2Token.from_dict(token) self.token_placement = token_placement self.client = client self.hooks = set() def set_token(self, token): self.token = OAuth2Token.from_dict(token) def prepare(self, uri, headers, body): token_type = self.token.get('token_type', self.DEFAULT_TOKEN_TYPE) sign = self.SIGN_METHODS[token_type.lower()] uri, headers, body = sign( self.token['access_token'], uri, headers, body, self.token_placement) for hook in self.hooks: uri, headers, body = hook(uri, headers, body) return uri, headers, body authlib-1.3.2/authlib/oauth2/base.py000066400000000000000000000016761466226534200173070ustar00rootroot00000000000000from authlib.common.errors import AuthlibHTTPError from authlib.common.urls import add_params_to_uri class OAuth2Error(AuthlibHTTPError): def __init__(self, description=None, uri=None, status_code=None, state=None, redirect_uri=None, redirect_fragment=False, error=None): super().__init__(error, description, uri, status_code) self.state = state self.redirect_uri = redirect_uri self.redirect_fragment = redirect_fragment def get_body(self): """Get a list of body.""" error = super().get_body() if self.state: error.append(('state', self.state)) return error def __call__(self, uri=None): if self.redirect_uri: params = self.get_body() loc = add_params_to_uri(self.redirect_uri, params, self.redirect_fragment) return 302, '', [('Location', loc)] return super().__call__(uri=uri) authlib-1.3.2/authlib/oauth2/client.py000066400000000000000000000431501466226534200176440ustar00rootroot00000000000000from authlib.common.security import generate_token from authlib.common.urls import url_decode from .rfc6749.parameters import ( prepare_grant_uri, prepare_token_request, parse_authorization_code_response, parse_implicit_response, ) from .rfc7009 import prepare_revoke_token_request from .rfc7636 import create_s256_code_challenge from .auth import TokenAuth, ClientAuth from .base import OAuth2Error DEFAULT_HEADERS = { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' } class OAuth2Client: """Construct a new OAuth 2 protocol client. :param session: Requests session object to communicate with authorization server. :param client_id: Client ID, which you get from client registration. :param client_secret: Client Secret, which you get from registration. :param token_endpoint_auth_method: client authentication method for token endpoint. :param revocation_endpoint_auth_method: client authentication method for revocation endpoint. :param scope: Scope that you needed to access user resources. :param state: Shared secret to prevent CSRF attack. :param redirect_uri: Redirect URI you registered as callback. :param code_challenge_method: PKCE method name, only S256 is supported. :param token: A dict of token attributes such as ``access_token``, ``token_type`` and ``expires_at``. :param token_placement: The place to put token in HTTP request. Available values: "header", "body", "uri". :param update_token: A function for you to update token. It accept a :class:`OAuth2Token` as parameter. :param leeway: Time window in seconds before the actual expiration of the authentication token, that the token is considered expired and will be refreshed. """ client_auth_class = ClientAuth token_auth_class = TokenAuth oauth_error_class = OAuth2Error EXTRA_AUTHORIZE_PARAMS = ( 'response_mode', 'nonce', 'prompt', 'login_hint' ) SESSION_REQUEST_PARAMS = [] def __init__(self, session, client_id=None, client_secret=None, token_endpoint_auth_method=None, revocation_endpoint_auth_method=None, scope=None, state=None, redirect_uri=None, code_challenge_method=None, token=None, token_placement='header', update_token=None, leeway=60, **metadata): self.session = session self.client_id = client_id self.client_secret = client_secret self.state = state if token_endpoint_auth_method is None: if client_secret: token_endpoint_auth_method = 'client_secret_basic' else: token_endpoint_auth_method = 'none' self.token_endpoint_auth_method = token_endpoint_auth_method if revocation_endpoint_auth_method is None: if client_secret: revocation_endpoint_auth_method = 'client_secret_basic' else: revocation_endpoint_auth_method = 'none' self.revocation_endpoint_auth_method = revocation_endpoint_auth_method self.scope = scope self.redirect_uri = redirect_uri self.code_challenge_method = code_challenge_method self.token_auth = self.token_auth_class(token, token_placement, self) self.update_token = update_token token_updater = metadata.pop('token_updater', None) if token_updater: raise ValueError('update token has been redesigned, checkout the documentation') self.metadata = metadata self.compliance_hook = { 'access_token_response': set(), 'refresh_token_request': set(), 'refresh_token_response': set(), 'revoke_token_request': set(), 'introspect_token_request': set(), } self._auth_methods = {} self.leeway = leeway def register_client_auth_method(self, auth): """Extend client authenticate for token endpoint. :param auth: an instance to sign the request """ if isinstance(auth, tuple): self._auth_methods[auth[0]] = auth[1] else: self._auth_methods[auth.name] = auth def client_auth(self, auth_method): if isinstance(auth_method, str) and auth_method in self._auth_methods: auth_method = self._auth_methods[auth_method] return self.client_auth_class( client_id=self.client_id, client_secret=self.client_secret, auth_method=auth_method, ) @property def token(self): return self.token_auth.token @token.setter def token(self, token): self.token_auth.set_token(token) def create_authorization_url(self, url, state=None, code_verifier=None, **kwargs): """Generate an authorization URL and state. :param url: Authorization endpoint url, must be HTTPS. :param state: An optional state string for CSRF protection. If not given it will be generated for you. :param code_verifier: An optional code_verifier for code challenge. :param kwargs: Extra parameters to include. :return: authorization_url, state """ if state is None: state = generate_token() response_type = self.metadata.get('response_type', 'code') response_type = kwargs.pop('response_type', response_type) if 'redirect_uri' not in kwargs: kwargs['redirect_uri'] = self.redirect_uri if 'scope' not in kwargs: kwargs['scope'] = self.scope if code_verifier and response_type == 'code' and self.code_challenge_method == 'S256': kwargs['code_challenge'] = create_s256_code_challenge(code_verifier) kwargs['code_challenge_method'] = self.code_challenge_method for k in self.EXTRA_AUTHORIZE_PARAMS: if k not in kwargs and k in self.metadata: kwargs[k] = self.metadata[k] uri = prepare_grant_uri( url, client_id=self.client_id, response_type=response_type, state=state, **kwargs) return uri, state def fetch_token(self, url=None, body='', method='POST', headers=None, auth=None, grant_type=None, state=None, **kwargs): """Generic method for fetching an access token from the token endpoint. :param url: Access Token endpoint URL, if not configured, ``authorization_response`` is used to extract token from its fragment (implicit way). :param body: Optional application/x-www-form-urlencoded body to add the include in the token request. Prefer kwargs over body. :param method: The HTTP method used to make the request. Defaults to POST, but may also be GET. Other methods should be added as needed. :param headers: Dict to default request headers with. :param auth: An auth tuple or method as accepted by requests. :param grant_type: Use specified grant_type to fetch token :return: A :class:`OAuth2Token` object (a dict too). """ state = state or self.state # implicit grant_type authorization_response = kwargs.pop('authorization_response', None) if authorization_response and '#' in authorization_response: return self.token_from_fragment(authorization_response, state) session_kwargs = self._extract_session_request_params(kwargs) if authorization_response and 'code=' in authorization_response: grant_type = 'authorization_code' params = parse_authorization_code_response( authorization_response, state=state, ) kwargs['code'] = params['code'] if grant_type is None: grant_type = self.metadata.get('grant_type') if grant_type is None: grant_type = _guess_grant_type(kwargs) self.metadata['grant_type'] = grant_type body = self._prepare_token_endpoint_body(body, grant_type, **kwargs) if auth is None: auth = self.client_auth(self.token_endpoint_auth_method) if headers is None: headers = DEFAULT_HEADERS if url is None: url = self.metadata.get('token_endpoint') return self._fetch_token( url, body=body, auth=auth, method=method, headers=headers, **session_kwargs ) def token_from_fragment(self, authorization_response, state=None): token = parse_implicit_response(authorization_response, state) if 'error' in token: raise self.oauth_error_class( error=token['error'], description=token.get('error_description') ) self.token = token return token def refresh_token(self, url=None, refresh_token=None, body='', auth=None, headers=None, **kwargs): """Fetch a new access token using a refresh token. :param url: Refresh Token endpoint, must be HTTPS. :param refresh_token: The refresh_token to use. :param body: Optional application/x-www-form-urlencoded body to add the include in the token request. Prefer kwargs over body. :param auth: An auth tuple or method as accepted by requests. :param headers: Dict to default request headers with. :return: A :class:`OAuth2Token` object (a dict too). """ session_kwargs = self._extract_session_request_params(kwargs) refresh_token = refresh_token or self.token.get('refresh_token') if 'scope' not in kwargs and self.scope: kwargs['scope'] = self.scope body = prepare_token_request( 'refresh_token', body, refresh_token=refresh_token, **kwargs ) if headers is None: headers = DEFAULT_HEADERS.copy() if url is None: url = self.metadata.get('token_endpoint') for hook in self.compliance_hook['refresh_token_request']: url, headers, body = hook(url, headers, body) if auth is None: auth = self.client_auth(self.token_endpoint_auth_method) return self._refresh_token( url, refresh_token=refresh_token, body=body, headers=headers, auth=auth, **session_kwargs) def ensure_active_token(self, token=None): if token is None: token = self.token if not token.is_expired(leeway=self.leeway): return True refresh_token = token.get('refresh_token') url = self.metadata.get('token_endpoint') if refresh_token and url: self.refresh_token(url, refresh_token=refresh_token) return True elif self.metadata.get('grant_type') == 'client_credentials': access_token = token['access_token'] new_token = self.fetch_token(url, grant_type='client_credentials') if self.update_token: self.update_token(new_token, access_token=access_token) return True def revoke_token(self, url, token=None, token_type_hint=None, body=None, auth=None, headers=None, **kwargs): """Revoke token method defined via `RFC7009`_. :param url: Revoke Token endpoint, must be HTTPS. :param token: The token to be revoked. :param token_type_hint: The type of the token that to be revoked. It can be "access_token" or "refresh_token". :param body: Optional application/x-www-form-urlencoded body to add the include in the token request. Prefer kwargs over body. :param auth: An auth tuple or method as accepted by requests. :param headers: Dict to default request headers with. :return: Revocation Response .. _`RFC7009`: https://tools.ietf.org/html/rfc7009 """ return self._handle_token_hint( 'revoke_token_request', url, token=token, token_type_hint=token_type_hint, body=body, auth=auth, headers=headers, **kwargs) def introspect_token(self, url, token=None, token_type_hint=None, body=None, auth=None, headers=None, **kwargs): """Implementation of OAuth 2.0 Token Introspection defined via `RFC7662`_. :param url: Introspection Endpoint, must be HTTPS. :param token: The token to be introspected. :param token_type_hint: The type of the token that to be revoked. It can be "access_token" or "refresh_token". :param body: Optional application/x-www-form-urlencoded body to add the include in the token request. Prefer kwargs over body. :param auth: An auth tuple or method as accepted by requests. :param headers: Dict to default request headers with. :return: Introspection Response .. _`RFC7662`: https://tools.ietf.org/html/rfc7662 """ return self._handle_token_hint( 'introspect_token_request', url, token=token, token_type_hint=token_type_hint, body=body, auth=auth, headers=headers, **kwargs) def register_compliance_hook(self, hook_type, hook): """Register a hook for request/response tweaking. Available hooks are: * access_token_response: invoked before token parsing. * refresh_token_request: invoked before refreshing token. * refresh_token_response: invoked before refresh token parsing. * protected_request: invoked before making a request. * revoke_token_request: invoked before revoking a token. * introspect_token_request: invoked before introspecting a token. """ if hook_type == 'protected_request': self.token_auth.hooks.add(hook) return if hook_type not in self.compliance_hook: raise ValueError('Hook type %s is not in %s.', hook_type, self.compliance_hook) self.compliance_hook[hook_type].add(hook) def parse_response_token(self, resp): if resp.status_code >= 500: resp.raise_for_status() token = resp.json() if 'error' in token: raise self.oauth_error_class( error=token['error'], description=token.get('error_description') ) self.token = token return self.token def _fetch_token(self, url, body='', headers=None, auth=None, method='POST', **kwargs): if method.upper() == 'POST': resp = self.session.post( url, data=dict(url_decode(body)), headers=headers, auth=auth, **kwargs) else: if '?' in url: url = '&'.join([url, body]) else: url = '?'.join([url, body]) resp = self.session.request(method, url, headers=headers, auth=auth, **kwargs) for hook in self.compliance_hook['access_token_response']: resp = hook(resp) return self.parse_response_token(resp) def _refresh_token(self, url, refresh_token=None, body='', headers=None, auth=None, **kwargs): resp = self._http_post(url, body=body, auth=auth, headers=headers, **kwargs) for hook in self.compliance_hook['refresh_token_response']: resp = hook(resp) token = self.parse_response_token(resp) if 'refresh_token' not in token: self.token['refresh_token'] = refresh_token if callable(self.update_token): self.update_token(self.token, refresh_token=refresh_token) return self.token def _handle_token_hint(self, hook, url, token=None, token_type_hint=None, body=None, auth=None, headers=None, **kwargs): if token is None and self.token: token = self.token.get('refresh_token') or self.token.get('access_token') if body is None: body = '' body, headers = prepare_revoke_token_request( token, token_type_hint, body, headers) for hook in self.compliance_hook[hook]: url, headers, body = hook(url, headers, body) if auth is None: auth = self.client_auth(self.revocation_endpoint_auth_method) session_kwargs = self._extract_session_request_params(kwargs) return self._http_post( url, body, auth=auth, headers=headers, **session_kwargs) def _prepare_token_endpoint_body(self, body, grant_type, **kwargs): if grant_type == 'authorization_code': if 'redirect_uri' not in kwargs: kwargs['redirect_uri'] = self.redirect_uri return prepare_token_request(grant_type, body, **kwargs) if 'scope' not in kwargs and self.scope: kwargs['scope'] = self.scope return prepare_token_request(grant_type, body, **kwargs) def _extract_session_request_params(self, kwargs): """Extract parameters for session object from the passing ``**kwargs``.""" rv = {} for k in self.SESSION_REQUEST_PARAMS: if k in kwargs: rv[k] = kwargs.pop(k) return rv def _http_post(self, url, body=None, auth=None, headers=None, **kwargs): return self.session.post( url, data=dict(url_decode(body)), headers=headers, auth=auth, **kwargs) def _guess_grant_type(kwargs): if 'code' in kwargs: grant_type = 'authorization_code' elif 'username' in kwargs and 'password' in kwargs: grant_type = 'password' else: grant_type = 'client_credentials' return grant_type authlib-1.3.2/authlib/oauth2/rfc6749/000077500000000000000000000000001466226534200171155ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc6749/__init__.py000066400000000000000000000044231466226534200212310ustar00rootroot00000000000000""" authlib.oauth2.rfc6749 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of The OAuth 2.0 Authorization Framework. https://tools.ietf.org/html/rfc6749 """ from .requests import OAuth2Request, JsonRequest from .wrappers import OAuth2Token from .errors import ( OAuth2Error, AccessDeniedError, MissingAuthorizationError, InvalidGrantError, InvalidClientError, InvalidRequestError, InvalidScopeError, InsecureTransportError, UnauthorizedClientError, UnsupportedResponseTypeError, UnsupportedGrantTypeError, UnsupportedTokenTypeError, # exceptions for clients MissingCodeException, MissingTokenException, MissingTokenTypeException, MismatchingStateException, ) from .models import ClientMixin, AuthorizationCodeMixin, TokenMixin from .authenticate_client import ClientAuthentication from .authorization_server import AuthorizationServer from .resource_protector import ResourceProtector, TokenValidator from .token_endpoint import TokenEndpoint from .grants import ( BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin, AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant, ) from .util import scope_to_list, list_to_scope __all__ = [ 'OAuth2Token', 'OAuth2Request', 'JsonRequest', 'OAuth2Error', 'AccessDeniedError', 'MissingAuthorizationError', 'InvalidGrantError', 'InvalidClientError', 'InvalidRequestError', 'InvalidScopeError', 'InsecureTransportError', 'UnauthorizedClientError', 'UnsupportedResponseTypeError', 'UnsupportedGrantTypeError', 'UnsupportedTokenTypeError', 'MissingCodeException', 'MissingTokenException', 'MissingTokenTypeException', 'MismatchingStateException', 'ClientMixin', 'AuthorizationCodeMixin', 'TokenMixin', 'ClientAuthentication', 'AuthorizationServer', 'ResourceProtector', 'TokenValidator', 'TokenEndpoint', 'BaseGrant', 'AuthorizationEndpointMixin', 'TokenEndpointMixin', 'AuthorizationCodeGrant', 'ImplicitGrant', 'ResourceOwnerPasswordCredentialsGrant', 'ClientCredentialsGrant', 'RefreshTokenGrant', 'scope_to_list', 'list_to_scope', ] authlib-1.3.2/authlib/oauth2/rfc6749/authenticate_client.py000066400000000000000000000072441466226534200235120ustar00rootroot00000000000000""" authlib.oauth2.rfc6749.authenticate_client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Registry of client authentication methods, with 3 built-in methods: 1. client_secret_basic 2. client_secret_post 3. none The "client_secret_basic" method is used a lot in examples of `RFC6749`_, but the concept of naming are introduced in `RFC7591`_. .. _`RFC6749`: https://tools.ietf.org/html/rfc6749 .. _`RFC7591`: https://tools.ietf.org/html/rfc7591 """ import logging from .errors import InvalidClientError from .util import extract_basic_authorization log = logging.getLogger(__name__) __all__ = ['ClientAuthentication'] class ClientAuthentication: def __init__(self, query_client): self.query_client = query_client self._methods = { 'none': authenticate_none, 'client_secret_basic': authenticate_client_secret_basic, 'client_secret_post': authenticate_client_secret_post, } def register(self, method, func): self._methods[method] = func def authenticate(self, request, methods, endpoint): for method in methods: func = self._methods[method] client = func(self.query_client, request) if client and client.check_endpoint_auth_method(method, endpoint): request.auth_method = method return client if 'client_secret_basic' in methods: raise InvalidClientError(state=request.state, status_code=401) raise InvalidClientError(state=request.state) def __call__(self, request, methods, endpoint='token'): return self.authenticate(request, methods, endpoint) def authenticate_client_secret_basic(query_client, request): """Authenticate client by ``client_secret_basic`` method. The client uses HTTP Basic for authentication. """ client_id, client_secret = extract_basic_authorization(request.headers) if client_id and client_secret: client = _validate_client(query_client, client_id, request.state, 401) if client.check_client_secret(client_secret): log.debug(f'Authenticate {client_id} via "client_secret_basic" success') return client log.debug(f'Authenticate {client_id} via "client_secret_basic" failed') def authenticate_client_secret_post(query_client, request): """Authenticate client by ``client_secret_post`` method. The client uses POST parameters for authentication. """ data = request.form client_id = data.get('client_id') client_secret = data.get('client_secret') if client_id and client_secret: client = _validate_client(query_client, client_id, request.state) if client.check_client_secret(client_secret): log.debug(f'Authenticate {client_id} via "client_secret_post" success') return client log.debug(f'Authenticate {client_id} via "client_secret_post" failed') def authenticate_none(query_client, request): """Authenticate public client by ``none`` method. The client does not have a client secret. """ client_id = request.client_id if client_id and not request.data.get('client_secret'): client = _validate_client(query_client, client_id, request.state) log.debug(f'Authenticate {client_id} via "none" success') return client log.debug(f'Authenticate {client_id} via "none" failed') def _validate_client(query_client, client_id, state=None, status_code=400): if client_id is None: raise InvalidClientError(state=state, status_code=status_code) client = query_client(client_id) if not client: raise InvalidClientError(state=state, status_code=status_code) return client authlib-1.3.2/authlib/oauth2/rfc6749/authorization_server.py000066400000000000000000000272701466226534200237650ustar00rootroot00000000000000from authlib.common.errors import ContinueIteration from .authenticate_client import ClientAuthentication from .requests import OAuth2Request, JsonRequest from .errors import ( OAuth2Error, InvalidScopeError, UnsupportedResponseTypeError, UnsupportedGrantTypeError, ) from .util import scope_to_list class AuthorizationServer: """Authorization server that handles Authorization Endpoint and Token Endpoint. :param scopes_supported: A list of supported scopes by this authorization server. """ def __init__(self, scopes_supported=None): self.scopes_supported = scopes_supported self._token_generators = {} self._client_auth = None self._authorization_grants = [] self._token_grants = [] self._endpoints = {} def query_client(self, client_id): """Query OAuth client by client_id. The client model class MUST implement the methods described by :class:`~authlib.oauth2.rfc6749.ClientMixin`. """ raise NotImplementedError() def save_token(self, token, request): """Define function to save the generated token into database.""" raise NotImplementedError() def generate_token(self, grant_type, client, user=None, scope=None, expires_in=None, include_refresh_token=True): """Generate the token dict. :param grant_type: current requested grant_type. :param client: the client that making the request. :param user: current authorized user. :param expires_in: if provided, use this value as expires_in. :param scope: current requested scope. :param include_refresh_token: should refresh_token be included. :return: Token dict """ # generator for a specified grant type func = self._token_generators.get(grant_type) if not func: # default generator for all grant types func = self._token_generators.get('default') if not func: raise RuntimeError('No configured token generator') return func( grant_type=grant_type, client=client, user=user, scope=scope, expires_in=expires_in, include_refresh_token=include_refresh_token) def register_token_generator(self, grant_type, func): """Register a function as token generator for the given ``grant_type``. Developers MUST register a default token generator with a special ``grant_type=default``:: def generate_bearer_token(grant_type, client, user=None, scope=None, expires_in=None, include_refresh_token=True): token = {'token_type': 'Bearer', 'access_token': ...} if include_refresh_token: token['refresh_token'] = ... ... return token authorization_server.register_token_generator('default', generate_bearer_token) If you register a generator for a certain grant type, that generator will only works for the given grant type:: authorization_server.register_token_generator('client_credentials', generate_bearer_token) :param grant_type: string name of the grant type :param func: a function to generate token """ self._token_generators[grant_type] = func def authenticate_client(self, request, methods, endpoint='token'): """Authenticate client via HTTP request information with the given methods, such as ``client_secret_basic``, ``client_secret_post``. """ if self._client_auth is None and self.query_client: self._client_auth = ClientAuthentication(self.query_client) return self._client_auth(request, methods, endpoint) def register_client_auth_method(self, method, func): """Add more client auth method. The default methods are: * none: The client is a public client and does not have a client secret * client_secret_post: The client uses the HTTP POST parameters * client_secret_basic: The client uses HTTP Basic :param method: Name of the Auth method :param func: Function to authenticate the client The auth method accept two parameters: ``query_client`` and ``request``, an example for this method:: def authenticate_client_via_custom(query_client, request): client_id = request.headers['X-Client-Id'] client = query_client(client_id) do_some_validation(client) return client authorization_server.register_client_auth_method( 'custom', authenticate_client_via_custom) """ if self._client_auth is None and self.query_client: self._client_auth = ClientAuthentication(self.query_client) self._client_auth.register(method, func) def get_error_uri(self, request, error): """Return a URI for the given error, framework may implement this method.""" return None def send_signal(self, name, *args, **kwargs): """Framework integration can re-implement this method to support signal system. """ raise NotImplementedError() def create_oauth2_request(self, request) -> OAuth2Request: """This method MUST be implemented in framework integrations. It is used to create an OAuth2Request instance. :param request: the "request" instance in framework :return: OAuth2Request instance """ raise NotImplementedError() def create_json_request(self, request) -> JsonRequest: """This method MUST be implemented in framework integrations. It is used to create an HttpRequest instance. :param request: the "request" instance in framework :return: HttpRequest instance """ raise NotImplementedError() def handle_response(self, status, body, headers): """Return HTTP response. Framework MUST implement this function.""" raise NotImplementedError() def validate_requested_scope(self, scope, state=None): """Validate if requested scope is supported by Authorization Server. Developers CAN re-write this method to meet your needs. """ if scope and self.scopes_supported: scopes = set(scope_to_list(scope)) if not set(self.scopes_supported).issuperset(scopes): raise InvalidScopeError(state=state) def register_grant(self, grant_cls, extensions=None): """Register a grant class into the endpoint registry. Developers can implement the grants in ``authlib.oauth2.rfc6749.grants`` and register with this method:: class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): def authenticate_user(self, credential): # ... authorization_server.register_grant(AuthorizationCodeGrant) :param grant_cls: a grant class. :param extensions: extensions for the grant class. """ if hasattr(grant_cls, 'check_authorization_endpoint'): self._authorization_grants.append((grant_cls, extensions)) if hasattr(grant_cls, 'check_token_endpoint'): self._token_grants.append((grant_cls, extensions)) def register_endpoint(self, endpoint): """Add extra endpoint to authorization server. e.g. RevocationEndpoint:: authorization_server.register_endpoint(RevocationEndpoint) :param endpoint_cls: A endpoint class or instance. """ if isinstance(endpoint, type): endpoint = endpoint(self) else: endpoint.server = self endpoints = self._endpoints.setdefault(endpoint.ENDPOINT_NAME, []) endpoints.append(endpoint) def get_authorization_grant(self, request): """Find the authorization grant for current request. :param request: OAuth2Request instance. :return: grant instance """ for (grant_cls, extensions) in self._authorization_grants: if grant_cls.check_authorization_endpoint(request): return _create_grant(grant_cls, extensions, request, self) raise UnsupportedResponseTypeError(request.response_type) def get_consent_grant(self, request=None, end_user=None): """Validate current HTTP request for authorization page. This page is designed for resource owner to grant or deny the authorization. """ request = self.create_oauth2_request(request) request.user = end_user grant = self.get_authorization_grant(request) grant.validate_no_multiple_request_parameter(request) grant.validate_consent_request() return grant def get_token_grant(self, request): """Find the token grant for current request. :param request: OAuth2Request instance. :return: grant instance """ for (grant_cls, extensions) in self._token_grants: if grant_cls.check_token_endpoint(request): return _create_grant(grant_cls, extensions, request, self) raise UnsupportedGrantTypeError(request.grant_type) def create_endpoint_response(self, name, request=None): """Validate endpoint request and create endpoint response. :param name: Endpoint name :param request: HTTP request instance. :return: Response """ if name not in self._endpoints: raise RuntimeError(f'There is no "{name}" endpoint.') endpoints = self._endpoints[name] for endpoint in endpoints: request = endpoint.create_endpoint_request(request) try: return self.handle_response(*endpoint(request)) except ContinueIteration: continue except OAuth2Error as error: return self.handle_error_response(request, error) def create_authorization_response(self, request=None, grant_user=None): """Validate authorization request and create authorization response. :param request: HTTP request instance. :param grant_user: if granted, it is resource owner. If denied, it is None. :returns: Response """ if not isinstance(request, OAuth2Request): request = self.create_oauth2_request(request) try: grant = self.get_authorization_grant(request) except UnsupportedResponseTypeError as error: return self.handle_error_response(request, error) try: redirect_uri = grant.validate_authorization_request() args = grant.create_authorization_response(redirect_uri, grant_user) return self.handle_response(*args) except OAuth2Error as error: return self.handle_error_response(request, error) def create_token_response(self, request=None): """Validate token request and create token response. :param request: HTTP request instance """ request = self.create_oauth2_request(request) try: grant = self.get_token_grant(request) except UnsupportedGrantTypeError as error: return self.handle_error_response(request, error) try: grant.validate_token_request() args = grant.create_token_response() return self.handle_response(*args) except OAuth2Error as error: return self.handle_error_response(request, error) def handle_error_response(self, request, error): return self.handle_response(*error(self.get_error_uri(request, error))) def _create_grant(grant_cls, extensions, request, server): grant = grant_cls(request, server) if extensions: for ext in extensions: ext(grant) return grant authlib-1.3.2/authlib/oauth2/rfc6749/errors.py000066400000000000000000000163131466226534200210070ustar00rootroot00000000000000""" authlib.oauth2.rfc6749.errors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Implementation for OAuth 2 Error Response. A basic error has parameters: error REQUIRED. A single ASCII [USASCII] error code. error_description OPTIONAL. Human-readable ASCII [USASCII] text providing additional information, used to assist the client developer in understanding the error that occurred. error_uri OPTIONAL. A URI identifying a human-readable web page with information about the error, used to provide the client developer with additional information about the error. Values for the "error_uri" parameter MUST conform to the URI-reference syntax and thus MUST NOT include characters outside the set %x21 / %x23-5B / %x5D-7E. state REQUIRED if a "state" parameter was present in the client authorization request. The exact value received from the client. https://tools.ietf.org/html/rfc6749#section-5.2 :copyright: (c) 2017 by Hsiaoming Yang. """ from authlib.oauth2.base import OAuth2Error from authlib.common.security import is_secure_transport __all__ = [ 'OAuth2Error', 'InsecureTransportError', 'InvalidRequestError', 'InvalidClientError', 'UnauthorizedClientError', 'InvalidGrantError', 'UnsupportedResponseTypeError', 'UnsupportedGrantTypeError', 'InvalidScopeError', 'AccessDeniedError', 'MissingAuthorizationError', 'UnsupportedTokenTypeError', 'MissingCodeException', 'MissingTokenException', 'MissingTokenTypeException', 'MismatchingStateException', ] class InsecureTransportError(OAuth2Error): error = 'insecure_transport' description = 'OAuth 2 MUST utilize https.' @classmethod def check(cls, uri): """Check and raise InsecureTransportError with the given URI.""" if not is_secure_transport(uri): raise cls() class InvalidRequestError(OAuth2Error): """The request is missing a required parameter, includes an unsupported parameter value (other than grant type), repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed. https://tools.ietf.org/html/rfc6749#section-5.2 """ error = 'invalid_request' class InvalidClientError(OAuth2Error): """Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The authorization server MAY return an HTTP 401 (Unauthorized) status code to indicate which HTTP authentication schemes are supported. If the client attempted to authenticate via the "Authorization" request header field, the authorization server MUST respond with an HTTP 401 (Unauthorized) status code and include the "WWW-Authenticate" response header field matching the authentication scheme used by the client. https://tools.ietf.org/html/rfc6749#section-5.2 """ error = 'invalid_client' status_code = 400 def get_headers(self): headers = super().get_headers() if self.status_code == 401: error_description = self.get_error_description() # safe escape error_description = error_description.replace('"', '|') extras = [ f'error="{self.error}"', f'error_description="{error_description}"' ] headers.append( ('WWW-Authenticate', 'Basic ' + ', '.join(extras)) ) return headers class InvalidGrantError(OAuth2Error): """The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. https://tools.ietf.org/html/rfc6749#section-5.2 """ error = 'invalid_grant' class UnauthorizedClientError(OAuth2Error): """ The authenticated client is not authorized to use this authorization grant type. https://tools.ietf.org/html/rfc6749#section-5.2 """ error = 'unauthorized_client' class UnsupportedResponseTypeError(OAuth2Error): """The authorization server does not support obtaining an access token using this method.""" error = 'unsupported_response_type' def __init__(self, response_type): super().__init__() self.response_type = response_type def get_error_description(self): return f'response_type={self.response_type} is not supported' class UnsupportedGrantTypeError(OAuth2Error): """The authorization grant type is not supported by the authorization server. https://tools.ietf.org/html/rfc6749#section-5.2 """ error = 'unsupported_grant_type' def __init__(self, grant_type): super().__init__() self.grant_type = grant_type def get_error_description(self): return f'grant_type={self.grant_type} is not supported' class InvalidScopeError(OAuth2Error): """The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. https://tools.ietf.org/html/rfc6749#section-5.2 """ error = 'invalid_scope' description = 'The requested scope is invalid, unknown, or malformed.' class AccessDeniedError(OAuth2Error): """The resource owner or authorization server denied the request. Used in authorization endpoint for "code" and "implicit". Defined in `Section 4.1.2.1`_. .. _`Section 4.1.2.1`: https://tools.ietf.org/html/rfc6749#section-4.1.2.1 """ error = 'access_denied' description = 'The resource owner or authorization server denied the request' # -- below are extended errors -- # class ForbiddenError(OAuth2Error): status_code = 401 def __init__(self, auth_type=None, realm=None): super().__init__() self.auth_type = auth_type self.realm = realm def get_headers(self): headers = super().get_headers() if not self.auth_type: return headers extras = [] if self.realm: extras.append(f'realm="{self.realm}"') extras.append(f'error="{self.error}"') error_description = self.description extras.append(f'error_description="{error_description}"') headers.append( ('WWW-Authenticate', f'{self.auth_type} ' + ', '.join(extras)) ) return headers class MissingAuthorizationError(ForbiddenError): error = 'missing_authorization' description = 'Missing "Authorization" in headers.' class UnsupportedTokenTypeError(ForbiddenError): error = 'unsupported_token_type' # -- exceptions for clients -- # class MissingCodeException(OAuth2Error): error = 'missing_code' description = 'Missing "code" in response.' class MissingTokenException(OAuth2Error): error = 'missing_token' description = 'Missing "access_token" in response.' class MissingTokenTypeException(OAuth2Error): error = 'missing_token_type' description = 'Missing "token_type" in response.' class MismatchingStateException(OAuth2Error): error = 'mismatching_state' description = 'CSRF Warning! State not equal in request and response.' authlib-1.3.2/authlib/oauth2/rfc6749/grants/000077500000000000000000000000001466226534200204135ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc6749/grants/__init__.py000066400000000000000000000024421466226534200225260ustar00rootroot00000000000000""" authlib.oauth2.rfc6749.grants ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Implementation for `Section 4`_ of "Obtaining Authorization". To request an access token, the client obtains authorization from the resource owner. The authorization is expressed in the form of an authorization grant, which the client uses to request the access token. OAuth defines four grant types: 1. authorization code 2. implicit 3. resource owner password credentials 4. client credentials. It also provides an extension mechanism for defining additional grant types. Authlib defines refresh_token as a grant type too. .. _`Section 4`: https://tools.ietf.org/html/rfc6749#section-4 """ # flake8: noqa from .base import BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin from .authorization_code import AuthorizationCodeGrant from .implicit import ImplicitGrant from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant from .client_credentials import ClientCredentialsGrant from .refresh_token import RefreshTokenGrant __all__ = [ 'BaseGrant', 'AuthorizationEndpointMixin', 'TokenEndpointMixin', 'AuthorizationCodeGrant', 'ImplicitGrant', 'ResourceOwnerPasswordCredentialsGrant', 'ClientCredentialsGrant', 'RefreshTokenGrant', ] authlib-1.3.2/authlib/oauth2/rfc6749/grants/authorization_code.py000066400000000000000000000360071466226534200246650ustar00rootroot00000000000000import logging from authlib.common.urls import add_params_to_uri from authlib.common.security import generate_token from .base import BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin from ..errors import ( OAuth2Error, UnauthorizedClientError, InvalidClientError, InvalidGrantError, InvalidRequestError, AccessDeniedError, ) log = logging.getLogger(__name__) class AuthorizationCodeGrant(BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin): """The authorization code grant type is used to obtain both access tokens and refresh tokens and is optimized for confidential clients. Since this is a redirection-based flow, the client must be capable of interacting with the resource owner's user-agent (typically a web browser) and capable of receiving incoming requests (via redirection) from the authorization server:: +----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI ---->| | | User- | | Authorization | | Agent -+----(B)-- User authenticates --->| Server | | | | | | -+----(C)-- Authorization Code ---<| | +-|----|---+ +---------------+ | | ^ v (A) (C) | | | | | | ^ v | | +---------+ | | | |>---(D)-- Authorization Code ---------' | | Client | & Redirection URI | | | | | |<---(E)----- Access Token -------------------' +---------+ (w/ Optional Refresh Token) """ #: Allowed client auth methods for token endpoint TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post'] #: Generated "code" length AUTHORIZATION_CODE_LENGTH = 48 RESPONSE_TYPES = {'code'} GRANT_TYPE = 'authorization_code' def validate_authorization_request(self): """The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI using the "application/x-www-form-urlencoded" format. Per `Section 4.1.1`_. response_type REQUIRED. Value MUST be set to "code". client_id REQUIRED. The client identifier as described in Section 2.2. redirect_uri OPTIONAL. As described in Section 3.1.2. scope OPTIONAL. The scope of the access request as described by Section 3.3. state RECOMMENDED. An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12. The client directs the resource owner to the constructed URI using an HTTP redirection response, or by other means available to it via the user-agent. For example, the client directs the user-agent to make the following HTTP request using TLS (with extra line breaks for display purposes only): .. code-block:: http GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com The authorization server validates the request to ensure that all required parameters are present and valid. If the request is valid, the authorization server authenticates the resource owner and obtains an authorization decision (by asking the resource owner or by establishing approval via other means). .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1 """ return validate_code_authorization_request(self) def create_authorization_response(self, redirect_uri: str, grant_user): """If the resource owner grants the access request, the authorization server issues an authorization code and delivers it to the client by adding the following parameters to the query component of the redirection URI using the "application/x-www-form-urlencoded" format. Per `Section 4.1.2`_. code REQUIRED. The authorization code generated by the authorization server. The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks. A maximum authorization code lifetime of 10 minutes is RECOMMENDED. The client MUST NOT use the authorization code more than once. If an authorization code is used more than once, the authorization server MUST deny the request and SHOULD revoke (when possible) all tokens previously issued based on that authorization code. The authorization code is bound to the client identifier and redirection URI. state REQUIRED if the "state" parameter was present in the client authorization request. The exact value received from the client. For example, the authorization server redirects the user-agent by sending the following HTTP response. .. code-block:: http HTTP/1.1 302 Found Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA &state=xyz .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc6749#section-4.1.2 :param redirect_uri: Redirect to the given URI for the authorization :param grant_user: if resource owner granted the request, pass this resource owner, otherwise pass None. :returns: (status_code, body, headers) """ if not grant_user: raise AccessDeniedError(state=self.request.state, redirect_uri=redirect_uri) self.request.user = grant_user code = self.generate_authorization_code() self.save_authorization_code(code, self.request) params = [('code', code)] if self.request.state: params.append(('state', self.request.state)) uri = add_params_to_uri(redirect_uri, params) headers = [('Location', uri)] return 302, '', headers def validate_token_request(self): """The client makes a request to the token endpoint by sending the following parameters using the "application/x-www-form-urlencoded" format per `Section 4.1.3`_: grant_type REQUIRED. Value MUST be set to "authorization_code". code REQUIRED. The authorization code received from the authorization server. redirect_uri REQUIRED, if the "redirect_uri" parameter was included in the authorization request as described in Section 4.1.1, and their values MUST be identical. client_id REQUIRED, if the client is not authenticating with the authorization server as described in Section 3.2.1. If the client type is confidential or the client was issued client credentials (or assigned other authentication requirements), the client MUST authenticate with the authorization server as described in Section 3.2.1. For example, the client makes the following HTTP request using TLS: .. code-block:: http POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb .. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3 """ # ignore validate for grant_type, since it is validated by # check_token_endpoint # authenticate the client if client authentication is included client = self.authenticate_token_endpoint_client() log.debug('Validate token request of %r', client) if not client.check_grant_type(self.GRANT_TYPE): raise UnauthorizedClientError( f'The client is not authorized to use "grant_type={self.GRANT_TYPE}"') code = self.request.form.get('code') if code is None: raise InvalidRequestError('Missing "code" in request.') # ensure that the authorization code was issued to the authenticated # confidential client, or if the client is public, ensure that the # code was issued to "client_id" in the request authorization_code = self.query_authorization_code(code, client) if not authorization_code: raise InvalidGrantError('Invalid "code" in request.') # validate redirect_uri parameter log.debug('Validate token redirect_uri of %r', client) redirect_uri = self.request.redirect_uri original_redirect_uri = authorization_code.get_redirect_uri() if original_redirect_uri and redirect_uri != original_redirect_uri: raise InvalidGrantError('Invalid "redirect_uri" in request.') # save for create_token_response self.request.client = client self.request.authorization_code = authorization_code self.execute_hook('after_validate_token_request') def create_token_response(self): """If the access token request is valid and authorized, the authorization server issues an access token and optional refresh token as described in Section 5.1. If the request client authentication failed or is invalid, the authorization server returns an error response as described in Section 5.2. Per `Section 4.1.4`_. An example successful response: .. code-block:: http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" } :returns: (status_code, body, headers) .. _`Section 4.1.4`: https://tools.ietf.org/html/rfc6749#section-4.1.4 """ client = self.request.client authorization_code = self.request.authorization_code user = self.authenticate_user(authorization_code) if not user: raise InvalidGrantError('There is no "user" for this code.') self.request.user = user scope = authorization_code.get_scope() token = self.generate_token( user=user, scope=scope, include_refresh_token=client.check_grant_type('refresh_token'), ) log.debug('Issue token %r to %r', token, client) self.save_token(token) self.execute_hook('process_token', token=token) self.delete_authorization_code(authorization_code) return 200, token, self.TOKEN_RESPONSE_HEADER def generate_authorization_code(self): """"The method to generate "code" value for authorization code data. Developers may rewrite this method, or customize the code length with:: class MyAuthorizationCodeGrant(AuthorizationCodeGrant): AUTHORIZATION_CODE_LENGTH = 32 # default is 48 """ return generate_token(self.AUTHORIZATION_CODE_LENGTH) def save_authorization_code(self, code, request): """Save authorization_code for later use. Developers MUST implement it in subclass. Here is an example:: def save_authorization_code(self, code, request): client = request.client item = AuthorizationCode( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, user_id=request.user.id, ) item.save() """ raise NotImplementedError() def query_authorization_code(self, code, client): # pragma: no cover """Get authorization_code from previously savings. Developers MUST implement it in subclass:: def query_authorization_code(self, code, client): return Authorization.get(code=code, client_id=client.client_id) :param code: a string represent the code. :param client: client related to this code. :return: authorization_code object """ raise NotImplementedError() def delete_authorization_code(self, authorization_code): """Delete authorization code from database or cache. Developers MUST implement it in subclass, e.g.:: def delete_authorization_code(self, authorization_code): authorization_code.delete() :param authorization_code: the instance of authorization_code """ raise NotImplementedError() def authenticate_user(self, authorization_code): """Authenticate the user related to this authorization_code. Developers MUST implement this method in subclass, e.g.:: def authenticate_user(self, authorization_code): return User.get(authorization_code.user_id) :param authorization_code: AuthorizationCode object :return: user """ raise NotImplementedError() def validate_code_authorization_request(grant): request = grant.request client_id = request.client_id log.debug('Validate authorization request of %r', client_id) if client_id is None: raise InvalidClientError(state=request.state) client = grant.server.query_client(client_id) if not client: raise InvalidClientError(state=request.state) redirect_uri = grant.validate_authorization_redirect_uri(request, client) response_type = request.response_type if not client.check_response_type(response_type): raise UnauthorizedClientError( f'The client is not authorized to use "response_type={response_type}"', state=grant.request.state, redirect_uri=redirect_uri, ) try: grant.request.client = client grant.validate_requested_scope() grant.execute_hook('after_validate_authorization_request') except OAuth2Error as error: error.redirect_uri = redirect_uri raise error return redirect_uri authlib-1.3.2/authlib/oauth2/rfc6749/grants/base.py000066400000000000000000000132421466226534200217010ustar00rootroot00000000000000from authlib.consts import default_json_headers from authlib.common.urls import urlparse from ..requests import OAuth2Request from ..errors import InvalidRequestError class BaseGrant: #: Allowed client auth methods for token endpoint TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic'] #: Designed for which "grant_type" GRANT_TYPE = None # NOTE: there is no charset for application/json, since # application/json should always in UTF-8. # The example on RFC is incorrect. # https://tools.ietf.org/html/rfc4627 TOKEN_RESPONSE_HEADER = default_json_headers def __init__(self, request: OAuth2Request, server): self.prompt = None self.redirect_uri = None self.request = request self.server = server self._hooks = { 'after_validate_authorization_request': set(), 'after_validate_consent_request': set(), 'after_validate_token_request': set(), 'process_token': set(), } @property def client(self): return self.request.client def generate_token(self, user=None, scope=None, grant_type=None, expires_in=None, include_refresh_token=True): if grant_type is None: grant_type = self.GRANT_TYPE return self.server.generate_token( client=self.request.client, grant_type=grant_type, user=user, scope=scope, expires_in=expires_in, include_refresh_token=include_refresh_token, ) def authenticate_token_endpoint_client(self): """Authenticate client with the given methods for token endpoint. For example, the client makes the following HTTP request using TLS: .. code-block:: http POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb Default available methods are: "none", "client_secret_basic" and "client_secret_post". :return: client """ client = self.server.authenticate_client( self.request, self.TOKEN_ENDPOINT_AUTH_METHODS) self.server.send_signal( 'after_authenticate_client', client=client, grant=self) return client def save_token(self, token): """A method to save token into database.""" return self.server.save_token(token, self.request) def validate_requested_scope(self): """Validate if requested scope is supported by Authorization Server.""" scope = self.request.scope state = self.request.state return self.server.validate_requested_scope(scope, state) def register_hook(self, hook_type, hook): if hook_type not in self._hooks: raise ValueError('Hook type %s is not in %s.', hook_type, self._hooks) self._hooks[hook_type].add(hook) def execute_hook(self, hook_type, *args, **kwargs): for hook in self._hooks[hook_type]: hook(self, *args, **kwargs) class TokenEndpointMixin: #: Allowed HTTP methods of this token endpoint TOKEN_ENDPOINT_HTTP_METHODS = ['POST'] #: Designed for which "grant_type" GRANT_TYPE = None @classmethod def check_token_endpoint(cls, request: OAuth2Request): return request.grant_type == cls.GRANT_TYPE and \ request.method in cls.TOKEN_ENDPOINT_HTTP_METHODS def validate_token_request(self): raise NotImplementedError() def create_token_response(self): raise NotImplementedError() class AuthorizationEndpointMixin: RESPONSE_TYPES = set() ERROR_RESPONSE_FRAGMENT = False @classmethod def check_authorization_endpoint(cls, request: OAuth2Request): return request.response_type in cls.RESPONSE_TYPES @staticmethod def validate_authorization_redirect_uri(request: OAuth2Request, client): if request.redirect_uri: if not client.check_redirect_uri(request.redirect_uri): raise InvalidRequestError( f'Redirect URI {request.redirect_uri} is not supported by client.', state=request.state) return request.redirect_uri else: redirect_uri = client.get_default_redirect_uri() if not redirect_uri: raise InvalidRequestError( 'Missing "redirect_uri" in request.', state=request.state) return redirect_uri @staticmethod def validate_no_multiple_request_parameter(request: OAuth2Request): """For the Authorization Endpoint, request and response parameters MUST NOT be included more than once. Per `Section 3.1`_. .. _`Section 3.1`: https://tools.ietf.org/html/rfc6749#section-3.1 """ datalist = request.datalist parameters = ["response_type", "client_id", "redirect_uri", "scope", "state"] for param in parameters: if len(datalist.get(param, [])) > 1: raise InvalidRequestError(f'Multiple "{param}" in request.', state=request.state) def validate_consent_request(self): redirect_uri = self.validate_authorization_request() self.execute_hook('after_validate_consent_request', redirect_uri) self.redirect_uri = redirect_uri def validate_authorization_request(self): raise NotImplementedError() def create_authorization_response(self, redirect_uri: str, grant_user): raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc6749/grants/client_credentials.py000066400000000000000000000074641466226534200246330ustar00rootroot00000000000000import logging from .base import BaseGrant, TokenEndpointMixin from ..errors import UnauthorizedClientError log = logging.getLogger(__name__) class ClientCredentialsGrant(BaseGrant, TokenEndpointMixin): """The client can request an access token using only its client credentials (or other supported means of authentication) when the client is requesting access to the protected resources under its control, or those of another resource owner that have been previously arranged with the authorization server. The client credentials grant type MUST only be used by confidential clients:: +---------+ +---------------+ | | | | | |>--(A)- Client Authentication --->| Authorization | | Client | | Server | | |<--(B)---- Access Token ---------<| | | | | | +---------+ +---------------+ https://tools.ietf.org/html/rfc6749#section-4.4 """ GRANT_TYPE = 'client_credentials' def validate_token_request(self): """The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" format per Appendix B with a character encoding of UTF-8 in the HTTP request entity-body: grant_type REQUIRED. Value MUST be set to "client_credentials". scope OPTIONAL. The scope of the access request as described by Section 3.3. The client MUST authenticate with the authorization server as described in Section 3.2.1. For example, the client makes the following HTTP request using transport-layer security (with extra line breaks for display purposes only): .. code-block:: http POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=client_credentials The authorization server MUST authenticate the client. """ # ignore validate for grant_type, since it is validated by # check_token_endpoint client = self.authenticate_token_endpoint_client() log.debug('Validate token request of %r', client) if not client.check_grant_type(self.GRANT_TYPE): raise UnauthorizedClientError() self.request.client = client self.validate_requested_scope() def create_token_response(self): """If the access token request is valid and authorized, the authorization server issues an access token as described in Section 5.1. A refresh token SHOULD NOT be included. If the request failed client authentication or is invalid, the authorization server returns an error response as described in Section 5.2. An example successful response: .. code-block:: http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "example_parameter":"example_value" } :returns: (status_code, body, headers) """ token = self.generate_token(scope=self.request.scope, include_refresh_token=False) log.debug('Issue token %r to %r', token, self.client) self.save_token(token) self.execute_hook('process_token', self, token=token) return 200, token, self.TOKEN_RESPONSE_HEADER authlib-1.3.2/authlib/oauth2/rfc6749/grants/implicit.py000066400000000000000000000221211466226534200225750ustar00rootroot00000000000000import logging from authlib.common.urls import add_params_to_uri from .base import BaseGrant, AuthorizationEndpointMixin from ..errors import ( OAuth2Error, UnauthorizedClientError, AccessDeniedError, ) log = logging.getLogger(__name__) class ImplicitGrant(BaseGrant, AuthorizationEndpointMixin): """The implicit grant type is used to obtain access tokens (it does not support the issuance of refresh tokens) and is optimized for public clients known to operate a particular redirection URI. These clients are typically implemented in a browser using a scripting language such as JavaScript. Since this is a redirection-based flow, the client must be capable of interacting with the resource owner's user-agent (typically a web browser) and capable of receiving incoming requests (via redirection) from the authorization server. Unlike the authorization code grant type, in which the client makes separate requests for authorization and for an access token, the client receives the access token as the result of the authorization request. The implicit grant type does not include client authentication, and relies on the presence of the resource owner and the registration of the redirection URI. Because the access token is encoded into the redirection URI, it may be exposed to the resource owner and other applications residing on the same device:: +----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI --->| | | User- | | Authorization | | Agent -|----(B)-- User authenticates -->| Server | | | | | | |<---(C)--- Redirection URI ----<| | | | with Access Token +---------------+ | | in Fragment | | +---------------+ | |----(D)--- Redirection URI ---->| Web-Hosted | | | without Fragment | Client | | | | Resource | | (F) |<---(E)------- Script ---------<| | | | +---------------+ +-|--------+ | | (A) (G) Access Token | | ^ v +---------+ | | | Client | | | +---------+ """ #: authorization_code grant type has authorization endpoint AUTHORIZATION_ENDPOINT = True #: Allowed client auth methods for token endpoint TOKEN_ENDPOINT_AUTH_METHODS = ['none'] RESPONSE_TYPES = {'token'} GRANT_TYPE = 'implicit' ERROR_RESPONSE_FRAGMENT = True def validate_authorization_request(self): """The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI using the "application/x-www-form-urlencoded" format. Per `Section 4.2.1`_. response_type REQUIRED. Value MUST be set to "token". client_id REQUIRED. The client identifier as described in Section 2.2. redirect_uri OPTIONAL. As described in Section 3.1.2. scope OPTIONAL. The scope of the access request as described by Section 3.3. state RECOMMENDED. An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12. The client directs the resource owner to the constructed URI using an HTTP redirection response, or by other means available to it via the user-agent. For example, the client directs the user-agent to make the following HTTP request using TLS: .. code-block:: http GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com .. _`Section 4.2.1`: https://tools.ietf.org/html/rfc6749#section-4.2.1 """ # ignore validate for response_type, since it is validated by # check_authorization_endpoint # The implicit grant type is optimized for public clients client = self.authenticate_token_endpoint_client() log.debug('Validate authorization request of %r', client) redirect_uri = self.validate_authorization_redirect_uri( self.request, client) response_type = self.request.response_type if not client.check_response_type(response_type): raise UnauthorizedClientError( 'The client is not authorized to use ' '"response_type={}"'.format(response_type), state=self.request.state, redirect_uri=redirect_uri, redirect_fragment=True, ) try: self.request.client = client self.validate_requested_scope() self.execute_hook('after_validate_authorization_request') except OAuth2Error as error: error.redirect_uri = redirect_uri error.redirect_fragment = True raise error return redirect_uri def create_authorization_response(self, redirect_uri, grant_user): """If the resource owner grants the access request, the authorization server issues an access token and delivers it to the client by adding the following parameters to the fragment component of the redirection URI using the "application/x-www-form-urlencoded" format. Per `Section 4.2.2`_. access_token REQUIRED. The access token issued by the authorization server. token_type REQUIRED. The type of the token issued as described in Section 7.1. Value is case insensitive. expires_in RECOMMENDED. The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. If omitted, the authorization server SHOULD provide the expiration time via other means or document the default value. scope OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The scope of the access token as described by Section 3.3. state REQUIRED if the "state" parameter was present in the client authorization request. The exact value received from the client. The authorization server MUST NOT issue a refresh token. For example, the authorization server redirects the user-agent by sending the following HTTP response: .. code-block:: http HTTP/1.1 302 Found Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA &state=xyz&token_type=example&expires_in=3600 Developers should note that some user-agents do not support the inclusion of a fragment component in the HTTP "Location" response header field. Such clients will require using other methods for redirecting the client than a 3xx redirection response -- for example, returning an HTML page that includes a 'continue' button with an action linked to the redirection URI. .. _`Section 4.2.2`: https://tools.ietf.org/html/rfc6749#section-4.2.2 :param redirect_uri: Redirect to the given URI for the authorization :param grant_user: if resource owner granted the request, pass this resource owner, otherwise pass None. :returns: (status_code, body, headers) """ state = self.request.state if grant_user: self.request.user = grant_user token = self.generate_token( user=grant_user, scope=self.request.scope, include_refresh_token=False, ) log.debug('Grant token %r to %r', token, self.request.client) self.save_token(token) self.execute_hook('process_token', token=token) params = [(k, token[k]) for k in token] if state: params.append(('state', state)) uri = add_params_to_uri(redirect_uri, params, fragment=True) headers = [('Location', uri)] return 302, '', headers else: raise AccessDeniedError( state=state, redirect_uri=redirect_uri, redirect_fragment=True ) authlib-1.3.2/authlib/oauth2/rfc6749/grants/refresh_token.py000066400000000000000000000144401466226534200236260ustar00rootroot00000000000000""" authlib.oauth2.rfc6749.grants.refresh_token ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A special grant endpoint for refresh_token grant_type. Refreshing an Access Token per `Section 6`_. .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6 """ import logging from .base import BaseGrant, TokenEndpointMixin from ..util import scope_to_list from ..errors import ( InvalidRequestError, InvalidScopeError, InvalidGrantError, UnauthorizedClientError, ) log = logging.getLogger(__name__) class RefreshTokenGrant(BaseGrant, TokenEndpointMixin): """A special grant endpoint for refresh_token grant_type. Refreshing an Access Token per `Section 6`_. .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6 """ GRANT_TYPE = 'refresh_token' #: The authorization server MAY issue a new refresh token INCLUDE_NEW_REFRESH_TOKEN = False def _validate_request_client(self): # require client authentication for confidential clients or for any # client that was issued client credentials (or with other # authentication requirements) client = self.authenticate_token_endpoint_client() log.debug('Validate token request of %r', client) if not client.check_grant_type(self.GRANT_TYPE): raise UnauthorizedClientError() return client def _validate_request_token(self, client): refresh_token = self.request.form.get('refresh_token') if refresh_token is None: raise InvalidRequestError('Missing "refresh_token" in request.') token = self.authenticate_refresh_token(refresh_token) if not token or not token.check_client(client): raise InvalidGrantError() return token def _validate_token_scope(self, token): scope = self.request.scope if not scope: return original_scope = token.get_scope() if not original_scope: raise InvalidScopeError() original_scope = set(scope_to_list(original_scope)) if not original_scope.issuperset(set(scope_to_list(scope))): raise InvalidScopeError() def validate_token_request(self): """If the authorization server issued a refresh token to the client, the client makes a refresh request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" format per Appendix B with a character encoding of UTF-8 in the HTTP request entity-body, per Section 6: grant_type REQUIRED. Value MUST be set to "refresh_token". refresh_token REQUIRED. The refresh token issued to the client. scope OPTIONAL. The scope of the access request as described by Section 3.3. The requested scope MUST NOT include any scope not originally granted by the resource owner, and if omitted is treated as equal to the scope originally granted by the resource owner. For example, the client makes the following HTTP request using transport-layer security (with extra line breaks for display purposes only): .. code-block:: http POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA """ client = self._validate_request_client() self.request.client = client refresh_token = self._validate_request_token(client) self._validate_token_scope(refresh_token) self.request.refresh_token = refresh_token def create_token_response(self): """If valid and authorized, the authorization server issues an access token as described in Section 5.1. If the request failed verification or is invalid, the authorization server returns an error response as described in Section 5.2. """ refresh_token = self.request.refresh_token user = self.authenticate_user(refresh_token) if not user: raise InvalidRequestError('There is no "user" for this token.') client = self.request.client token = self.issue_token(user, refresh_token) log.debug('Issue token %r to %r', token, client) self.request.user = user self.save_token(token) self.execute_hook('process_token', token=token) self.revoke_old_credential(refresh_token) return 200, token, self.TOKEN_RESPONSE_HEADER def issue_token(self, user, refresh_token): scope = self.request.scope if not scope: scope = refresh_token.get_scope() token = self.generate_token( user=user, scope=scope, include_refresh_token=self.INCLUDE_NEW_REFRESH_TOKEN, ) return token def authenticate_refresh_token(self, refresh_token): """Get token information with refresh_token string. Developers MUST implement this method in subclass:: def authenticate_refresh_token(self, refresh_token): token = Token.get(refresh_token=refresh_token) if token and not token.refresh_token_revoked: return token :param refresh_token: The refresh token issued to the client :return: token """ raise NotImplementedError() def authenticate_user(self, refresh_token): """Authenticate the user related to this credential. Developers MUST implement this method in subclass:: def authenticate_user(self, credential): return User.get(credential.user_id) :param refresh_token: Token object :return: user """ raise NotImplementedError() def revoke_old_credential(self, refresh_token): """The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client. Developers MUST implement this method in subclass:: def revoke_old_credential(self, refresh_token): credential.revoked = True credential.save() :param refresh_token: Token object """ raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc6749/grants/resource_owner_password_credentials.py000066400000000000000000000131731466226534200303320ustar00rootroot00000000000000import logging from .base import BaseGrant, TokenEndpointMixin from ..errors import ( UnauthorizedClientError, InvalidRequestError, ) log = logging.getLogger(__name__) class ResourceOwnerPasswordCredentialsGrant(BaseGrant, TokenEndpointMixin): """The resource owner password credentials grant type is suitable in cases where the resource owner has a trust relationship with the client, such as the device operating system or a highly privileged application. The authorization server should take special care when enabling this grant type and only allow it when other flows are not viable. This grant type is suitable for clients capable of obtaining the resource owner's credentials (username and password, typically using an interactive form). It is also used to migrate existing clients using direct authentication schemes such as HTTP Basic or Digest authentication to OAuth by converting the stored credentials to an access token:: +----------+ | Resource | | Owner | | | +----------+ v | Resource Owner (A) Password Credentials | v +---------+ +---------------+ | |>--(B)---- Resource Owner ------->| | | | Password Credentials | Authorization | | Client | | Server | | |<--(C)---- Access Token ---------<| | | | (w/ Optional Refresh Token) | | +---------+ +---------------+ """ GRANT_TYPE = 'password' def validate_token_request(self): """The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" format per Appendix B with a character encoding of UTF-8 in the HTTP request entity-body: grant_type REQUIRED. Value MUST be set to "password". username REQUIRED. The resource owner username. password REQUIRED. The resource owner password. scope OPTIONAL. The scope of the access request as described by Section 3.3. If the client type is confidential or the client was issued client credentials (or assigned other authentication requirements), the client MUST authenticate with the authorization server as described in Section 3.2.1. For example, the client makes the following HTTP request using transport-layer security (with extra line breaks for display purposes only): .. code-block:: http POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=password&username=johndoe&password=A3ddj3w """ # ignore validate for grant_type, since it is validated by # check_token_endpoint client = self.authenticate_token_endpoint_client() log.debug('Validate token request of %r', client) if not client.check_grant_type(self.GRANT_TYPE): raise UnauthorizedClientError() params = self.request.form if 'username' not in params: raise InvalidRequestError('Missing "username" in request.') if 'password' not in params: raise InvalidRequestError('Missing "password" in request.') log.debug('Authenticate user of %r', params['username']) user = self.authenticate_user( params['username'], params['password'] ) if not user: raise InvalidRequestError( 'Invalid "username" or "password" in request.', ) self.request.client = client self.request.user = user self.validate_requested_scope() def create_token_response(self): """If the access token request is valid and authorized, the authorization server issues an access token and optional refresh token as described in Section 5.1. If the request failed client authentication or is invalid, the authorization server returns an error response as described in Section 5.2. An example successful response: .. code-block:: http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" } :returns: (status_code, body, headers) """ user = self.request.user scope = self.request.scope token = self.generate_token(user=user, scope=scope) log.debug('Issue token %r to %r', token, self.client) self.save_token(token) self.execute_hook('process_token', token=token) return 200, token, self.TOKEN_RESPONSE_HEADER def authenticate_user(self, username, password): """validate the resource owner password credentials using its existing password validation algorithm:: def authenticate_user(self, username, password): user = get_user_by_username(username) if user.check_password(password): return user """ raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc6749/models.py000066400000000000000000000165161466226534200207630ustar00rootroot00000000000000""" authlib.oauth2.rfc6749.models ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This module defines how to construct Client, AuthorizationCode and Token. """ from authlib.deprecate import deprecate class ClientMixin: """Implementation of OAuth 2 Client described in `Section 2`_ with some methods to help validation. A client has at least these information: * client_id: A string represents client identifier. * client_secret: A string represents client password. * token_endpoint_auth_method: A way to authenticate client at token endpoint. .. _`Section 2`: https://tools.ietf.org/html/rfc6749#section-2 """ def get_client_id(self): """A method to return client_id of the client. For instance, the value in database is saved in a column called ``client_id``:: def get_client_id(self): return self.client_id :return: string """ raise NotImplementedError() def get_default_redirect_uri(self): """A method to get client default redirect_uri. For instance, the database table for client has a column called ``default_redirect_uri``:: def get_default_redirect_uri(self): return self.default_redirect_uri :return: A URL string """ raise NotImplementedError() def get_allowed_scope(self, scope): """A method to return a list of requested scopes which are supported by this client. For instance, there is a ``scope`` column:: def get_allowed_scope(self, scope): if not scope: return '' allowed = set(scope_to_list(self.scope)) return list_to_scope([s for s in scope.split() if s in allowed]) :param scope: the requested scope. :return: string of scope """ raise NotImplementedError() def check_redirect_uri(self, redirect_uri): """Validate redirect_uri parameter in Authorization Endpoints. For instance, in the client table, there is an ``allowed_redirect_uris`` column:: def check_redirect_uri(self, redirect_uri): return redirect_uri in self.allowed_redirect_uris :param redirect_uri: A URL string for redirecting. :return: bool """ raise NotImplementedError() def check_client_secret(self, client_secret): """Check client_secret matching with the client. For instance, in the client table, the column is called ``client_secret``:: import secrets def check_client_secret(self, client_secret): return secrets.compare_digest(self.client_secret, client_secret) :param client_secret: A string of client secret :return: bool """ raise NotImplementedError() def check_endpoint_auth_method(self, method, endpoint): """Check if client support the given method for the given endpoint. There is a ``token_endpoint_auth_method`` defined via `RFC7591`_. Developers MAY re-implement this method with:: def check_endpoint_auth_method(self, method, endpoint): if endpoint == 'token': # if client table has ``token_endpoint_auth_method`` return self.token_endpoint_auth_method == method return True Method values defined by this specification are: * "none": The client is a public client as defined in OAuth 2.0, and does not have a client secret. * "client_secret_post": The client uses the HTTP POST parameters as defined in OAuth 2.0 * "client_secret_basic": The client uses HTTP Basic as defined in OAuth 2.0 .. _`RFC7591`: https://tools.ietf.org/html/rfc7591 """ raise NotImplementedError() def check_token_endpoint_auth_method(self, method): deprecate('Please implement ``check_endpoint_auth_method`` instead.') return self.check_endpoint_auth_method(method, 'token') def check_response_type(self, response_type): """Validate if the client can handle the given response_type. There are two response types defined by RFC6749: code and token. For instance, there is a ``allowed_response_types`` column in your client:: def check_response_type(self, response_type): return response_type in self.response_types :param response_type: the requested response_type string. :return: bool """ raise NotImplementedError() def check_grant_type(self, grant_type): """Validate if the client can handle the given grant_type. There are four grant types defined by RFC6749: * authorization_code * implicit * client_credentials * password For instance, there is a ``allowed_grant_types`` column in your client:: def check_grant_type(self, grant_type): return grant_type in self.grant_types :param grant_type: the requested grant_type string. :return: bool """ raise NotImplementedError() class AuthorizationCodeMixin: def get_redirect_uri(self): """A method to get authorization code's ``redirect_uri``. For instance, the database table for authorization code has a column called ``redirect_uri``:: def get_redirect_uri(self): return self.redirect_uri :return: A URL string """ raise NotImplementedError() def get_scope(self): """A method to get scope of the authorization code. For instance, the column is called ``scope``:: def get_scope(self): return self.scope :return: scope string """ raise NotImplementedError() class TokenMixin: def check_client(self, client): """A method to check if this token is issued to the given client. For instance, ``client_id`` is saved on token table:: def check_client(self, client): return self.client_id == client.client_id :return: bool """ raise NotImplementedError() def get_scope(self): """A method to get scope of the authorization code. For instance, the column is called ``scope``:: def get_scope(self): return self.scope :return: scope string """ raise NotImplementedError() def get_expires_in(self): """A method to get the ``expires_in`` value of the token. e.g. the column is called ``expires_in``:: def get_expires_in(self): return self.expires_in :return: timestamp int """ raise NotImplementedError() def is_expired(self): """A method to define if this token is expired. For instance, there is a column ``expired_at`` in the table:: def is_expired(self): return self.expired_at < now :return: boolean """ raise NotImplementedError() def is_revoked(self): """A method to define if this token is revoked. For instance, there is a boolean column ``revoked`` in the table:: def is_revoked(self): return self.revoked :return: boolean """ raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc6749/parameters.py000066400000000000000000000201641466226534200216350ustar00rootroot00000000000000from authlib.common.urls import ( urlparse, add_params_to_uri, add_params_to_qs, ) from authlib.common.encoding import to_unicode from .errors import ( MissingCodeException, MissingTokenException, MissingTokenTypeException, MismatchingStateException, ) from .util import list_to_scope def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, scope=None, state=None, **kwargs): """Prepare the authorization grant request URI. The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI using the ``application/x-www-form-urlencoded`` format: :param uri: The authorize endpoint to fetch "code" or "token". :param client_id: The client identifier as described in `Section 2.2`_. :param response_type: To indicate which OAuth 2 grant/flow is required, "code" and "token". :param redirect_uri: The client provided URI to redirect back to after authorization as described in `Section 3.1.2`_. :param scope: The scope of the access request as described by `Section 3.3`_. :param state: An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in `Section 10.12`_. :param kwargs: Extra arguments to embed in the grant/authorization URL. An example of an authorization code grant authorization URL:: /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 .. _`section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ params = [ ('response_type', response_type), ('client_id', client_id) ] if redirect_uri: params.append(('redirect_uri', redirect_uri)) if scope: params.append(('scope', list_to_scope(scope))) if state: params.append(('state', state)) for k in kwargs: if kwargs[k] is not None: params.append((to_unicode(k), kwargs[k])) return add_params_to_uri(uri, params) def prepare_token_request(grant_type, body='', redirect_uri=None, **kwargs): """Prepare the access token request. Per `Section 4.1.3`_. The client makes a request to the token endpoint by adding the following parameters using the ``application/x-www-form-urlencoded`` format in the HTTP request entity-body: :param grant_type: To indicate grant type being used, i.e. "password", "authorization_code" or "client_credentials". :param body: Existing request body to embed parameters in. :param redirect_uri: If the "redirect_uri" parameter was included in the authorization request as described in `Section 4.1.1`_, and their values MUST be identical. :param kwargs: Extra arguments to embed in the request body. An example of an authorization code token request body:: grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1 .. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3 """ params = [('grant_type', grant_type)] if redirect_uri: params.append(('redirect_uri', redirect_uri)) if 'scope' in kwargs: kwargs['scope'] = list_to_scope(kwargs['scope']) if grant_type == 'authorization_code' and 'code' not in kwargs: raise MissingCodeException() for k in kwargs: if kwargs[k]: params.append((to_unicode(k), kwargs[k])) return add_params_to_qs(body, params) def parse_authorization_code_response(uri, state=None): """Parse authorization grant response URI into a dict. If the resource owner grants the access request, the authorization server issues an authorization code and delivers it to the client by adding the following parameters to the query component of the redirection URI using the ``application/x-www-form-urlencoded`` format: **code** REQUIRED. The authorization code generated by the authorization server. The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks. A maximum authorization code lifetime of 10 minutes is RECOMMENDED. The client MUST NOT use the authorization code more than once. If an authorization code is used more than once, the authorization server MUST deny the request and SHOULD revoke (when possible) all tokens previously issued based on that authorization code. The authorization code is bound to the client identifier and redirection URI. **state** REQUIRED if the "state" parameter was present in the client authorization request. The exact value received from the client. :param uri: The full redirect URL back to the client. :param state: The state parameter from the authorization request. For example, the authorization server redirects the user-agent by sending the following HTTP response: .. code-block:: http HTTP/1.1 302 Found Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA &state=xyz """ query = urlparse.urlparse(uri).query params = dict(urlparse.parse_qsl(query)) if 'code' not in params: raise MissingCodeException() params_state = params.get('state') if state and params_state != state: raise MismatchingStateException() return params def parse_implicit_response(uri, state=None): """Parse the implicit token response URI into a dict. If the resource owner grants the access request, the authorization server issues an access token and delivers it to the client by adding the following parameters to the fragment component of the redirection URI using the ``application/x-www-form-urlencoded`` format: **access_token** REQUIRED. The access token issued by the authorization server. **token_type** REQUIRED. The type of the token issued as described in Section 7.1. Value is case insensitive. **expires_in** RECOMMENDED. The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. If omitted, the authorization server SHOULD provide the expiration time via other means or document the default value. **scope** OPTIONAL, if identical to the scope requested by the client, otherwise REQUIRED. The scope of the access token as described by Section 3.3. **state** REQUIRED if the "state" parameter was present in the client authorization request. The exact value received from the client. Similar to the authorization code response, but with a full token provided in the URL fragment: .. code-block:: http HTTP/1.1 302 Found Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA &state=xyz&token_type=example&expires_in=3600 """ fragment = urlparse.urlparse(uri).fragment params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True)) if 'access_token' not in params: raise MissingTokenException() if 'token_type' not in params: raise MissingTokenTypeException() if state and params.get('state', None) != state: raise MismatchingStateException() return params authlib-1.3.2/authlib/oauth2/rfc6749/requests.py000066400000000000000000000054311466226534200213450ustar00rootroot00000000000000from collections import defaultdict from typing import DefaultDict from authlib.common.encoding import json_loads from authlib.common.urls import urlparse, url_decode from .errors import InsecureTransportError class OAuth2Request: def __init__(self, method: str, uri: str, body=None, headers=None): InsecureTransportError.check(uri) #: HTTP method self.method = method self.uri = uri self.body = body #: HTTP headers self.headers = headers or {} self.client = None self.auth_method = None self.user = None self.authorization_code = None self.refresh_token = None self.credential = None self._parsed_query = None @property def args(self): if self._parsed_query is None: self._parsed_query = url_decode(urlparse.urlparse(self.uri).query) return dict(self._parsed_query) @property def form(self): return self.body or {} @property def data(self): data = {} data.update(self.args) data.update(self.form) return data @property def datalist(self) -> DefaultDict[str, list]: """ Return all the data in query parameters and the body of the request as a dictionary with all the values in lists. """ if self._parsed_query is None: self._parsed_query = url_decode(urlparse.urlparse(self.uri).query) values = defaultdict(list) for k, v in self._parsed_query: values[k].append(v) for k, v in self.form.items(): values[k].append(v) return values @property def client_id(self) -> str: """The authorization server issues the registered client a client identifier -- a unique string representing the registration information provided by the client. The value is extracted from request. :return: string """ return self.data.get('client_id') @property def response_type(self) -> str: rt = self.data.get('response_type') if rt and ' ' in rt: # sort multiple response types return ' '.join(sorted(rt.split())) return rt @property def grant_type(self) -> str: return self.form.get('grant_type') @property def redirect_uri(self): return self.data.get('redirect_uri') @property def scope(self) -> str: return self.data.get('scope') @property def state(self): return self.data.get('state') class JsonRequest: def __init__(self, method, uri, body=None, headers=None): self.method = method self.uri = uri self.body = body self.headers = headers or {} @property def data(self): return json_loads(self.body) authlib-1.3.2/authlib/oauth2/rfc6749/resource_protector.py000066400000000000000000000122751466226534200234260ustar00rootroot00000000000000""" authlib.oauth2.rfc6749.resource_protector ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Implementation of Accessing Protected Resources per `Section 7`_. .. _`Section 7`: https://tools.ietf.org/html/rfc6749#section-7 """ from .util import scope_to_list from .errors import MissingAuthorizationError, UnsupportedTokenTypeError class TokenValidator: """Base token validator class. Subclass this validator to register into ResourceProtector instance. """ TOKEN_TYPE = 'bearer' def __init__(self, realm=None, **extra_attributes): self.realm = realm self.extra_attributes = extra_attributes @staticmethod def scope_insufficient(token_scopes, required_scopes): if not required_scopes: return False token_scopes = scope_to_list(token_scopes) if not token_scopes: return True token_scopes = set(token_scopes) for scope in required_scopes: resource_scopes = set(scope_to_list(scope)) if token_scopes.issuperset(resource_scopes): return False return True def authenticate_token(self, token_string): """A method to query token from database with the given token string. Developers MUST re-implement this method. For instance:: def authenticate_token(self, token_string): return get_token_from_database(token_string) :param token_string: A string to represent the access_token. :return: token """ raise NotImplementedError() def validate_request(self, request): """A method to validate if the HTTP request is valid or not. Developers MUST re-implement this method. For instance, your server requires a "X-Device-Version" in the header:: def validate_request(self, request): if 'X-Device-Version' not in request.headers: raise InvalidRequestError() Usually, you don't have to detect if the request is valid or not. If you have to, you MUST re-implement this method. :param request: instance of HttpRequest :raise: InvalidRequestError """ def validate_token(self, token, scopes, request): """A method to validate if the authorized token is valid, if it has the permission on the given scopes. Developers MUST re-implement this method. e.g, check if token is expired, revoked:: def validate_token(self, token, scopes, request): if not token: raise InvalidTokenError() if token.is_expired() or token.is_revoked(): raise InvalidTokenError() if not match_token_scopes(token, scopes): raise InsufficientScopeError() """ raise NotImplementedError() class ResourceProtector: def __init__(self): self._token_validators = {} self._default_realm = None self._default_auth_type = None def register_token_validator(self, validator: TokenValidator): """Register a token validator for a given Authorization type. Authlib has a built-in BearerTokenValidator per rfc6750. """ if not self._default_auth_type: self._default_realm = validator.realm self._default_auth_type = validator.TOKEN_TYPE if validator.TOKEN_TYPE not in self._token_validators: self._token_validators[validator.TOKEN_TYPE] = validator def get_token_validator(self, token_type): """Get token validator from registry for the given token type.""" validator = self._token_validators.get(token_type.lower()) if not validator: raise UnsupportedTokenTypeError(self._default_auth_type, self._default_realm) return validator def parse_request_authorization(self, request): """Parse the token and token validator from request Authorization header. Here is an example of Authorization header:: Authorization: Bearer a-token-string This method will parse this header, if it can find the validator for ``Bearer``, it will return the validator and ``a-token-string``. :return: validator, token_string :raise: MissingAuthorizationError :raise: UnsupportedTokenTypeError """ auth = request.headers.get('Authorization') if not auth: raise MissingAuthorizationError(self._default_auth_type, self._default_realm) # https://tools.ietf.org/html/rfc6749#section-7.1 token_parts = auth.split(None, 1) if len(token_parts) != 2: raise UnsupportedTokenTypeError(self._default_auth_type, self._default_realm) token_type, token_string = token_parts validator = self.get_token_validator(token_type) return validator, token_string def validate_request(self, scopes, request, **kwargs): """Validate the request and return a token.""" validator, token_string = self.parse_request_authorization(request) validator.validate_request(request) token = validator.authenticate_token(token_string) validator.validate_token(token, scopes, request, **kwargs) return token authlib-1.3.2/authlib/oauth2/rfc6749/token_endpoint.py000066400000000000000000000021171466226534200225100ustar00rootroot00000000000000class TokenEndpoint: #: Endpoint name to be registered ENDPOINT_NAME = None #: Supported token types SUPPORTED_TOKEN_TYPES = ('access_token', 'refresh_token') #: Allowed client authenticate methods CLIENT_AUTH_METHODS = ['client_secret_basic'] def __init__(self, server): self.server = server def __call__(self, request): # make it callable for authorization server # ``create_endpoint_response`` return self.create_endpoint_response(request) def create_endpoint_request(self, request): return self.server.create_oauth2_request(request) def authenticate_endpoint_client(self, request): """Authentication client for endpoint with ``CLIENT_AUTH_METHODS``. """ client = self.server.authenticate_client( request, self.CLIENT_AUTH_METHODS, self.ENDPOINT_NAME) request.client = client return client def authenticate_token(self, request, client): raise NotImplementedError() def create_endpoint_response(self, request): raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc6749/util.py000066400000000000000000000022251466226534200204450ustar00rootroot00000000000000import base64 import binascii from urllib.parse import unquote from authlib.common.encoding import to_unicode def list_to_scope(scope): """Convert a list of scopes to a space separated string.""" if isinstance(scope, (set, tuple, list)): return " ".join([to_unicode(s) for s in scope]) if scope is None: return scope return to_unicode(scope) def scope_to_list(scope): """Convert a space separated string to a list of scopes.""" if isinstance(scope, (tuple, list, set)): return [to_unicode(s) for s in scope] elif scope is None: return None return scope.strip().split() def extract_basic_authorization(headers): auth = headers.get('Authorization') if not auth or ' ' not in auth: return None, None auth_type, auth_token = auth.split(None, 1) if auth_type.lower() != 'basic': return None, None try: query = to_unicode(base64.b64decode(auth_token)) except (binascii.Error, TypeError): return None, None if ':' in query: username, password = query.split(':', 1) return unquote(username), unquote(password) return query, None authlib-1.3.2/authlib/oauth2/rfc6749/wrappers.py000066400000000000000000000015121466226534200213310ustar00rootroot00000000000000import time class OAuth2Token(dict): def __init__(self, params): if params.get('expires_at'): params['expires_at'] = int(params['expires_at']) elif params.get('expires_in'): params['expires_at'] = int(time.time()) + \ int(params['expires_in']) super().__init__(params) def is_expired(self, leeway=60): expires_at = self.get('expires_at') if not expires_at: return None # small timedelta to consider token as expired before it actually expires expiration_threshold = expires_at - leeway return expiration_threshold < time.time() @classmethod def from_dict(cls, token): if isinstance(token, dict) and not isinstance(token, cls): token = cls(token) return token authlib-1.3.2/authlib/oauth2/rfc6750/000077500000000000000000000000001466226534200171055ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc6750/__init__.py000066400000000000000000000011731466226534200212200ustar00rootroot00000000000000""" authlib.oauth2.rfc6750 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of The OAuth 2.0 Authorization Framework: Bearer Token Usage. https://tools.ietf.org/html/rfc6750 """ from .errors import InvalidTokenError, InsufficientScopeError from .parameters import add_bearer_token from .token import BearerTokenGenerator from .validator import BearerTokenValidator # TODO: add deprecation BearerToken = BearerTokenGenerator __all__ = [ 'InvalidTokenError', 'InsufficientScopeError', 'add_bearer_token', 'BearerToken', 'BearerTokenGenerator', 'BearerTokenValidator', ] authlib-1.3.2/authlib/oauth2/rfc6750/errors.py000066400000000000000000000054131466226534200207760ustar00rootroot00000000000000""" authlib.rfc6750.errors ~~~~~~~~~~~~~~~~~~~~~~ OAuth Extensions Error Registration. When a request fails, the resource server responds using the appropriate HTTP status code and includes one of the following error codes in the response. https://tools.ietf.org/html/rfc6750#section-6.2 :copyright: (c) 2017 by Hsiaoming Yang. """ from ..base import OAuth2Error __all__ = [ 'InvalidTokenError', 'InsufficientScopeError' ] class InvalidTokenError(OAuth2Error): """The access token provided is expired, revoked, malformed, or invalid for other reasons. The resource SHOULD respond with the HTTP 401 (Unauthorized) status code. The client MAY request a new access token and retry the protected resource request. https://tools.ietf.org/html/rfc6750#section-3.1 """ error = 'invalid_token' description = ( 'The access token provided is expired, revoked, malformed, ' 'or invalid for other reasons.' ) status_code = 401 def __init__(self, description=None, uri=None, status_code=None, state=None, realm=None, **extra_attributes): super().__init__( description, uri, status_code, state) self.realm = realm self.extra_attributes = extra_attributes def get_headers(self): """If the protected resource request does not include authentication credentials or does not contain an access token that enables access to the protected resource, the resource server MUST include the HTTP "WWW-Authenticate" response header field; it MAY include it in response to other conditions as well. https://tools.ietf.org/html/rfc6750#section-3 """ headers = super().get_headers() extras = [] if self.realm: extras.append(f'realm="{self.realm}"') if self.extra_attributes: extras.extend([f'{k}="{self.extra_attributes[k]}"' for k in self.extra_attributes]) extras.append(f'error="{self.error}"') error_description = self.get_error_description() extras.append(f'error_description="{error_description}"') headers.append( ('WWW-Authenticate', 'Bearer ' + ', '.join(extras)) ) return headers class InsufficientScopeError(OAuth2Error): """The request requires higher privileges than provided by the access token. The resource server SHOULD respond with the HTTP 403 (Forbidden) status code and MAY include the "scope" attribute with the scope necessary to access the protected resource. https://tools.ietf.org/html/rfc6750#section-3.1 """ error = 'insufficient_scope' description = 'The request requires higher privileges than provided by the access token.' status_code = 403 authlib-1.3.2/authlib/oauth2/rfc6750/parameters.py000066400000000000000000000022641466226534200216260ustar00rootroot00000000000000from authlib.common.urls import add_params_to_qs, add_params_to_uri def add_to_uri(token, uri): """Add a Bearer Token to the request URI. Not recommended, use only if client can't use authorization header or body. http://www.example.com/path?access_token=h480djs93hd8 """ return add_params_to_uri(uri, [('access_token', token)]) def add_to_headers(token, headers=None): """Add a Bearer Token to the request URI. Recommended method of passing bearer tokens. Authorization: Bearer h480djs93hd8 """ headers = headers or {} headers['Authorization'] = f'Bearer {token}' return headers def add_to_body(token, body=None): """Add a Bearer Token to the request body. access_token=h480djs93hd8 """ if body is None: body = '' return add_params_to_qs(body, [('access_token', token)]) def add_bearer_token(token, uri, headers, body, placement='header'): if placement in ('uri', 'url', 'query'): uri = add_to_uri(token, uri) elif placement in ('header', 'headers'): headers = add_to_headers(token, headers) elif placement == 'body': body = add_to_body(token, body) return uri, headers, body authlib-1.3.2/authlib/oauth2/rfc6750/token.py000066400000000000000000000064261466226534200206070ustar00rootroot00000000000000class BearerTokenGenerator: """Bearer token generator which can create the payload for token response by OAuth 2 server. A typical token response would be: .. code-block:: http HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"mF_9.B5f-4.1JqM", "token_type":"Bearer", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" } """ #: default expires_in value DEFAULT_EXPIRES_IN = 3600 #: default expires_in value differentiate by grant_type GRANT_TYPES_EXPIRES_IN = { 'authorization_code': 864000, 'implicit': 3600, 'password': 864000, 'client_credentials': 864000 } def __init__(self, access_token_generator, refresh_token_generator=None, expires_generator=None): self.access_token_generator = access_token_generator self.refresh_token_generator = refresh_token_generator self.expires_generator = expires_generator def _get_expires_in(self, client, grant_type): if self.expires_generator is None: expires_in = self.GRANT_TYPES_EXPIRES_IN.get( grant_type, self.DEFAULT_EXPIRES_IN) elif callable(self.expires_generator): expires_in = self.expires_generator(client, grant_type) elif isinstance(self.expires_generator, int): expires_in = self.expires_generator else: expires_in = self.DEFAULT_EXPIRES_IN return expires_in @staticmethod def get_allowed_scope(client, scope): if scope: scope = client.get_allowed_scope(scope) return scope def generate(self, grant_type, client, user=None, scope=None, expires_in=None, include_refresh_token=True): """Generate a bearer token for OAuth 2.0 authorization token endpoint. :param client: the client that making the request. :param grant_type: current requested grant_type. :param user: current authorized user. :param expires_in: if provided, use this value as expires_in. :param scope: current requested scope. :param include_refresh_token: should refresh_token be included. :return: Token dict """ scope = self.get_allowed_scope(client, scope) access_token = self.access_token_generator( client=client, grant_type=grant_type, user=user, scope=scope) if expires_in is None: expires_in = self._get_expires_in(client, grant_type) token = { 'token_type': 'Bearer', 'access_token': access_token, } if expires_in: token['expires_in'] = expires_in if include_refresh_token and self.refresh_token_generator: token['refresh_token'] = self.refresh_token_generator( client=client, grant_type=grant_type, user=user, scope=scope) if scope: token['scope'] = scope return token def __call__(self, grant_type, client, user=None, scope=None, expires_in=None, include_refresh_token=True): return self.generate(grant_type, client, user, scope, expires_in, include_refresh_token) authlib-1.3.2/authlib/oauth2/rfc6750/validator.py000066400000000000000000000025411466226534200214460ustar00rootroot00000000000000""" authlib.oauth2.rfc6750.validator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Validate Bearer Token for in request, scope and token. """ from ..rfc6749 import TokenValidator from .errors import ( InvalidTokenError, InsufficientScopeError ) class BearerTokenValidator(TokenValidator): TOKEN_TYPE = 'bearer' def authenticate_token(self, token_string): """A method to query token from database with the given token string. Developers MUST re-implement this method. For instance:: def authenticate_token(self, token_string): return get_token_from_database(token_string) :param token_string: A string to represent the access_token. :return: token """ raise NotImplementedError() def validate_token(self, token, scopes, request): """Check if token is active and matches the requested scopes.""" if not token: raise InvalidTokenError(realm=self.realm, extra_attributes=self.extra_attributes) if token.is_expired(): raise InvalidTokenError(realm=self.realm, extra_attributes=self.extra_attributes) if token.is_revoked(): raise InvalidTokenError(realm=self.realm, extra_attributes=self.extra_attributes) if self.scope_insufficient(token.get_scope(), scopes): raise InsufficientScopeError() authlib-1.3.2/authlib/oauth2/rfc7009/000077500000000000000000000000001466226534200171035ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc7009/__init__.py000066400000000000000000000005411466226534200212140ustar00rootroot00000000000000""" authlib.oauth2.rfc7009 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of OAuth 2.0 Token Revocation. https://tools.ietf.org/html/rfc7009 """ from .parameters import prepare_revoke_token_request from .revocation import RevocationEndpoint __all__ = ['prepare_revoke_token_request', 'RevocationEndpoint'] authlib-1.3.2/authlib/oauth2/rfc7009/parameters.py000066400000000000000000000015261466226534200216240ustar00rootroot00000000000000from authlib.common.urls import add_params_to_qs def prepare_revoke_token_request(token, token_type_hint=None, body=None, headers=None): """Construct request body and headers for revocation endpoint. :param token: access_token or refresh_token string. :param token_type_hint: Optional, `access_token` or `refresh_token`. :param body: current request body. :param headers: current request headers. :return: tuple of (body, headers) https://tools.ietf.org/html/rfc7009#section-2.1 """ params = [('token', token)] if token_type_hint: params.append(('token_type_hint', token_type_hint)) body = add_params_to_qs(body or '', params) if headers is None: headers = {} headers['Content-Type'] = 'application/x-www-form-urlencoded' return body, headers authlib-1.3.2/authlib/oauth2/rfc7009/revocation.py000066400000000000000000000100261466226534200216250ustar00rootroot00000000000000from authlib.consts import default_json_headers from ..rfc6749 import TokenEndpoint, InvalidGrantError from ..rfc6749 import ( InvalidRequestError, UnsupportedTokenTypeError, ) class RevocationEndpoint(TokenEndpoint): """Implementation of revocation endpoint which is described in `RFC7009`_. .. _RFC7009: https://tools.ietf.org/html/rfc7009 """ #: Endpoint name to be registered ENDPOINT_NAME = 'revocation' def authenticate_token(self, request, client): """The client constructs the request by including the following parameters using the "application/x-www-form-urlencoded" format in the HTTP request entity-body: token REQUIRED. The token that the client wants to get revoked. token_type_hint OPTIONAL. A hint about the type of the token submitted for revocation. """ self.check_params(request, client) token = self.query_token(request.form['token'], request.form.get('token_type_hint')) if token and not token.check_client(client): raise InvalidGrantError() return token def check_params(self, request, client): if 'token' not in request.form: raise InvalidRequestError() hint = request.form.get('token_type_hint') if hint and hint not in self.SUPPORTED_TOKEN_TYPES: raise UnsupportedTokenTypeError() def create_endpoint_response(self, request): """Validate revocation request and create the response for revocation. For example, a client may request the revocation of a refresh token with the following request:: POST /revoke HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token :returns: (status_code, body, headers) """ # The authorization server first validates the client credentials client = self.authenticate_endpoint_client(request) # then verifies whether the token was issued to the client making # the revocation request token = self.authenticate_token(request, client) # the authorization server invalidates the token if token: self.revoke_token(token, request) self.server.send_signal( 'after_revoke_token', token=token, client=client, ) return 200, {}, default_json_headers def query_token(self, token_string, token_type_hint): """Get the token from database/storage by the given token string. Developers should implement this method:: def query_token(self, token_string, token_type_hint): if token_type_hint == 'access_token': return Token.query_by_access_token(token_string) if token_type_hint == 'refresh_token': return Token.query_by_refresh_token(token_string) return Token.query_by_access_token(token_string) or \ Token.query_by_refresh_token(token_string) """ raise NotImplementedError() def revoke_token(self, token, request): """Mark token as revoked. Since token MUST be unique, it would be dangerous to delete it. Consider this situation: 1. Jane obtained a token XYZ 2. Jane revoked (deleted) token XYZ 3. Bob generated a new token XYZ 4. Jane can use XYZ to access Bob's resource It would be secure to mark a token as revoked:: def revoke_token(self, token, request): hint = request.form.get('token_type_hint') if hint == 'access_token': token.access_token_revoked = True else: token.access_token_revoked = True token.refresh_token_revoked = True token.save() """ raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc7521/000077500000000000000000000000001466226534200171025ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc7521/__init__.py000066400000000000000000000001031466226534200212050ustar00rootroot00000000000000from .client import AssertionClient __all__ = ['AssertionClient'] authlib-1.3.2/authlib/oauth2/rfc7521/client.py000066400000000000000000000052431466226534200207360ustar00rootroot00000000000000from authlib.common.encoding import to_native from authlib.oauth2.base import OAuth2Error class AssertionClient: """Constructs a new Assertion Framework for OAuth 2.0 Authorization Grants per RFC7521_. .. _RFC7521: https://tools.ietf.org/html/rfc7521 """ DEFAULT_GRANT_TYPE = None ASSERTION_METHODS = {} token_auth_class = None oauth_error_class = OAuth2Error def __init__(self, session, token_endpoint, issuer, subject, audience=None, grant_type=None, claims=None, token_placement='header', scope=None, leeway=60, **kwargs): self.session = session if audience is None: audience = token_endpoint self.token_endpoint = token_endpoint if grant_type is None: grant_type = self.DEFAULT_GRANT_TYPE self.grant_type = grant_type # https://tools.ietf.org/html/rfc7521#section-5.1 self.issuer = issuer self.subject = subject self.audience = audience self.claims = claims self.scope = scope if self.token_auth_class is not None: self.token_auth = self.token_auth_class(None, token_placement, self) self._kwargs = kwargs self.leeway = leeway @property def token(self): return self.token_auth.token @token.setter def token(self, token): self.token_auth.set_token(token) def refresh_token(self): """Using Assertions as Authorization Grants to refresh token as described in `Section 4.1`_. .. _`Section 4.1`: https://tools.ietf.org/html/rfc7521#section-4.1 """ generate_assertion = self.ASSERTION_METHODS[self.grant_type] assertion = generate_assertion( issuer=self.issuer, subject=self.subject, audience=self.audience, claims=self.claims, **self._kwargs ) data = { 'assertion': to_native(assertion), 'grant_type': self.grant_type, } if self.scope: data['scope'] = self.scope return self._refresh_token(data) def parse_response_token(self, resp): if resp.status_code >= 500: resp.raise_for_status() token = resp.json() if 'error' in token: raise self.oauth_error_class( error=token['error'], description=token.get('error_description') ) self.token = token return self.token def _refresh_token(self, data): resp = self.session.request( 'POST', self.token_endpoint, data=data, withhold_token=True) return self.parse_response_token(resp) authlib-1.3.2/authlib/oauth2/rfc7523/000077500000000000000000000000001466226534200171045ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc7523/__init__.py000066400000000000000000000015241466226534200212170ustar00rootroot00000000000000""" authlib.oauth2.rfc7523 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants. https://tools.ietf.org/html/rfc7523 """ from .jwt_bearer import JWTBearerGrant from .client import ( JWTBearerClientAssertion, ) from .assertion import ( client_secret_jwt_sign, private_key_jwt_sign, ) from .auth import ( ClientSecretJWT, PrivateKeyJWT, ) from .token import JWTBearerTokenGenerator from .validator import JWTBearerToken, JWTBearerTokenValidator __all__ = [ 'JWTBearerGrant', 'JWTBearerClientAssertion', 'client_secret_jwt_sign', 'private_key_jwt_sign', 'ClientSecretJWT', 'PrivateKeyJWT', 'JWTBearerToken', 'JWTBearerTokenGenerator', 'JWTBearerTokenValidator', ] authlib-1.3.2/authlib/oauth2/rfc7523/assertion.py000066400000000000000000000037501466226534200214720ustar00rootroot00000000000000import time from authlib.jose import jwt from authlib.common.security import generate_token def sign_jwt_bearer_assertion( key, issuer, audience, subject=None, issued_at=None, expires_at=None, claims=None, header=None, **kwargs): if header is None: header = {} alg = kwargs.pop('alg', None) if alg: header['alg'] = alg if 'alg' not in header: raise ValueError('Missing "alg" in header') payload = {'iss': issuer, 'aud': audience} # subject is not required in Google service if subject: payload['sub'] = subject if not issued_at: issued_at = int(time.time()) expires_in = kwargs.pop('expires_in', 3600) if not expires_at: expires_at = issued_at + expires_in payload['iat'] = issued_at payload['exp'] = expires_at if claims: payload.update(claims) return jwt.encode(header, payload, key) def client_secret_jwt_sign(client_secret, client_id, token_endpoint, alg='HS256', claims=None, **kwargs): return _sign(client_secret, client_id, token_endpoint, alg, claims, **kwargs) def private_key_jwt_sign(private_key, client_id, token_endpoint, alg='RS256', claims=None, **kwargs): return _sign(private_key, client_id, token_endpoint, alg, claims, **kwargs) def _sign(key, client_id, token_endpoint, alg, claims=None, **kwargs): # REQUIRED. Issuer. This MUST contain the client_id of the OAuth Client. issuer = client_id # REQUIRED. Subject. This MUST contain the client_id of the OAuth Client. subject = client_id # The Audience SHOULD be the URL of the Authorization Server's Token Endpoint. audience = token_endpoint # jti is required if claims is None: claims = {} if 'jti' not in claims: claims['jti'] = generate_token(36) return sign_jwt_bearer_assertion( key=key, issuer=issuer, audience=audience, subject=subject, claims=claims, alg=alg, **kwargs) authlib-1.3.2/authlib/oauth2/rfc7523/auth.py000066400000000000000000000064221466226534200204230ustar00rootroot00000000000000from authlib.common.urls import add_params_to_qs from .assertion import client_secret_jwt_sign, private_key_jwt_sign from .client import ASSERTION_TYPE class ClientSecretJWT: """Authentication method for OAuth 2.0 Client. This authentication method is called ``client_secret_jwt``, which is using ``client_id`` and ``client_secret`` constructed with JWT to identify a client. Here is an example of use ``client_secret_jwt`` with Requests Session:: from authlib.integrations.requests_client import OAuth2Session token_endpoint = 'https://example.com/oauth/token' session = OAuth2Session( 'your-client-id', 'your-client-secret', token_endpoint_auth_method='client_secret_jwt' ) session.register_client_auth_method(ClientSecretJWT(token_endpoint)) session.fetch_token(token_endpoint) :param token_endpoint: A string URL of the token endpoint :param claims: Extra JWT claims :param headers: Extra JWT headers :param alg: ``alg`` value, default is HS256 """ name = 'client_secret_jwt' alg = 'HS256' def __init__(self, token_endpoint=None, claims=None, headers=None, alg=None): self.token_endpoint = token_endpoint self.claims = claims self.headers = headers if alg is not None: self.alg = alg def sign(self, auth, token_endpoint): return client_secret_jwt_sign( auth.client_secret, client_id=auth.client_id, token_endpoint=token_endpoint, claims=self.claims, header=self.headers, alg=self.alg, ) def __call__(self, auth, method, uri, headers, body): token_endpoint = self.token_endpoint if not token_endpoint: token_endpoint = uri client_assertion = self.sign(auth, token_endpoint) body = add_params_to_qs(body or '', [ ('client_assertion_type', ASSERTION_TYPE), ('client_assertion', client_assertion) ]) return uri, headers, body class PrivateKeyJWT(ClientSecretJWT): """Authentication method for OAuth 2.0 Client. This authentication method is called ``private_key_jwt``, which is using ``client_id`` and ``private_key`` constructed with JWT to identify a client. Here is an example of use ``private_key_jwt`` with Requests Session:: from authlib.integrations.requests_client import OAuth2Session token_endpoint = 'https://example.com/oauth/token' session = OAuth2Session( 'your-client-id', 'your-client-private-key', token_endpoint_auth_method='private_key_jwt' ) session.register_client_auth_method(PrivateKeyJWT(token_endpoint)) session.fetch_token(token_endpoint) :param token_endpoint: A string URL of the token endpoint :param claims: Extra JWT claims :param headers: Extra JWT headers :param alg: ``alg`` value, default is RS256 """ name = 'private_key_jwt' alg = 'RS256' def sign(self, auth, token_endpoint): return private_key_jwt_sign( auth.client_secret, client_id=auth.client_id, token_endpoint=token_endpoint, claims=self.claims, header=self.headers, alg=self.alg, ) authlib-1.3.2/authlib/oauth2/rfc7523/client.py000066400000000000000000000104441466226534200207370ustar00rootroot00000000000000import logging from authlib.jose import jwt from authlib.jose.errors import JoseError from ..rfc6749 import InvalidClientError ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' log = logging.getLogger(__name__) class JWTBearerClientAssertion: """Implementation of Using JWTs for Client Authentication, which is defined by RFC7523. """ #: Value of ``client_assertion_type`` of JWTs CLIENT_ASSERTION_TYPE = ASSERTION_TYPE #: Name of the client authentication method CLIENT_AUTH_METHOD = 'client_assertion_jwt' def __init__(self, token_url, validate_jti=True): self.token_url = token_url self._validate_jti = validate_jti def __call__(self, query_client, request): data = request.form assertion_type = data.get('client_assertion_type') assertion = data.get('client_assertion') if assertion_type == ASSERTION_TYPE and assertion: resolve_key = self.create_resolve_key_func(query_client, request) self.process_assertion_claims(assertion, resolve_key) return self.authenticate_client(request.client) log.debug('Authenticate via %r failed', self.CLIENT_AUTH_METHOD) def create_claims_options(self): """Create a claims_options for verify JWT payload claims. Developers MAY overwrite this method to create a more strict options.""" # https://tools.ietf.org/html/rfc7523#section-3 # The Audience SHOULD be the URL of the Authorization Server's Token Endpoint options = { 'iss': {'essential': True, 'validate': _validate_iss}, 'sub': {'essential': True}, 'aud': {'essential': True, 'value': self.token_url}, 'exp': {'essential': True}, } if self._validate_jti: options['jti'] = {'essential': True, 'validate': self.validate_jti} return options def process_assertion_claims(self, assertion, resolve_key): """Extract JWT payload claims from request "assertion", per `Section 3.1`_. :param assertion: assertion string value in the request :param resolve_key: function to resolve the sign key :return: JWTClaims :raise: InvalidClientError .. _`Section 3.1`: https://tools.ietf.org/html/rfc7523#section-3.1 """ try: claims = jwt.decode( assertion, resolve_key, claims_options=self.create_claims_options() ) claims.validate() except JoseError as e: log.debug('Assertion Error: %r', e) raise InvalidClientError() return claims def authenticate_client(self, client): if client.check_endpoint_auth_method(self.CLIENT_AUTH_METHOD, 'token'): return client raise InvalidClientError() def create_resolve_key_func(self, query_client, request): def resolve_key(headers, payload): # https://tools.ietf.org/html/rfc7523#section-3 # For client authentication, the subject MUST be the # "client_id" of the OAuth client client_id = payload['sub'] client = query_client(client_id) if not client: raise InvalidClientError() request.client = client return self.resolve_client_public_key(client, headers) return resolve_key def validate_jti(self, claims, jti): """Validate if the given ``jti`` value is used before. Developers MUST implement this method:: def validate_jti(self, claims, jti): key = 'jti:{}-{}'.format(claims['sub'], jti) if redis.get(key): return False redis.set(key, 1, ex=3600) return True """ raise NotImplementedError() def resolve_client_public_key(self, client, headers): """Resolve the client public key for verifying the JWT signature. A client may have many public keys, in this case, we can retrieve it via ``kid`` value in headers. Developers MUST implement this method:: def resolve_client_public_key(self, client, headers): return client.public_key """ raise NotImplementedError() def _validate_iss(claims, iss): return claims['sub'] == iss authlib-1.3.2/authlib/oauth2/rfc7523/jwt_bearer.py000066400000000000000000000146041466226534200216070ustar00rootroot00000000000000import logging from authlib.jose import jwt, JoseError from ..rfc6749 import BaseGrant, TokenEndpointMixin from ..rfc6749 import ( UnauthorizedClientError, InvalidRequestError, InvalidGrantError, InvalidClientError, ) from .assertion import sign_jwt_bearer_assertion log = logging.getLogger(__name__) JWT_BEARER_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer' class JWTBearerGrant(BaseGrant, TokenEndpointMixin): GRANT_TYPE = JWT_BEARER_GRANT_TYPE #: Options for verifying JWT payload claims. Developers MAY #: overwrite this constant to create a more strict options. CLAIMS_OPTIONS = { 'iss': {'essential': True}, 'aud': {'essential': True}, 'exp': {'essential': True}, } @staticmethod def sign(key, issuer, audience, subject=None, issued_at=None, expires_at=None, claims=None, **kwargs): return sign_jwt_bearer_assertion( key, issuer, audience, subject, issued_at, expires_at, claims, **kwargs) def process_assertion_claims(self, assertion): """Extract JWT payload claims from request "assertion", per `Section 3.1`_. :param assertion: assertion string value in the request :return: JWTClaims :raise: InvalidGrantError .. _`Section 3.1`: https://tools.ietf.org/html/rfc7523#section-3.1 """ try: claims = jwt.decode( assertion, self.resolve_public_key, claims_options=self.CLAIMS_OPTIONS) claims.validate() except JoseError as e: log.debug('Assertion Error: %r', e) raise InvalidGrantError(description=e.description) return claims def resolve_public_key(self, headers, payload): client = self.resolve_issuer_client(payload['iss']) return self.resolve_client_key(client, headers, payload) def validate_token_request(self): """The client makes a request to the token endpoint by sending the following parameters using the "application/x-www-form-urlencoded" format per `Section 2.1`_: grant_type REQUIRED. Value MUST be set to "urn:ietf:params:oauth:grant-type:jwt-bearer". assertion REQUIRED. Value MUST contain a single JWT. scope OPTIONAL. The following example demonstrates an access token request with a JWT as an authorization grant: .. code-block:: http POST /token.oauth2 HTTP/1.1 Host: as.example.com Content-Type: application/x-www-form-urlencoded grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer &assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0. eyJpc3Mi[...omitted for brevity...]. J9l-ZhwP[...omitted for brevity...] .. _`Section 2.1`: https://tools.ietf.org/html/rfc7523#section-2.1 """ assertion = self.request.form.get('assertion') if not assertion: raise InvalidRequestError('Missing "assertion" in request') claims = self.process_assertion_claims(assertion) client = self.resolve_issuer_client(claims['iss']) log.debug('Validate token request of %s', client) if not client.check_grant_type(self.GRANT_TYPE): raise UnauthorizedClientError() self.request.client = client self.validate_requested_scope() subject = claims.get('sub') if subject: user = self.authenticate_user(subject) if not user: raise InvalidGrantError(description='Invalid "sub" value in assertion') log.debug('Check client(%s) permission to User(%s)', client, user) if not self.has_granted_permission(client, user): raise InvalidClientError( description='Client has no permission to access user data') self.request.user = user def create_token_response(self): """If valid and authorized, the authorization server issues an access token. """ token = self.generate_token( scope=self.request.scope, user=self.request.user, include_refresh_token=False, ) log.debug('Issue token %r to %r', token, self.request.client) self.save_token(token) return 200, token, self.TOKEN_RESPONSE_HEADER def resolve_issuer_client(self, issuer): """Fetch client via "iss" in assertion claims. Developers MUST implement this method in subclass, e.g.:: def resolve_issuer_client(self, issuer): return Client.query_by_iss(issuer) :param issuer: "iss" value in assertion :return: Client instance """ raise NotImplementedError() def resolve_client_key(self, client, headers, payload): """Resolve client key to decode assertion data. Developers MUST implement this method in subclass. For instance, there is a "jwks" column on client table, e.g.:: def resolve_client_key(self, client, headers, payload): # from authlib.jose import JsonWebKey key_set = JsonWebKey.import_key_set(client.jwks) return key_set.find_by_kid(headers['kid']) :param client: instance of OAuth client model :param headers: headers part of the JWT :param payload: payload part of the JWT :return: ``authlib.jose.Key`` instance """ raise NotImplementedError() def authenticate_user(self, subject): """Authenticate user with the given assertion claims. Developers MUST implement it in subclass, e.g.:: def authenticate_user(self, subject): return User.get_by_sub(subject) :param subject: "sub" value in claims :return: User instance """ raise NotImplementedError() def has_granted_permission(self, client, user): """Check if the client has permission to access the given user's resource. Developers MUST implement it in subclass, e.g.:: def has_granted_permission(self, client, user): permission = ClientUserGrant.query(client=client, user=user) return permission.granted :param client: instance of OAuth client model :param user: instance of User model :return: bool """ raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc7523/token.py000066400000000000000000000063751466226534200206110ustar00rootroot00000000000000import time from authlib.common.encoding import to_native from authlib.jose import jwt class JWTBearerTokenGenerator: """A JSON Web Token formatted bearer token generator for jwt-bearer grant type. This token generator can be registered into authorization server:: authorization_server.register_token_generator( 'urn:ietf:params:oauth:grant-type:jwt-bearer', JWTBearerTokenGenerator(private_rsa_key), ) In this way, we can generate the token into JWT format. And we don't have to save this token into database, since it will be short time valid. Consider to rewrite ``JWTBearerGrant.save_token``:: class MyJWTBearerGrant(JWTBearerGrant): def save_token(self, token): pass :param secret_key: private RSA key in bytes, JWK or JWK Set. :param issuer: a string or URI of the issuer :param alg: ``alg`` to use in JWT """ DEFAULT_EXPIRES_IN = 3600 def __init__(self, secret_key, issuer=None, alg='RS256'): self.secret_key = secret_key self.issuer = issuer self.alg = alg @staticmethod def get_allowed_scope(client, scope): if scope: scope = client.get_allowed_scope(scope) return scope @staticmethod def get_sub_value(user): """Return user's ID as ``sub`` value in token payload. For instance:: @staticmethod def get_sub_value(user): return str(user.id) """ return user.get_user_id() def get_token_data(self, grant_type, client, expires_in, user=None, scope=None): scope = self.get_allowed_scope(client, scope) issued_at = int(time.time()) data = { 'scope': scope, 'grant_type': grant_type, 'iat': issued_at, 'exp': issued_at + expires_in, 'client_id': client.get_client_id(), } if self.issuer: data['iss'] = self.issuer if user: data['sub'] = self.get_sub_value(user) return data def generate(self, grant_type, client, user=None, scope=None, expires_in=None): """Generate a bearer token for OAuth 2.0 authorization token endpoint. :param client: the client that making the request. :param grant_type: current requested grant_type. :param user: current authorized user. :param expires_in: if provided, use this value as expires_in. :param scope: current requested scope. :return: Token dict """ if expires_in is None: expires_in = self.DEFAULT_EXPIRES_IN token_data = self.get_token_data(grant_type, client, expires_in, user, scope) access_token = jwt.encode({'alg': self.alg}, token_data, key=self.secret_key, check=False) token = { 'token_type': 'Bearer', 'access_token': to_native(access_token), 'expires_in': expires_in } if scope: token['scope'] = scope return token def __call__(self, grant_type, client, user=None, scope=None, expires_in=None, include_refresh_token=True): # there is absolutely no refresh token in JWT format return self.generate(grant_type, client, user, scope, expires_in) authlib-1.3.2/authlib/oauth2/rfc7523/validator.py000066400000000000000000000031021466226534200214370ustar00rootroot00000000000000import time import logging from authlib.jose import jwt, JoseError, JWTClaims from ..rfc6749 import TokenMixin from ..rfc6750 import BearerTokenValidator logger = logging.getLogger(__name__) class JWTBearerToken(TokenMixin, JWTClaims): def check_client(self, client): return self['client_id'] == client.get_client_id() def get_scope(self): return self.get('scope') def get_expires_in(self): return self['exp'] - self['iat'] def is_expired(self): return self['exp'] < time.time() def is_revoked(self): return False class JWTBearerTokenValidator(BearerTokenValidator): TOKEN_TYPE = 'bearer' token_cls = JWTBearerToken def __init__(self, public_key, issuer=None, realm=None, **extra_attributes): super().__init__(realm, **extra_attributes) self.public_key = public_key claims_options = { 'exp': {'essential': True}, 'client_id': {'essential': True}, 'grant_type': {'essential': True}, } if issuer: claims_options['iss'] = {'essential': True, 'value': issuer} self.claims_options = claims_options def authenticate_token(self, token_string): try: claims = jwt.decode( token_string, self.public_key, claims_options=self.claims_options, claims_cls=self.token_cls, ) claims.validate() return claims except JoseError as error: logger.debug('Authenticate token failed. %r', error) return None authlib-1.3.2/authlib/oauth2/rfc7591/000077500000000000000000000000001466226534200171115ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc7591/__init__.py000066400000000000000000000012331466226534200212210ustar00rootroot00000000000000""" authlib.oauth2.rfc7591 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of OAuth 2.0 Dynamic Client Registration Protocol. https://tools.ietf.org/html/rfc7591 """ from .claims import ClientMetadataClaims from .endpoint import ClientRegistrationEndpoint from .errors import ( InvalidRedirectURIError, InvalidClientMetadataError, InvalidSoftwareStatementError, UnapprovedSoftwareStatementError, ) __all__ = [ 'ClientMetadataClaims', 'ClientRegistrationEndpoint', 'InvalidRedirectURIError', 'InvalidClientMetadataError', 'InvalidSoftwareStatementError', 'UnapprovedSoftwareStatementError', ] authlib-1.3.2/authlib/oauth2/rfc7591/claims.py000066400000000000000000000231211466226534200207320ustar00rootroot00000000000000from authlib.jose import BaseClaims, JsonWebKey from authlib.jose.errors import InvalidClaimError from authlib.common.urls import is_valid_url class ClientMetadataClaims(BaseClaims): # https://tools.ietf.org/html/rfc7591#section-2 REGISTERED_CLAIMS = [ 'redirect_uris', 'token_endpoint_auth_method', 'grant_types', 'response_types', 'client_name', 'client_uri', 'logo_uri', 'scope', 'contacts', 'tos_uri', 'policy_uri', 'jwks_uri', 'jwks', 'software_id', 'software_version', ] def validate(self): self._validate_essential_claims() self.validate_redirect_uris() self.validate_token_endpoint_auth_method() self.validate_grant_types() self.validate_response_types() self.validate_client_name() self.validate_client_uri() self.validate_logo_uri() self.validate_scope() self.validate_contacts() self.validate_tos_uri() self.validate_policy_uri() self.validate_jwks_uri() self.validate_jwks() self.validate_software_id() self.validate_software_version() def validate_redirect_uris(self): """Array of redirection URI strings for use in redirect-based flows such as the authorization code and implicit flows. As required by Section 2 of OAuth 2.0 [RFC6749], clients using flows with redirection MUST register their redirection URI values. Authorization servers that support dynamic registration for redirect-based flows MUST implement support for this metadata value. """ uris = self.get('redirect_uris') if uris: for uri in uris: self._validate_uri('redirect_uris', uri) def validate_token_endpoint_auth_method(self): """String indicator of the requested authentication method for the token endpoint. """ # If unspecified or omitted, the default is "client_secret_basic" if 'token_endpoint_auth_method' not in self: self['token_endpoint_auth_method'] = 'client_secret_basic' self._validate_claim_value('token_endpoint_auth_method') def validate_grant_types(self): """Array of OAuth 2.0 grant type strings that the client can use at the token endpoint. """ self._validate_claim_value('grant_types') def validate_response_types(self): """Array of the OAuth 2.0 response type strings that the client can use at the authorization endpoint. """ self._validate_claim_value('response_types') def validate_client_name(self): """Human-readable string name of the client to be presented to the end-user during authorization. If omitted, the authorization server MAY display the raw "client_id" value to the end-user instead. It is RECOMMENDED that clients always send this field. The value of this field MAY be internationalized, as described in Section 2.2. """ def validate_client_uri(self): """URL string of a web page providing information about the client. If present, the server SHOULD display this URL to the end-user in a clickable fashion. It is RECOMMENDED that clients always send this field. The value of this field MUST point to a valid web page. The value of this field MAY be internationalized, as described in Section 2.2. """ self._validate_uri('client_uri') def validate_logo_uri(self): """URL string that references a logo for the client. If present, the server SHOULD display this image to the end-user during approval. The value of this field MUST point to a valid image file. The value of this field MAY be internationalized, as described in Section 2.2. """ self._validate_uri('logo_uri') def validate_scope(self): """String containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. The semantics of values in this list are service specific. If omitted, an authorization server MAY register a client with a default set of scopes. """ self._validate_claim_value('scope') def validate_contacts(self): """Array of strings representing ways to contact people responsible for this client, typically email addresses. The authorization server MAY make these contact addresses available to end-users for support requests for the client. See Section 6 for information on Privacy Considerations. """ if 'contacts' in self and not isinstance(self['contacts'], list): raise InvalidClaimError('contacts') def validate_tos_uri(self): """URL string that points to a human-readable terms of service document for the client that describes a contractual relationship between the end-user and the client that the end-user accepts when authorizing the client. The authorization server SHOULD display this URL to the end-user if it is provided. The value of this field MUST point to a valid web page. The value of this field MAY be internationalized, as described in Section 2.2. """ self._validate_uri('tos_uri') def validate_policy_uri(self): """URL string that points to a human-readable privacy policy document that describes how the deployment organization collects, uses, retains, and discloses personal data. The authorization server SHOULD display this URL to the end-user if it is provided. The value of this field MUST point to a valid web page. The value of this field MAY be internationalized, as described in Section 2.2. """ self._validate_uri('policy_uri') def validate_jwks_uri(self): """URL string referencing the client's JSON Web Key (JWK) Set [RFC7517] document, which contains the client's public keys. The value of this field MUST point to a valid JWK Set document. These keys can be used by higher-level protocols that use signing or encryption. For instance, these keys might be used by some applications for validating signed requests made to the token endpoint when using JWTs for client authentication [RFC7523]. Use of this parameter is preferred over the "jwks" parameter, as it allows for easier key rotation. The "jwks_uri" and "jwks" parameters MUST NOT both be present in the same request or response. """ # TODO: use real HTTP library self._validate_uri('jwks_uri') def validate_jwks(self): """Client's JSON Web Key Set [RFC7517] document value, which contains the client's public keys. The value of this field MUST be a JSON object containing a valid JWK Set. These keys can be used by higher-level protocols that use signing or encryption. This parameter is intended to be used by clients that cannot use the "jwks_uri" parameter, such as native clients that cannot host public URLs. The "jwks_uri" and "jwks" parameters MUST NOT both be present in the same request or response. """ if 'jwks' in self: if 'jwks_uri' in self: # The "jwks_uri" and "jwks" parameters MUST NOT both be present raise InvalidClaimError('jwks') jwks = self['jwks'] try: key_set = JsonWebKey.import_key_set(jwks) if not key_set: raise InvalidClaimError('jwks') except ValueError: raise InvalidClaimError('jwks') def validate_software_id(self): """A unique identifier string (e.g., a Universally Unique Identifier (UUID)) assigned by the client developer or software publisher used by registration endpoints to identify the client software to be dynamically registered. Unlike "client_id", which is issued by the authorization server and SHOULD vary between instances, the "software_id" SHOULD remain the same for all instances of the client software. The "software_id" SHOULD remain the same across multiple updates or versions of the same piece of software. The value of this field is not intended to be human readable and is usually opaque to the client and authorization server. """ def validate_software_version(self): """A version identifier string for the client software identified by "software_id". The value of the "software_version" SHOULD change on any update to the client software identified by the same "software_id". The value of this field is intended to be compared using string equality matching and no other comparison semantics are defined by this specification. The value of this field is outside the scope of this specification, but it is not intended to be human readable and is usually opaque to the client and authorization server. The definition of what constitutes an update to client software that would trigger a change to this value is specific to the software itself and is outside the scope of this specification. """ def _validate_uri(self, key, uri=None): if uri is None: uri = self.get(key) if uri and not is_valid_url(uri): raise InvalidClaimError(key) authlib-1.3.2/authlib/oauth2/rfc7591/endpoint.py000066400000000000000000000176501466226534200213140ustar00rootroot00000000000000import os import time import binascii from authlib.consts import default_json_headers from authlib.common.security import generate_token from authlib.jose import JsonWebToken, JoseError from ..rfc6749 import AccessDeniedError, InvalidRequestError from ..rfc6749 import scope_to_list from .claims import ClientMetadataClaims from .errors import ( InvalidClientMetadataError, UnapprovedSoftwareStatementError, InvalidSoftwareStatementError, ) class ClientRegistrationEndpoint: """The client registration endpoint is an OAuth 2.0 endpoint designed to allow a client to be registered with the authorization server. """ ENDPOINT_NAME = 'client_registration' #: The claims validation class claims_class = ClientMetadataClaims #: Rewrite this value with a list to support ``software_statement`` #: e.g. ``software_statement_alg_values_supported = ['RS256']`` software_statement_alg_values_supported = None def __init__(self, server): self.server = server def __call__(self, request): return self.create_registration_response(request) def create_registration_response(self, request): token = self.authenticate_token(request) if not token: raise AccessDeniedError() request.credential = token client_metadata = self.extract_client_metadata(request) client_info = self.generate_client_info() body = {} body.update(client_metadata) body.update(client_info) client = self.save_client(client_info, client_metadata, request) registration_info = self.generate_client_registration_info(client, request) if registration_info: body.update(registration_info) return 201, body, default_json_headers def extract_client_metadata(self, request): if not request.data: raise InvalidRequestError() json_data = request.data.copy() software_statement = json_data.pop('software_statement', None) if software_statement and self.software_statement_alg_values_supported: data = self.extract_software_statement(software_statement, request) json_data.update(data) options = self.get_claims_options() claims = self.claims_class(json_data, {}, options, self.get_server_metadata()) try: claims.validate() except JoseError as error: raise InvalidClientMetadataError(error.description) return claims.get_registered_claims() def extract_software_statement(self, software_statement, request): key = self.resolve_public_key(request) if not key: raise UnapprovedSoftwareStatementError() try: jwt = JsonWebToken(self.software_statement_alg_values_supported) claims = jwt.decode(software_statement, key) # there is no need to validate claims return claims except JoseError: raise InvalidSoftwareStatementError() def get_claims_options(self): """Generate claims options validation from Authorization Server metadata.""" metadata = self.get_server_metadata() if not metadata: return {} scopes_supported = metadata.get('scopes_supported') response_types_supported = metadata.get('response_types_supported') grant_types_supported = metadata.get('grant_types_supported') auth_methods_supported = metadata.get('token_endpoint_auth_methods_supported') options = {} if scopes_supported is not None: scopes_supported = set(scopes_supported) def _validate_scope(claims, value): if not value: return True scopes = set(scope_to_list(value)) return scopes_supported.issuperset(scopes) options['scope'] = {'validate': _validate_scope} if response_types_supported is not None: response_types_supported = set(response_types_supported) def _validate_response_types(claims, value): # If omitted, the default is that the client will use only the "code" # response type. response_types = set(value) if value else {"code"} return response_types_supported.issuperset(response_types) options['response_types'] = {'validate': _validate_response_types} if grant_types_supported is not None: grant_types_supported = set(grant_types_supported) def _validate_grant_types(claims, value): # If omitted, the default behavior is that the client will use only # the "authorization_code" Grant Type. grant_types = set(value) if value else {"authorization_code"} return grant_types_supported.issuperset(grant_types) options['grant_types'] = {'validate': _validate_grant_types} if auth_methods_supported is not None: options['token_endpoint_auth_method'] = {'values': auth_methods_supported} return options def generate_client_info(self): # https://tools.ietf.org/html/rfc7591#section-3.2.1 client_id = self.generate_client_id() client_secret = self.generate_client_secret() client_id_issued_at = int(time.time()) client_secret_expires_at = 0 return dict( client_id=client_id, client_secret=client_secret, client_id_issued_at=client_id_issued_at, client_secret_expires_at=client_secret_expires_at, ) def generate_client_registration_info(self, client, request): """Generate ```registration_client_uri`` and ``registration_access_token`` for RFC7592. This method returns ``None`` by default. Developers MAY rewrite this method to return registration information.""" return None def create_endpoint_request(self, request): return self.server.create_json_request(request) def generate_client_id(self): """Generate ``client_id`` value. Developers MAY rewrite this method to use their own way to generate ``client_id``. """ return generate_token(42) def generate_client_secret(self): """Generate ``client_secret`` value. Developers MAY rewrite this method to use their own way to generate ``client_secret``. """ return binascii.hexlify(os.urandom(24)).decode('ascii') def get_server_metadata(self): """Return server metadata which includes supported grant types, response types and etc. """ raise NotImplementedError() def authenticate_token(self, request): """Authenticate current credential who is requesting to register a client. Developers MUST implement this method in subclass:: def authenticate_token(self, request): auth = request.headers.get('Authorization') return get_token_by_auth(auth) :return: token instance """ raise NotImplementedError() def resolve_public_key(self, request): """Resolve a public key for decoding ``software_statement``. If ``enable_software_statement=True``, developers MUST implement this method in subclass:: def resolve_public_key(self, request): return get_public_key_from_user(request.credential) :return: JWK or Key string """ raise NotImplementedError() def save_client(self, client_info, client_metadata, request): """Save client into database. Developers MUST implement this method in subclass:: def save_client(self, client_info, client_metadata, request): client = OAuthClient( client_id=client_info['client_id'], client_secret=client_info['client_secret'], ... ) client.save() return client """ raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc7591/errors.py000066400000000000000000000021121466226534200207730ustar00rootroot00000000000000from ..rfc6749 import OAuth2Error class InvalidRedirectURIError(OAuth2Error): """The value of one or more redirection URIs is invalid. https://tools.ietf.org/html/rfc7591#section-3.2.2 """ error = 'invalid_redirect_uri' class InvalidClientMetadataError(OAuth2Error): """The value of one of the client metadata fields is invalid and the server has rejected this request. Note that an authorization server MAY choose to substitute a valid value for any requested parameter of a client's metadata. https://tools.ietf.org/html/rfc7591#section-3.2.2 """ error = 'invalid_client_metadata' class InvalidSoftwareStatementError(OAuth2Error): """The software statement presented is invalid. https://tools.ietf.org/html/rfc7591#section-3.2.2 """ error = 'invalid_software_statement' class UnapprovedSoftwareStatementError(OAuth2Error): """The software statement presented is not approved for use by this authorization server. https://tools.ietf.org/html/rfc7591#section-3.2.2 """ error = 'unapproved_software_statement' authlib-1.3.2/authlib/oauth2/rfc7592/000077500000000000000000000000001466226534200171125ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc7592/__init__.py000066400000000000000000000004731466226534200212270ustar00rootroot00000000000000""" authlib.oauth2.rfc7592 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of OAuth 2.0 Dynamic Client Registration Management Protocol. https://tools.ietf.org/html/rfc7592 """ from .endpoint import ClientConfigurationEndpoint __all__ = ['ClientConfigurationEndpoint'] authlib-1.3.2/authlib/oauth2/rfc7592/endpoint.py000066400000000000000000000236671466226534200213220ustar00rootroot00000000000000from authlib.consts import default_json_headers from authlib.jose import JoseError from ..rfc7591.claims import ClientMetadataClaims from ..rfc6749 import scope_to_list from ..rfc6749 import AccessDeniedError from ..rfc6749 import InvalidClientError from ..rfc6749 import InvalidRequestError from ..rfc6749 import UnauthorizedClientError from ..rfc7591 import InvalidClientMetadataError class ClientConfigurationEndpoint: ENDPOINT_NAME = 'client_configuration' #: The claims validation class claims_class = ClientMetadataClaims def __init__(self, server): self.server = server def __call__(self, request): return self.create_configuration_response(request) def create_configuration_response(self, request): # This request is authenticated by the registration access token issued # to the client. token = self.authenticate_token(request) if not token: raise AccessDeniedError() request.credential = token client = self.authenticate_client(request) if not client: # If the client does not exist on this server, the server MUST respond # with HTTP 401 Unauthorized and the registration access token used to # make this request SHOULD be immediately revoked. self.revoke_access_token(request, token) raise InvalidClientError(status_code=401) if not self.check_permission(client, request): # If the client does not have permission to read its record, the server # MUST return an HTTP 403 Forbidden. raise UnauthorizedClientError(status_code=403) request.client = client if request.method == 'GET': return self.create_read_client_response(client, request) elif request.method == 'DELETE': return self.create_delete_client_response(client, request) elif request.method == 'PUT': return self.create_update_client_response(client, request) def create_endpoint_request(self, request): return self.server.create_json_request(request) def create_read_client_response(self, client, request): body = self.introspect_client(client) body.update(self.generate_client_registration_info(client, request)) return 200, body, default_json_headers def create_delete_client_response(self, client, request): self.delete_client(client, request) headers = [ ('Cache-Control', 'no-store'), ('Pragma', 'no-cache'), ] return 204, '', headers def create_update_client_response(self, client, request): # The updated client metadata fields request MUST NOT include the # 'registration_access_token', 'registration_client_uri', # 'client_secret_expires_at', or 'client_id_issued_at' fields must_not_include = ( 'registration_access_token', 'registration_client_uri', 'client_secret_expires_at', 'client_id_issued_at', ) for k in must_not_include: if k in request.data: raise InvalidRequestError() # The client MUST include its 'client_id' field in the request client_id = request.data.get('client_id') if not client_id: raise InvalidRequestError() if client_id != client.get_client_id(): raise InvalidRequestError() # If the client includes the 'client_secret' field in the request, # the value of this field MUST match the currently issued client # secret for that client. if 'client_secret' in request.data: if not client.check_client_secret(request.data['client_secret']): raise InvalidRequestError() client_metadata = self.extract_client_metadata(request) client = self.update_client(client, client_metadata, request) return self.create_read_client_response(client, request) def extract_client_metadata(self, request): json_data = request.data.copy() options = self.get_claims_options() claims = self.claims_class(json_data, {}, options, self.get_server_metadata()) try: claims.validate() except JoseError as error: raise InvalidClientMetadataError(error.description) return claims.get_registered_claims() def get_claims_options(self): metadata = self.get_server_metadata() if not metadata: return {} scopes_supported = metadata.get('scopes_supported') response_types_supported = metadata.get('response_types_supported') grant_types_supported = metadata.get('grant_types_supported') auth_methods_supported = metadata.get('token_endpoint_auth_methods_supported') options = {} if scopes_supported is not None: scopes_supported = set(scopes_supported) def _validate_scope(claims, value): if not value: return True scopes = set(scope_to_list(value)) return scopes_supported.issuperset(scopes) options['scope'] = {'validate': _validate_scope} if response_types_supported is not None: response_types_supported = set(response_types_supported) def _validate_response_types(claims, value): # If omitted, the default is that the client will use only the "code" # response type. response_types = set(value) if value else {"code"} return response_types_supported.issuperset(response_types) options['response_types'] = {'validate': _validate_response_types} if grant_types_supported is not None: grant_types_supported = set(grant_types_supported) def _validate_grant_types(claims, value): # If omitted, the default behavior is that the client will use only # the "authorization_code" Grant Type. grant_types = set(value) if value else {"authorization_code"} return grant_types_supported.issuperset(grant_types) options['grant_types'] = {'validate': _validate_grant_types} if auth_methods_supported is not None: options['token_endpoint_auth_method'] = {'values': auth_methods_supported} return options def introspect_client(self, client): return {**client.client_info, **client.client_metadata} def generate_client_registration_info(self, client, request): """Generate ```registration_client_uri`` and ``registration_access_token`` for RFC7592. By default this method returns the values sent in the current request. Developers MUST rewrite this method to return different registration information.:: def generate_client_registration_info(self, client, request):{ access_token = request.headers['Authorization'].split(' ')[1] return { 'registration_client_uri': request.uri, 'registration_access_token': access_token, } :param client: the instance of OAuth client :param request: formatted request instance """ raise NotImplementedError() def authenticate_token(self, request): """Authenticate current credential who is requesting to register a client. Developers MUST implement this method in subclass:: def authenticate_token(self, request): auth = request.headers.get('Authorization') return get_token_by_auth(auth) :return: token instance """ raise NotImplementedError() def authenticate_client(self, request): """Read a client from the request payload. Developers MUST implement this method in subclass:: def authenticate_client(self, request): client_id = request.data.get('client_id') return Client.get(client_id=client_id) :return: client instance """ raise NotImplementedError() def revoke_access_token(self, token, request): """Revoke a token access in case an invalid client has been requested. Developers MUST implement this method in subclass:: def revoke_access_token(self, token, request): token.revoked = True token.save() """ raise NotImplementedError() def check_permission(self, client, request): """Checks wether the current client is allowed to be accessed, edited or deleted. Developers MUST implement it in subclass, e.g.:: def check_permission(self, client, request): return client.editable :return: boolean """ raise NotImplementedError() def delete_client(self, client, request): """Delete authorization code from database or cache. Developers MUST implement it in subclass, e.g.:: def delete_client(self, client, request): client.delete() :param client: the instance of OAuth client :param request: formatted request instance """ raise NotImplementedError() def update_client(self, client, client_metadata, request): """Update the client in the database. Developers MUST implement this method in subclass:: def update_client(self, client, client_metadata, request): client.set_client_metadata({**client.client_metadata, **client_metadata}) client.save() return client :param client: the instance of OAuth client :param client_metadata: a dict of the client claims to update :param request: formatted request instance :return: client instance """ raise NotImplementedError() def get_server_metadata(self): """Return server metadata which includes supported grant types, response types and etc. """ raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc7636/000077500000000000000000000000001466226534200171115ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc7636/__init__.py000066400000000000000000000005241466226534200212230ustar00rootroot00000000000000""" authlib.oauth2.rfc7636 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of Proof Key for Code Exchange by OAuth Public Clients. https://tools.ietf.org/html/rfc7636 """ from .challenge import CodeChallenge, create_s256_code_challenge __all__ = ['CodeChallenge', 'create_s256_code_challenge'] authlib-1.3.2/authlib/oauth2/rfc7636/challenge.py000066400000000000000000000130411466226534200214040ustar00rootroot00000000000000import re import hashlib from authlib.common.encoding import to_bytes, to_unicode, urlsafe_b64encode from ..rfc6749 import ( InvalidRequestError, InvalidGrantError, OAuth2Request, ) CODE_VERIFIER_PATTERN = re.compile(r'^[a-zA-Z0-9\-._~]{43,128}$') CODE_CHALLENGE_PATTERN = re.compile(r'^[a-zA-Z0-9\-._~]{43,128}$') def create_s256_code_challenge(code_verifier): """Create S256 code_challenge with the given code_verifier.""" data = hashlib.sha256(to_bytes(code_verifier, 'ascii')).digest() return to_unicode(urlsafe_b64encode(data)) def compare_plain_code_challenge(code_verifier, code_challenge): # If the "code_challenge_method" from Section 4.3 was "plain", # they are compared directly return code_verifier == code_challenge def compare_s256_code_challenge(code_verifier, code_challenge): # BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge return create_s256_code_challenge(code_verifier) == code_challenge class CodeChallenge: """CodeChallenge extension to Authorization Code Grant. It is used to improve the security of Authorization Code flow for public clients by sending extra "code_challenge" and "code_verifier" to the authorization server. The AuthorizationCodeGrant SHOULD save the ``code_challenge`` and ``code_challenge_method`` into database when ``save_authorization_code``. Then register this extension via:: server.register_grant( AuthorizationCodeGrant, [CodeChallenge(required=True)] ) """ #: defaults to "plain" if not present in the request DEFAULT_CODE_CHALLENGE_METHOD = 'plain' #: supported ``code_challenge_method`` SUPPORTED_CODE_CHALLENGE_METHOD = ['plain', 'S256'] CODE_CHALLENGE_METHODS = { 'plain': compare_plain_code_challenge, 'S256': compare_s256_code_challenge, } def __init__(self, required=True): self.required = required def __call__(self, grant): grant.register_hook( 'after_validate_authorization_request', self.validate_code_challenge, ) grant.register_hook( 'after_validate_token_request', self.validate_code_verifier, ) def validate_code_challenge(self, grant): request: OAuth2Request = grant.request challenge = request.data.get('code_challenge') method = request.data.get('code_challenge_method') if not challenge and not method: return if not challenge: raise InvalidRequestError('Missing "code_challenge"') if len(request.datalist.get('code_challenge', [])) > 1: raise InvalidRequestError('Multiple "code_challenge" in request.') if not CODE_CHALLENGE_PATTERN.match(challenge): raise InvalidRequestError('Invalid "code_challenge"') if method and method not in self.SUPPORTED_CODE_CHALLENGE_METHOD: raise InvalidRequestError('Unsupported "code_challenge_method"') if len(request.datalist.get('code_challenge_method', [])) > 1: raise InvalidRequestError('Multiple "code_challenge_method" in request.') def validate_code_verifier(self, grant): request: OAuth2Request = grant.request verifier = request.form.get('code_verifier') # public client MUST verify code challenge if self.required and request.auth_method == 'none' and not verifier: raise InvalidRequestError('Missing "code_verifier"') authorization_code = request.authorization_code challenge = self.get_authorization_code_challenge(authorization_code) # ignore, it is the normal RFC6749 authorization_code request if not challenge and not verifier: return # challenge exists, code_verifier is required if not verifier: raise InvalidRequestError('Missing "code_verifier"') if not CODE_VERIFIER_PATTERN.match(verifier): raise InvalidRequestError('Invalid "code_verifier"') # 4.6. Server Verifies code_verifier before Returning the Tokens method = self.get_authorization_code_challenge_method(authorization_code) if method is None: method = self.DEFAULT_CODE_CHALLENGE_METHOD func = self.CODE_CHALLENGE_METHODS.get(method) if not func: raise RuntimeError(f'No verify method for "{method}"') # If the values are not equal, an error response indicating # "invalid_grant" MUST be returned. if not func(verifier, challenge): raise InvalidGrantError(description='Code challenge failed.') def get_authorization_code_challenge(self, authorization_code): """Get "code_challenge" associated with this authorization code. Developers MAY re-implement it in subclass, the default logic:: def get_authorization_code_challenge(self, authorization_code): return authorization_code.code_challenge :param authorization_code: the instance of authorization_code """ return authorization_code.code_challenge def get_authorization_code_challenge_method(self, authorization_code): """Get "code_challenge_method" associated with this authorization code. Developers MAY re-implement it in subclass, the default logic:: def get_authorization_code_challenge_method(self, authorization_code): return authorization_code.code_challenge_method :param authorization_code: the instance of authorization_code """ return authorization_code.code_challenge_method authlib-1.3.2/authlib/oauth2/rfc7662/000077500000000000000000000000001466226534200171105ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc7662/__init__.py000066400000000000000000000006471466226534200212300ustar00rootroot00000000000000""" authlib.oauth2.rfc7662 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of OAuth 2.0 Token Introspection. https://tools.ietf.org/html/rfc7662 """ from .introspection import IntrospectionEndpoint from .models import IntrospectionToken from .token_validator import IntrospectTokenValidator __all__ = ['IntrospectionEndpoint', 'IntrospectionToken', 'IntrospectTokenValidator'] authlib-1.3.2/authlib/oauth2/rfc7662/introspection.py000066400000000000000000000121741466226534200223670ustar00rootroot00000000000000from authlib.consts import default_json_headers from ..rfc6749 import ( TokenEndpoint, InvalidRequestError, UnsupportedTokenTypeError, ) class IntrospectionEndpoint(TokenEndpoint): """Implementation of introspection endpoint which is described in `RFC7662`_. .. _RFC7662: https://tools.ietf.org/html/rfc7662 """ #: Endpoint name to be registered ENDPOINT_NAME = 'introspection' def authenticate_token(self, request, client): """The protected resource calls the introspection endpoint using an HTTP ``POST`` request with parameters sent as "application/x-www-form-urlencoded" data. The protected resource sends a parameter representing the token along with optional parameters representing additional context that is known by the protected resource to aid the authorization server in its response. token **REQUIRED** The string value of the token. For access tokens, this is the ``access_token`` value returned from the token endpoint defined in OAuth 2.0. For refresh tokens, this is the ``refresh_token`` value returned from the token endpoint as defined in OAuth 2.0. token_type_hint **OPTIONAL** A hint about the type of the token submitted for introspection. """ self.check_params(request, client) token = self.query_token(request.form['token'], request.form.get('token_type_hint')) if token and self.check_permission(token, client, request): return token def check_params(self, request, client): params = request.form if 'token' not in params: raise InvalidRequestError() hint = params.get('token_type_hint') if hint and hint not in self.SUPPORTED_TOKEN_TYPES: raise UnsupportedTokenTypeError() def create_endpoint_response(self, request): """Validate introspection request and create the response. :returns: (status_code, body, headers) """ # The authorization server first validates the client credentials client = self.authenticate_endpoint_client(request) # then verifies whether the token was issued to the client making # the revocation request token = self.authenticate_token(request, client) # the authorization server invalidates the token body = self.create_introspection_payload(token) return 200, body, default_json_headers def create_introspection_payload(self, token): # the token is not active, does not exist on this server, or the # protected resource is not allowed to introspect this particular # token, then the authorization server MUST return an introspection # response with the "active" field set to "false" if not token: return {'active': False} if token.is_expired() or token.is_revoked(): return {'active': False} payload = self.introspect_token(token) if 'active' not in payload: payload['active'] = True return payload def check_permission(self, token, client, request): """Check if the request has permission to introspect the token. Developers MUST implement this method:: def check_permission(self, token, client, request): # only allow a special client to introspect the token return client.client_id == 'introspection_client' :return: bool """ raise NotImplementedError() def query_token(self, token_string, token_type_hint): """Get the token from database/storage by the given token string. Developers should implement this method:: def query_token(self, token_string, token_type_hint): if token_type_hint == 'access_token': tok = Token.query_by_access_token(token_string) elif token_type_hint == 'refresh_token': tok = Token.query_by_refresh_token(token_string) else: tok = Token.query_by_access_token(token_string) if not tok: tok = Token.query_by_refresh_token(token_string) return tok """ raise NotImplementedError() def introspect_token(self, token): """Read given token and return its introspection metadata as a dictionary following `Section 2.2`_:: def introspect_token(self, token): return { 'active': True, 'client_id': token.client_id, 'token_type': token.token_type, 'username': get_token_username(token), 'scope': token.get_scope(), 'sub': get_token_user_sub(token), 'aud': token.client_id, 'iss': 'https://server.example.com/', 'exp': token.expires_at, 'iat': token.issued_at, } .. _`Section 2.2`: https://tools.ietf.org/html/rfc7662#section-2.2 """ raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc7662/models.py000066400000000000000000000015441466226534200207510ustar00rootroot00000000000000from ..rfc6749 import TokenMixin class IntrospectionToken(dict, TokenMixin): def get_client_id(self): return self.get('client_id') def get_scope(self): return self.get('scope') def get_expires_in(self): # this method is only used in refresh token, # no need to implement it return 0 def get_expires_at(self): return self.get('exp', 0) def __getattr__(self, key): # https://tools.ietf.org/html/rfc7662#section-2.2 available_keys = { 'active', 'scope', 'client_id', 'username', 'token_type', 'exp', 'iat', 'nbf', 'sub', 'aud', 'iss', 'jti' } try: return object.__getattribute__(self, key) except AttributeError as error: if key in available_keys: return self.get(key) raise error authlib-1.3.2/authlib/oauth2/rfc7662/token_validator.py000066400000000000000000000024731466226534200226550ustar00rootroot00000000000000from ..rfc6749 import TokenValidator from ..rfc6750 import ( InvalidTokenError, InsufficientScopeError ) class IntrospectTokenValidator(TokenValidator): TOKEN_TYPE = 'bearer' def introspect_token(self, token_string): """Request introspection token endpoint with the given token string, authorization server will return token information in JSON format. Developers MUST implement this method before using it:: def introspect_token(self, token_string): # for example, introspection token endpoint has limited # internal IPs to access, so there is no need to add # authentication. url = 'https://example.com/oauth/introspect' resp = requests.post(url, data={'token': token_string}) resp.raise_for_status() return resp.json() """ raise NotImplementedError() def authenticate_token(self, token_string): return self.introspect_token(token_string) def validate_token(self, token, scopes, request): if not token or not token['active']: raise InvalidTokenError(realm=self.realm, extra_attributes=self.extra_attributes) if self.scope_insufficient(token.get('scope'), scopes): raise InsufficientScopeError() authlib-1.3.2/authlib/oauth2/rfc8414/000077500000000000000000000000001466226534200171045ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc8414/__init__.py000066400000000000000000000005511466226534200212160ustar00rootroot00000000000000""" authlib.oauth2.rfc8414 ~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of OAuth 2.0 Authorization Server Metadata. https://tools.ietf.org/html/rfc8414 """ from .models import AuthorizationServerMetadata from .well_known import get_well_known_url __all__ = ['AuthorizationServerMetadata', 'get_well_known_url'] authlib-1.3.2/authlib/oauth2/rfc8414/models.py000066400000000000000000000420521466226534200207440ustar00rootroot00000000000000from authlib.common.urls import urlparse, is_valid_url from authlib.common.security import is_secure_transport class AuthorizationServerMetadata(dict): """Define Authorization Server Metadata via `Section 2`_ in RFC8414_. .. _RFC8414: https://tools.ietf.org/html/rfc8414 .. _`Section 2`: https://tools.ietf.org/html/rfc8414#section-2 """ REGISTRY_KEYS = [ 'issuer', 'authorization_endpoint', 'token_endpoint', 'jwks_uri', 'registration_endpoint', 'scopes_supported', 'response_types_supported', 'response_modes_supported', 'grant_types_supported', 'token_endpoint_auth_methods_supported', 'token_endpoint_auth_signing_alg_values_supported', 'service_documentation', 'ui_locales_supported', 'op_policy_uri', 'op_tos_uri', 'revocation_endpoint', 'revocation_endpoint_auth_methods_supported', 'revocation_endpoint_auth_signing_alg_values_supported', 'introspection_endpoint', 'introspection_endpoint_auth_methods_supported', 'introspection_endpoint_auth_signing_alg_values_supported', 'code_challenge_methods_supported', ] def validate_issuer(self): """REQUIRED. The authorization server's issuer identifier, which is a URL that uses the "https" scheme and has no query or fragment components. """ issuer = self.get('issuer') #: 1. REQUIRED if not issuer: raise ValueError('"issuer" is required') parsed = urlparse.urlparse(issuer) #: 2. uses the "https" scheme if not is_secure_transport(issuer): raise ValueError('"issuer" MUST use "https" scheme') #: 3. has no query or fragment if parsed.query or parsed.fragment: raise ValueError('"issuer" has no query or fragment') def validate_authorization_endpoint(self): """URL of the authorization server's authorization endpoint [RFC6749]. This is REQUIRED unless no grant types are supported that use the authorization endpoint. """ url = self.get('authorization_endpoint') if url: if not is_secure_transport(url): raise ValueError( '"authorization_endpoint" MUST use "https" scheme') return grant_types_supported = set(self.grant_types_supported) authorization_grant_types = {'authorization_code', 'implicit'} if grant_types_supported & authorization_grant_types: raise ValueError('"authorization_endpoint" is required') def validate_token_endpoint(self): """URL of the authorization server's token endpoint [RFC6749]. This is REQUIRED unless only the implicit grant type is supported. """ grant_types_supported = self.get('grant_types_supported') if grant_types_supported and len(grant_types_supported) == 1 and \ grant_types_supported[0] == 'implicit': return url = self.get('token_endpoint') if not url: raise ValueError('"token_endpoint" is required') if not is_secure_transport(url): raise ValueError('"token_endpoint" MUST use "https" scheme') def validate_jwks_uri(self): """OPTIONAL. URL of the authorization server's JWK Set [JWK] document. The referenced document contains the signing key(s) the client uses to validate signatures from the authorization server. This URL MUST use the "https" scheme. The JWK Set MAY also contain the server's encryption key or keys, which are used by clients to encrypt requests to the server. When both signing and encryption keys are made available, a "use" (public key use) parameter value is REQUIRED for all keys in the referenced JWK Set to indicate each key's intended usage. """ url = self.get('jwks_uri') if url and not is_secure_transport(url): raise ValueError('"jwks_uri" MUST use "https" scheme') def validate_registration_endpoint(self): """OPTIONAL. URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint [RFC7591]. """ url = self.get('registration_endpoint') if url and not is_secure_transport(url): raise ValueError( '"registration_endpoint" MUST use "https" scheme') def validate_scopes_supported(self): """RECOMMENDED. JSON array containing a list of the OAuth 2.0 [RFC6749] "scope" values that this authorization server supports. Servers MAY choose not to advertise some supported scope values even when this parameter is used. """ validate_array_value(self, 'scopes_supported') def validate_response_types_supported(self): """REQUIRED. JSON array containing a list of the OAuth 2.0 "response_type" values that this authorization server supports. The array values used are the same as those used with the "response_types" parameter defined by "OAuth 2.0 Dynamic Client Registration Protocol" [RFC7591]. """ response_types_supported = self.get('response_types_supported') if not response_types_supported: raise ValueError('"response_types_supported" is required') if not isinstance(response_types_supported, list): raise ValueError('"response_types_supported" MUST be JSON array') def validate_response_modes_supported(self): """OPTIONAL. JSON array containing a list of the OAuth 2.0 "response_mode" values that this authorization server supports, as specified in "OAuth 2.0 Multiple Response Type Encoding Practices" [OAuth.Responses]. If omitted, the default is "["query", "fragment"]". The response mode value "form_post" is also defined in "OAuth 2.0 Form Post Response Mode" [OAuth.Post]. """ validate_array_value(self, 'response_modes_supported') def validate_grant_types_supported(self): """OPTIONAL. JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. The array values used are the same as those used with the "grant_types" parameter defined by "OAuth 2.0 Dynamic Client Registration Protocol" [RFC7591]. If omitted, the default value is "["authorization_code", "implicit"]". """ validate_array_value(self, 'grant_types_supported') def validate_token_endpoint_auth_methods_supported(self): """OPTIONAL. JSON array containing a list of client authentication methods supported by this token endpoint. Client authentication method values are used in the "token_endpoint_auth_method" parameter defined in Section 2 of [RFC7591]. If omitted, the default is "client_secret_basic" -- the HTTP Basic Authentication Scheme specified in Section 2.3.1 of OAuth 2.0 [RFC6749]. """ validate_array_value(self, 'token_endpoint_auth_methods_supported') def validate_token_endpoint_auth_signing_alg_values_supported(self): """OPTIONAL. JSON array containing a list of the JWS signing algorithms ("alg" values) supported by the token endpoint for the signature on the JWT [JWT] used to authenticate the client at the token endpoint for the "private_key_jwt" and "client_secret_jwt" authentication methods. This metadata entry MUST be present if either of these authentication methods are specified in the "token_endpoint_auth_methods_supported" entry. No default algorithms are implied if this entry is omitted. Servers SHOULD support "RS256". The value "none" MUST NOT be used. """ _validate_alg_values( self, 'token_endpoint_auth_signing_alg_values_supported', self.token_endpoint_auth_methods_supported ) def validate_service_documentation(self): """OPTIONAL. URL of a page containing human-readable information that developers might want or need to know when using the authorization server. In particular, if the authorization server does not support Dynamic Client Registration, then information on how to register clients needs to be provided in this documentation. """ value = self.get('service_documentation') if value and not is_valid_url(value): raise ValueError('"service_documentation" MUST be a URL') def validate_ui_locales_supported(self): """OPTIONAL. Languages and scripts supported for the user interface, represented as a JSON array of language tag values from BCP 47 [RFC5646]. If omitted, the set of supported languages and scripts is unspecified. """ validate_array_value(self, 'ui_locales_supported') def validate_op_policy_uri(self): """OPTIONAL. URL that the authorization server provides to the person registering the client to read about the authorization server's requirements on how the client can use the data provided by the authorization server. The registration process SHOULD display this URL to the person registering the client if it is given. As described in Section 5, despite the identifier "op_policy_uri" appearing to be OpenID-specific, its usage in this specification is actually referring to a general OAuth 2.0 feature that is not specific to OpenID Connect. """ value = self.get('op_policy_uri') if value and not is_valid_url(value): raise ValueError('"op_policy_uri" MUST be a URL') def validate_op_tos_uri(self): """OPTIONAL. URL that the authorization server provides to the person registering the client to read about the authorization server's terms of service. The registration process SHOULD display this URL to the person registering the client if it is given. As described in Section 5, despite the identifier "op_tos_uri", appearing to be OpenID-specific, its usage in this specification is actually referring to a general OAuth 2.0 feature that is not specific to OpenID Connect. """ value = self.get('op_tos_uri') if value and not is_valid_url(value): raise ValueError('"op_tos_uri" MUST be a URL') def validate_revocation_endpoint(self): """OPTIONAL. URL of the authorization server's OAuth 2.0 revocation endpoint [RFC7009].""" url = self.get('revocation_endpoint') if url and not is_secure_transport(url): raise ValueError('"revocation_endpoint" MUST use "https" scheme') def validate_revocation_endpoint_auth_methods_supported(self): """OPTIONAL. JSON array containing a list of client authentication methods supported by this revocation endpoint. The valid client authentication method values are those registered in the IANA "OAuth Token Endpoint Authentication Methods" registry [IANA.OAuth.Parameters]. If omitted, the default is "client_secret_basic" -- the HTTP Basic Authentication Scheme specified in Section 2.3.1 of OAuth 2.0 [RFC6749]. """ validate_array_value(self, 'revocation_endpoint_auth_methods_supported') def validate_revocation_endpoint_auth_signing_alg_values_supported(self): """OPTIONAL. JSON array containing a list of the JWS signing algorithms ("alg" values) supported by the revocation endpoint for the signature on the JWT [JWT] used to authenticate the client at the revocation endpoint for the "private_key_jwt" and "client_secret_jwt" authentication methods. This metadata entry MUST be present if either of these authentication methods are specified in the "revocation_endpoint_auth_methods_supported" entry. No default algorithms are implied if this entry is omitted. The value "none" MUST NOT be used. """ _validate_alg_values( self, 'revocation_endpoint_auth_signing_alg_values_supported', self.revocation_endpoint_auth_methods_supported ) def validate_introspection_endpoint(self): """OPTIONAL. URL of the authorization server's OAuth 2.0 introspection endpoint [RFC7662]. """ url = self.get('introspection_endpoint') if url and not is_secure_transport(url): raise ValueError( '"introspection_endpoint" MUST use "https" scheme') def validate_introspection_endpoint_auth_methods_supported(self): """OPTIONAL. JSON array containing a list of client authentication methods supported by this introspection endpoint. The valid client authentication method values are those registered in the IANA "OAuth Token Endpoint Authentication Methods" registry [IANA.OAuth.Parameters] or those registered in the IANA "OAuth Access Token Types" registry [IANA.OAuth.Parameters]. (These values are and will remain distinct, due to Section 7.2.) If omitted, the set of supported authentication methods MUST be determined by other means. """ validate_array_value(self, 'introspection_endpoint_auth_methods_supported') def validate_introspection_endpoint_auth_signing_alg_values_supported(self): """OPTIONAL. JSON array containing a list of the JWS signing algorithms ("alg" values) supported by the introspection endpoint for the signature on the JWT [JWT] used to authenticate the client at the introspection endpoint for the "private_key_jwt" and "client_secret_jwt" authentication methods. This metadata entry MUST be present if either of these authentication methods are specified in the "introspection_endpoint_auth_methods_supported" entry. No default algorithms are implied if this entry is omitted. The value "none" MUST NOT be used. """ _validate_alg_values( self, 'introspection_endpoint_auth_signing_alg_values_supported', self.introspection_endpoint_auth_methods_supported ) def validate_code_challenge_methods_supported(self): """OPTIONAL. JSON array containing a list of Proof Key for Code Exchange (PKCE) [RFC7636] code challenge methods supported by this authorization server. Code challenge method values are used in the "code_challenge_method" parameter defined in Section 4.3 of [RFC7636]. The valid code challenge method values are those registered in the IANA "PKCE Code Challenge Methods" registry [IANA.OAuth.Parameters]. If omitted, the authorization server does not support PKCE. """ validate_array_value(self, 'code_challenge_methods_supported') @property def response_modes_supported(self): #: If omitted, the default is ["query", "fragment"] return self.get('response_modes_supported', ["query", "fragment"]) @property def grant_types_supported(self): #: If omitted, the default value is ["authorization_code", "implicit"] return self.get('grant_types_supported', ["authorization_code", "implicit"]) @property def token_endpoint_auth_methods_supported(self): #: If omitted, the default is "client_secret_basic" return self.get('token_endpoint_auth_methods_supported', ["client_secret_basic"]) @property def revocation_endpoint_auth_methods_supported(self): #: If omitted, the default is "client_secret_basic" return self.get('revocation_endpoint_auth_methods_supported', ["client_secret_basic"]) @property def introspection_endpoint_auth_methods_supported(self): #: If omitted, the set of supported authentication methods MUST be #: determined by other means #: here, we use "client_secret_basic" return self.get('introspection_endpoint_auth_methods_supported', ["client_secret_basic"]) def validate(self): """Validate all server metadata value.""" for key in self.REGISTRY_KEYS: object.__getattribute__(self, f'validate_{key}')() def __getattr__(self, key): try: return object.__getattribute__(self, key) except AttributeError as error: if key in self.REGISTRY_KEYS: return self.get(key) raise error def _validate_alg_values(data, key, auth_methods_supported): value = data.get(key) if value and not isinstance(value, list): raise ValueError(f'"{key}" MUST be JSON array') auth_methods = set(auth_methods_supported) jwt_auth_methods = {'private_key_jwt', 'client_secret_jwt'} if auth_methods & jwt_auth_methods: if not value: raise ValueError(f'"{key}" is required') if value and 'none' in value: raise ValueError( f'the value "none" MUST NOT be used in "{key}"') def validate_array_value(metadata, key): values = metadata.get(key) if values is not None and not isinstance(values, list): raise ValueError(f'"{key}" MUST be JSON array') authlib-1.3.2/authlib/oauth2/rfc8414/well_known.py000066400000000000000000000013271466226534200216400ustar00rootroot00000000000000from authlib.common.urls import urlparse def get_well_known_url(issuer, external=False, suffix='oauth-authorization-server'): """Get well-known URI with issuer via `Section 3.1`_. .. _`Section 3.1`: https://tools.ietf.org/html/rfc8414#section-3.1 :param issuer: URL of the issuer :param external: return full external url or not :param suffix: well-known URI suffix for RFC8414 :return: URL """ parsed = urlparse.urlparse(issuer) path = parsed.path if path and path != '/': url_path = f'/.well-known/{suffix}{path}' else: url_path = f'/.well-known/{suffix}' if not external: return url_path return parsed.scheme + '://' + parsed.netloc + url_path authlib-1.3.2/authlib/oauth2/rfc8628/000077500000000000000000000000001466226534200171135ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc8628/__init__.py000066400000000000000000000012461466226534200212270ustar00rootroot00000000000000""" authlib.oauth2.rfc8628 ~~~~~~~~~~~~~~~~~~~~~~ This module represents an implementation of OAuth 2.0 Device Authorization Grant. https://tools.ietf.org/html/rfc8628 """ from .endpoint import DeviceAuthorizationEndpoint from .device_code import DeviceCodeGrant, DEVICE_CODE_GRANT_TYPE from .models import DeviceCredentialMixin, DeviceCredentialDict from .errors import AuthorizationPendingError, SlowDownError, ExpiredTokenError __all__ = [ 'DeviceAuthorizationEndpoint', 'DeviceCodeGrant', 'DEVICE_CODE_GRANT_TYPE', 'DeviceCredentialMixin', 'DeviceCredentialDict', 'AuthorizationPendingError', 'SlowDownError', 'ExpiredTokenError', ] authlib-1.3.2/authlib/oauth2/rfc8628/device_code.py000066400000000000000000000170431466226534200217230ustar00rootroot00000000000000import logging from ..rfc6749.errors import ( InvalidRequestError, UnauthorizedClientError, AccessDeniedError, ) from ..rfc6749 import BaseGrant, TokenEndpointMixin from .errors import ( AuthorizationPendingError, ExpiredTokenError, SlowDownError, ) log = logging.getLogger(__name__) DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code' class DeviceCodeGrant(BaseGrant, TokenEndpointMixin): """This OAuth 2.0 [RFC6749] protocol extension enables OAuth clients to request user authorization from applications on devices that have limited input capabilities or lack a suitable browser. Such devices include smart TVs, media consoles, picture frames, and printers, which lack an easy input method or a suitable browser required for traditional OAuth interactions. Here is the authorization flow:: +----------+ +----------------+ | |>---(A)-- Client Identifier --->| | | | | | | |<---(B)-- Device Code, ---<| | | | User Code, | | | Device | & Verification URI | | | Client | | | | | [polling] | | | |>---(E)-- Device Code --->| | | | & Client Identifier | | | | | Authorization | | |<---(F)-- Access Token ---<| Server | +----------+ (& Optional Refresh Token) | | v | | : | | (C) User Code & Verification URI | | : | | v | | +----------+ | | | End User | | | | at |<---(D)-- End user reviews --->| | | Browser | authorization request | | +----------+ +----------------+ This DeviceCodeGrant is the implementation of step (E) and (F). (E) While the end user reviews the client's request (step D), the client repeatedly polls the authorization server to find out if the user completed the user authorization step. The client includes the device code and its client identifier. (F) The authorization server validates the device code provided by the client and responds with the access token if the client is granted access, an error if they are denied access, or an indication that the client should continue to poll. """ GRANT_TYPE = DEVICE_CODE_GRANT_TYPE TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none'] def validate_token_request(self): """After displaying instructions to the user, the client creates an access token request and sends it to the token endpoint with the following parameters: grant_type REQUIRED. Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code". device_code REQUIRED. The device verification code, "device_code" from the device authorization response. client_id REQUIRED if the client is not authenticating with the authorization server as described in Section 3.2.1. of [RFC6749]. The client identifier as described in Section 2.2 of [RFC6749]. For example, the client makes the following HTTPS request:: POST /token HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code &device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS &client_id=1406020730 """ device_code = self.request.data.get('device_code') if not device_code: raise InvalidRequestError('Missing "device_code" in payload') client = self.authenticate_token_endpoint_client() if not client.check_grant_type(self.GRANT_TYPE): raise UnauthorizedClientError() credential = self.query_device_credential(device_code) if not credential: raise InvalidRequestError('Invalid "device_code" in payload') if credential.get_client_id() != client.get_client_id(): raise UnauthorizedClientError() user = self.validate_device_credential(credential) self.request.user = user self.request.client = client self.request.credential = credential def create_token_response(self): """If the access token request is valid and authorized, the authorization server issues an access token and optional refresh token. """ client = self.request.client scope = self.request.credential.get_scope() token = self.generate_token( user=self.request.user, scope=scope, include_refresh_token=client.check_grant_type('refresh_token'), ) log.debug('Issue token %r to %r', token, client) self.save_token(token) self.execute_hook('process_token', token=token) return 200, token, self.TOKEN_RESPONSE_HEADER def validate_device_credential(self, credential): if credential.is_expired(): raise ExpiredTokenError() user_code = credential.get_user_code() user_grant = self.query_user_grant(user_code) if user_grant is not None: user, approved = user_grant if not approved: raise AccessDeniedError() return user if self.should_slow_down(credential): raise SlowDownError() raise AuthorizationPendingError() def query_device_credential(self, device_code): """Get device credential from previously savings via ``DeviceAuthorizationEndpoint``. Developers MUST implement it in subclass:: def query_device_credential(self, device_code): return DeviceCredential.get(device_code) :param device_code: a string represent the code. :return: DeviceCredential instance """ raise NotImplementedError() def query_user_grant(self, user_code): """Get user and grant via the given user code. Developers MUST implement it in subclass:: def query_user_grant(self, user_code): # e.g. we saved user grant info in redis data = redis.get('oauth_user_grant:' + user_code) if not data: return None user_id, allowed = data.split() user = User.get(user_id) return user, bool(allowed) Note, user grant information is saved by verification endpoint. """ raise NotImplementedError() def should_slow_down(self, credential): """The authorization request is still pending and polling should continue, but the interval MUST be increased by 5 seconds for this and all subsequent requests. """ raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc8628/endpoint.py000066400000000000000000000157031466226534200213130ustar00rootroot00000000000000from authlib.consts import default_json_headers from authlib.common.security import generate_token from authlib.common.urls import add_params_to_uri class DeviceAuthorizationEndpoint: """This OAuth 2.0 [RFC6749] protocol extension enables OAuth clients to request user authorization from applications on devices that have limited input capabilities or lack a suitable browser. Such devices include smart TVs, media consoles, picture frames, and printers, which lack an easy input method or a suitable browser required for traditional OAuth interactions. Here is the authorization flow:: +----------+ +----------------+ | |>---(A)-- Client Identifier --->| | | | | | | |<---(B)-- Device Code, ---<| | | | User Code, | | | Device | & Verification URI | | | Client | | | | | [polling] | | | |>---(E)-- Device Code --->| | | | & Client Identifier | | | | | Authorization | | |<---(F)-- Access Token ---<| Server | +----------+ (& Optional Refresh Token) | | v | | : | | (C) User Code & Verification URI | | : | | v | | +----------+ | | | End User | | | | at |<---(D)-- End user reviews --->| | | Browser | authorization request | | +----------+ +----------------+ This DeviceAuthorizationEndpoint is the implementation of step (A) and (B). (A) The client requests access from the authorization server and includes its client identifier in the request. (B) The authorization server issues a device code and an end-user code and provides the end-user verification URI. """ ENDPOINT_NAME = 'device_authorization' CLIENT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none'] #: customize "user_code" type, string or digital USER_CODE_TYPE = 'string' #: The lifetime in seconds of the "device_code" and "user_code" EXPIRES_IN = 1800 #: The minimum amount of time in seconds that the client SHOULD #: wait between polling requests to the token endpoint. INTERVAL = 5 def __init__(self, server): self.server = server def __call__(self, request): # make it callable for authorization server # ``create_endpoint_response`` return self.create_endpoint_response(request) def create_endpoint_request(self, request): return self.server.create_oauth2_request(request) def authenticate_client(self, request): """client_id is REQUIRED **if the client is not** authenticating with the authorization server as described in Section 3.2.1. of [RFC6749]. This means the endpoint support "none" authentication method. In this case, this endpoint's auth methods are: - client_secret_basic - client_secret_post - none Developers change the value of ``CLIENT_AUTH_METHODS`` in subclass. For instance:: class MyDeviceAuthorizationEndpoint(DeviceAuthorizationEndpoint): # only support ``client_secret_basic`` auth method CLIENT_AUTH_METHODS = ['client_secret_basic'] """ client = self.server.authenticate_client( request, self.CLIENT_AUTH_METHODS, self.ENDPOINT_NAME) request.client = client return client def create_endpoint_response(self, request): # https://tools.ietf.org/html/rfc8628#section-3.1 self.authenticate_client(request) self.server.validate_requested_scope(request.scope) device_code = self.generate_device_code() user_code = self.generate_user_code() verification_uri = self.get_verification_uri() verification_uri_complete = add_params_to_uri( verification_uri, [('user_code', user_code)]) data = { 'device_code': device_code, 'user_code': user_code, 'verification_uri': verification_uri, 'verification_uri_complete': verification_uri_complete, 'expires_in': self.EXPIRES_IN, 'interval': self.INTERVAL, } self.save_device_credential(request.client_id, request.scope, data) return 200, data, default_json_headers def generate_user_code(self): """A method to generate ``user_code`` value for device authorization endpoint. This method will generate a random string like MQNA-JPOZ. Developers can rewrite this method to create their own ``user_code``. """ # https://tools.ietf.org/html/rfc8628#section-6.1 if self.USER_CODE_TYPE == 'digital': return create_digital_user_code() return create_string_user_code() def generate_device_code(self): """A method to generate ``device_code`` value for device authorization endpoint. This method will generate a random string of 42 characters. Developers can rewrite this method to create their own ``device_code``. """ return generate_token(42) def get_verification_uri(self): """Define the ``verification_uri`` of device authorization endpoint. Developers MUST implement this method in subclass:: def get_verification_uri(self): return 'https://your-company.com/active' """ raise NotImplementedError() def save_device_credential(self, client_id, scope, data): """Save device token into database for later use. Developers MUST implement this method in subclass:: def save_device_credential(self, client_id, scope, data): item = DeviceCredential( client_id=client_id, scope=scope, **data ) item.save() """ raise NotImplementedError() def create_string_user_code(): base = 'BCDFGHJKLMNPQRSTVWXZ' return '-'.join([generate_token(4, base), generate_token(4, base)]) def create_digital_user_code(): base = '0123456789' return '-'.join([ generate_token(3, base), generate_token(3, base), generate_token(3, base), ]) authlib-1.3.2/authlib/oauth2/rfc8628/errors.py000066400000000000000000000016271466226534200210070ustar00rootroot00000000000000from ..rfc6749.errors import OAuth2Error # https://tools.ietf.org/html/rfc8628#section-3.5 class AuthorizationPendingError(OAuth2Error): """The authorization request is still pending as the end user hasn't yet completed the user-interaction steps (Section 3.3). """ error = 'authorization_pending' class SlowDownError(OAuth2Error): """A variant of "authorization_pending", the authorization request is still pending and polling should continue, but the interval MUST be increased by 5 seconds for this and all subsequent requests. """ error = 'slow_down' class ExpiredTokenError(OAuth2Error): """The "device_code" has expired, and the device authorization session has concluded. The client MAY commence a new device authorization request but SHOULD wait for user interaction before restarting to avoid unnecessary polling. """ error = 'expired_token' authlib-1.3.2/authlib/oauth2/rfc8628/models.py000066400000000000000000000014731466226534200207550ustar00rootroot00000000000000import time class DeviceCredentialMixin: def get_client_id(self): raise NotImplementedError() def get_scope(self): raise NotImplementedError() def get_user_code(self): raise NotImplementedError() def is_expired(self): raise NotImplementedError() class DeviceCredentialDict(dict, DeviceCredentialMixin): def get_client_id(self): return self['client_id'] def get_scope(self): return self.get('scope') def get_user_code(self): return self['user_code'] def get_nonce(self): return self.get('nonce') def get_auth_time(self): return self.get('auth_time') def is_expired(self): expires_at = self.get('expires_at') if expires_at: return expires_at < time.time() return False authlib-1.3.2/authlib/oauth2/rfc8693/000077500000000000000000000000001466226534200171155ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc8693/__init__.py000066400000000000000000000002661466226534200212320ustar00rootroot00000000000000""" authlib.oauth2.rfc8693 ~~~~~~~~~~~~~~~~~~~~~~ This module represents an implementation of OAuth 2.0 Token Exchange. https://tools.ietf.org/html/rfc8693 """ authlib-1.3.2/authlib/oauth2/rfc9068/000077500000000000000000000000001466226534200171125ustar00rootroot00000000000000authlib-1.3.2/authlib/oauth2/rfc9068/__init__.py000066400000000000000000000005141466226534200212230ustar00rootroot00000000000000from .introspection import JWTIntrospectionEndpoint from .revocation import JWTRevocationEndpoint from .token import JWTBearerTokenGenerator from .token_validator import JWTBearerTokenValidator __all__ = [ 'JWTBearerTokenGenerator', 'JWTBearerTokenValidator', 'JWTIntrospectionEndpoint', 'JWTRevocationEndpoint', ] authlib-1.3.2/authlib/oauth2/rfc9068/claims.py000066400000000000000000000035121466226534200207350ustar00rootroot00000000000000from authlib.jose.errors import InvalidClaimError from authlib.jose.rfc7519 import JWTClaims class JWTAccessTokenClaims(JWTClaims): REGISTERED_CLAIMS = JWTClaims.REGISTERED_CLAIMS + [ 'client_id', 'auth_time', 'acr', 'amr', 'scope', 'groups', 'roles', 'entitlements', ] def validate(self, **kwargs): self.validate_typ() super().validate(**kwargs) self.validate_client_id() self.validate_auth_time() self.validate_acr() self.validate_amr() self.validate_scope() self.validate_groups() self.validate_roles() self.validate_entitlements() def validate_typ(self): # The resource server MUST verify that the 'typ' header value is 'at+jwt' # or 'application/at+jwt' and reject tokens carrying any other value. if self.header['typ'].lower() not in ('at+jwt', 'application/at+jwt'): raise InvalidClaimError('typ') def validate_client_id(self): return self._validate_claim_value('client_id') def validate_auth_time(self): auth_time = self.get('auth_time') if auth_time and not isinstance(auth_time, (int, float)): raise InvalidClaimError('auth_time') def validate_acr(self): return self._validate_claim_value('acr') def validate_amr(self): amr = self.get('amr') if amr and not isinstance(self['amr'], list): raise InvalidClaimError('amr') def validate_scope(self): return self._validate_claim_value('scope') def validate_groups(self): return self._validate_claim_value('groups') def validate_roles(self): return self._validate_claim_value('roles') def validate_entitlements(self): return self._validate_claim_value('entitlements') authlib-1.3.2/authlib/oauth2/rfc9068/introspection.py000066400000000000000000000102331466226534200223630ustar00rootroot00000000000000from ..rfc7662 import IntrospectionEndpoint from authlib.common.errors import ContinueIteration from authlib.consts import default_json_headers from authlib.jose.errors import ExpiredTokenError from authlib.jose.errors import InvalidClaimError from authlib.oauth2.rfc6750.errors import InvalidTokenError from authlib.oauth2.rfc9068.token_validator import JWTBearerTokenValidator class JWTIntrospectionEndpoint(IntrospectionEndpoint): ''' JWTIntrospectionEndpoint inherits from :ref:`specs/rfc7662` :class:`~authlib.oauth2.rfc7662.IntrospectionEndpoint` and implements the machinery to automatically process the JWT access tokens. :param issuer: The issuer identifier for which tokens will be introspected. :param \\*\\*kwargs: Other parameters are inherited from :class:`~authlib.oauth2.rfc7662.introspection.IntrospectionEndpoint`. :: class MyJWTAccessTokenIntrospectionEndpoint(JWTRevocationEndpoint): def get_jwks(self): ... def get_username(self, user_id): ... authorization_server.register_endpoint( MyJWTAccessTokenIntrospectionEndpoint( issuer="https://authorization-server.example.org", ) ) authorization_server.register_endpoint(MyRefreshTokenIntrospectionEndpoint) ''' #: Endpoint name to be registered ENDPOINT_NAME = 'introspection' def __init__(self, issuer, server=None, *args, **kwargs): super().__init__(*args, server=server, **kwargs) self.issuer = issuer def create_endpoint_response(self, request): '''''' # The authorization server first validates the client credentials client = self.authenticate_endpoint_client(request) # then verifies whether the token was issued to the client making # the revocation request token = self.authenticate_token(request, client) # the authorization server invalidates the token body = self.create_introspection_payload(token) return 200, body, default_json_headers def authenticate_token(self, request, client): '''''' self.check_params(request, client) # do not attempt to decode refresh_tokens if request.form.get('token_type_hint') not in ('access_token', None): raise ContinueIteration() validator = JWTBearerTokenValidator(issuer=self.issuer, resource_server=None) validator.get_jwks = self.get_jwks try: token = validator.authenticate_token(request.form['token']) # if the token is not a JWT, fall back to the regular flow except InvalidTokenError: raise ContinueIteration() if token and self.check_permission(token, client, request): return token def create_introspection_payload(self, token): if not token: return {'active': False} try: token.validate() except ExpiredTokenError: return {'active': False} except InvalidClaimError as exc: if exc.claim_name == 'iss': raise ContinueIteration() raise InvalidTokenError() payload = { 'active': True, 'token_type': 'Bearer', 'client_id': token['client_id'], 'scope': token['scope'], 'sub': token['sub'], 'aud': token['aud'], 'iss': token['iss'], 'exp': token['exp'], 'iat': token['iat'], } if username := self.get_username(token['sub']): payload['username'] = username return payload def get_jwks(self): '''Return the JWKs that will be used to check the JWT access token signature. Developers MUST re-implement this method:: def get_jwks(self): return load_jwks("jwks.json") ''' raise NotImplementedError() def get_username(self, user_id: str) -> str: '''Returns an username from a user ID. Developers MAY re-implement this method:: def get_username(self, user_id): return User.get(id=user_id).username ''' return None authlib-1.3.2/authlib/oauth2/rfc9068/revocation.py000066400000000000000000000047311466226534200216420ustar00rootroot00000000000000from ..rfc6749 import UnsupportedTokenTypeError from ..rfc7009 import RevocationEndpoint from authlib.common.errors import ContinueIteration from authlib.oauth2.rfc6750.errors import InvalidTokenError from authlib.oauth2.rfc9068.token_validator import JWTBearerTokenValidator class JWTRevocationEndpoint(RevocationEndpoint): '''JWTRevocationEndpoint inherits from `RFC7009`_ :class:`~authlib.oauth2.rfc7009.RevocationEndpoint`. The JWT access tokens cannot be revoked. If the submitted token is a JWT access token, then revocation returns a `invalid_token_error`. :param issuer: The issuer identifier. :param \\*\\*kwargs: Other parameters are inherited from :class:`~authlib.oauth2.rfc7009.RevocationEndpoint`. Plain text access tokens and other kind of tokens such as refresh_tokens will be ignored by this endpoint and passed to the next revocation endpoint:: class MyJWTAccessTokenRevocationEndpoint(JWTRevocationEndpoint): def get_jwks(self): ... authorization_server.register_endpoint( MyJWTAccessTokenRevocationEndpoint( issuer="https://authorization-server.example.org", ) ) authorization_server.register_endpoint(MyRefreshTokenRevocationEndpoint) .. _RFC7009: https://tools.ietf.org/html/rfc7009 ''' def __init__(self, issuer, server=None, *args, **kwargs): super().__init__(*args, server=server, **kwargs) self.issuer = issuer def authenticate_token(self, request, client): '''''' self.check_params(request, client) # do not attempt to revoke refresh_tokens if request.form.get('token_type_hint') not in ('access_token', None): raise ContinueIteration() validator = JWTBearerTokenValidator(issuer=self.issuer, resource_server=None) validator.get_jwks = self.get_jwks try: validator.authenticate_token(request.form['token']) # if the token is not a JWT, fall back to the regular flow except InvalidTokenError: raise ContinueIteration() # JWT access token cannot be revoked raise UnsupportedTokenTypeError() def get_jwks(self): '''Return the JWKs that will be used to check the JWT access token signature. Developers MUST re-implement this method:: def get_jwks(self): return load_jwks("jwks.json") ''' raise NotImplementedError() authlib-1.3.2/authlib/oauth2/rfc9068/token.py000066400000000000000000000206771466226534200206200ustar00rootroot00000000000000import time from typing import List from typing import Optional from typing import Union from authlib.common.security import generate_token from authlib.jose import jwt from authlib.oauth2.rfc6750.token import BearerTokenGenerator class JWTBearerTokenGenerator(BearerTokenGenerator): '''A JWT formatted access token generator. :param issuer: The issuer identifier. Will appear in the JWT ``iss`` claim. :param \\*\\*kwargs: Other parameters are inherited from :class:`~authlib.oauth2.rfc6750.token.BearerTokenGenerator`. This token generator can be registered into the authorization server:: class MyJWTBearerTokenGenerator(JWTBearerTokenGenerator): def get_jwks(self): ... def get_extra_claims(self, client, grant_type, user, scope): ... authorization_server.register_token_generator( 'default', MyJWTBearerTokenGenerator(issuer='https://authorization-server.example.org'), ) ''' def __init__( self, issuer, alg='RS256', refresh_token_generator=None, expires_generator=None, ): super().__init__( self.access_token_generator, refresh_token_generator, expires_generator ) self.issuer = issuer self.alg = alg def get_jwks(self): '''Return the JWKs that will be used to sign the JWT access token. Developers MUST re-implement this method:: def get_jwks(self): return load_jwks("jwks.json") ''' raise NotImplementedError() def get_extra_claims(self, client, grant_type, user, scope): '''Return extra claims to add in the JWT access token. Developers MAY re-implement this method to add identity claims like the ones in :ref:`specs/oidc` ID Token, or any other arbitrary claims:: def get_extra_claims(self, client, grant_type, user, scope): return generate_user_info(user, scope) ''' return {} def get_audiences(self, client, user, scope) -> Union[str, List[str]]: '''Return the audience for the token. By default this simply returns the client ID. Developpers MAY re-implement this method to add extra audiences:: def get_audiences(self, client, user, scope): return [ client.get_client_id(), resource_server.get_id(), ] ''' return client.get_client_id() def get_acr(self, user) -> Optional[str]: '''Authentication Context Class Reference. Returns a user-defined case sensitive string indicating the class of authentication the used performed. Token audience may refuse to give access to some resources if some ACR criterias are not met. :ref:`specs/oidc` defines one special value: ``0`` means that the user authentication did not respect `ISO29115`_ level 1, and will be refused monetary operations. Developers MAY re-implement this method:: def get_acr(self, user): if user.insecure_session(): return '0' return 'urn:mace:incommon:iap:silver' .. _ISO29115: https://www.iso.org/standard/45138.html ''' return None def get_auth_time(self, user) -> Optional[int]: '''User authentication time. Time when the End-User authentication occurred. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. Developers MAY re-implement this method:: def get_auth_time(self, user): return datetime.timestamp(user.get_auth_time()) ''' return None def get_amr(self, user) -> Optional[List[str]]: '''Authentication Methods References. Defined by :ref:`specs/oidc` as an option list of user-defined case-sensitive strings indication which authentication methods have been used to authenticate the user. Developers MAY re-implement this method:: def get_amr(self, user): return ['2FA'] if user.has_2fa_enabled() else [] ''' return None def get_jti(self, client, grant_type, user, scope) -> str: '''JWT ID. Create an unique identifier for the token. Developers MAY re-implement this method:: def get_jti(self, client, grant_type, user scope): return generate_random_string(16) ''' return generate_token(16) def access_token_generator(self, client, grant_type, user, scope): now = int(time.time()) expires_in = now + self._get_expires_in(client, grant_type) token_data = { 'iss': self.issuer, 'exp': expires_in, 'client_id': client.get_client_id(), 'iat': now, 'jti': self.get_jti(client, grant_type, user, scope), 'scope': scope, } # In cases of access tokens obtained through grants where a resource owner is # involved, such as the authorization code grant, the value of 'sub' SHOULD # correspond to the subject identifier of the resource owner. if user: token_data['sub'] = user.get_user_id() # In cases of access tokens obtained through grants where no resource owner is # involved, such as the client credentials grant, the value of 'sub' SHOULD # correspond to an identifier the authorization server uses to indicate the # client application. else: token_data['sub'] = client.get_client_id() # If the request includes a 'resource' parameter (as defined in [RFC8707]), the # resulting JWT access token 'aud' claim SHOULD have the same value as the # 'resource' parameter in the request. # TODO: Implement this with RFC8707 if False: # pragma: no cover ... # If the request does not include a 'resource' parameter, the authorization # server MUST use a default resource indicator in the 'aud' claim. If a 'scope' # parameter is present in the request, the authorization server SHOULD use it to # infer the value of the default resource indicator to be used in the 'aud' # claim. The mechanism through which scopes are associated with default resource # indicator values is outside the scope of this specification. else: token_data['aud'] = self.get_audiences(client, user, scope) # If the values in the 'scope' parameter refer to different default resource # indicator values, the authorization server SHOULD reject the request with # 'invalid_scope' as described in Section 4.1.2.1 of [RFC6749]. # TODO: Implement this with RFC8707 if auth_time := self.get_auth_time(user): token_data['auth_time'] = auth_time # The meaning and processing of acr Claim Values is out of scope for this # specification. if acr := self.get_acr(user): token_data['acr'] = acr # The definition of particular values to be used in the amr Claim is beyond the # scope of this specification. if amr := self.get_amr(user): token_data['amr'] = amr # Authorization servers MAY return arbitrary attributes not defined in any # existing specification, as long as the corresponding claim names are collision # resistant or the access tokens are meant to be used only within a private # subsystem. Please refer to Sections 4.2 and 4.3 of [RFC7519] for details. token_data.update(self.get_extra_claims(client, grant_type, user, scope)) # This specification registers the 'application/at+jwt' media type, which can # be used to indicate that the content is a JWT access token. JWT access tokens # MUST include this media type in the 'typ' header parameter to explicitly # declare that the JWT represents an access token complying with this profile. # Per the definition of 'typ' in Section 4.1.9 of [RFC7515], it is RECOMMENDED # that the 'application/' prefix be omitted. Therefore, the 'typ' value used # SHOULD be 'at+jwt'. header = {'alg': self.alg, 'typ': 'at+jwt'} access_token = jwt.encode( header, token_data, key=self.get_jwks(), check=False, ) return access_token.decode() authlib-1.3.2/authlib/oauth2/rfc9068/token_validator.py000066400000000000000000000153521466226534200226570ustar00rootroot00000000000000''' authlib.oauth2.rfc9068.token_validator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Implementation of Validating JWT Access Tokens per `Section 4`_. .. _`Section 7`: https://www.rfc-editor.org/rfc/rfc9068.html#name-validating-jwt-access-token ''' from authlib.jose import jwt from authlib.jose.errors import DecodeError from authlib.jose.errors import JoseError from authlib.oauth2.rfc6750.errors import InsufficientScopeError from authlib.oauth2.rfc6750.errors import InvalidTokenError from authlib.oauth2.rfc6750.validator import BearerTokenValidator from .claims import JWTAccessTokenClaims class JWTBearerTokenValidator(BearerTokenValidator): '''JWTBearerTokenValidator can protect your resource server endpoints. :param issuer: The issuer from which tokens will be accepted. :param resource_server: An identifier for the current resource server, which must appear in the JWT ``aud`` claim. Developers needs to implement the missing methods:: class MyJWTBearerTokenValidator(JWTBearerTokenValidator): def get_jwks(self): ... require_oauth = ResourceProtector() require_oauth.register_token_validator( MyJWTBearerTokenValidator( issuer='https://authorization-server.example.org', resource_server='https://resource-server.example.org', ) ) You can then protect resources depending on the JWT `scope`, `groups`, `roles` or `entitlements` claims:: @require_oauth( scope='profile', groups='admins', roles='student', entitlements='captain', ) def resource_endpoint(): ... ''' def __init__(self, issuer, resource_server, *args, **kwargs): self.issuer = issuer self.resource_server = resource_server super().__init__(*args, **kwargs) def get_jwks(self): '''Return the JWKs that will be used to check the JWT access token signature. Developers MUST re-implement this method. Typically the JWKs are statically stored in the resource server configuration, or dynamically downloaded and cached using :ref:`specs/rfc8414`:: def get_jwks(self): if 'jwks' in cache: return cache.get('jwks') server_metadata = get_server_metadata(self.issuer) jwks_uri = server_metadata.get('jwks_uri') cache['jwks'] = requests.get(jwks_uri).json() return cache['jwks'] ''' raise NotImplementedError() def validate_iss(self, claims, iss: 'str') -> bool: # The issuer identifier for the authorization server (which is typically # obtained during discovery) MUST exactly match the value of the 'iss' # claim. return iss == self.issuer def authenticate_token(self, token_string): '''''' # empty docstring avoids to display the irrelevant parent docstring claims_options = { 'iss': {'essential': True, 'validate': self.validate_iss}, 'exp': {'essential': True}, 'aud': {'essential': True, 'value': self.resource_server}, 'sub': {'essential': True}, 'client_id': {'essential': True}, 'iat': {'essential': True}, 'jti': {'essential': True}, 'auth_time': {'essential': False}, 'acr': {'essential': False}, 'amr': {'essential': False}, 'scope': {'essential': False}, 'groups': {'essential': False}, 'roles': {'essential': False}, 'entitlements': {'essential': False}, } jwks = self.get_jwks() # If the JWT access token is encrypted, decrypt it using the keys and algorithms # that the resource server specified during registration. If encryption was # negotiated with the authorization server at registration time and the incoming # JWT access token is not encrypted, the resource server SHOULD reject it. # The resource server MUST validate the signature of all incoming JWT access # tokens according to [RFC7515] using the algorithm specified in the JWT 'alg' # Header Parameter. The resource server MUST reject any JWT in which the value # of 'alg' is 'none'. The resource server MUST use the keys provided by the # authorization server. try: return jwt.decode( token_string, key=jwks, claims_cls=JWTAccessTokenClaims, claims_options=claims_options, ) except DecodeError: raise InvalidTokenError( realm=self.realm, extra_attributes=self.extra_attributes ) def validate_token( self, token, scopes, request, groups=None, roles=None, entitlements=None ): '''''' # empty docstring avoids to display the irrelevant parent docstring try: token.validate() except JoseError as exc: raise InvalidTokenError( realm=self.realm, extra_attributes=self.extra_attributes ) from exc # If an authorization request includes a scope parameter, the corresponding # issued JWT access token SHOULD include a 'scope' claim as defined in Section # 4.2 of [RFC8693]. All the individual scope strings in the 'scope' claim MUST # have meaning for the resources indicated in the 'aud' claim. See Section 5 for # more considerations about the relationship between scope strings and resources # indicated by the 'aud' claim. if self.scope_insufficient(token.get('scope', []), scopes): raise InsufficientScopeError() # Many authorization servers embed authorization attributes that go beyond the # delegated scenarios described by [RFC7519] in the access tokens they issue. # Typical examples include resource owner memberships in roles and groups that # are relevant to the resource being accessed, entitlements assigned to the # resource owner for the targeted resource that the authorization server knows # about, and so on. An authorization server wanting to include such attributes # in a JWT access token SHOULD use the 'groups', 'roles', and 'entitlements' # attributes of the 'User' resource schema defined by Section 4.1.2 of # [RFC7643]) as claim types. if self.scope_insufficient(token.get('groups'), groups): raise InvalidTokenError() if self.scope_insufficient(token.get('roles'), roles): raise InvalidTokenError() if self.scope_insufficient(token.get('entitlements'), entitlements): raise InvalidTokenError() authlib-1.3.2/authlib/oidc/000077500000000000000000000000001466226534200155255ustar00rootroot00000000000000authlib-1.3.2/authlib/oidc/__init__.py000066400000000000000000000000001466226534200176240ustar00rootroot00000000000000authlib-1.3.2/authlib/oidc/core/000077500000000000000000000000001466226534200164555ustar00rootroot00000000000000authlib-1.3.2/authlib/oidc/core/__init__.py000066400000000000000000000012121466226534200205620ustar00rootroot00000000000000""" authlib.oidc.core ~~~~~~~~~~~~~~~~~ OpenID Connect Core 1.0 Implementation. http://openid.net/specs/openid-connect-core-1_0.html """ from .models import AuthorizationCodeMixin from .claims import ( IDToken, CodeIDToken, ImplicitIDToken, HybridIDToken, UserInfo, get_claim_cls_by_response_type, ) from .grants import OpenIDToken, OpenIDCode, OpenIDHybridGrant, OpenIDImplicitGrant __all__ = [ 'AuthorizationCodeMixin', 'IDToken', 'CodeIDToken', 'ImplicitIDToken', 'HybridIDToken', 'UserInfo', 'get_claim_cls_by_response_type', 'OpenIDToken', 'OpenIDCode', 'OpenIDHybridGrant', 'OpenIDImplicitGrant', ] authlib-1.3.2/authlib/oidc/core/claims.py000066400000000000000000000237131466226534200203050ustar00rootroot00000000000000import time import hmac from authlib.common.encoding import to_bytes from authlib.jose import JWTClaims from authlib.jose.errors import ( MissingClaimError, InvalidClaimError, ) from .util import create_half_hash __all__ = [ 'IDToken', 'CodeIDToken', 'ImplicitIDToken', 'HybridIDToken', 'UserInfo', 'get_claim_cls_by_response_type' ] _REGISTERED_CLAIMS = [ 'iss', 'sub', 'aud', 'exp', 'nbf', 'iat', 'auth_time', 'nonce', 'acr', 'amr', 'azp', 'at_hash', ] class IDToken(JWTClaims): ESSENTIAL_CLAIMS = ['iss', 'sub', 'aud', 'exp', 'iat'] def validate(self, now=None, leeway=0): for k in self.ESSENTIAL_CLAIMS: if k not in self: raise MissingClaimError(k) self._validate_essential_claims() if now is None: now = int(time.time()) self.validate_iss() self.validate_sub() self.validate_aud() self.validate_exp(now, leeway) self.validate_nbf(now, leeway) self.validate_iat(now, leeway) self.validate_auth_time() self.validate_nonce() self.validate_acr() self.validate_amr() self.validate_azp() self.validate_at_hash() def validate_auth_time(self): """Time when the End-User authentication occurred. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. When a max_age request is made or when auth_time is requested as an Essential Claim, then this Claim is REQUIRED; otherwise, its inclusion is OPTIONAL. """ auth_time = self.get('auth_time') if self.params.get('max_age') and not auth_time: raise MissingClaimError('auth_time') if auth_time and not isinstance(auth_time, (int, float)): raise InvalidClaimError('auth_time') def validate_nonce(self): """String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request. If present in the Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token with the Claim Value being the nonce value sent in the Authentication Request. Authorization Servers SHOULD perform no other processing on nonce values used. The nonce value is a case sensitive string. """ nonce_value = self.params.get('nonce') if nonce_value: if 'nonce' not in self: raise MissingClaimError('nonce') if nonce_value != self['nonce']: raise InvalidClaimError('nonce') def validate_acr(self): """OPTIONAL. Authentication Context Class Reference. String specifying an Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied. The value "0" indicates the End-User authentication did not meet the requirements of `ISO/IEC 29115`_ level 1. Authentication using a long-lived browser cookie, for instance, is one example where the use of "level 0" is appropriate. Authentications with level 0 SHOULD NOT be used to authorize access to any resource of any monetary value. An absolute URI or an `RFC 6711`_ registered name SHOULD be used as the acr value; registered names MUST NOT be used with a different meaning than that which is registered. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. The acr value is a case sensitive string. .. _`ISO/IEC 29115`: https://www.iso.org/standard/45138.html .. _`RFC 6711`: https://tools.ietf.org/html/rfc6711 """ return self._validate_claim_value('acr') def validate_amr(self): """OPTIONAL. Authentication Methods References. JSON array of strings that are identifiers for authentication methods used in the authentication. For instance, values might indicate that both password and OTP authentication methods were used. The definition of particular values to be used in the amr Claim is beyond the scope of this specification. Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. The amr value is an array of case sensitive strings. """ amr = self.get('amr') if amr and not isinstance(self['amr'], list): raise InvalidClaimError('amr') def validate_azp(self): """OPTIONAL. Authorized party - the party to which the ID Token was issued. If present, it MUST contain the OAuth 2.0 Client ID of this party. This Claim is only needed when the ID Token has a single audience value and that audience is different than the authorized party. It MAY be included even when the authorized party is the same as the sole audience. The azp value is a case sensitive string containing a StringOrURI value. """ aud = self.get('aud') client_id = self.params.get('client_id') required = False if aud and client_id: if isinstance(aud, list) and len(aud) == 1: aud = aud[0] if aud != client_id: required = True azp = self.get('azp') if required and not azp: raise MissingClaimError('azp') if azp and client_id and azp != client_id: raise InvalidClaimError('azp') def validate_at_hash(self): """OPTIONAL. Access Token hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the access_token value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the access_token value with SHA-256, then take the left-most 128 bits and base64url encode them. The at_hash value is a case sensitive string. """ access_token = self.params.get('access_token') at_hash = self.get('at_hash') if at_hash and access_token: if not _verify_hash(at_hash, access_token, self.header['alg']): raise InvalidClaimError('at_hash') class CodeIDToken(IDToken): RESPONSE_TYPES = ('code',) REGISTERED_CLAIMS = _REGISTERED_CLAIMS class ImplicitIDToken(IDToken): RESPONSE_TYPES = ('id_token', 'id_token token') ESSENTIAL_CLAIMS = ['iss', 'sub', 'aud', 'exp', 'iat', 'nonce'] REGISTERED_CLAIMS = _REGISTERED_CLAIMS def validate_at_hash(self): """If the ID Token is issued from the Authorization Endpoint with an access_token value, which is the case for the response_type value id_token token, this is REQUIRED; it MAY NOT be used when no Access Token is issued, which is the case for the response_type value id_token. """ access_token = self.params.get('access_token') if access_token and 'at_hash' not in self: raise MissingClaimError('at_hash') super().validate_at_hash() class HybridIDToken(ImplicitIDToken): RESPONSE_TYPES = ('code id_token', 'code token', 'code id_token token') REGISTERED_CLAIMS = _REGISTERED_CLAIMS + ['c_hash'] def validate(self, now=None, leeway=0): super().validate(now=now, leeway=leeway) self.validate_c_hash() def validate_c_hash(self): """Code hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the code value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is HS512, hash the code value with SHA-512, then take the left-most 256 bits and base64url encode them. The c_hash value is a case sensitive string. If the ID Token is issued from the Authorization Endpoint with a code, which is the case for the response_type values code id_token and code id_token token, this is REQUIRED; otherwise, its inclusion is OPTIONAL. """ code = self.params.get('code') c_hash = self.get('c_hash') if code: if not c_hash: raise MissingClaimError('c_hash') if not _verify_hash(c_hash, code, self.header['alg']): raise InvalidClaimError('c_hash') class UserInfo(dict): """The standard claims of a UserInfo object. Defined per `Section 5.1`_. .. _`Section 5.1`: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims """ #: registered claims that UserInfo supports REGISTERED_CLAIMS = [ 'sub', 'name', 'given_name', 'family_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'email', 'email_verified', 'gender', 'birthdate', 'zoneinfo', 'locale', 'phone_number', 'phone_number_verified', 'address', 'updated_at', ] def __getattr__(self, key): try: return object.__getattribute__(self, key) except AttributeError as error: if key in self.REGISTERED_CLAIMS: return self.get(key) raise error def get_claim_cls_by_response_type(response_type): claims_classes = (CodeIDToken, ImplicitIDToken, HybridIDToken) for claims_cls in claims_classes: if response_type in claims_cls.RESPONSE_TYPES: return claims_cls def _verify_hash(signature, s, alg): hash_value = create_half_hash(s, alg) if not hash_value: return True return hmac.compare_digest(hash_value, to_bytes(signature)) authlib-1.3.2/authlib/oidc/core/errors.py000066400000000000000000000055031466226534200203460ustar00rootroot00000000000000from authlib.oauth2 import OAuth2Error class InteractionRequiredError(OAuth2Error): """The Authorization Server requires End-User interaction of some form to proceed. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for End-User interaction. http://openid.net/specs/openid-connect-core-1_0.html#AuthError """ error = 'interaction_required' class LoginRequiredError(OAuth2Error): """The Authorization Server requires End-User authentication. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for End-User authentication. http://openid.net/specs/openid-connect-core-1_0.html#AuthError """ error = 'login_required' class AccountSelectionRequiredError(OAuth2Error): """The End-User is REQUIRED to select a session at the Authorization Server. The End-User MAY be authenticated at the Authorization Server with different associated accounts, but the End-User did not select a session. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface to prompt for a session to use. http://openid.net/specs/openid-connect-core-1_0.html#AuthError """ error = 'account_selection_required' class ConsentRequiredError(OAuth2Error): """The Authorization Server requires End-User consent. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for End-User consent. http://openid.net/specs/openid-connect-core-1_0.html#AuthError """ error = 'consent_required' class InvalidRequestURIError(OAuth2Error): """The request_uri in the Authorization Request returns an error or contains invalid data. http://openid.net/specs/openid-connect-core-1_0.html#AuthError """ error = 'invalid_request_uri' class InvalidRequestObjectError(OAuth2Error): """The request parameter contains an invalid Request Object.""" error = 'invalid_request_object' class RequestNotSupportedError(OAuth2Error): """The OP does not support use of the request parameter.""" error = 'request_not_supported' class RequestURINotSupportedError(OAuth2Error): """The OP does not support use of the request_uri parameter.""" error = 'request_uri_not_supported' class RegistrationNotSupportedError(OAuth2Error): """The OP does not support use of the registration parameter.""" error = 'registration_not_supported' authlib-1.3.2/authlib/oidc/core/grants/000077500000000000000000000000001466226534200177535ustar00rootroot00000000000000authlib-1.3.2/authlib/oidc/core/grants/__init__.py000066400000000000000000000003421466226534200220630ustar00rootroot00000000000000from .code import OpenIDToken, OpenIDCode from .implicit import OpenIDImplicitGrant from .hybrid import OpenIDHybridGrant __all__ = [ 'OpenIDToken', 'OpenIDCode', 'OpenIDImplicitGrant', 'OpenIDHybridGrant', ] authlib-1.3.2/authlib/oidc/core/grants/code.py000066400000000000000000000113601466226534200212400ustar00rootroot00000000000000""" authlib.oidc.core.grants.code ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Implementation of Authentication using the Authorization Code Flow per `Section 3.1`_. .. _`Section 3.1`: http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth """ import logging from authlib.oauth2.rfc6749 import OAuth2Request from .util import ( is_openid_scope, validate_nonce, validate_request_prompt, generate_id_token, ) log = logging.getLogger(__name__) class OpenIDToken: def get_jwt_config(self, grant): # pragma: no cover """Get the JWT configuration for OpenIDCode extension. The JWT configuration will be used to generate ``id_token``. Developers MUST implement this method in subclass, e.g.:: def get_jwt_config(self, grant): return { 'key': read_private_key_file(key_path), 'alg': 'RS256', 'iss': 'issuer-identity', 'exp': 3600 } :param grant: AuthorizationCodeGrant instance :return: dict """ raise NotImplementedError() def generate_user_info(self, user, scope): """Provide user information for the given scope. Developers MUST implement this method in subclass, e.g.:: from authlib.oidc.core import UserInfo def generate_user_info(self, user, scope): user_info = UserInfo(sub=user.id, name=user.name) if 'email' in scope: user_info['email'] = user.email return user_info :param user: user instance :param scope: scope of the token :return: ``authlib.oidc.core.UserInfo`` instance """ raise NotImplementedError() def get_audiences(self, request): """Parse `aud` value for id_token, default value is client id. Developers MAY rewrite this method to provide a customized audience value. """ client = request.client return [client.get_client_id()] def process_token(self, grant, token): scope = token.get('scope') if not scope or not is_openid_scope(scope): # standard authorization code flow return token request: OAuth2Request = grant.request authorization_code = request.authorization_code config = self.get_jwt_config(grant) config['aud'] = self.get_audiences(request) if authorization_code: config['nonce'] = authorization_code.get_nonce() config['auth_time'] = authorization_code.get_auth_time() user_info = self.generate_user_info(request.user, token['scope']) id_token = generate_id_token(token, user_info, **config) token['id_token'] = id_token return token def __call__(self, grant): grant.register_hook('process_token', self.process_token) class OpenIDCode(OpenIDToken): """An extension from OpenID Connect for "grant_type=code" request. Developers MUST implement the missing methods:: class MyOpenIDCode(OpenIDCode): def get_jwt_config(self, grant): return {...} def exists_nonce(self, nonce, request): return check_if_nonce_in_cache(request.client_id, nonce) def generate_user_info(self, user, scope): return {...} The register this extension with AuthorizationCodeGrant:: authorization_server.register_grant(AuthorizationCodeGrant, extensions=[MyOpenIDCode()]) """ def __init__(self, require_nonce=False): self.require_nonce = require_nonce def exists_nonce(self, nonce, request): """Check if the given nonce is existing in your database. Developers MUST implement this method in subclass, e.g.:: def exists_nonce(self, nonce, request): exists = AuthorizationCode.query.filter_by( client_id=request.client_id, nonce=nonce ).first() return bool(exists) :param nonce: A string of "nonce" parameter in request :param request: OAuth2Request instance :return: Boolean """ raise NotImplementedError() def validate_openid_authorization_request(self, grant): validate_nonce(grant.request, self.exists_nonce, self.require_nonce) def __call__(self, grant): grant.register_hook('process_token', self.process_token) if is_openid_scope(grant.request.scope): grant.register_hook( 'after_validate_authorization_request', self.validate_openid_authorization_request ) grant.register_hook( 'after_validate_consent_request', validate_request_prompt ) authlib-1.3.2/authlib/oidc/core/grants/hybrid.py000066400000000000000000000064421466226534200216140ustar00rootroot00000000000000import logging from authlib.common.security import generate_token from authlib.oauth2.rfc6749 import InvalidScopeError from authlib.oauth2.rfc6749.grants.authorization_code import ( validate_code_authorization_request ) from .implicit import OpenIDImplicitGrant from .util import is_openid_scope, validate_nonce log = logging.getLogger(__name__) class OpenIDHybridGrant(OpenIDImplicitGrant): #: Generated "code" length AUTHORIZATION_CODE_LENGTH = 48 RESPONSE_TYPES = {'code id_token', 'code token', 'code id_token token'} GRANT_TYPE = 'code' DEFAULT_RESPONSE_MODE = 'fragment' def generate_authorization_code(self): """"The method to generate "code" value for authorization code data. Developers may rewrite this method, or customize the code length with:: class MyAuthorizationCodeGrant(AuthorizationCodeGrant): AUTHORIZATION_CODE_LENGTH = 32 # default is 48 """ return generate_token(self.AUTHORIZATION_CODE_LENGTH) def save_authorization_code(self, code, request): """Save authorization_code for later use. Developers MUST implement it in subclass. Here is an example:: def save_authorization_code(self, code, request): client = request.client auth_code = AuthorizationCode( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, nonce=request.data.get('nonce'), user_id=request.user.id, ) auth_code.save() """ raise NotImplementedError() def validate_authorization_request(self): if not is_openid_scope(self.request.scope): raise InvalidScopeError( 'Missing "openid" scope', redirect_uri=self.request.redirect_uri, redirect_fragment=True, ) self.register_hook( 'after_validate_authorization_request', lambda grant: validate_nonce( grant.request, grant.exists_nonce, required=True) ) return validate_code_authorization_request(self) def create_granted_params(self, grant_user): self.request.user = grant_user client = self.request.client code = self.generate_authorization_code() self.save_authorization_code(code, self.request) params = [('code', code)] token = self.generate_token( grant_type='implicit', user=grant_user, scope=self.request.scope, include_refresh_token=False ) response_types = self.request.response_type.split() if 'token' in response_types: log.debug('Grant token %r to %r', token, client) self.server.save_token(token, self.request) if 'id_token' in response_types: token = self.process_implicit_token(token, code) else: # response_type is "code id_token" token = { 'expires_in': token['expires_in'], 'scope': token['scope'] } token = self.process_implicit_token(token, code) params.extend([(k, token[k]) for k in token]) return params authlib-1.3.2/authlib/oidc/core/grants/implicit.py000066400000000000000000000122501466226534200221370ustar00rootroot00000000000000import logging from authlib.oauth2.rfc6749 import ( OAuth2Error, InvalidScopeError, AccessDeniedError, ImplicitGrant, ) from .util import ( is_openid_scope, validate_nonce, validate_request_prompt, create_response_mode_response, generate_id_token, ) log = logging.getLogger(__name__) class OpenIDImplicitGrant(ImplicitGrant): RESPONSE_TYPES = {'id_token token', 'id_token'} DEFAULT_RESPONSE_MODE = 'fragment' def exists_nonce(self, nonce, request): """Check if the given nonce is existing in your database. Developers should implement this method in subclass, e.g.:: def exists_nonce(self, nonce, request): exists = AuthorizationCode.query.filter_by( client_id=request.client_id, nonce=nonce ).first() return bool(exists) :param nonce: A string of "nonce" parameter in request :param request: OAuth2Request instance :return: Boolean """ raise NotImplementedError() def get_jwt_config(self): """Get the JWT configuration for OpenIDImplicitGrant. The JWT configuration will be used to generate ``id_token``. Developers MUST implement this method in subclass, e.g.:: def get_jwt_config(self): return { 'key': read_private_key_file(key_path), 'alg': 'RS256', 'iss': 'issuer-identity', 'exp': 3600 } :return: dict """ raise NotImplementedError() def generate_user_info(self, user, scope): """Provide user information for the given scope. Developers MUST implement this method in subclass, e.g.:: from authlib.oidc.core import UserInfo def generate_user_info(self, user, scope): user_info = UserInfo(sub=user.id, name=user.name) if 'email' in scope: user_info['email'] = user.email return user_info :param user: user instance :param scope: scope of the token :return: ``authlib.oidc.core.UserInfo`` instance """ raise NotImplementedError() def get_audiences(self, request): """Parse `aud` value for id_token, default value is client id. Developers MAY rewrite this method to provide a customized audience value. """ client = request.client return [client.get_client_id()] def validate_authorization_request(self): if not is_openid_scope(self.request.scope): raise InvalidScopeError( 'Missing "openid" scope', redirect_uri=self.request.redirect_uri, redirect_fragment=True, ) redirect_uri = super().validate_authorization_request() try: validate_nonce(self.request, self.exists_nonce, required=True) except OAuth2Error as error: error.redirect_uri = redirect_uri error.redirect_fragment = True raise error return redirect_uri def validate_consent_request(self): redirect_uri = self.validate_authorization_request() validate_request_prompt(self, redirect_uri, redirect_fragment=True) def create_authorization_response(self, redirect_uri, grant_user): state = self.request.state if grant_user: params = self.create_granted_params(grant_user) if state: params.append(('state', state)) else: error = AccessDeniedError(state=state) params = error.get_body() # http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes response_mode = self.request.data.get('response_mode', self.DEFAULT_RESPONSE_MODE) return create_response_mode_response( redirect_uri=redirect_uri, params=params, response_mode=response_mode, ) def create_granted_params(self, grant_user): self.request.user = grant_user client = self.request.client token = self.generate_token( user=grant_user, scope=self.request.scope, include_refresh_token=False ) if self.request.response_type == 'id_token': token = { 'expires_in': token['expires_in'], 'scope': token['scope'], } token = self.process_implicit_token(token) else: log.debug('Grant token %r to %r', token, client) self.server.save_token(token, self.request) token = self.process_implicit_token(token) params = [(k, token[k]) for k in token] return params def process_implicit_token(self, token, code=None): config = self.get_jwt_config() config['aud'] = self.get_audiences(self.request) config['nonce'] = self.request.data.get('nonce') if code is not None: config['code'] = code user_info = self.generate_user_info(self.request.user, token['scope']) id_token = generate_id_token(token, user_info, **config) token['id_token'] = id_token return token authlib-1.3.2/authlib/oidc/core/grants/util.py000066400000000000000000000100321466226534200212760ustar00rootroot00000000000000import time from authlib.oauth2.rfc6749 import InvalidRequestError from authlib.oauth2.rfc6749 import scope_to_list from authlib.jose import jwt from authlib.common.encoding import to_native from authlib.common.urls import add_params_to_uri, quote_url from ..util import create_half_hash from ..errors import ( LoginRequiredError, AccountSelectionRequiredError, ConsentRequiredError, ) def is_openid_scope(scope): scopes = scope_to_list(scope) return scopes and 'openid' in scopes def validate_request_prompt(grant, redirect_uri, redirect_fragment=False): prompt = grant.request.data.get('prompt') end_user = grant.request.user if not prompt: if not end_user: grant.prompt = 'login' return grant if prompt == 'none' and not end_user: raise LoginRequiredError( redirect_uri=redirect_uri, redirect_fragment=redirect_fragment) prompts = prompt.split() if 'none' in prompts and len(prompts) > 1: # If this parameter contains none with any other value, # an error is returned raise InvalidRequestError( 'Invalid "prompt" parameter.', redirect_uri=redirect_uri, redirect_fragment=redirect_fragment) prompt = _guess_prompt_value( end_user, prompts, redirect_uri, redirect_fragment=redirect_fragment) if prompt: grant.prompt = prompt return grant def validate_nonce(request, exists_nonce, required=False): nonce = request.data.get('nonce') if not nonce: if required: raise InvalidRequestError('Missing "nonce" in request.') return True if exists_nonce(nonce, request): raise InvalidRequestError('Replay attack') def generate_id_token( token, user_info, key, iss, aud, alg='RS256', exp=3600, nonce=None, auth_time=None, code=None): now = int(time.time()) if auth_time is None: auth_time = now payload = { 'iss': iss, 'aud': aud, 'iat': now, 'exp': now + exp, 'auth_time': auth_time, } if nonce: payload['nonce'] = nonce if code: payload['c_hash'] = to_native(create_half_hash(code, alg)) access_token = token.get('access_token') if access_token: payload['at_hash'] = to_native(create_half_hash(access_token, alg)) payload.update(user_info) return to_native(jwt.encode({'alg': alg}, payload, key)) def create_response_mode_response(redirect_uri, params, response_mode): if response_mode == 'form_post': tpl = ( 'Redirecting' '' '
{}
' ) inputs = ''.join([ ''.format( quote_url(k), quote_url(v)) for k, v in params ]) body = tpl.format(quote_url(redirect_uri), inputs) return 200, body, [('Content-Type', 'text/html; charset=utf-8')] if response_mode == 'query': uri = add_params_to_uri(redirect_uri, params, fragment=False) elif response_mode == 'fragment': uri = add_params_to_uri(redirect_uri, params, fragment=True) else: raise InvalidRequestError('Invalid "response_mode" value') return 302, '', [('Location', uri)] def _guess_prompt_value(end_user, prompts, redirect_uri, redirect_fragment): # http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest if not end_user and 'login' in prompts: return 'login' if 'consent' in prompts: if not end_user: raise ConsentRequiredError( redirect_uri=redirect_uri, redirect_fragment=redirect_fragment) return 'consent' elif 'select_account' in prompts: if not end_user: raise AccountSelectionRequiredError( redirect_uri=redirect_uri, redirect_fragment=redirect_fragment) return 'select_account' authlib-1.3.2/authlib/oidc/core/models.py000066400000000000000000000006351466226534200203160ustar00rootroot00000000000000from authlib.oauth2.rfc6749 import ( AuthorizationCodeMixin as _AuthorizationCodeMixin ) class AuthorizationCodeMixin(_AuthorizationCodeMixin): def get_nonce(self): """Get "nonce" value of the authorization code object.""" raise NotImplementedError() def get_auth_time(self): """Get "auth_time" value of the authorization code object.""" raise NotImplementedError() authlib-1.3.2/authlib/oidc/core/util.py000066400000000000000000000005761466226534200200140ustar00rootroot00000000000000import hashlib from authlib.common.encoding import to_bytes, urlsafe_b64encode def create_half_hash(s, alg): hash_type = f'sha{alg[2:]}' hash_alg = getattr(hashlib, hash_type, None) if not hash_alg: return None data_digest = hash_alg(to_bytes(s)).digest() slice_index = int(len(data_digest) / 2) return urlsafe_b64encode(data_digest[:slice_index]) authlib-1.3.2/authlib/oidc/discovery/000077500000000000000000000000001466226534200175345ustar00rootroot00000000000000authlib-1.3.2/authlib/oidc/discovery/__init__.py000066400000000000000000000005011466226534200216410ustar00rootroot00000000000000""" authlib.oidc.discover ~~~~~~~~~~~~~~~~~~~~~ OpenID Connect Discovery 1.0 Implementation. https://openid.net/specs/openid-connect-discovery-1_0.html """ from .models import OpenIDProviderMetadata from .well_known import get_well_known_url __all__ = ['OpenIDProviderMetadata', 'get_well_known_url'] authlib-1.3.2/authlib/oidc/discovery/models.py000066400000000000000000000304371466226534200214000ustar00rootroot00000000000000from authlib.oauth2.rfc8414 import AuthorizationServerMetadata from authlib.oauth2.rfc8414.models import validate_array_value class OpenIDProviderMetadata(AuthorizationServerMetadata): REGISTRY_KEYS = [ 'issuer', 'authorization_endpoint', 'token_endpoint', 'jwks_uri', 'registration_endpoint', 'scopes_supported', 'response_types_supported', 'response_modes_supported', 'grant_types_supported', 'token_endpoint_auth_methods_supported', 'token_endpoint_auth_signing_alg_values_supported', 'service_documentation', 'ui_locales_supported', 'op_policy_uri', 'op_tos_uri', # added by OpenID 'acr_values_supported', 'subject_types_supported', 'id_token_signing_alg_values_supported', 'id_token_encryption_alg_values_supported', 'id_token_encryption_enc_values_supported', 'userinfo_signing_alg_values_supported', 'userinfo_encryption_alg_values_supported', 'userinfo_encryption_enc_values_supported', 'request_object_signing_alg_values_supported', 'request_object_encryption_alg_values_supported', 'request_object_encryption_enc_values_supported', 'display_values_supported', 'claim_types_supported', 'claims_supported', 'claims_locales_supported', 'claims_parameter_supported', 'request_parameter_supported', 'request_uri_parameter_supported', 'require_request_uri_registration', # not defined by OpenID # 'revocation_endpoint', # 'revocation_endpoint_auth_methods_supported', # 'revocation_endpoint_auth_signing_alg_values_supported', # 'introspection_endpoint', # 'introspection_endpoint_auth_methods_supported', # 'introspection_endpoint_auth_signing_alg_values_supported', # 'code_challenge_methods_supported', ] def validate_jwks_uri(self): # REQUIRED in OpenID Connect jwks_uri = self.get('jwks_uri') if jwks_uri is None: raise ValueError('"jwks_uri" is required') return super().validate_jwks_uri() def validate_acr_values_supported(self): """OPTIONAL. JSON array containing a list of the Authentication Context Class References that this OP supports. """ validate_array_value(self, 'acr_values_supported') def validate_subject_types_supported(self): """REQUIRED. JSON array containing a list of the Subject Identifier types that this OP supports. Valid types include pairwise and public. """ # 1. REQUIRED values = self.get('subject_types_supported') if values is None: raise ValueError('"subject_types_supported" is required') # 2. JSON array if not isinstance(values, list): raise ValueError('"subject_types_supported" MUST be JSON array') # 3. Valid types include pairwise and public valid_types = {'pairwise', 'public'} if not valid_types.issuperset(set(values)): raise ValueError( '"subject_types_supported" contains invalid values') def validate_id_token_signing_alg_values_supported(self): """REQUIRED. JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for the ID Token to encode the Claims in a JWT [JWT]. The algorithm RS256 MUST be included. The value none MAY be supported, but MUST NOT be used unless the Response Type used returns no ID Token from the Authorization Endpoint (such as when using the Authorization Code Flow). """ # 1. REQUIRED values = self.get('id_token_signing_alg_values_supported') if values is None: raise ValueError('"id_token_signing_alg_values_supported" is required') # 2. JSON array if not isinstance(values, list): raise ValueError('"id_token_signing_alg_values_supported" MUST be JSON array') # 3. The algorithm RS256 MUST be included if 'RS256' not in values: raise ValueError( '"RS256" MUST be included in "id_token_signing_alg_values_supported"') def validate_id_token_encryption_alg_values_supported(self): """OPTIONAL. JSON array containing a list of the JWE encryption algorithms (alg values) supported by the OP for the ID Token to encode the Claims in a JWT. """ validate_array_value(self, 'id_token_encryption_alg_values_supported') def validate_id_token_encryption_enc_values_supported(self): """OPTIONAL. JSON array containing a list of the JWE encryption algorithms (enc values) supported by the OP for the ID Token to encode the Claims in a JWT. """ validate_array_value(self, 'id_token_encryption_enc_values_supported') def validate_userinfo_signing_alg_values_supported(self): """OPTIONAL. JSON array containing a list of the JWS signing algorithms (alg values) [JWA] supported by the UserInfo Endpoint to encode the Claims in a JWT. The value none MAY be included. """ validate_array_value(self, 'userinfo_signing_alg_values_supported') def validate_userinfo_encryption_alg_values_supported(self): """OPTIONAL. JSON array containing a list of the JWE encryption algorithms (alg values) [JWA] supported by the UserInfo Endpoint to encode the Claims in a JWT. """ validate_array_value(self, 'userinfo_encryption_alg_values_supported') def validate_userinfo_encryption_enc_values_supported(self): """OPTIONAL. JSON array containing a list of the JWE encryption algorithms (enc values) [JWA] supported by the UserInfo Endpoint to encode the Claims in a JWT. """ validate_array_value(self, 'userinfo_encryption_enc_values_supported') def validate_request_object_signing_alg_values_supported(self): """OPTIONAL. JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for Request Objects, which are described in Section 6.1 of OpenID Connect Core 1.0. These algorithms are used both when the Request Object is passed by value (using the request parameter) and when it is passed by reference (using the request_uri parameter). Servers SHOULD support none and RS256. """ values = self.get('request_object_signing_alg_values_supported') if not values: return if not isinstance(values, list): raise ValueError('"request_object_signing_alg_values_supported" MUST be JSON array') # Servers SHOULD support none and RS256 if 'none' not in values or 'RS256' not in values: raise ValueError( '"request_object_signing_alg_values_supported" ' 'SHOULD support none and RS256') def validate_request_object_encryption_alg_values_supported(self): """OPTIONAL. JSON array containing a list of the JWE encryption algorithms (alg values) supported by the OP for Request Objects. These algorithms are used both when the Request Object is passed by value and when it is passed by reference. """ validate_array_value(self, 'request_object_encryption_alg_values_supported') def validate_request_object_encryption_enc_values_supported(self): """OPTIONAL. JSON array containing a list of the JWE encryption algorithms (enc values) supported by the OP for Request Objects. These algorithms are used both when the Request Object is passed by value and when it is passed by reference. """ validate_array_value(self, 'request_object_encryption_enc_values_supported') def validate_display_values_supported(self): """OPTIONAL. JSON array containing a list of the display parameter values that the OpenID Provider supports. These values are described in Section 3.1.2.1 of OpenID Connect Core 1.0. """ values = self.get('display_values_supported') if not values: return if not isinstance(values, list): raise ValueError('"display_values_supported" MUST be JSON array') valid_values = {'page', 'popup', 'touch', 'wap'} if not valid_values.issuperset(set(values)): raise ValueError('"display_values_supported" contains invalid values') def validate_claim_types_supported(self): """OPTIONAL. JSON array containing a list of the Claim Types that the OpenID Provider supports. These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0. Values defined by this specification are normal, aggregated, and distributed. If omitted, the implementation supports only normal Claims. """ values = self.get('claim_types_supported') if not values: return if not isinstance(values, list): raise ValueError('"claim_types_supported" MUST be JSON array') valid_values = {'normal', 'aggregated', 'distributed'} if not valid_values.issuperset(set(values)): raise ValueError('"claim_types_supported" contains invalid values') def validate_claims_supported(self): """RECOMMENDED. JSON array containing a list of the Claim Names of the Claims that the OpenID Provider MAY be able to supply values for. Note that for privacy or other reasons, this might not be an exhaustive list. """ validate_array_value(self, 'claims_supported') def validate_claims_locales_supported(self): """OPTIONAL. Languages and scripts supported for values in Claims being returned, represented as a JSON array of BCP47 [RFC5646] language tag values. Not all languages and scripts are necessarily supported for all Claim values. """ validate_array_value(self, 'claims_locales_supported') def validate_claims_parameter_supported(self): """OPTIONAL. Boolean value specifying whether the OP supports use of the claims parameter, with true indicating support. If omitted, the default value is false. """ _validate_boolean_value(self, 'claims_parameter_supported') def validate_request_parameter_supported(self): """OPTIONAL. Boolean value specifying whether the OP supports use of the request parameter, with true indicating support. If omitted, the default value is false. """ _validate_boolean_value(self, 'request_parameter_supported') def validate_request_uri_parameter_supported(self): """OPTIONAL. Boolean value specifying whether the OP supports use of the request_uri parameter, with true indicating support. If omitted, the default value is true. """ _validate_boolean_value(self, 'request_uri_parameter_supported') def validate_require_request_uri_registration(self): """OPTIONAL. Boolean value specifying whether the OP requires any request_uri values used to be pre-registered using the request_uris registration parameter. Pre-registration is REQUIRED when the value is true. If omitted, the default value is false. """ _validate_boolean_value(self, 'require_request_uri_registration') @property def claim_types_supported(self): # If omitted, the implementation supports only normal Claims return self.get('claim_types_supported', ['normal']) @property def claims_parameter_supported(self): # If omitted, the default value is false. return self.get('claims_parameter_supported', False) @property def request_parameter_supported(self): # If omitted, the default value is false. return self.get('request_parameter_supported', False) @property def request_uri_parameter_supported(self): # If omitted, the default value is true. return self.get('request_uri_parameter_supported', True) @property def require_request_uri_registration(self): # If omitted, the default value is false. return self.get('require_request_uri_registration', False) def _validate_boolean_value(metadata, key): if key not in metadata: return if metadata[key] not in (True, False): raise ValueError(f'"{key}" MUST be boolean') authlib-1.3.2/authlib/oidc/discovery/well_known.py000066400000000000000000000010761466226534200222710ustar00rootroot00000000000000from authlib.common.urls import urlparse def get_well_known_url(issuer, external=False): """Get well-known URI with issuer via Section 4.1. :param issuer: URL of the issuer :param external: return full external url or not :return: URL """ # https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest if external: return issuer.rstrip('/') + '/.well-known/openid-configuration' parsed = urlparse.urlparse(issuer) path = parsed.path return path.rstrip('/') + '/.well-known/openid-configuration' authlib-1.3.2/docs/000077500000000000000000000000001466226534200141075ustar00rootroot00000000000000authlib-1.3.2/docs/Makefile000066400000000000000000000011341466226534200155460ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = Authlib SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)authlib-1.3.2/docs/_static/000077500000000000000000000000001466226534200155355ustar00rootroot00000000000000authlib-1.3.2/docs/_static/authlib.png000066400000000000000000000272151466226534200177020ustar00rootroot00000000000000PNG  IHDRæ$gAMA a cHRMz&u0`:pQ<PLTEnM>@YxڕjבFEvڷ]Jΐ@i׶b՜CIΞ拲~܄ފߏᔸK搵dNHgR`la?I΃z~ܖBq٠RXGͨA̼UhrUOLϪC̉ߚ{\ӌyZXwڍmVұSLQo؀ݗ{ꇯTP}sWOϟ催݇ߺFfkׄވߍut٦tڳep[x|܊ce־_ԁD^Ի݁u|bKGDHtIME  8*]IDATxN-0ƈa2ĸ%G3cFwc-r)%*S*2RrI.oTOk&yfs}sf'yZ{seaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaQt*_LJ .U*<^U+$#k4ݍU NFK0!blhf[=EhDF:[2Iߵӵ3Ӣ[A+Ohn7jݴ" KG|qg4MBӅ [fxC)/v1>q@u6/ ZNh>ޚ@(gӌtvY&94BJ}w~J39 ڸ~`'j1 co1kl8S+,a0CD?D„k>վn9sRВ R.,Wk>9jQ/R֊T?mȪ\}C H ^whJ)2M%kymߐ(Njw1dFx3 k<0s۲zO/ipeUDmg^7Llۻ.uS2-(yI9N[O%ge#v8ɧ].(2Hg"XF#(be\Jp\`;(=#xKZr;e,9S9A6/y7":OkH'^6DlA~-&lԐ2Zbtm*+7Z^2`UT6WoC_ʩȕk0 tqn>2ndkaEY3EW2RN!أ䵛%7ZRVLN?՝'8wWxd䬶%\pI>t"wj͍( ddu+'2K 9w0{^D5 KE_?1R 2t-w\p+#nb=^1<(Mv dظϳgWϔkf7:OڧJvlEodN_ D中ڟ[,7̕hDcZ 3pL מBoڔ͗F6H\Hcq5@vܿOyd灎C*8iIu85sSx mg([Ɵ\qӟVsEO# < `C 祃_v OCƁڏ\;wP`'6~K7Ûm,^a\"txo:; һ?\1ks<^?m_~TگOWW7K8.ňcÛ.pyw)ci7\絉5{=#fB]#Ƙڲ1;ۣF%JqwNL>'5Bn\p]R z'{f}_>d/3 [3[=>6Ө:8? 1O뙃6 /cjLTPoI S2/صBz }lQ"=ˎ񺏔fyK7 DGEEuW(Oaqu)}c"j{>qf15AGUꨤ'Z'%[iVܡeʞvgc_cnDbrm#ono]jSǑg/.γݣţ+o zsSY No ^_}s]A=%k)&wn&#Cvڸdo1=@?QSFFSn4و?[]e}-[N"ٹ\88ZIqÜǐ[j+An"{Ι=`2ƿ]L#T>4h1}vȎ-;˥rz:"MnQͅTZ0MF/-bJ&^l"9m__ >ڂk}]xvQ;o :luec35 hINk/ٵo/aby8ou3ފoSj10i{\mx}U۪pK!Hktd%"Iz#Ygo|*H |p`q˹P@x.X.hk.iS;#I p6@K0.C4h07aM?AMy0 ԰- 9?T ^_0F*9[f[ā ,B!o8W H1 >6Y5z :f m]0yn^ֿh(C @ޚsi$PnJյgw@:Mw6~ϫ} @K$Gh`Ţ1x`u2$o,2V>nU"bؑ&~`^]'[VyJ#+A5s*b4au$6  3eG yMX:$9! x %NNxXW!%)@7Uݎ~hՀ5( @HșjKB->oN;˸#V"L Ad `\וU=dGo*\wA u'!{'7Ul98&0]X"2ǘN4 U5 ȍyJҔC'A7S6^]cYR"qyJ|UO ̊.mjIg%yԄMrC;*HHH<1f, ୟi4$/( mwb:9̊ ocDYcZs:@#aV@ߦ1>9 'aNb(VfKD,ۏX.vq] E*4!uQ3;lsxVkk%C$0/L0.K2KXZR.ZJ+#FjsE@3~$1]0Qګu8vMgP~]E|,9O6z/6:eG[ZIpt>b=FI9Ic.1g#Y_6;#H, J ɴq$wi եI Oݦ/.67 #F '],@b&%nb^ b"uA.a$"{: 'U4[l IT= "*듲e.tBY]`nge*Hq󘽭mDn ! e3,\=+"|r јLђC!HpYoÉ hX 1ΡU-+nL-Ϥ׉WxT19 /2u_dJcP1"+$4-bA|dPz@bFz_ΠMc~k's{gi빷I4.`d S[ ?s2LӉG Vc[d~͌SdYa+1^џQG.hHTH$/dAw ƌþ3~3#费M ~.Z=-J+ cu{wO浄q}9%r#K7cG("{Ӎ- MBkJ+Sk4`p:9b0oreoLc{_6y!,fWA#&nC|1,a_s[\_|ۓx X4n217IԐuEb/=ݒ,G,E%* ̷E&"p$`P') 6~C /N%| r8(dls9n'-^=xv+=rPPRJ2.U5 ^< 'b7s׭~,@ׯħN0/p$x x*z7NX_A)θ*漒B ­\u<wsSvwp5mϱ \ϊ4Ϋb3~3JЩ )gG7[a;.j/M`NZ6DWMʤ%оٹ{9<6-HǚnOzt|_[MU5nhF0?ìu) ݡ|_<`DҀʞ 8l[VMJ=T[52 )4]L$k6f/ _qGYȗ.m 6j? p fg$N򙊦_Nc:ݩ.ܱZDiW#\C&txCb l#^ x55#~r(J.6ݘ<qÆ~'0Yp2p\7<\h0Ysح5'sś~p.t-2-Mykto|sc=+AXlh݃.:\`2~ i+kޙGʚgTVoD?7%R7E]<\)mu4OX{йW65k;m '(;/IpVi{}',0;dB3!±MoxL@@kj җ#H Mk6@sGGG@zsF qmKRiK6"(s>˝ Ճ ko4%>c0^uK&x$;θ,t$Eqb \9UD# KRi \hOY \S7`v+O@Fq/K|LTѢY@ ;0KEDPRpڈ~f Vb<DxKY1J&xCmk]Iw(ڈ~^M)HAg2PqV }E`.BDHLG>ɹ#::9 5|{Z<zsmRkxs|tAAN7FosNق| y,7sTziN $OLz<{>|k0Fϓ&| uL#O_e#Vs j0y!b;8ɚ/5 0jVc0x/U'ڎE[@G,u!ȜLIخ9G tGCj^䂎pz:}j 6Q>|`CG6qh iCw~Ą$ pt /@B9_6VA P&m۲i#"#`i:C.^8Y -7mXI1&+k$ ,`^8āڒn'41cIZd|k90Oh;b> 3]j5p '4tIXH/:ӤHTMĊ%ý6psZ2n5TE #J@R-MF;;u4:[iu)WymgBm4E@,2`A> Qk^M(2>HΪ-@ԃi[jiY4Qҽ6聘\c @/ܸ G^y1`eMlh %_Җ@`kvzVGR>| mDX9U ڶ6jGo]6{ZV-%!n1jme`7kM#ð+q,Qrɥͫ~Q?Ykvѕ!I4BQ"bϸfpn?{o>#RVDV2=mז .6qGH2T!T:?Zz@UAO޳4̥ԌEʴڬr,, a8MpflDr2bߩ8qYD7>[Z D[lq3nx!/kSDܡorx!}Ked/f5aI„uGy RIֵT] 0ObAF'k!! )LA=fWT\ӨPjX?ߣˣusޫfۏ8KdHa>s+dVq -M!:ziCIk(s7- )Nrxʺ?5#mI&DI\pl1[{]7"ϼ=|ۃcǹ\_4vvE?0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0GY8dHm%tEXtdate:create2017-11-06T15:07:11+01:00N)&%tEXtdate:modify2017-11-06T15:07:11+01:00?t.WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`authlib-1.3.2/docs/_static/authlib.svg000066400000000000000000000030001466226534200176770ustar00rootroot00000000000000Authlibauthlib-1.3.2/docs/_static/custom.css000066400000000000000000000012261466226534200175620ustar00rootroot00000000000000:root { --syntax-light-pre-bg: #ecf5ff; --syntax-light-cap-bg: #d6e7fb; --syntax-dark-pre-bg: #1a2b3e; --syntax-dark-cap-bg: #223e5e; } #ethical-ad-placement { display: none; } .site-sponsors { margin-bottom: 2rem; } .site-sponsors > .sponsor { display: flex; align-items: center; background: var(--sy-c-bg-weak); border-radius: 6px; padding: 0.5rem; margin-bottom: 0.5rem; } .site-sponsors .image { flex-shrink: 0; display: block; width: 32px; margin-right: 0.8rem; } .site-sponsors .text { font-size: 0.86rem; line-height: 1.2; } .site-sponsors .text a { color: var(--sy-c-link); border-color: var(--sy-c-link); } authlib-1.3.2/docs/_static/dark-logo.svg000066400000000000000000000130411466226534200201340ustar00rootroot00000000000000authlib-1.3.2/docs/_static/icon.svg000066400000000000000000000012421466226534200172050ustar00rootroot00000000000000authlib-1.3.2/docs/_static/light-logo.svg000066400000000000000000000130441466226534200203250ustar00rootroot00000000000000authlib-1.3.2/docs/_templates/000077500000000000000000000000001466226534200162445ustar00rootroot00000000000000authlib-1.3.2/docs/_templates/funding.html000066400000000000000000000010101466226534200205540ustar00rootroot00000000000000 authlib-1.3.2/docs/_templates/links.html000066400000000000000000000011151466226534200202500ustar00rootroot00000000000000 authlib-1.3.2/docs/_templates/partials/000077500000000000000000000000001466226534200200635ustar00rootroot00000000000000authlib-1.3.2/docs/_templates/partials/globaltoc-above.html000066400000000000000000000006521466226534200240140ustar00rootroot00000000000000
authlib-1.3.2/docs/_templates/sponsors.html000066400000000000000000000020251466226534200210170ustar00rootroot00000000000000
authlib-1.3.2/docs/_templates/tidelift.html000066400000000000000000000011311466226534200207320ustar00rootroot00000000000000
authlib-1.3.2/docs/basic/000077500000000000000000000000001466226534200151705ustar00rootroot00000000000000authlib-1.3.2/docs/basic/index.rst000066400000000000000000000003161466226534200170310ustar00rootroot00000000000000Get Started =========== This part of the documentation begins with some background information about Authlib, and installation of Authlib. .. toctree:: :maxdepth: 2 intro install logging authlib-1.3.2/docs/basic/install.rst000066400000000000000000000035011466226534200173670ustar00rootroot00000000000000.. _install: Installation ============ .. meta:: :description: How to install Authlib with pip, source code, etc. This part of the documentation covers the installation of Authlib, just like any other software package needs to be installed first. $ pip install Authlib --------------------- Installing Authlib is simple with `pip `_:: $ pip install Authlib It will also install the dependencies: - cryptography .. note:: You may enter problems when installing cryptography, check its official document at https://cryptography.io/en/latest/installation/ Using Authlib with requests:: $ pip install Authlib requests Using Authlib with httpx:: $ pip install Authlib httpx Using Authlib with Flask:: $ pip install Authlib Flask Using Authlib with Django:: $ pip install Authlib Django Using Authlib with Starlette:: $ pip install Authlib httpx Starlette .. versionchanged:: v0.12 "requests" is an optional dependency since v0.12. If you want to use Authlib client, you have to install "requests" by yourself:: $ pip install Authlib requests Get the Source Code ------------------- Authlib is actively developed on GitHub, where the code is `always available `_. You can either clone the public repository:: $ git clone git://github.com/lepture/authlib.git Download the `tarball `_:: $ curl -OL https://github.com/lepture/authlib/tarball/master Or, download the `zipball `_:: $ curl -OL https://github.com/lepture/authlib/zipball/master Once you have a copy of the source, you can embed it in your Python package, or install it into your site-packages easily:: $ cd authlib $ pip install . authlib-1.3.2/docs/basic/intro.rst000066400000000000000000000041501466226534200170550ustar00rootroot00000000000000.. _intro: Introduction ============ .. meta:: :description: A general introduction to Authlib, a project that powers from low-level specification implementation to high-level framework integrations. Authlib is the ultimate Python library in building OAuth and OpenID Connect clients and servers. It offers generic implementations of RFCs, including OAuth 1.0, OAuth 2.0, JWT and many more. It becomes a :ref:`monolithic` project that powers from low-level specification implementation to high-level framework integrations. I'm intended to make it profitable so that it can be :ref:`sustainable`, check out the :ref:`funding` section. .. _monolithic: Monolithic ---------- Authlib is a monolithic library. While being monolithic, it keeps everything synchronized, from spec implementation to framework integrations, from client requests to service providers. The benefits are obvious; it won't break things. When specifications changed, implementation will change too. Let the developers of Authlib take the pain, users of Authlib should not suffer from it. You don't have to worry about monolithic, it doesn't cost your memory. If you don't import a module, it won't be loaded. We don't madly import everything into the root **__init__.py**. Extendable ---------- Authlib is designed as flexible as possible. Since it is built from low-level specification implementation to high-level framework integrations, if a high level can't meet your needs, you can always create one for your purpose based on the low-level implementation. Most of the cases, you don't need to do so. Extendable has been taken into account from the start of the project. Take OAuth 2.0 server as an example, instead of a pre-configured server, Authlib takes advantage of ``register``. .. code-block:: python authorization_server.register_grant(AuthorizationCodeGrant) authorization_server.register_endpoint(RevocationEndpoint) If you find anything not that extendable, you can ask help on StackOverflow or open an issue on GitHub. Credits ------- This project is inspired by: * OAuthLib * Flask-OAuthlib * requests-oauthlib * pyjwt authlib-1.3.2/docs/basic/logging.rst000066400000000000000000000004451466226534200173530ustar00rootroot00000000000000Logging ======= You can always enable debug logging when you run into issues in your code:: import logging import sys log = logging.getLogger('authlib') log.addHandler(logging.StreamHandler(sys.stdout)) log.setLevel(logging.DEBUG) We are still designing the logging system. (TBD) authlib-1.3.2/docs/changelog.rst000066400000000000000000000130631466226534200165730ustar00rootroot00000000000000Changelog ========= .. meta:: :description: The full list of changes between each Authlib release. Here you can see the full list of changes between each Authlib release. Version 1.3.2 ------------- - Prevent ever-growing session size for OAuth clients. - Revert ``quote`` client id and secret. - ``unquote`` basic auth header for authorization server. Version 1.3.1 ------------- **Released on June 4, 2024** - Prevent ``OctKey`` to import ssh and PEM strings. Version 1.3.0 ------------- **Released on Dec 17, 2023** - Restore ``AuthorizationServer.create_authorization_response`` behavior, via :PR:`558` - Include ``leeway`` in ``validate_iat()`` for JWT, via :PR:`565` - Fix ``encode_client_secret_basic``, via :PR:`594` - Use single key in JWK if JWS does not specify ``kid``, via :PR:`596` - Fix error when RFC9068 JWS has no scope field, via :PR:`598` - Get werkzeug version using importlib, via :PR:`591` **New features**: - RFC9068 implementation, via :PR:`586`, by @azmeuk. **Breaking changes**: - End support for python 3.7 Version 1.2.1 ------------- **Released on Jun 25, 2023** - Apply headers in ``ClientSecretJWT.sign`` method, via :PR:`552` - Allow falsy but non-None grant uri params, via :PR:`544` - Fixed ``authorize_redirect`` for Starlette v0.26.0, via :PR:`533` - Removed ``has_client_secret`` method and documentation, via :PR:`513` - Removed ``request_invalid`` and ``token_revoked`` remaining occurences and documentation. :PR:`514` - Fixed RFC7591 ``grant_types`` and ``response_types`` default values, via :PR:`509`. - Add support for python 3.12, via :PR:`590`. Version 1.2.0 ------------- **Released on Dec 6, 2022** - Not passing ``request.body`` to ``ResourceProtector``, via :issue:`485`. - Use ``flask.g`` instead of ``_app_ctx_stack``, via :issue:`482`. - Add ``headers`` parameter back to ``ClientSecretJWT``, via :issue:`457`. - Always passing ``realm`` parameter in OAuth 1 clients, via :issue:`339`. - Implemented RFC7592 Dynamic Client Registration Management Protocol, via :PR:`505`. - Add ``default_timeout`` for requests ``OAuth2Session`` and ``AssertionSession``. - Deprecate ``jwk.loads`` and ``jwk.dumps`` Version 1.1.0 ------------- **Released on Sep 13, 2022** This release contains breaking changes and security fixes. - Allow to pass ``claims_options`` to Framework OpenID Connect clients, via :PR:`446`. - Fix ``.stream`` with context for HTTPX OAuth clients, via :PR:`465`. - Fix Starlette OAuth client for cache store, via :PR:`478`. **Breaking changes**: - Raise ``InvalidGrantError`` for invalid code, redirect_uri and no user errors in OAuth 2.0 server. - The default ``authlib.jose.jwt`` would only work with JSON Web Signature algorithms, if you would like to use JWT with JWE algorithms, please pass the algorithms parameter:: jwt = JsonWebToken(['A128KW', 'A128GCM', 'DEF']) **Security fixes**: CVE-2022-39175 and CVE-2022-39174, both related to JOSE. Version 1.0.1 ------------- **Released on Apr 6, 2022** - Fix authenticate_none method, via :issue:`438`. - Allow to pass in alternative signing algorithm to RFC7523 authentication methods via :PR:`447`. - Fix ``missing_token`` for Flask OAuth client, via :issue:`448`. - Allow ``openid`` in any place of the scope, via :issue:`449`. - Security fix for validating essential value on blank value in JWT, via :issue:`445`. Version 1.0.0 ------------- **Released on Mar 15, 2022.** We have dropped support for Python 2 in this release. We have removed built-in SQLAlchemy integration. **OAuth Client Changes:** The whole framework client integrations have been restructured, if you are using the client properly, e.g. ``oauth.register(...)``, it would work as before. **OAuth Provider Changes:** In Flask OAuth 2.0 provider, we have removed the deprecated ``OAUTH2_JWT_XXX`` configuration, instead, developers should define `.get_jwt_config` on OpenID extensions and grant types. **SQLAlchemy** integrations has been removed from Authlib. Developers should define the database by themselves. **JOSE Changes** - ``JWS`` has been renamed to ``JsonWebSignature`` - ``JWE`` has been renamed to ``JsonWebEncryption`` - ``JWK`` has been renamed to ``JsonWebKey`` - ``JWT`` has been renamed to ``JsonWebToken`` The "Key" model has been re-designed, checkout the :ref:`jwk_guide` for updates. Added ``ES256K`` algorithm for JWS and JWT. **Breaking Changes**: find how to solve the deprecate issues via https://git.io/JkY4f Old Versions ------------ Find old changelog at https://github.com/lepture/authlib/releases - Version 0.15.5: Released on Oct 18, 2021 - Version 0.15.4: Released on Jul 17, 2021 - Version 0.15.3: Released on Jan 15, 2021 - Version 0.15.2: Released on Oct 18, 2020 - Version 0.15.1: Released on Oct 14, 2020 - Version 0.15.0: Released on Oct 10, 2020 - Version 0.14.3: Released on May 18, 2020 - Version 0.14.2: Released on May 6, 2020 - Version 0.14.1: Released on Feb 12, 2020 - Version 0.14.0: Released on Feb 11, 2020 - Version 0.13.0: Released on Nov 11, 2019 - Version 0.12.0: Released on Sep 3, 2019 - Version 0.11.0: Released on Apr 6, 2019 - Version 0.10.0: Released on Oct 12, 2018 - Version 0.9.0: Released on Aug 12, 2018 - Version 0.8.0: Released on Jun 17, 2018 - Version 0.7.0: Released on Apr 28, 2018 - Version 0.6.0: Released on Mar 20, 2018 - Version 0.5.1: Released on Feb 11, 2018 - Version 0.5.0: Released on Feb 11, 2018 - Version 0.4.1: Released on Feb 2, 2018 - Version 0.4.0: Released on Jan 31, 2018 - Version 0.3.0: Released on Dec 24, 2017 - Version 0.2.1: Released on Dec 6, 2017 - Version 0.2.0: Released on Nov 25, 2017 - Version 0.1.0: Released on Nov 18, 2017 authlib-1.3.2/docs/client/000077500000000000000000000000001466226534200153655ustar00rootroot00000000000000authlib-1.3.2/docs/client/api.rst000066400000000000000000000046061466226534200166760ustar00rootroot00000000000000Client API References ===================== .. meta:: :description: API references on Authlib Client and its related Flask/Django integrations. This part of the documentation covers the interface of Authlib Client. Requests OAuth Sessions ----------------------- .. module:: authlib.integrations.requests_client .. autoclass:: OAuth1Session :members: create_authorization_url, fetch_request_token, fetch_access_token, parse_authorization_response .. autoclass:: OAuth1Auth :members: .. autoclass:: OAuth2Session :members: register_client_auth_method, create_authorization_url, fetch_token, refresh_token, revoke_token, introspect_token, register_compliance_hook .. autoclass:: OAuth2Auth .. autoclass:: AssertionSession HTTPX OAuth Clients ------------------- .. module:: authlib.integrations.httpx_client .. autoclass:: OAuth1Auth :members: .. autoclass:: OAuth1Client :members: create_authorization_url, fetch_request_token, fetch_access_token, parse_authorization_response .. autoclass:: AsyncOAuth1Client :members: create_authorization_url, fetch_request_token, fetch_access_token, parse_authorization_response .. autoclass:: OAuth2Auth .. autoclass:: OAuth2Client :members: register_client_auth_method, create_authorization_url, fetch_token, refresh_token, revoke_token, introspect_token, register_compliance_hook .. autoclass:: AsyncOAuth2Client :members: register_client_auth_method, create_authorization_url, fetch_token, refresh_token, revoke_token, introspect_token, register_compliance_hook .. autoclass:: AsyncAssertionClient Flask Registry and RemoteApp ---------------------------- .. module:: authlib.integrations.flask_client .. autoclass:: OAuth :members: init_app, register, create_client Django Registry and RemoteApp ----------------------------- .. module:: authlib.integrations.django_client .. autoclass:: OAuth :members: register, create_client Starlette Registry and RemoteApp -------------------------------- .. module:: authlib.integrations.starlette_client .. autoclass:: OAuth :members: register, create_client authlib-1.3.2/docs/client/django.rst000066400000000000000000000112631466226534200173640ustar00rootroot00000000000000.. _django_client: Django OAuth Client =================== .. meta:: :description: The built-in Django integrations for OAuth 1.0 and OAuth 2.0 clients, powered by Authlib. .. module:: authlib.integrations.django_client :noindex: Looking for OAuth providers? - :ref:`django_oauth1_server` - :ref:`django_oauth2_server` The Django client can handle OAuth 1 and OAuth 2 services. Authlib has a shared API design among framework integrations. Get started with :ref:`frameworks_clients`. Create a registry with :class:`OAuth` object:: from authlib.integrations.django_client import OAuth oauth = OAuth() The common use case for OAuth is authentication, e.g. let your users log in with Twitter, GitHub, Google etc. .. important:: Please read :ref:`frameworks_clients` at first. Authlib has a shared API design among framework integrations, learn them from :ref:`frameworks_clients`. Configuration ------------- Authlib Django OAuth registry can load the configuration from your Django application settings automatically. Every key value pair can be omitted. They can be configured from your Django settings:: AUTHLIB_OAUTH_CLIENTS = { 'twitter': { 'client_id': 'Twitter Consumer Key', 'client_secret': 'Twitter Consumer Secret', 'request_token_url': 'https://api.twitter.com/oauth/request_token', 'request_token_params': None, 'access_token_url': 'https://api.twitter.com/oauth/access_token', 'access_token_params': None, 'refresh_token_url': None, 'authorize_url': 'https://api.twitter.com/oauth/authenticate', 'api_base_url': 'https://api.twitter.com/1.1/', 'client_kwargs': None } } There are differences between OAuth 1.0 and OAuth 2.0, please check the parameters in ``.register`` in :ref:`frameworks_clients`. Saving Temporary Credential --------------------------- In OAuth 1.0, we need to use a temporary credential to exchange access token, this temporary credential was created before redirecting to the provider (Twitter), we need to save this temporary credential somewhere in order to use it later. In OAuth 1, Django client will save the request token in sessions. In this case, you just need to configure Session Middleware in Django:: MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware' ] Follow the official Django documentation to set a proper session. Either a database backend or a cache backend would work well. .. warning:: Be aware, using secure cookie as session backend will expose your request token. Routes for Authorization ------------------------ Just like the example in :ref:`frameworks_clients`, everything is the same. But there is a hint to create ``redirect_uri`` with ``request`` in Django:: def login(request): # build a full authorize callback uri redirect_uri = request.build_absolute_uri('/authorize') return oauth.twitter.authorize_redirect(request, redirect_uri) Auto Update Token via Signal ---------------------------- Instead of defining an ``update_token`` method and passing it into OAuth registry, it is also possible to use signals to listen for token updates:: from django.dispatch import receiver from authlib.integrations.django_client import token_update @receiver(token_update) def on_token_update(sender, name, token, refresh_token=None, access_token=None, **kwargs): if refresh_token: item = OAuth2Token.find(name=name, refresh_token=refresh_token) elif access_token: item = OAuth2Token.find(name=name, access_token=access_token) else: return # update old token item.access_token = token['access_token'] item.refresh_token = token.get('refresh_token') item.expires_at = token['expires_at'] item.save() Django OpenID Connect Client ---------------------------- An OpenID Connect client is no different than a normal OAuth 2.0 client. When registered with the ``openid`` scope, the built-in Django OAuth client will handle everything automatically:: oauth.register( 'google', ... server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', client_kwargs={'scope': 'openid profile email'} ) When we get the returned token:: token = oauth.google.authorize_access_token(request) There should be a ``id_token`` in the response. Authlib has called `.parse_id_token` automatically, we can get ``userinfo`` in the ``token``:: userinfo = token['userinfo'] Find Django Google login example at https://github.com/authlib/demo-oauth-client/tree/master/django-google-login authlib-1.3.2/docs/client/fastapi.rst000066400000000000000000000040031466226534200175430ustar00rootroot00000000000000.. _fastapi_client: FastAPI OAuth Client ==================== .. meta:: :description: Use Authlib built-in Starlette integrations to build OAuth 1.0, OAuth 2.0 and OpenID Connect clients for FastAPI. .. module:: authlib.integrations.starlette_client :noindex: FastAPI_ is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. It is build on top of **Starlette**, that means most of the code looks similar with Starlette code. You should first read documentation of: 1. :ref:`frameworks_clients` 2. :ref:`starlette_client` Here is how you would create a FastAPI application:: from fastapi import FastAPI from starlette.middleware.sessions import SessionMiddleware app = FastAPI() # we need this to save temporary code & state in session app.add_middleware(SessionMiddleware, secret_key="some-random-string") Since Authlib starlette requires using ``request`` instance, we need to expose that ``request`` to Authlib. According to the documentation on `Using the Request Directly `_:: from starlette.requests import Request @app.get("/login/google") async def login_via_google(request: Request): redirect_uri = request.url_for('auth_via_google') return await oauth.google.authorize_redirect(request, redirect_uri) @app.get("/auth/google") async def auth_via_google(request: Request): token = await oauth.google.authorize_access_token(request) user = token['userinfo'] return dict(user) .. _FastAPI: https://fastapi.tiangolo.com/ All other APIs are the same with Starlette. FastAPI OAuth 1.0 Client ------------------------ We have a blog post about how to create Twitter login in FastAPI: https://blog.authlib.org/2020/fastapi-twitter-login FastAPI OAuth 2.0 Client ------------------------ We have a blog post about how to create Google login in FastAPI: https://blog.authlib.org/2020/fastapi-google-login authlib-1.3.2/docs/client/flask.rst000066400000000000000000000167161466226534200172320ustar00rootroot00000000000000.. _flask_client: Flask OAuth Client ================== .. meta:: :description: The built-in Flask integrations for OAuth 1.0, OAuth 2.0 and OpenID Connect clients, powered by Authlib. .. module:: authlib.integrations.flask_client :noindex: This documentation covers OAuth 1.0, OAuth 2.0 and OpenID Connect Client support for Flask. Looking for OAuth providers? - :ref:`flask_oauth1_server` - :ref:`flask_oauth2_server` Flask OAuth client can handle OAuth 1 and OAuth 2 services. It shares a similar API with Flask-OAuthlib, you can transfer your code from Flask-OAuthlib to Authlib with ease. Create a registry with :class:`OAuth` object:: from authlib.integrations.flask_client import OAuth oauth = OAuth(app) You can also initialize it later with :meth:`~OAuth.init_app` method:: oauth = OAuth() oauth.init_app(app) The common use case for OAuth is authentication, e.g. let your users log in with Twitter, GitHub, Google etc. .. important:: Please read :ref:`frameworks_clients` at first. Authlib has a shared API design among framework integrations, learn them from :ref:`frameworks_clients`. Configuration ------------- Authlib Flask OAuth registry can load the configuration from Flask ``app.config`` automatically. Every key value pair in ``.register`` can be omitted. They can be configured in your Flask App configuration. Config keys are formatted as ``{name}_{key}`` in uppercase, e.g. ========================== ================================ TWITTER_CLIENT_ID Twitter Consumer Key TWITTER_CLIENT_SECRET Twitter Consumer Secret TWITTER_REQUEST_TOKEN_URL URL to fetch OAuth request token ========================== ================================ If you register your remote app as ``oauth.register('example', ...)``, the config keys would look like: ========================== =============================== EXAMPLE_CLIENT_ID OAuth Consumer Key EXAMPLE_CLIENT_SECRET OAuth Consumer Secret EXAMPLE_ACCESS_TOKEN_URL URL to fetch OAuth access token ========================== =============================== Here is a full list of the configuration keys: - ``{name}_CLIENT_ID``: Client key of OAuth 1, or Client ID of OAuth 2 - ``{name}_CLIENT_SECRET``: Client secret of OAuth 2, or Client Secret of OAuth 2 - ``{name}_REQUEST_TOKEN_URL``: Request Token endpoint for OAuth 1 - ``{name}_REQUEST_TOKEN_PARAMS``: Extra parameters for Request Token endpoint - ``{name}_ACCESS_TOKEN_URL``: Access Token endpoint for OAuth 1 and OAuth 2 - ``{name}_ACCESS_TOKEN_PARAMS``: Extra parameters for Access Token endpoint - ``{name}_AUTHORIZE_URL``: Endpoint for user authorization of OAuth 1 ro OAuth 2 - ``{name}_AUTHORIZE_PARAMS``: Extra parameters for Authorization Endpoint. - ``{name}_API_BASE_URL``: A base URL endpoint to make requests simple - ``{name}_CLIENT_KWARGS``: Extra keyword arguments for OAuth1Session or OAuth2Session We suggest that you keep ONLY ``{name}_CLIENT_ID`` and ``{name}_CLIENT_SECRET`` in your Flask application configuration. Using Cache for Temporary Credential ------------------------------------ By default, Flask OAuth registry will use Flask session to store OAuth 1.0 temporary credential (request token). However in this way, there are chances your temporary credential will be exposed. Our ``OAuth`` registry provides a simple way to store temporary credentials in a cache system. When initializing ``OAuth``, you can pass an ``cache`` instance:: oauth = OAuth(app, cache=cache) # or initialize lazily oauth = OAuth() oauth.init_app(app, cache=cache) A ``cache`` instance MUST have methods: - ``.get(key)`` - ``.set(key, value, expires=None)`` Routes for Authorization ------------------------ Unlike the examples in :ref:`frameworks_clients`, Flask does not pass a ``request`` into routes. In this case, the routes for authorization should look like:: from flask import url_for, redirect @app.route('/login') def login(): redirect_uri = url_for('authorize', _external=True) return oauth.twitter.authorize_redirect(redirect_uri) @app.route('/authorize') def authorize(): token = oauth.twitter.authorize_access_token() resp = oauth.twitter.get('account/verify_credentials.json') resp.raise_for_status() profile = resp.json() # do something with the token and profile return redirect('/') Accessing OAuth Resources ------------------------- There is no ``request`` in accessing OAuth resources either. Just like above, we don't need to pass ``request`` parameter, everything is handled by Authlib automatically:: from flask import render_template @app.route('/github') def show_github_profile(): resp = oauth.github.get('user') resp.raise_for_status() profile = resp.json() return render_template('github.html', profile=profile) In this case, our ``fetch_token`` could look like:: from your_project import current_user def fetch_token(name): if name in OAUTH1_SERVICES: model = OAuth1Token else: model = OAuth2Token token = model.find( name=name, user=current_user, ) return token.to_token() # initialize OAuth registry with this fetch_token function oauth = OAuth(fetch_token=fetch_token) You don't have to pass ``token``, you don't have to pass ``request``. That is the fantasy of Flask. Auto Update Token via Signal ---------------------------- .. versionadded:: v0.13 The signal is added since v0.13 Instead of define a ``update_token`` method and passing it into OAuth registry, it is also possible to use signal to listen for token updating. Before using signal, make sure you have installed **blinker** library:: $ pip install blinker Connect the ``token_update`` signal:: from authlib.integrations.flask_client import token_update @token_update.connect_via(app) def on_token_update(sender, name, token, refresh_token=None, access_token=None): if refresh_token: item = OAuth2Token.find(name=name, refresh_token=refresh_token) elif access_token: item = OAuth2Token.find(name=name, access_token=access_token) else: return # update old token item.access_token = token['access_token'] item.refresh_token = token.get('refresh_token') item.expires_at = token['expires_at'] item.save() Flask OpenID Connect Client --------------------------- An OpenID Connect client is no different than a normal OAuth 2.0 client. When register with ``openid`` scope, the built-in Flask OAuth client will handle everything automatically:: oauth.register( 'google', ... server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', client_kwargs={'scope': 'openid profile email'} ) When we get the returned token:: token = oauth.google.authorize_access_token() There should be a ``id_token`` in the response. Authlib has called `.parse_id_token` automatically, we can get ``userinfo`` in the ``token``:: userinfo = token['userinfo'] Examples --------- Here are some example projects for you to learn Flask OAuth client integrations: 1. OAuth 1.0: `Flask Twitter Login`_. 2. OAuth 2.0 & OpenID Connect: `Flask Google Login`_. .. _`Flask Twitter Login`: https://github.com/authlib/demo-oauth-client/tree/master/flask-twitter-tool .. _`Flask Google Login`: https://github.com/authlib/demo-oauth-client/tree/master/flask-google-login authlib-1.3.2/docs/client/frameworks.rst000066400000000000000000000450031466226534200203010ustar00rootroot00000000000000.. _frameworks_clients: Web OAuth Clients ================= .. module:: authlib.integrations :noindex: This documentation covers OAuth 1.0 and OAuth 2.0 integrations for Python Web Frameworks like: * Django: The web framework for perfectionists with deadlines * Flask: The Python micro framework for building web applications * Starlette: The little ASGI framework that shines Authlib shares a common API design among these web frameworks. Instead of introducing them one by one, this documentation contains the common usage for them all. We start with creating a registry with the ``OAuth`` class:: # for Flask framework from authlib.integrations.flask_client import OAuth # for Django framework from authlib.integrations.django_client import OAuth # for Starlette framework from authlib.integrations.starlette_client import OAuth oauth = OAuth() There are little differences among each framework, you can read their documentation later: 1. :class:`flask_client.OAuth` for :ref:`flask_client` 2. :class:`django_client.OAuth` for :ref:`django_client` 3. :class:`starlette_client.OAuth` for :ref:`starlette_client` The common use case for OAuth is authentication, e.g. let your users log in with Twitter, GitHub, Google etc. Log In with OAuth 1.0 --------------------- For instance, Twitter is an OAuth 1.0 service, you want your users to log in your website with Twitter. The first step is register a remote application on the ``OAuth`` registry via ``oauth.register`` method:: oauth.register( name='twitter', client_id='{{ your-twitter-consumer-key }}', client_secret='{{ your-twitter-consumer-secret }}', request_token_url='https://api.twitter.com/oauth/request_token', request_token_params=None, access_token_url='https://api.twitter.com/oauth/access_token', access_token_params=None, authorize_url='https://api.twitter.com/oauth/authenticate', authorize_params=None, api_base_url='https://api.twitter.com/1.1/', client_kwargs=None, ) The first parameter in ``register`` method is the **name** of the remote application. You can access the remote application with:: twitter = oauth.create_client('twitter') # or simply with twitter = oauth.twitter The configuration of those parameters can be loaded from the framework configuration. Each framework has its own config system, read the framework specified documentation later. For instance, if ``client_id`` and ``client_secret`` can be loaded via configuration, we can simply register the remote app with:: oauth.register( name='twitter', request_token_url='https://api.twitter.com/oauth/request_token', access_token_url='https://api.twitter.com/oauth/access_token', authorize_url='https://api.twitter.com/oauth/authenticate', api_base_url='https://api.twitter.com/1.1/', ) The ``client_kwargs`` is a dict configuration to pass extra parameters to :ref:`oauth_1_session`. If you are using ``RSA-SHA1`` signature method:: client_kwargs = { 'signature_method': 'RSA-SHA1', 'signature_type': 'HEADER', 'rsa_key': 'Your-RSA-Key' } Saving Temporary Credential ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Usually, the framework integration has already implemented this part through the framework session system. All you need to do is enable session for the chosen framework. Routes for Authorization ~~~~~~~~~~~~~~~~~~~~~~~~ After configuring the ``OAuth`` registry and the remote application, the rest steps are much simpler. The only required parts are routes: 1. redirect to 3rd party provider (Twitter) for authentication 2. redirect back to your website to fetch access token and profile Here is the example for Twitter login:: def login(request): twitter = oauth.create_client('twitter') redirect_uri = 'https://example.com/authorize' return twitter.authorize_redirect(request, redirect_uri) def authorize(request): twitter = oauth.create_client('twitter') token = twitter.authorize_access_token(request) resp = twitter.get('account/verify_credentials.json') resp.raise_for_status() profile = resp.json() # do something with the token and profile return '...' After user confirmed on Twitter authorization page, it will redirect back to your website ``authorize`` page. In this route, you can get your user's twitter profile information, you can store the user information in your database, mark your user as logged in and etc. Using OAuth 2.0 to Log In ------------------------- For instance, GitHub is an OAuth 2.0 service, you want your users to log in your website with GitHub. The first step is register a remote application on the ``OAuth`` registry via ``oauth.register`` method:: oauth.register( name='github', client_id='{{ your-github-client-id }}', client_secret='{{ your-github-client-secret }}', access_token_url='https://github.com/login/oauth/access_token', access_token_params=None, authorize_url='https://github.com/login/oauth/authorize', authorize_params=None, api_base_url='https://api.github.com/', client_kwargs={'scope': 'user:email'}, ) The first parameter in ``register`` method is the **name** of the remote application. You can access the remote application with:: github = oauth.create_client('github') # or simply with github = oauth.github The configuration of those parameters can be loaded from the framework configuration. Each framework has its own config system, read the framework specified documentation later. The ``client_kwargs`` is a dict configuration to pass extra parameters to :ref:`oauth_2_session`, you can pass extra parameters like:: client_kwargs = { 'scope': 'profile', 'token_endpoint_auth_method': 'client_secret_basic', 'token_placement': 'header', } There are several ``token_endpoint_auth_method``, get a deep inside the :ref:`client_auth_methods`. .. note:: Authlib is using ``request_token_url`` to detect if the client is an OAuth 1.0 or OAuth 2.0 client. In OAuth 2.0, there is no ``request_token_url``. Routes for Authorization ~~~~~~~~~~~~~~~~~~~~~~~~ After configuring the ``OAuth`` registry and the remote application, the rest steps are much simpler. The only required parts are routes: 1. redirect to 3rd party provider (GitHub) for authentication 2. redirect back to your website to fetch access token and profile Here is the example for GitHub login:: def login(request): github = oauth.create_client('github') redirect_uri = 'https://example.com/authorize' return github.authorize_redirect(request, redirect_uri) def authorize(request): token = oauth.github.authorize_access_token(request) resp = oauth.github.get('user', token=token) resp.raise_for_status() profile = resp.json() # do something with the token and profile return '...' After user confirmed on GitHub authorization page, it will redirect back to your website ``authorize``. In this route, you can get your user's GitHub profile information, you can store the user information in your database, mark your user as logged in and etc. .. note:: You may find that our documentation for OAuth 1.0 and OAuth 2.0 are the same. They are designed to share the same API, so that you use the same code for both OAuth 1.0 and OAuth 2.0. The ONLY difference is the configuration. OAuth 1.0 contains ``request_token_url`` and ``request_token_params`` while OAuth 2.0 not. Also, the ``client_kwargs`` are different. Client Authentication Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When fetching access token, the authorization server will require a client authentication, Authlib provides **three default methods** defined by RFC7591: - ``client_secret_basic`` - ``client_secret_post`` - ``none`` But if the remote provider does not support these three methods, we need to register our own authentication methods, like :ref:`oauth2_client_auth`:: from authlib.oauth2.rfc7523 import ClientSecretJWT oauth.register( 'name', ... client_auth_methods=[ ClientSecretJWT(token_endpoint), # client_secret_jwt ] ) .. versionadded:: v0.15 Starting from v0.15, developers can add custom authentication methods directly to token endpoint:: oauth.register( 'name', ... token_endpoint_auth_method=ClientSecretJWT(token_endpoint), ) Accessing OAuth Resources ------------------------- .. note:: If your application ONLY needs login via 3rd party services like Twitter, Google, Facebook and GitHub to login, you DON'T need to create the token database. There are also chances that you need to access your user's 3rd party OAuth provider resources. For instance, you want to display the logged in user's twitter time line and GitHub repositories. You will use **access token** to fetch the resources:: def get_twitter_tweets(request): token = OAuth1Token.find( name='twitter', user=request.user ) # API URL: https://api.twitter.com/1.1/statuses/user_timeline.json resp = oauth.twitter.get('statuses/user_timeline.json', token=token.to_token()) resp.raise_for_status() return resp.json() def get_github_repositories(request): token = OAuth2Token.find( name='github', user=request.user ) # API URL: https://api.github.com/user/repos resp = oauth.github.get('user/repos', token=token.to_token()) resp.raise_for_status() return resp.json() In this case, we need a place to store the access token in order to use it later. Usually we will save the token into database. In the previous **Routes for Authorization** ``authorize`` part, we can save the token into database. Design Database ~~~~~~~~~~~~~~~ It is possible to share one database table for both OAuth 1.0 token and OAuth 2.0 token. It is also good to use different database tables for OAuth 1.0 and OAuth 2.0. In the above example, we are using two tables. Here are some hints on how to design the database:: class OAuth1Token(Model): name = String(length=40) oauth_token = String(length=200) oauth_token_secret = String(length=200) user = ForeignKey(User) def to_token(self): return dict( oauth_token=self.access_token, oauth_token_secret=self.alt_token, ) class OAuth2Token(Model): name = String(length=40) token_type = String(length=40) access_token = String(length=200) refresh_token = String(length=200) expires_at = PositiveIntegerField() user = ForeignKey(User) def to_token(self): return dict( access_token=self.access_token, token_type=self.token_type, refresh_token=self.refresh_token, expires_at=self.expires_at, ) And then we can save user's access token into database when user was redirected back to our ``authorize`` page. Fetch User OAuth Token ~~~~~~~~~~~~~~~~~~~~~~ You can always pass a ``token`` parameter to the remote application request methods, like:: token = OAuth1Token.find(name='twitter', user=request.user) oauth.twitter.get(url, token=token) oauth.twitter.post(url, token=token) oauth.twitter.put(url, token=token) oauth.twitter.delete(url, token=token) token = OAuth2Token.find(name='github', user=request.user) oauth.github.get(url, token=token) oauth.github.post(url, token=token) oauth.github.put(url, token=token) oauth.github.delete(url, token=token) However, it is not a good practice to query the token database in every request function. Authlib provides a way to fetch current user's token automatically for you, just ``register`` with ``fetch_token`` function:: def fetch_twitter_token(request): token = OAuth1Token.find( name='twitter', user=request.user ) return token.to_token() def fetch_github_token(request): token = OAuth2Token.find( name='github', user=request.user ) return token.to_token() # we can registry this ``fetch_token`` with oauth.register oauth.register( 'twitter', # ... fetch_token=fetch_twitter_token, ) oauth.register( 'github', # ... fetch_token=fetch_github_token, ) Not good enough. In this way, you have to write ``fetch_token`` for every remote application. There is also a shared way to fetch token:: def fetch_token(name, request): if name in OAUTH1_SERVICES: model = OAuth1Token else: model = OAuth2Token token = model.find( name=name, user=request.user ) return token.to_token() # initialize OAuth registry with this fetch_token function oauth = OAuth(fetch_token=fetch_token) Now, developers don't have to pass a ``token`` in the HTTP requests, instead, they can pass the ``request``:: def get_twitter_tweets(request): resp = oauth.twitter.get('statuses/user_timeline.json', request=request) resp.raise_for_status() return resp.json() .. note:: Flask is different, you don't need to pass the ``request`` either. OAuth 2.0 Enhancement --------------------- OAuth 1.0 is a protocol, while OAuth 2.0 is a framework. There are so many features in OAuth 2.0 than OAuth 1.0. This section is designed for OAuth 2.0 specially. Auto Update Token ~~~~~~~~~~~~~~~~~ In OAuth 1.0, access token never expires. But in OAuth 2.0, token MAY expire. If there is a ``refresh_token`` value, Authlib will auto update the access token if it is expired. We do this by passing a ``update_token`` function to ``OAuth`` registry:: def update_token(name, token, refresh_token=None, access_token=None): if refresh_token: item = OAuth2Token.find(name=name, refresh_token=refresh_token) elif access_token: item = OAuth2Token.find(name=name, access_token=access_token) else: return # update old token item.access_token = token['access_token'] item.refresh_token = token.get('refresh_token') item.expires_at = token['expires_at'] item.save() oauth = OAuth(update_token=update_token) In this way, OAuth 2.0 integration will update expired token automatically. There is also a **signal** way to update token. Checkout the frameworks documentation. OAuth 2.0 Code Challenge ~~~~~~~~~~~~~~~~~~~~~~~~ Adding ``code_challenge`` provided by :ref:`specs/rfc7636` is simple. You register your remote app with a ``code_challenge_method`` in ``client_kwargs``:: oauth.register( 'example', client_id='Example Client ID', client_secret='Example Client Secret', access_token_url='https://example.com/oauth/access_token', authorize_url='https://example.com/oauth/authorize', api_base_url='https://api.example.com/', client_kwargs={'code_challenge_method': 'S256'}, ) Note, the only supported ``code_challenge_method`` is ``S256``. Compliance Fix for OAuth 2.0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For non standard OAuth 2.0 service, you can pass a ``compliance_fix`` when ``.register``. For example, Slack has a compliance problem, we can construct a method to fix the requests session:: def slack_compliance_fix(session): def _fix(resp): resp.raise_for_status() token = resp.json() # slack returns no token_type token['token_type'] = 'Bearer' resp._content = to_unicode(json.dumps(token)).encode('utf-8') return resp session.register_compliance_hook('access_token_response', _fix) Then pass this ``slack_compliance_fix`` into ``.register`` parameters:: oauth.register( 'slack', client_id='...', client_secret='...', ..., compliance_fix=slack_compliance_fix, ... ) Find all the available compliance hooks at :ref:`compliance_fix_oauth2`. OpenID Connect & UserInfo ------------------------- When logging in with OpenID Connect, "access_token" is not what developers want. Instead, what developers want is **user info**, Authlib wrap it with :class:`~authlib.oidc.core.UserInfo`. There are two ways to fetch **userinfo** from 3rd party providers. If the provider supports OpenID Connect, we can get the user info from the returned ``id_token``. userinfo_endpoint ~~~~~~~~~~~~~~~~~ Passing a ``userinfo_endpoint`` when ``.register`` remote client:: oauth.register( 'google', client_id='...', client_secret='...', userinfo_endpoint='https://openidconnect.googleapis.com/v1/userinfo', ) And later, when the client has obtained the access token, we can call:: def authorize(request): token = oauth.google.authorize_access_token(request) user = oauth.google.userinfo(token=token) return '...' Parsing ``id_token`` ~~~~~~~~~~~~~~~~~~~~ For OpenID Connect provider, when ``.authorize_access_token``, the provider will include a ``id_token`` in the response. This ``id_token`` contains the ``UserInfo`` we need so that we don't have to fetch userinfo endpoint again. The ``id_token`` is a JWT, with Authlib :ref:`jwt_guide`, we can decode it easily. Frameworks integrations will handle it automatically if configurations are correct. A simple solution is to provide the OpenID Connect Discovery Endpoint:: oauth.register( 'google', client_id='...', client_secret='...', server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', client_kwargs={'scope': 'openid email profile'}, ) The discovery endpoint provides all the information we need so that we don't have to add ``authorize_url`` and ``access_token_url``. Check out our client example: https://github.com/authlib/demo-oauth-client But if there is no discovery endpoint, developers MUST add all the missing information themselves:: * authorize_url * access_token_url * jwks_uri This ``jwks_uri`` is the URL to get provider's public JWKs. Developers MAY also provide the value of ``jwks`` instead of ``jwks_uri``:: oauth.register( 'google', client_id='...', client_secret='...', access_token_url='https://example.com/oauth/access_token', authorize_url='https://example.com/oauth/authorize', jwks={"keys": [...]} ) authlib-1.3.2/docs/client/httpx.rst000066400000000000000000000144101466226534200172660ustar00rootroot00000000000000.. _httpx_client: OAuth for HTTPX =============== .. meta:: :description: An OAuth 1.0 and OAuth 2.0 Client implementation for a next generation HTTP client for Python, including support for OpenID Connect and service account, powered by Authlib. .. module:: authlib.integrations.httpx_client :noindex: HTTPX is a next-generation HTTP client for Python. Authlib enables OAuth 1.0 and OAuth 2.0 for HTTPX with its async versions: * :class:`OAuth1Client` * :class:`OAuth2Client` * :class:`AssertionClient` * :class:`AsyncOAuth1Client` * :class:`AsyncOAuth2Client` * :class:`AsyncAssertionClient` .. note:: HTTPX is still in its "alpha" stage, use it with caution. HTTPX OAuth 1.0 --------------- There are three steps in OAuth 1 to obtain an access token: 1. fetch a temporary credential 2. visit the authorization page 3. exchange access token with the temporary credential It shares a common API design with :ref:`requests_client`. Read the common guide of :ref:`oauth_1_session` to understand the whole OAuth 1.0 flow. HTTPX OAuth 2.0 --------------- In :ref:`oauth_2_session`, there are many grant types, including: 1. Authorization Code Flow 2. Implicit Flow 3. Password Flow 4. Client Credentials Flow And also, Authlib supports non Standard OAuth 2.0 providers via Compliance Fix. Read the common guide of :ref:`oauth_2_session` to understand the whole OAuth 2.0 flow. Using ``client_secret_jwt`` in HTTPX ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Here is how you could register and use ``client_secret_jwt`` client authentication method for HTTPX:: from authlib.integrations.httpx_client import AsyncOAuth2Client from authlib.oauth2.rfc7523 import ClientSecretJWT client = AsyncOAuth2Client( 'your-client-id', 'your-client-secret', token_endpoint_auth_method='client_secret_jwt' ) token_endpoint = 'https://example.com/oauth/token' client.register_client_auth_method(ClientSecretJWT(token_endpoint)) client.fetch_token(token_endpoint) The ``ClientSecretJWT`` is provided by :ref:`specs/rfc7523`. Using ``private_key_jwt`` in HTTPX ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Here is how you could register and use ``private_key_jwt`` client authentication method for HTTPX:: from authlib.integrations.httpx_client import AsyncOAuth2Client from authlib.oauth2.rfc7523 import PrivateKeyJWT with open('your-private-key.pem', 'rb') as f: private_key = f.read() client = AsyncOAuth2Client( 'your-client-id', private_key, token_endpoint_auth_method='private_key_jwt', ) token_endpoint = 'https://example.com/oauth/token' client.register_client_auth_method(PrivateKeyJWT(token_endpoint)) client.fetch_token(token_endpoint) The ``PrivateKeyJWT`` is provided by :ref:`specs/rfc7523`. Async OAuth 1.0 --------------- The async version of :class:`AsyncOAuth1Client` works the same as :ref:`oauth_1_session`, except that we need to add ``await`` when required:: # fetching request token request_token = await client.fetch_request_token(request_token_url) # fetching access token access_token = await client.fetch_access_token(access_token_url) # normal requests await client.get(...) await client.post(...) await client.put(...) await client.delete(...) Async OAuth 2.0 --------------- The async version of :class:`AsyncOAuth2Client` works the same as :ref:`oauth_2_session`, except that we need to add ``await`` when required:: # fetching access token token = await client.fetch_token(token_endpoint, ...) # normal requests await client.get(...) await client.post(...) await client.put(...) await client.delete(...) Auto Update Token ~~~~~~~~~~~~~~~~~ The :class:`AsyncOAuth2Client` also supports ``update_token`` parameter, the ``update_token`` can either be sync and async. For instance:: async def update_token(token, refresh_token=None, access_token=None): if refresh_token: item = await OAuth2Token.find(name=name, refresh_token=refresh_token) elif access_token: item = await OAuth2Token.find(name=name, access_token=access_token) else: return # update old token item.access_token = token['access_token'] item.refresh_token = token.get('refresh_token') item.expires_at = token['expires_at'] await item.save() Then pass this ``update_token`` into ``AsyncOAuth2Client``. Async Service Account --------------------- :class:`AsyncAssertionClient` is the async version for Assertion Framework of OAuth 2.0 Authorization Grants. It is also know as service account. A configured ``AsyncAssertionClient`` will handle token authorization automatically, which means you can just use it. Take Google Service Account as an example, with the information in your service account JSON configure file:: import json from authlib.integrations.httpx_client import AsyncAssertionClient with open('MyProject-1234.json') as f: conf = json.load(f) token_uri = conf['token_uri'] header = {'alg': 'RS256'} key_id = conf.get('private_key_id') if key_id: header['kid'] = key_id # Google puts scope in payload claims = {'scope': scope} async def main(): client = AsyncAssertionClient( token_endpoint=token_uri, issuer=conf['client_email'], audience=token_uri, claims=claims, subject=None, key=conf['private_key'], header=header, ) resp = await client.get(...) resp = await client.post(...) Close Client Hint ----------------- Developers SHOULD **close** a HTTPX Session when the jobs are done. You can call ``.close()`` manually, or use a ``with`` context to automatically close the session:: client = OAuth2Client(client_id, client_secret) client.get(url) client.close() with OAuth2Client(client_id, client_secret) as client: client.get(url) For **async** OAuth Client, use ``await client.close()``:: client = AsyncOAuth2Client(client_id, client_secret) await client.get(url) await client.close() async with AsyncOAuth2Client(client_id, client_secret) as client: await client.get(url) Our :ref:`frameworks_clients` will close every session automatically, no need to worry. authlib-1.3.2/docs/client/index.rst000066400000000000000000000036001466226534200172250ustar00rootroot00000000000000OAuth Clients ============= .. meta:: :description: This documentation contains Python OAuth 1.0 and OAuth 2.0 Clients implementation with requests, HTTPX, Flask, Django and Starlette. This part of the documentation contains information on the client parts. Authlib provides many frameworks integrations, including: * The famous Python Requests_ * A next generation HTTP client for Python: httpx_ * Flask_ web framework integration * Django_ web framework integration * Starlette_ web framework integration * FastAPI_ web framework integration In order to use Authlib client, you have to install each library yourself. For example, you want to use ``requests`` OAuth clients:: $ pip install Authlib requests For instance, you want to use ``httpx`` OAuth clients:: $ pip install -U Authlib httpx Here is a simple overview of Flask OAuth client:: from flask import Flask, jsonify from authlib.integrations.flask_client import OAuth app = Flask(__name__) oauth = OAuth(app) github = oauth.register('github', {...}) @app.route('/login') def login(): redirect_uri = url_for('authorize', _external=True) return github.authorize_redirect(redirect_uri) @app.route('/authorize') def authorize(): token = github.authorize_access_token() # you can save the token into database profile = github.get('/user', token=token) return jsonify(profile) Follow the documentation below to find out more in detail. .. toctree:: :maxdepth: 2 oauth1 oauth2 requests httpx frameworks flask django starlette fastapi api .. _Requests: https://requests.readthedocs.io/en/master/ .. _httpx: https://www.encode.io/httpx/ .. _Flask: https://flask.palletsprojects.com .. _Django: https://djangoproject.com .. _Starlette: https://www.starlette.io .. _FastAPI: https://fastapi.tiangolo.com/ authlib-1.3.2/docs/client/oauth1.rst000066400000000000000000000162131466226534200173230ustar00rootroot00000000000000.. _oauth_1_session: OAuth 1 Session =============== .. meta:: :description: An OAuth 1.0 protocol Client implementation for Python requests and httpx, powered by Authlib. .. module:: authlib.integrations :noindex: This documentation covers the common design of a Python OAuth 1.0 client. Authlib provides three implementations of OAuth 1.0 client: 1. :class:`requests_client.OAuth1Session` implementation of :ref:`requests_client`, which is a replacement for **requests-oauthlib**. 2. :class:`httpx_client.AsyncOAuth1Client` implementation of :ref:`httpx_client`, which is an **async** OAuth 1.0 client. :class:`requests_client.OAuth1Session` and :class:`httpx_client.AsyncOAuth1Client` shares the same API. There are also frameworks integrations of :ref:`flask_client`, :ref:`django_client` and :ref:`starlette_client`. If you are using these frameworks, you may have interests in their own documentation. If you are not familiar with OAuth 1.0, it is better to read :ref:`intro_oauth1` now. Initialize OAuth 1.0 Client --------------------------- There are three steps in OAuth 1 to obtain an access token: 1. fetch a temporary credential 2. visit the authorization page 3. exchange access token with the temporary credential But first, we need to initialize an OAuth 1.0 client:: >>> client_id = 'Your Twitter client key' >>> client_secret = 'Your Twitter client secret' >>> # using requests client >>> from authlib.integrations.requests_client import OAuth1Session >>> client = OAuth1Session(client_id, client_secret) >>> # using httpx client >>> from authlib.integrations.httpx_client import AsyncOAuth1Client >>> client = AsyncOAuth1Client(client_id, client_secret) .. _fetch_request_token: Fetch Temporary Credential -------------------------- The first step is to fetch temporary credential, which will be used to generate authorization URL:: >>> request_token_url = 'https://api.twitter.com/oauth/request_token' >>> request_token = client.fetch_request_token(request_token_url) >>> print(request_token) {'oauth_token': 'gA..H', 'oauth_token_secret': 'lp..X', 'oauth_callback_confirmed': 'true'} Save this temporary credential for later use (if required). You can assign a ``redirect_uri`` before fetching the request token, if you want to redirect back to another URL other than the one you registered:: >>> client.redirect_uri = 'https://your-domain.org/auth' >>> client.fetch_request_token(request_token_url) Redirect to Authorization Endpoint ---------------------------------- The second step is to generate the authorization URL:: >>> authenticate_url = 'https://api.twitter.com/oauth/authenticate' >>> client.create_authorization_url(authenticate_url, request_token['oauth_token']) 'https://api.twitter.com/oauth/authenticate?oauth_token=gA..H' Actually, the second parameter ``request_token`` can be omitted, since session is re-used:: >>> client.create_authorization_url(authenticate_url) Now visit the authorization url that `create_authorization_url` generated, and grant the authorization. .. _fetch_oauth1_access_token: Fetch Access Token ------------------ When the authorization is granted, you will be redirected back to your registered callback URI. For instance:: https://example.com/twitter?oauth_token=gA..H&oauth_verifier=fcg..1Dq If you assigned ``redirect_uri`` in :ref:`fetch_oauth1_access_token`, the authorize response would be something like:: https://your-domain.org/auth?oauth_token=gA..H&oauth_verifier=fcg..1Dq Now fetch the access token with this response:: >>> resp_url = 'https://example.com/twitter?oauth_token=gA..H&oauth_verifier=fcg..1Dq' >>> client.parse_authorization_response(resp_url) >>> access_token_url = 'https://api.twitter.com/oauth/access_token' >>> token = client.fetch_access_token(access_token_url) >>> print(token) { 'oauth_token': '12345-st..E', 'oauth_token_secret': 'o67..X', 'user_id': '12345', 'screen_name': 'lepture', 'x_auth_expires': '0' } >>> save_access_token(token) Save this token to access protected resources. The above flow is not always what we will use in a real project. When we are redirected to authorization endpoint, our session is over. In this case, when the authorization server send us back to our server, we need to create another session:: >>> # restore your saved request token, which is a dict >>> request_token = restore_request_token() >>> oauth_token = request_token['oauth_token'] >>> oauth_token_secret = request_token['oauth_token_secret'] >>> from authlib.integrations.requests_client import OAuth1Session >>> # if using httpx: from authlib.integrations.httpx_client import AsyncOAuth1Client >>> client = OAuth1Session( ... client_id, client_secret, ... token=oauth_token, ... token_secret=oauth_token_secret) >>> # there is no need for `parse_authorization_response` if you can get `verifier` >>> verifier = request.args.get('verifier') >>> access_token_url = 'https://api.twitter.com/oauth/access_token' >>> token = client.fetch_access_token(access_token_url, verifier) Access Protected Resources -------------------------- Now you can access the protected resources. If you re-use the session, you don't need to do anything:: >>> account_url = 'https://api.twitter.com/1.1/account/verify_credentials.json' >>> resp = client.get(account_url) >>> resp.json() {...} The above is not the real flow, just like what we did in :ref:`fetch_oauth1_access_token`, we need to create another session ourselves:: >>> access_token = restore_access_token_from_database() >>> oauth_token = access_token['oauth_token'] >>> oauth_token_secret = access_token['oauth_token_secret'] >>> # if using httpx: from authlib.integrations.httpx_client import AsyncOAuth1Client >>> client = OAuth1Session( ... client_id, client_secret, ... token=oauth_token, ... token_secret=oauth_token_secret) >>> account_url = 'https://api.twitter.com/1.1/account/verify_credentials.json' >>> resp = client.get(account_url) Please note, there are duplicated steps in the documentation, read carefully and ignore the duplicated explains. Using OAuth1Auth ---------------- It is also possible to access protected resources with ``OAuth1Auth`` object. Create an instance of OAuth1Auth with an access token:: # if using requests from authlib.integrations.requests_client import OAuth1Auth # if using httpx from authlib.integrations.httpx_client import OAuth1Auth auth = OAuth1Auth( client_id='..', client_secret='..', token='oauth_token value', token_secret='oauth_token_secret value', ... ) If using ``requests``, pass this ``auth`` to access protected resources:: import requests url = 'https://api.twitter.com/1.1/account/verify_credentials.json' resp = requests.get(url, auth=auth) If using ``httpx``, pass this ``auth`` to access protected resources:: import httpx url = 'https://api.twitter.com/1.1/account/verify_credentials.json' resp = await httpx.get(url, auth=auth) authlib-1.3.2/docs/client/oauth2.rst000066400000000000000000000417531466226534200173330ustar00rootroot00000000000000.. _oauth_2_session: OAuth 2 Session =============== .. meta:: :description: An OAuth 2.0 Client implementation for Python requests, and httpx, powered by Authlib. .. module:: authlib.integrations :noindex: .. versionchanged:: v0.13 All client related code have been moved into ``authlib.integrations``. For earlier versions of Authlib, check out their own versions documentation. This documentation covers the common design of a Python OAuth 2.0 client. Authlib provides three implementations of OAuth 2.0 client: 1. :class:`requests_client.OAuth2Session` implementation of :ref:`requests_client`, which is a replacement for **requests-oauthlib**. 2. :class:`httpx_client.AsyncOAuth2Client` implementation of :ref:`httpx_client`, which is **async** OAuth 2.0 client powered by **HTTPX**. :class:`requests_client.OAuth2Session` and :class:`httpx_client.AsyncOAuth2Client` shares the same API. There are also frameworks integrations of :ref:`flask_client`, :ref:`django_client` and :ref:`starlette_client`. If you are using these frameworks, you may have interests in their own documentation. If you are not familiar with OAuth 2.0, it is better to read :ref:`intro_oauth2` now. OAuth2Session for Authorization Code ------------------------------------ There are two steps in OAuth 2 to obtain an access token with authorization code grant type. Initialize the session for reuse:: >>> client_id = 'Your GitHub client ID' >>> client_secret = 'Your GitHub client secret' >>> scope = 'user:email' # we want to fetch user's email >>> >>> # using requests implementation >>> from authlib.integrations.requests_client import OAuth2Session >>> client = OAuth2Session(client_id, client_secret, scope=scope) >>> >>> # using httpx implementation >>> from authlib.integrations.httpx_client import AsyncOAuth2Client >>> client = AsyncOAuth2Client(client_id, client_secret, scope=scope) You can assign a ``redirect_uri`` in case you want to specify the callback url. Redirect to Authorization Endpoint ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Unlike OAuth 1, there is no request token. The first step is to jump to the remote authorization server:: >>> authorization_endpoint = 'https://github.com/login/oauth/authorize' >>> uri, state = client.create_authorization_url(authorization_endpoint) >>> print(uri) https://github.com/login/oauth/authorize?response_type=code&client_id=c..id&scope=user%3Aemail&state=d..t The ``create_authorization_url`` returns a tuple of ``(uri, state)``, in real project, you should save the state for later use. Now head over to the generated authorization url, and grant the authorization. .. _fetch_oauth2_access_token: Fetch Token ~~~~~~~~~~~ The authorization server will redirect you back to your site with a code and state arguments:: https://example.com/github?code=42..e9&state=d..t Use ``.fetch_token`` to obtain access token. This method will also verify the state in case of CSRF attack:: >>> authorization_response = 'https://example.com/github?code=42..e9&state=d..t' >>> token_endpoint = 'https://github.com/login/oauth/access_token' >>> token = client.fetch_token(token_endpoint, authorization_response=authorization_response) >>> print(token) { 'access_token': 'e..ad', 'token_type': 'bearer', 'scope': 'user:email' } Save this token to access users' protected resources. In real project, this session can not be re-used since you are redirected to another website. You need to create another session yourself:: >>> state = restore_previous_state() >>> >>> # using requests >>> from authlib.integrations.requests_client import OAuth2Session >>> client = OAuth2Session(client_id, client_secret, state=state) >>> >>> # using httpx >>> from authlib.integrations.httpx_client import AsyncOAuth2Client >>> client = AsyncOAuth2Client(client_id, client_secret, state=state) >>> >>> await client.fetch_token(token_endpoint, authorization_response=authorization_response) Authlib has a built-in Flask/Django integration. Learn from them. Add PKCE for Authorization Code ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Authlib client can handle PKCE automatically, just pass ``code_verifier`` to ``create_authorization_url`` and ``fetch_token``:: >>> client = OAuth2Session(..., code_challenge_method='S256') >>> code_verifier = generate_token(48) >>> uri, state = client.create_authorization_url(authorization_endpoint, code_verifier=code_verifier) >>> # ... >>> token = client.fetch_token(..., code_verifier=code_verifier) OAuth2Session for Implicit -------------------------- OAuth2Session supports implicit grant type. It can fetch the access token with the ``response_type`` of ``token``:: >>> uri, state = client.create_authorization_url(authorization_endpoint, response_type='token') >>> print(uri) https://some-service.com/oauth/authorize?response_type=token&client_id=be..4d&... Visit this link, and grant the authorization, the OAuth authorization server will redirect back to your redirect_uri, the response url would be something like:: https://example.com/cb#access_token=2..WpA&state=xyz&token_type=bearer&expires_in=3600 Fetch access token from the fragment with ``.fetch_token`` method: >>> token = client.fetch_token(authorization_response=authorization_response) >>> # if you don't specify access token endpoint, it will fetch from fragment. >>> print(token) {'access_token': '2..WpA', 'token_type': 'bearer', 'expires_in': 3600} .. note:: GitHub doesn't support ``token`` response type, try with other services. OAuth2Session for Password -------------------------- The ``password`` grant type is supported since Version 0.5. Use ``username`` and ``password`` to fetch the access token:: >>> token = client.fetch_token(token_endpoint, username='a-name', password='a-password') OAuth2Session for Client Credentials ------------------------------------ The ``client_credentials`` grant type is supported since Version 0.5. If no ``code`` or no user info provided, it would be a ``client_credentials`` request. But it is suggested that you specify a ``grant_type`` for it:: >>> token = client.fetch_token(token_endpoint) >>> # or with grant_type >>> token = client.fetch_token(token_endpoint, grant_type='client_credentials') .. _oauth2_client_auth: Client Authentication --------------------- When fetching access token, the authorization server will require a client authentication, Authlib provides **three default methods** defined by RFC7591: - client_secret_basic - client_secret_post - none The default value is ``client_secret_basic``. You can change the auth method with ``token_endpoint_auth_method``:: >>> client = OAuth2Session(token_endpoint_auth_method='client_secret_post') If the authorization server requires other means of authentication, you can construct an ``auth`` for your own need, and pass it to ``fetch_token``:: >>> auth = YourAuth(...) >>> token = client.fetch_token(token_endpoint, auth=auth, ...) It is also possible to extend the client authentication method with ``.register_client_auth_method``. Besides the default three authentication methods, there are more provided by Authlib. e.g. - client_secret_jwt - private_key_jwt These two methods are defined by RFC7523 and OpenID Connect. Find more in :ref:`jwt_oauth2session`. There are still cases that developers need to define a custom client authentication method. Take :issue:`158` as an example, the provider requires us put ``client_id`` and ``client_secret`` on URL when sending POST request:: POST /oauth/token?grant_type=code&code=...&client_id=...&client_secret=... Let's call this weird authentication method ``client_secret_uri``, and this is how we can get our OAuth 2.0 client authenticated:: from authlib.common.urls import add_params_to_uri def auth_client_secret_uri(client, method, uri, headers, body): uri = add_params_to_uri(uri, [ ('client_id', client.client_id), ('client_secret', client.client_secret), ]) uri = uri + '&' + body body = '' return uri, headers, body client = OAuth2Session( 'client_id', 'client_secret', token_endpoint_auth_method='client_secret_uri', ... ) client.register_client_auth_method(('client_secret_uri', auth_client_secret_uri)) With ``client_secret_uri`` registered, OAuth 2.0 client will authenticate with the signed URI. It is also possible to assign the function to ``token_endpoint_auth_method`` directly:: client = OAuth2Session( 'client_id', 'client_secret', token_endpoint_auth_method=auth_client_secret_uri, ) Access Protected Resources -------------------------- Now you can access the protected resources. If you re-use the session, you don't need to do anything:: >>> account_url = 'https://api.github.com/user' >>> resp = client.get(account_url) >>> resp.json() {...} The above is not the real flow, just like what we did in :ref:`fetch_oauth2_access_token`, we need to create another session ourselves:: >>> token = restore_previous_token_from_database() >>> # token is a dict which must contain ``access_token``, ``token_type`` >>> client = OAuth2Session(client_id, client_secret, token=token) >>> account_url = 'https://api.github.com/user' >>> resp = client.get(account_url) Refresh & Auto Update Token --------------------------- It is possible that your previously saved token is expired when accessing protected resources. In this case, we can refresh the token manually, or even better, Authlib will refresh the token automatically and update the token for us. Automatically refreshing tokens ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If your :class:`~requests_client.OAuth2Session` class was created with the `token_endpoint` parameter, Authlib will automatically refresh the token when it has expired:: >>> openid_configuration = requests.get("https://example.org/.well-known/openid-configuration").json() >>> session = OAuth2Session(…, token_endpoint=openid_configuration["token_endpoint"]) By default, the token will be refreshed 60 seconds before its actual expiry time, to avoid clock skew issues. You can control this behaviour by setting the ``leeway`` parameter of the :class:`~requests_client.OAuth2Session` class. Manually refreshing tokens ~~~~~~~~~~~~~~~~~~~~~~~~~~ To call :meth:`~requests_client.OAuth2Session.refresh_token` manually means we are going to exchange a new "access_token" with "refresh_token":: >>> token = restore_previous_token_from_database() >>> new_token = client.refresh_token(token_endpoint, refresh_token=token.refresh_token) Authlib can also refresh a new token automatically when requesting resources. This is done by passing a ``update_token`` function when constructing the client instance:: def update_token(token, refresh_token=None, access_token=None): if refresh_token: item = OAuth2Token.find(name=name, refresh_token=refresh_token) elif access_token: item = OAuth2Token.find(name=name, access_token=access_token) else: return # update old token item.access_token = token['access_token'] item.refresh_token = token.get('refresh_token') item.expires_at = token['expires_at'] item.save() client = OAuth2Session(client_id, client_secret, update_token=update_token) When sending a request to resources endpoint, if our previously saved token is expired, this ``client`` will invoke ``.refresh_token`` method itself and call this our defined ``update_token`` to save the new token:: token = restore_previous_token_from_database() client.token = token # if the token is expired, this GET request will update token client.get('https://openidconnect.googleapis.com/v1/userinfo') Revoke and Introspect Token --------------------------- If the provider support token revocation and introspection, you can revoke and introspect the token with:: token_endpoint = 'https://example.com/oauth/token' token = get_your_previous_saved_token() client.revoke_token(token_endpoint, token=token) client.introspect_token(token_endpoint, token=token) You can find the available parameters in API docs: - :meth:`requests_client.OAuth2Session.revoke_token` - :meth:`requests_client.OAuth2Session.introspect_token` - :meth:`httpx_client.AsyncOAuth2Client.revoke_token` - :meth:`httpx_client.AsyncOAuth2Client.introspect_token` .. _compliance_fix_oauth2: Compliance Fix for non Standard ------------------------------- There are services that claimed they are providing OAuth API, but with a little differences. Some services even return with the wrong Content Type. Compliance hooks are provided to solve those problems: * ``access_token_response``: invoked before token parsing. * ``refresh_token_response``: invoked before refresh token parsing. * ``protected_request``: invoked before making a request. For instance, Stackoverflow MUST add a `site` parameter in query string to protect users' resources. And stackoverflow's response is not in JSON. Let's fix it:: from authlib.common.urls import add_params_to_uri, url_decode def _non_compliant_param_name(url, headers, data): params = {'site': 'stackoverflow'} url = add_params_to_uri(url, params) return url, headers, body def _fix_token_response(resp): data = dict(url_decode(resp.text)) data['token_type'] = 'Bearer' data['expires_in'] = int(data['expires']) resp.json = lambda: data return resp session.register_compliance_hook( 'protected_request', _non_compliant_param_name) session.register_compliance_hook( 'access_token_response', _fix_token_response) If you find a non standard OAuth 2 services, and you can't fix it. Please report it in GitHub issues. .. _oidc_session: OAuth 2 OpenID Connect ---------------------- For services that support OpenID Connect, if a scope of ``openid`` is provided, the authorization server will return a value of ``id_token`` in response:: >>> client_id = 'Your Google client ID' >>> client_secret = 'Your Google client secret' >>> scope = 'openid email profile' >>> # using requests >>> client = OAuth2Session(client_id, client_secret, scope=scope) >>> # using httpx >>> client = AsyncOAuth2Client(client_id, client_secret, scope=scope) The remote server may require other parameters for OpenID Connect requests, for instance, it may require a ``nonce`` parameter, in this case, you need to generate it yourself, and pass it to ``create_authorization_url``:: >>> from authlib.common.security import generate_token >>> # remember to save this nonce for verification >>> nonce = generate_token() >>> client.create_authorization_url(url, redirect_uri='xxx', nonce=nonce, ...) At the last step of ``client.fetch_token``, the return value contains a ``id_token``:: >>> resp = session.fetch_token(...) >>> print(resp['id_token']) This ``id_token`` is a JWT text, it can not be used unless it is parsed. Authlib has provided tools for parsing and validating OpenID Connect id_token:: >>> from authlib.oidc.core import CodeIDToken >>> from authlib.jose import jwt >>> # GET keys from https://www.googleapis.com/oauth2/v3/certs >>> claims = jwt.decode(resp['id_token'], keys, claims_cls=CodeIDToken) >>> claims.validate() Get deep inside with :class:`~authlib.jose.JsonWebToken` and :class:`~authlib.oidc.core.CodeIDToken`. Learn how to validate JWT claims at :ref:`jwt_guide`. .. _assertion_session: AssertionSession ---------------- :class:`~requests_client.AssertionSession` is a Requests Session for Assertion Framework of OAuth 2.0 Authorization Grants. It is also know as service account. A configured ``AssertionSession`` with handle token authorization automatically, which means you can just use it. Take `Google Service Account`_ as an example, with the information in your service account JSON configure file:: import json from authlib.integrations.requests_client import AssertionSession with open('MyProject-1234.json') as f: conf = json.load(f) token_uri = conf['token_uri'] header = {'alg': 'RS256'} key_id = conf.get('private_key_id') if key_id: header['kid'] = key_id # Google puts scope in payload claims = {'scope': scope} session = AssertionSession( token_endpoint=token_uri, issuer=conf['client_email'], audience=token_uri, claims=claims, subject=None, key=conf['private_key'], header=header, ) session.get(...) session.post(...) There is a ready to use ``GoogleServiceAccount`` in loginpass_. You can also read these posts: - `Access Google Analytics API `_. - `Using Authlib with gspread `_. .. _loginpass: https://github.com/authlib/loginpass .. _`Google Service Account`: https://developers.google.com/identity/protocols/OAuth2ServiceAccount authlib-1.3.2/docs/client/requests.rst000066400000000000000000000127631466226534200200030ustar00rootroot00000000000000.. _requests_client: OAuth for Requests ================== .. meta:: :description: An OAuth 1.0 and OAuth 2.0 Client implementation for Python requests, including support for OpenID Connect and service account, powered by Authlib. .. module:: authlib.integrations.requests_client :noindex: Requests is a very popular HTTP library for Python. Authlib enables OAuth 1.0 and OAuth 2.0 for Requests with its :class:`OAuth1Session`, :class:`OAuth2Session` and :class:`AssertionSession`. Requests OAuth 1.0 ------------------ There are three steps in :ref:`oauth_1_session` to obtain an access token: 1. fetch a temporary credential 2. visit the authorization page 3. exchange access token with the temporary credential It shares a common API design with :ref:`httpx_client`. OAuth1Session ~~~~~~~~~~~~~ The requests integration follows our common guide of :ref:`oauth_1_session`. Follow the documentation in :ref:`oauth_1_session` instead. OAuth1Auth ~~~~~~~~~~ It is also possible to use :class:`OAuth1Auth` directly with in requests. After we obtained access token from an OAuth 1.0 provider, we can construct an ``auth`` instance for requests:: auth = OAuth1Auth( client_id='YOUR-CLIENT-ID', client_secret='YOUR-CLIENT-SECRET', token='oauth_token', token_secret='oauth_token_secret', ) requests.get(url, auth=auth) Requests OAuth 2.0 ------------------ In :ref:`oauth_2_session`, there are many grant types, including: 1. Authorization Code Flow 2. Implicit Flow 3. Password Flow 4. Client Credentials Flow And also, Authlib supports non Standard OAuth 2.0 providers via Compliance Fix. Follow the common guide of :ref:`oauth_2_session` to find out how to use requests integration of OAuth 2.0 flow. Using ``client_secret_jwt`` in Requests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are **three default client authentication methods** defined for ``OAuth2Session``. But what if you want to use ``client_secret_jwt`` instead? ``client_secret_jwt`` is defined in RFC7523, use it for Requests:: from authlib.integrations.requests_client import OAuth2Session from authlib.oauth2.rfc7523 import ClientSecretJWT token_endpoint = 'https://example.com/oauth/token' session = OAuth2Session( 'your-client-id', 'your-client-secret', token_endpoint_auth_method=ClientSecretJWT(token_endpoint), ) session.fetch_token(token_endpoint) The ``ClientSecretJWT`` is provided by :ref:`specs/rfc7523`. Using ``private_key_jwt`` in Requests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ What if you want to use ``private_key_jwt`` client authentication method, here is the way with ``PrivateKeyJWT`` for Requests:: from authlib.integrations.requests_client import OAuth2Session from authlib.oauth2.rfc7523 import PrivateKeyJWT with open('your-private-key.pem', 'rb') as f: private_key = f.read() token_endpoint = 'https://example.com/oauth/token' session = OAuth2Session( 'your-client-id', private_key, token_endpoint_auth_method=PrivateKeyJWT(token_endpoint), ) session.fetch_token(token_endpoint) The ``PrivateKeyJWT`` is provided by :ref:`specs/rfc7523`. OAuth2Auth ~~~~~~~~~~ Already obtained access token? We can use :class:`OAuth2Auth` directly in requests. But this OAuth2Auth can not refresh token automatically for you. Here is how to use it in requests:: token = {'token_type': 'bearer', 'access_token': '....', ...} auth = OAuth2Auth(token) requests.get(url, auth=auth) Requests OpenID Connect ----------------------- OpenID Connect is built on OAuth 2.0. It is pretty simple to communicate with an OpenID Connect provider via Authlib. With Authlib built-in OAuth 2.0 system and JsonWebToken (JWT), parsing OpenID Connect ``id_token`` could be very easy. Understand how it works with :ref:`oidc_session`. Requests Service Account ------------------------ The Assertion Framework of OAuth 2.0 Authorization Grants is also known as service account. With the implementation of :class:`AssertionSession`, we can easily integrate with a "assertion" service. Checking out an example of Google Service Account with :ref:`assertion_session`. Close Session Hint ------------------ Developers SHOULD **close** a Requests Session when the jobs are done. You can call ``.close()`` manually, or use a ``with`` context to automatically close the session:: session = OAuth2Session(client_id, client_secret) session.get(url) session.close() with OAuth2Session(client_id, client_secret) as session: session.get(url) Self-Signed Certificate ----------------------- Self-signed certificate mutual-TLS method internet standard is defined in `RFC8705 Section 2.2`_ . For specifics development purposes only, you may need to **disable SSL verification**. You can force all requests to disable SSL verification by setting your environment variable ``CURL_CA_BUNDLE=""``. This solutions works because Python requests (and most of the packages) overwrites the default value for ssl verifications from environment variables ``CURL_CA_BUNDLE`` and ``REQUESTS_CA_BUNDLE``. This hack will **only work** with ``CURL_CA_BUNDLE``, as you can see in `requests/sessions.py`_ :: verify = (os.environ.get('REQUESTS_CA_BUNDLE') or os.environ.get('CURL_CA_BUNDLE')) Please remember to set the env variable only in you development environment. .. _RFC8705 Section 2.2: https://tools.ietf.org/html/rfc8705#section-2.2 .. _requests/sessions.py: https://github.com/requests/requests/blob/master/requests/sessions.py#L706 authlib-1.3.2/docs/client/starlette.rst000066400000000000000000000113141466226534200201260ustar00rootroot00000000000000.. _starlette_client: Starlette OAuth Client ====================== .. meta:: :description: The built-in Starlette integrations for OAuth 1.0, OAuth 2.0 and OpenID Connect clients, powered by Authlib. .. module:: authlib.integrations.starlette_client :noindex: Starlette_ is a lightweight ASGI framework/toolkit, which is ideal for building high performance asyncio services. .. _Starlette: https://www.starlette.io/ This documentation covers OAuth 1.0, OAuth 2.0 and OpenID Connect Client support for Starlette. Because all the frameworks integrations share the same API, it is best to: Read :ref:`frameworks_clients` at first. The difference between Starlette and Flask/Django integrations is Starlette is **async**. We will use ``await`` for the functions we need to call. But first, let's create an :class:`OAuth` instance:: from authlib.integrations.starlette_client import OAuth oauth = OAuth() The common use case for OAuth is authentication, e.g. let your users log in with Twitter, GitHub, Google etc. Configuration ------------- Starlette can load configuration from environment; Authlib implementation for Starlette client can use this configuration. Here is an example of how to do it:: from starlette.config import Config config = Config('.env') oauth = OAuth(config) Authlib will load ``client_id`` and ``client_secret`` from the configuration, take google as an example:: oauth.register(name='google', ...) It will load **GOOGLE_CLIENT_ID** and **GOOGLE_CLIENT_SECRET** from the environment. Register Remote Apps -------------------- ``oauth.register`` is the same as :ref:`frameworks_clients`:: oauth.register( 'google', client_id='...', client_secret='...', ... ) However, unlike Flask/Django, Starlette OAuth registry is using HTTPX :class:`~authlib.integrations.httpx_client.AsyncOAuth1Client` and :class:`~authlib.integrations.httpx_client.AsyncOAuth2Client` as the OAuth backends. While Flask and Django are using the Requests version of :class:`~authlib.integrations.requests_client.OAuth1Session` and :class:`~authlib.integrations.requests_client.OAuth2Session`. Enable Session for OAuth 1.0 ---------------------------- With OAuth 1.0, we need to use a temporary credential to exchange for an access token. This temporary credential is created before redirecting to the provider (Twitter), and needs to be saved somewhere in order to use it later. With OAuth 1, the Starlette client will save the request token in sessions. To enable this, we need to add the ``SessionMiddleware`` middleware to the application, which requires the installation of the ``itsdangerous`` package:: from starlette.applications import Starlette from starlette.middleware.sessions import SessionMiddleware app = Starlette() app.add_middleware(SessionMiddleware, secret_key="some-random-string") However, using the ``SessionMiddleware`` will store the temporary credential as a secure cookie which will expose your request token to the client. Routes for Authorization ------------------------ Just like the examples in :ref:`frameworks_clients`, but Starlette is **async**, the routes for authorization should look like:: @app.route('/login/google') async def login_via_google(request): google = oauth.create_client('google') redirect_uri = request.url_for('authorize_google') return await google.authorize_redirect(request, redirect_uri) @app.route('/auth/google') async def authorize_google(request): google = oauth.create_client('google') token = await google.authorize_access_token(request) # do something with the token and userinfo return '...' Starlette OpenID Connect ------------------------ An OpenID Connect client is no different than a normal OAuth 2.0 client, just add ``openid`` scope when ``.register``. The built-in Starlette OAuth client will handle everything automatically:: oauth.register( 'google', ... server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', client_kwargs={'scope': 'openid profile email'} ) When we get the returned token:: token = await oauth.google.authorize_access_token() There should be a ``id_token`` in the response. Authlib has called `.parse_id_token` automatically, we can get ``userinfo`` in the ``token``:: userinfo = token['userinfo'] Examples -------- We have Starlette demos at https://github.com/authlib/demo-oauth-client 1. OAuth 1.0: `Starlette Twitter login `_ 2. OAuth 2.0: `Starlette Google login `_ authlib-1.3.2/docs/community/000077500000000000000000000000001466226534200161335ustar00rootroot00000000000000authlib-1.3.2/docs/community/authors.rst000066400000000000000000000023331466226534200203530ustar00rootroot00000000000000Authors ======= Authlib is written and maintained by `Hsiaoming Yang `_. Contributors ------------ Here is the list of the main contributors: - Ber Zoidberg - Tom Christie - Grey Li - Pablo Marti - Mario Jimenez Carrasco - Bastian Venthur - Nuno Santos - Éloi Rivard And more on https://github.com/lepture/authlib/graphs/contributors Sponsors -------- Become a sponsor via `GitHub Sponsors`_ or Patreon_ to support Authlib. Here is a full list of our sponsors, including past sponsors: * `Auth0 `_ * `Authing `_ Find out the :ref:`benefits for sponsorship `. Backers ------- Become a backer `GitHub Sponsors`_ or via Patreon_ to support Authlib. Here is a full list of our backers: * `Evilham `_ * `Aveline `_ * `Callam `_ * `Krishna Kumar `_ * `Yaal Coop `_ .. _`GitHub Sponsors`: https://github.com/sponsors/lepture .. _Patreon: https://www.patreon.com/lepture authlib-1.3.2/docs/community/awesome.rst000066400000000000000000000043631466226534200203330ustar00rootroot00000000000000Awesome Articles and Projects ============================= This awesome list contains awesome articles and talks on Authlib, and awesome projects built with Authlib. If you have an awesome article on Authlib, please submit a pull request. Note, some links may be removed in the future, we keep ONLY the awesome ones. Examples -------- OAuth Clients Demo ~~~~~~~~~~~~~~~~~~ An official example on how to use OAuth clients with Authlib in Flask, Django, Starlette, and FastAPI. - Source Code: https://github.com/authlib/demo-oauth-client OAuth 2.0 Provider ~~~~~~~~~~~~~~~~~~ An official example on how to create an OAuth 2.0 server with Authlib. - Source Code: https://github.com/authlib/example-oauth2-server OpenID Connect 1.0 Provider ~~~~~~~~~~~~~~~~~~~~~~~~~~~ An official example on how to create an OpenID Connect server with Authlib. - Source Code: https://github.com/authlib/example-oidc-server Projects -------- Open source projects that are using Authlib to create an OAuth server. uData ~~~~~ Customizable and skinnable social platform dedicated to (open)data. - Documentation: https://udata.readthedocs.io/ - Source Code: https://github.com/opendatateam/udata NMOS Authorisation Server ~~~~~~~~~~~~~~~~~~~~~~~~~ AMWA NMOS BCP-003-02 Authorisation Server - Source Code: https://github.com/bbc/nmos-auth-server anchore engine ~~~~~~~~~~~~~~ A service that analyzes docker images and applies user-defined acceptance policies to allow automated container image validation and certification. - Source Code: https://github.com/anchore/anchore-engine Articles -------- - `Access Google Analytics API `_: An example on how to use :class:`~authlib.integrations.requests_client.AssertionSession`. - `Using Authlib with gspread `_. - `Multipart Upload to Google Cloud Storage `_. - `Create Twitter login for FastAPI `_. - `Google login for FastAPI `_. - `FastAPI with Google OAuth `_. authlib-1.3.2/docs/community/contribute.rst000066400000000000000000000051061466226534200210450ustar00rootroot00000000000000Contribution ============ Thanks for reading this section, you have a good intention. Since you are interested in contributing to Authlib, there are few things you need to know: 1. **All contributions are welcome**, as long as everyone involved is treated with respect. 2. Your contribution may be rejected, but don't despair. It's just that this certain pull request doesn't fit Authlib. Be brave for a next contribution. 3. Some issues will be labeled as **good first issue**, if you're new to Authlib, you may find these issues are a good start for contribution. Bug Reports ----------- It's welcome for everyone to submit a bug report. However, before you submit a report on GitHub issues, please check the old issues both open and closed, to ensure that you are not submitting a duplicate issue. Please don't ask for help on GitHub issues. Ask them on :ref:`stackoverflow`. Documentation Contributions --------------------------- Documentation improvements are welcome, both on grammar and the sentences. I'm not a native English speaker, you may find errors in this documentation, don't hesitated to submit an improvement. Our documentation is generated with Sphinx. All documentation should be written in reStructuredText, if you are not familiar reStructuredText, please read its documentation. We keep a soft limit of 79 characters wide in text files. Yet, if you have to exceed this limit, it's OK, but no more than 110 characters. Make a Pull Request ------------------- Thank you. Now that you have a fix for Authlib, please describe it clearly in your pull request. There are some requirements for a pull request to be accepted: * Follow PEP8 code style. You can use flake8 to check your code style. * Tests for the code changes are required. * Please add documentation for it, if it requires. .. note:: By making a pull request, you consent that the copyright of your pull request source code belongs to Authlib's author. Become a Backer --------------- Finance support is also welcome. A better finance can make Authlib :ref:`sustainable`. Here I offer two options: 1. **Recurring Pledges** Recurring pledges come with exclusive benefits, e.g. having your name listed in the Authlib GitHub repository, or have your company logo placed on this website. * `Become a backer or sponsor via Patreon `_ * `Become a backer or sponsor via GitHub `_ 2. **One Time Donation** I accept one time donation via Stripe, Alipay and Wechat. Donate via `Support Hsiaoming Yang `_ authlib-1.3.2/docs/community/funding.rst000066400000000000000000000050501466226534200203170ustar00rootroot00000000000000.. _funding: Funding ======= If you use Authlib and its related projects commercially we strongly encourage you to invest in its **sustainable** development by sponsorship. We accept funding with paid license and sponsorship. With the funding, it will: * contribute to faster releases, more features, and higher quality software. * allow more time to be invested in the documentation, issues, and community support. And you can also get benefits from us: 1. access to some of our private repositories 2. access to our `private PyPI `_. 3. join our security mail list. Get more details on our sponsor tiers page at: 1. GitHub sponsors: https://github.com/sponsors/lepture 2. Patreon: https://www.patreon.com/lepture Insiders -------- Insiders are people who have access to our private repositories, you can become an insider with: 1. purchasing a paid license at https://authlib.org/plans 2. Become a sponsor with tiers including "Access to our private repos" benefit PyPI ---- We offer a private PyPI server to release early security fixes and features. You can find more details about this PyPI server at: https://authlib.org/pypi Goals ----- The following list of funding goals shows features and additional addons we are going to add. Funding Goal: $500/month ~~~~~~~~~~~~~~~~~~~~~~~~ * :bdg-success:`done` setup a private PyPI * :bdg-warning:`todo` A running demo of loginpass services * :bdg-warning:`todo` Starlette integration of loginpass Funding Goal: $2000/month ~~~~~~~~~~~~~~~~~~~~~~~~~ * :bdg-warning:`todo` A simple running demo of OIDC provider in Flask When the demo is complete, source code of the demo will only be available to our insiders. Funding Goal: $5000/month ~~~~~~~~~~~~~~~~~~~~~~~~~ In Authlib v2.0, we will start working on async provider integrations. * :bdg-warning:`todo` Starlette (FastAPI) OAuth 1.0 provider integration * :bdg-warning:`todo` Starlette (FastAPI) OAuth 2.0 provider integration * :bdg-warning:`todo` Starlette (FastAPI) OIDC provider integration Funding Goal: $9000/month ~~~~~~~~~~~~~~~~~~~~~~~~~ In Authlib v3.0, we will add built-in support for SAML. * :bdg-warning:`todo` SAML 2.0 implementation * :bdg-warning:`todo` RFC7522 (SAML) 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants * :bdg-warning:`todo` CBOR Object Signing and Encryption * :bdg-warning:`todo` A complex running demo of OIDC provider Our Sponsors ------------ Here is our current sponsors, we keep a full list of our sponsors in the Authors page. .. raw:: html :file: ../_templates/funding.html authlib-1.3.2/docs/community/index.rst000066400000000000000000000003771466226534200200030ustar00rootroot00000000000000Community ========= This section aims to make Authlib sustainable, on governance, code commits, issues and finance. .. toctree:: :maxdepth: 2 funding support security contribute awesome sustainable authors licenses authlib-1.3.2/docs/community/licenses.rst000066400000000000000000000035501466226534200204750ustar00rootroot00000000000000Authlib Licenses ================ Authlib offers two licenses, one is BSD for open source projects, one is a commercial license for closed source projects. Open Source License ------------------- Copyright (c) 2019, Hsiaoming Yang All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the creator nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Commercial License ------------------ The content of the commercial license can be found in the repository in a file named **COMMERCIAL-LICENSE**. You can get a commercial license at: https://authlib.org/plans authlib-1.3.2/docs/community/security.rst000066400000000000000000000017741466226534200205450ustar00rootroot00000000000000Security Vulnerability ====================== If you think you have found a potential security vulnerability in Authlib, please email directly. .. warning:: Do not file a public issue. Please do not disclose this to anyone else. We will retrieve a CVE identifier if necessary and give you full credit under whatever name or alias you provide. We will only request an identifier when we have a fix and can publish it in a release. The Process ----------- Here is the process when we have received a security report: 1. we will reply to you in 24 hours 2. we will confirm it in 2 days, if we can't reproduce it, we will send emails to you for more information 3. we will fix the issue in 1 week after we confirm it. If we can't fix it for the moment, we will let you know. 4. we will push the source code to GitHub when it has been released in PyPI for 1 week. 5. if necessary, we will retrieve a CVE after releasing to PyPI. Previous CVEs ------------- - CVE-2022-39174 - CVE-2022-39175 authlib-1.3.2/docs/community/support.rst000066400000000000000000000022701466226534200204020ustar00rootroot00000000000000Support ======= If you have questions or issues about Authlib, there are several options: .. _stackoverflow: StackOverflow ------------- If your question does not contain sensitive information, and it is a help request instead of bug reports, please ask a question on StackOverflow and use the tag **authlib**. Twitter ------- If your StackOverflow question is not answered for a long time, please ping me at Twitter `@authlib `_. GitHub Issues ------------- GitHub issues is used for but reporting and feature requests. Please don't ask questions on GitHub issues. Feature Requests ---------------- If you have feature requests, please comment on `Features Checklist`_. If they are accepted, they will be listed in the post. .. _`Features Checklist`: https://github.com/lepture/authlib/issues/1 Commercial Support ------------------ I do provide commercial support on Authlib. If you need personal help or consulting on your project, please send an email with a brief of what you need to , I'll decide whether I can take the job. Note that work must be Authlib related. Find more information on https://authlib.org/support#consulting-and-supports authlib-1.3.2/docs/community/sustainable.rst000066400000000000000000000027151466226534200212040ustar00rootroot00000000000000.. _sustainable: Sustainable =========== A sustainable project is trustworthy to use in your production environment. To make this project sustainable, we need your help. Here are several options: Community Contribute -------------------- To make Authlib sustainable, we need your contribution. There are many ways for you, some of them even don't require code writing: 1. File a bug report when you found one. 2. Solve issues already there. 3. Write a blog post on Authlib. 4. Give a star on GitHub and spread Authlib to other people. Sponsor the Project ------------------- Authlib is accepting sponsorships via `GitHub Sponsors`_ or Patreon_. You are welcome to become a backer or a sponsor. .. _`GitHub Sponsors`: https://github.com/sponsors/lepture .. _Patreon: https://www.patreon.com/lepture Find out the :ref:`benefits for sponsorship `. Commercial License ------------------ Authlib is licensed under BSD for open source projects. If you are running a business, consider to purchase a commercial license instead. Find more information on https://authlib.org/support#commercial-license Commercial Support ------------------ I do provide commercial support on Authlib. If you need personal help or consulting on your project, please send an email with a brief of what you need to , I'll decide whether I can take the job. Note that work must be Authlib related. Find more information on https://authlib.org/support#consulting-and-supports authlib-1.3.2/docs/conf.py000066400000000000000000000036401466226534200154110ustar00rootroot00000000000000import authlib project = 'Authlib' copyright = '© 2017, Hsiaoming Ltd' author = 'Hsiaoming Yang' version = authlib.__version__ release = version templates_path = ["_templates"] html_static_path = ["_static"] html_css_files = [ 'custom.css', ] html_theme = "shibuya" html_copy_source = False html_show_sourcelink = False language = 'en' extensions = [ "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx_copybutton", "sphinx_design", ] extlinks = { 'issue': ('https://github.com/lepture/authlib/issues/%s', 'issue #%s'), 'PR': ('https://github.com/lepture/authlib/pull/%s', 'pull request #%s'), } intersphinx_mapping = { "python": ("https://docs.python.org/3", None), } html_favicon = '_static/icon.svg' html_theme_options = { "accent_color": "blue", "og_image_url": 'https://authlib.org/logo.png', "light_logo": "_static/light-logo.svg", "dark_logo": "_static/dark-logo.svg", "twitter_site": "authlib", "twitter_creator": "lepture", "twitter_url": "https://twitter.com/authlib", "github_url": "https://github.com/lepture/authlib", "discord_url": "https://discord.gg/HvBVAeNAaV", "nav_links": [ { "title": "Projects", "children": [ { "title": "Authlib", "url": "https://authlib.org/", "summary": "OAuth, JOSE, OpenID, etc." }, { "title": "JOSE RFC", "url": "https://jose.authlib.org/", "summary": "JWS, JWE, JWK, and JWT." }, { "title": "OTP Auth", "url": "https://otp.authlib.org/", "summary": "One time password, HOTP/TOTP.", }, ] }, {"title": "Sponsor me", "url": "https://github.com/sponsors/lepture"}, ] } html_context = {} authlib-1.3.2/docs/django/000077500000000000000000000000001466226534200153515ustar00rootroot00000000000000authlib-1.3.2/docs/django/1/000077500000000000000000000000001466226534200155115ustar00rootroot00000000000000authlib-1.3.2/docs/django/1/api.rst000066400000000000000000000005141466226534200170140ustar00rootroot00000000000000Django OAuth 1.0 Server ======================= This part of the documentation covers the interface of Django OAuth 1.0 Server. .. module:: authlib.integrations.django_oauth1 .. autoclass:: CacheAuthorizationServer :inherited-members: :members: .. autoclass:: ResourceProtector :member-order: bysource :members: authlib-1.3.2/docs/django/1/authorization-server.rst000066400000000000000000000117561466226534200224610ustar00rootroot00000000000000Authorization Server ==================== The Authorization Server provides several endpoints for temporary credentials, authorization, and issuing token credentials. When the resource owner (user) grants the authorization, this server will issue a token credential to the client. Currently, Authlib Django implementation is using cache a lot, which means you don't have to handle temporary credentials, timestamp and nonce yourself, they are all built-in. To create an authorization server, only **Client** and **Token** models are required:: from your_project.models import Client, Token from authlib.integrations.django_oauth1 import CacheAuthorizationServer authorization_server = CacheAuthorizationServer(Client, Token) Resource Owner -------------- Resource Owner is the user who is using your service. A resource owner can log in your website with username/email and password, or other methods. In Django, we can use the built in contrib user:: from django.contrib.auth.models import User Client ------ A client is an application making protected resource requests on behalf of the resource owner and with its authorization. It contains at least three information: - Client Identifier, usually called **client_id** - Client Password, usually called **client_secret** - Client RSA Public Key (if RSA-SHA1 signature method supported) Authlib has no implementation for client model in Django. You need to implement it yourself:: from django.db import models from django.contrib.auth.models import User from authlib.oauth1 import ClientMixin class Client(models.Model, ClientMixin): user = models.ForeignKey(User, on_delete=CASCADE) client_id = models.CharField(max_length=48, unique=True, db_index=True) client_secret = models.CharField(max_length=48, blank=True) default_redirect_uri = models.TextField(blank=False, default='') def get_default_redirect_uri(self): return self.default_redirect_uri def get_client_secret(self): return self.client_secret def get_rsa_public_key(self): return None A client is registered by a user (developer) on your website. Get a deep inside with :class:`~authlib.oauth1.rfc5849.ClientMixin` API reference. Token ----- A token credential is used to access resource owners' resources. Unlike OAuth 2, the token credential will not expire in OAuth 1. This token credentials are supposed to be saved into a persist database rather than a cache. Here is an example of how it looks in Django:: from django.db import models from django.contrib.auth.models import User from authlib.oauth1 import TokenCredentialMixin class Token(models.Model, TokenCredentialMixin): user = models.ForeignKey(User, on_delete=CASCADE) client_id = models.CharField(max_length=48, db_index=True) oauth_token = models.CharField(max_length=84, unique=True, db_index=True) oauth_token_secret = models.CharField(max_length=84) def get_oauth_token(self): return self.oauth_token def get_oauth_token_secret(self): return self.oauth_token_secret Server Implementation --------------------- It is ready to create the endpoints for authorization and issuing tokens. Let's start with the temporary credentials endpoint, which is used for clients to fetch a temporary credential:: from django.views.decorators.http import require_http_methods @require_http_methods(["POST"]) def initiate_temporary_credential(request): return server.create_temporary_credential_response(request) The endpoint for resource owner authorization. OAuth 1 Client will redirect user to this authorization page, so that resource owner can grant or deny this request:: from django.shortcuts import render def authorize(request): # make sure that user is logged in for yourself if request.method == 'GET': try: req = server.check_authorization_request(request) context = {'req': req} return render(request, 'authorize.html', context) except OAuth1Error as error: context = {'error': error} return render(request, 'error.html', context) granted = request.POST.get('granted') if granted: grant_user = request.user else: grant_user = None try: return server.create_authorization_response(request, grant_user) except OAuth1Error as error: context = {'error': error} return render(request, 'error.html', context) Then the final token endpoint. OAuth 1 Client will use the given temporary credential and the ``oauth_verifier`` authorized by resource owner to exchange the token credential:: from django.views.decorators.http import require_http_methods @require_http_methods(["POST"]) def issue_token(request): return server.create_token_response(request) At last, you need to register these views into url patterns. authlib-1.3.2/docs/django/1/index.rst000066400000000000000000000015001466226534200173460ustar00rootroot00000000000000.. _django_oauth1_server: Django OAuth 1.0 Server ======================= .. meta:: :description: How to create an OAuth 1.0 server in Django with Authlib. And understand how OAuth 1.0 works. This is just an alpha implementation of Django OAuth 1.0 provider. An OAuth 1 provider contains two servers: - Authorization Server: to issue access tokens - Resources Server: to serve your users' resources At the very beginning, we need to have some basic understanding of :ref:`the OAuth 1.0 `. .. important:: If you are developing on your localhost, remember to set the environment variable:: export AUTHLIB_INSECURE_TRANSPORT=true Looking for Django OAuth 1.0 client? Check out :ref:`django_client`. .. toctree:: :maxdepth: 2 authorization-server resource-server api authlib-1.3.2/docs/django/1/resource-server.rst000066400000000000000000000014171466226534200214010ustar00rootroot00000000000000Protect Resources ================= Protect users resources, so that only the authorized clients with the authorized access token can access the given scope resources. A resource server can be a different server other than the authorization server. Here is the way to protect your users' resources:: from django.http import JsonResponse from authlib.integrations.django_oauth1 import ResourceProtector require_oauth = ResourceProtector(Client, TokenCredential) @require_oauth() def user_api(request): user = request.oauth1_credential.user return JsonResponse(dict(username=user.username)) The ``require_oauth`` decorator will add a ``oauth1_credential`` to ``request`` parameter. This ``oauth1_credential`` is an instance of the Token model. authlib-1.3.2/docs/django/2/000077500000000000000000000000001466226534200155125ustar00rootroot00000000000000authlib-1.3.2/docs/django/2/api.rst000066400000000000000000000015601466226534200170170ustar00rootroot00000000000000API References of Django OAuth 2.0 Server ========================================= This part of the documentation covers the interface of Django OAuth 2.0 Server. .. module:: authlib.integrations.django_oauth2 .. autoclass:: AuthorizationServer :members: register_grant, register_endpoint, get_consent_grant, create_authorization_response, create_token_response, create_endpoint_response .. autoclass:: ResourceProtector :member-order: bysource :members: .. autoclass:: BearerTokenValidator :member-order: bysource :members: .. autoclass:: RevocationEndpoint :member-order: bysource :members: .. data:: client_authenticated Signal when client is authenticated .. data:: token_revoked Signal when token is revoked .. data:: token_authenticated Signal when token is authenticated authlib-1.3.2/docs/django/2/authorization-server.rst000066400000000000000000000150411466226534200224510ustar00rootroot00000000000000Authorization Server ==================== .. meta:: :description: How to create a Django OAuth 2.0 Authorization server with Authlib. Learn the required concepts in OAuth 2.0 Authorization server. The Authorization Server provides several endpoints for authorization, issuing tokens, refreshing tokens and revoking tokens. When the resource owner (user) grants the authorization, this server will issue an access token to the client. Before creating the authorization server, we need to understand several concepts: Resource Owner -------------- Resource Owner is the user who is using your service. A resource owner can log in your website with username/email and password, or other methods. In this documentation, we will use the ``django.contrib.auth.models.User`` as an example. Client ------ .. versionchanged:: v1.0 ``check_token_endpoint_auth_method`` is deprecated, developers should implement ``check_endpoint_auth_method`` instead. A client is an application making protected resource requests on behalf of the resource owner and with its authorization. It contains at least three information: - Client Identifier, usually called **client_id** - Client Password, usually called **client_secret** - Client Token Endpoint Authentication Method A client is registered by a user (developer) on your website; you MUST implement the missing methods of :class:`~authlib.oauth2.rfc6749.ClientMixin`:: class OAuth2Client(Model, ClientMixin): user = ForeignKey(User, on_delete=CASCADE) client_id = CharField(max_length=48, unique=True, db_index=True) client_secret = CharField(max_length=48, blank=True) client_name = CharField(max_length=120) redirect_uris = TextField(default='') default_redirect_uri = TextField(blank=False, default='') scope = TextField(default='') response_type = TextField(default='') grant_type = TextField(default='') token_endpoint_auth_method = CharField(max_length=120, default='') # you can add more fields according to your own need # check https://tools.ietf.org/html/rfc7591#section-2 def get_client_id(self): return self.client_id def get_default_redirect_uri(self): return self.default_redirect_uri def get_allowed_scope(self, scope): if not scope: return '' allowed = set(scope_to_list(self.scope)) return list_to_scope([s for s in scope.split() if s in allowed]) def check_redirect_uri(self, redirect_uri): if redirect_uri == self.default_redirect_uri: return True return redirect_uri in self.redirect_uris def check_client_secret(self, client_secret): return self.client_secret == client_secret def check_endpoint_auth_method(self, method, endpoint): if endpoint == 'token': return self.token_endpoint_auth_method == method # TODO: developers can update this check method return True def check_response_type(self, response_type): allowed = self.response_type.split() return response_type in allowed def check_grant_type(self, grant_type): allowed = self.grant_type.split() return grant_type in allowed Token ----- Tokens are used to access the users' resources. A token is issued with a valid duration, limited scopes and etc. It contains at least: - **access_token**: a token to authorize the http requests. - **refresh_token**: (optional) a token to exchange a new access token - **client_id**: this token is issued to which client - **expires_at**: when will this token expired - **scope**: a limited scope of resources that this token can access A token is associated with a resource owner; you MUST implement the missing methods of :class:`~authlib.oauth2.rfc6749.TokenMixin`:: import time def now_timestamp(): return int(time.time()) class OAuth2Token(Model, TokenMixin): user = ForeignKey(User, on_delete=CASCADE) client_id = CharField(max_length=48, db_index=True) token_type = CharField(max_length=40) access_token = CharField(max_length=255, unique=True, null=False) refresh_token = CharField(max_length=255, db_index=True) scope = TextField(default='') revoked = BooleanField(default=False) issued_at = IntegerField(null=False, default=now_timestamp) expires_in = IntegerField(null=False, default=0) def get_client_id(self): return self.client_id def get_scope(self): return self.scope def get_expires_in(self): return self.expires_in def get_expires_at(self): return self.issued_at + self.expires_in Server ------ Authlib provides a ready to use :class:`~authlib.integrations.django_oauth2.AuthorizationServer` which has built-in tools to handle requests and responses:: from authlib.integrations.django_oauth2 import AuthorizationServer server = AuthorizationServer(OAuth2Client, OAuth2Token) The Authorization Server has to provide endpoints: 1. authorization endpoint if it supports ``authorization_code`` or ``implicit`` grant types 2. token endpoint to issue tokens The ``AuthorizationServer`` has provided built-in methods to handle these endpoints:: from django.shortcuts import render from django.views.decorators.http import require_http_methods # use ``server.create_authorization_response`` to handle authorization endpoint def authorize(request): if request.method == 'GET': grant = server.get_consent_grant(request, end_user=request.user) client = grant.client scope = client.get_allowed_scope(grant.request.scope) context = dict(grant=grant, client=client, scope=scope, user=request.user) return render(request, 'authorize.html', context) if is_user_confirmed(request): # granted by resource owner return server.create_authorization_response(request, grant_user=request.user) # denied by resource owner return server.create_authorization_response(request, grant_user=None) # use ``server.create_token_response`` to handle token endpoint @require_http_methods(["POST"]) # we only allow POST for token endpoint def issue_token(request): return server.create_token_response(request) For now, you have set up the authorization server. But it won't work since it doesn't support any grant types yet. Let's head over to the next chapter. authlib-1.3.2/docs/django/2/endpoints.rst000066400000000000000000000032761466226534200202570ustar00rootroot00000000000000Token Endpoints =============== Django OAuth 2.0 authorization server has a method to register other token endpoints: ``authorization_server.register_endpoint``. Available endpoints for now: 1. Revocation Endpoint from RFC7009 2. Introspection Endpoint from RFC7662 Revocation Endpoint ------------------- The revocation endpoint for OAuth authorization servers allows clients to notify the authorization server that a previously obtained refresh or access token is no longer needed. This allows the authorization server to clean up security credentials. A revocation request will invalidate the actual token and, if applicable, other tokens based on the same authorization grant. For example, a client may request the revocation of a refresh token with the following request: .. code-block:: http POST /oauth/revoke HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token In Authlib Django OAuth 2.0 provider, we can simply add this feature:: from authlib.integrations.django_oauth2 import RevocationEndpoint from django.views.decorators.http import require_http_methods # see Authorization Server chapter server.register_endpoint(RevocationEndpoint) @require_http_methods(["POST"]) def revoke_token(request): return server.create_endpoint_response(RevocationEndpoint.ENDPOINT_NAME, request) That's all we need. Add this ``revoke_token`` to your routes to enable it. The suggested url path is ``/oauth/revoke``. Introspection Endpoint ---------------------- Check :ref:`register_introspection_endpoint` to get more details. authlib-1.3.2/docs/django/2/grants.rst000066400000000000000000000214741466226534200175520ustar00rootroot00000000000000Register Grants =============== .. meta:: :description: Register Authorization Code Grant, Implicit Grant, Resource Owner Password Credentials Grant, Client Credentials Grant and Refresh Token Grant into Django OAuth 2.0 provider. .. module:: authlib.oauth2.rfc6749.grants :noindex: There are four grant types defined by RFC6749, you can also create your own extended grant. Register the supported grant types to the authorization server. .. _django_oauth2_code_grant: Authorization Code Grant ------------------------ Authorization Code Grant is a very common grant type, it is supported by almost every OAuth 2 providers. It uses an authorization code to exchange access token. In this case, we need a place to store the authorization code. It can be kept in a database or a cache like redis. Here is an example of database **AuthorizationCode**:: from django.db.models import ForeignKey, CASCADE from django.contrib.auth.models import User from authlib.oauth2.rfc6749 import AuthorizationCodeMixin def now_timestamp(): return int(time.time()) class AuthorizationCode(Model, AuthorizationCodeMixin): user = ForeignKey(User, on_delete=CASCADE) client_id = CharField(max_length=48, db_index=True) code = CharField(max_length=120, unique=True, null=False) redirect_uri = TextField(default='', null=True) response_type = TextField(default='') scope = TextField(default='', null=True) auth_time = IntegerField(null=False, default=now_timestamp) def is_expired(self): return self.auth_time + 300 < time.time() def get_redirect_uri(self): return self.redirect_uri def get_scope(self): return self.scope or '' def get_auth_time(self): return self.auth_time Note here, you **MUST** implement the missing methods of :class:`~authlib.oauth2.rfc6749.AuthorizationCodeMixin` API interface. Later, you can use this ``AuthorizationCode`` database model to handle ``authorization_code`` grant type. Here is how:: from authlib.oauth2.rfc6749 import grants from authlib.common.security import generate_token class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): def save_authorization_code(self, code, request): client = request.client auth_code = AuthorizationCode( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, response_type=request.response_type, scope=request.scope, user=request.user, ) auth_code.save() return auth_code def query_authorization_code(self, code, client): try: item = AuthorizationCode.objects.get(code=code, client_id=client.client_id) except AuthorizationCode.DoesNotExist: return None if not item.is_expired(): return item def delete_authorization_code(self, authorization_code): authorization_code.delete() def authenticate_user(self, authorization_code): return authorization_code.user # register it to grant endpoint server.register_grant(AuthorizationCodeGrant) .. note:: AuthorizationCodeGrant is the most complex grant. Default allowed :ref:`client_auth_methods` are: 1. client_secret_basic 2. client_secret_post 3. none You can change it in the subclass, e.g. remove the ``none`` authentication method:: class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post'] .. note:: This is important when you want to support OpenID Connect. Implicit Grant -------------- The implicit grant type is usually used in a browser, when resource owner granted the access, access token is issued in the redirect URI, there is no missing implementation, which means it can be easily registered with:: from authlib.oauth2.rfc6749 import grants # register it to grant endpoint server.register_grant(grants.ImplicitGrant) Implicit Grant is used by **public** client which has no **client_secret**. Only allowed :ref:`client_auth_methods`: ``none``. Resource Owner Password Credentials Grant ----------------------------------------- Resource owner uses their username and password to exchange an access token, this grant type should be used only when the client is trustworthy, implement it with a subclass of :class:`ResourceOwnerPasswordCredentialsGrant`:: from authlib.oauth2.rfc6749 import grants from django.contrib.auth.models import User class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant): def authenticate_user(self, username, password): try: user = User.objects.get(username=username) if user.check_password(password): return user except User.DoesNotExist: return None # register it to grant endpoint server.register_grant(PasswordGrant) Default allowed :ref:`client_auth_methods`: ``client_secret_basic``. You can add more in the subclass:: class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ 'client_secret_basic', 'client_secret_post' ] Client Credentials Grant ------------------------ Client credentials grant type can access public resources and the client's creator's resources. It can be easily registered with:: from authlib.oauth2.rfc6749 import grants # register it to grant endpoint server.register_grant(grants.ClientCredentialsGrant) Default allowed :ref:`client_auth_methods`: ``client_secret_basic``. You can add more in the subclass:: class ClientCredentialsGrant(grants.ClientCredentialsGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ 'client_secret_basic', 'client_secret_post' ] Refresh Token Grant ------------------- Many OAuth 2 providers haven't implemented refresh token endpoint. Authlib provides it as a grant type, implement it with a subclass of :class:`RefreshTokenGrant`:: from authlib.oauth2.rfc6749 import grants class RefreshTokenGrant(grants.RefreshTokenGrant): def authenticate_refresh_token(self, refresh_token): try: item = OAuth2Token.objects.get(refresh_token=refresh_token) if item.is_refresh_token_active(): return item except OAuth2Token.DoesNotExist: return None def authenticate_user(self, credential): return credential.user def revoke_old_credential(self, credential): credential.revoked = True credential.save() # register it to grant endpoint server.register_grant(RefreshTokenGrant) Default allowed :ref:`client_auth_methods`: ``client_secret_basic``. You can add more in the subclass:: class RefreshTokenGrant(grants.RefreshTokenGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ 'client_secret_basic', 'client_secret_post' ] By default, RefreshTokenGrant will not issue a ``refresh_token`` in the token response. Developers can change this behavior with:: class RefreshTokenGrant(grants.RefreshTokenGrant): INCLUDE_NEW_REFRESH_TOKEN = True Custom Grant Types ------------------ It is also possible to create your own grant types. In Authlib, a **Grant** supports two endpoints: 1. Authorization Endpoint: which can handle requests with ``response_type``. 2. Token Endpoint: which is the endpoint to issue tokens. Creating a custom grant type with **BaseGrant**:: from authlib.oauth2.rfc6749.grants import ( BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin ) class MyCustomGrant(BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin): GRANT_TYPE = 'custom-grant-type-name' def validate_authorization_request(self): # only needed if using AuthorizationEndpointMixin def create_authorization_response(self, grant_user): # only needed if using AuthorizationEndpointMixin def validate_token_request(self): # only needed if using TokenEndpointMixin def create_token_response(self): # only needed if using TokenEndpointMixin For a better understanding, you can read the source code of the built-in grant types. And there are extended grant types defined by other specs: 1. :ref:`jwt_grant_type` Grant Extensions ---------------- Grant can accept extensions. Developers can pass extensions when registering grant:: server.register_grant(AuthorizationCodeGrant, [extension]) For instance, there is ``CodeChallenge`` extension in Authlib:: server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=False)]) Learn more about ``CodeChallenge`` at :ref:`specs/rfc7636`. authlib-1.3.2/docs/django/2/index.rst000066400000000000000000000016711466226534200173600ustar00rootroot00000000000000.. _django_oauth2_server: Django OAuth 2.0 Server ======================= .. meta:: :description: How to create an OAuth 2.0 provider in Django with Authlib. And understand how OAuth 2.0 works. Authlib has all built-in grant types for you. .. versionadded:: v0.12 This section is not a step by step guide on how to create an OAuth 2.0 provider in Django. Instead, we will learn how the Django implementation works, and some technical details in an OAuth 2.0 provider. At the very beginning, we need to have some basic understanding of :ref:`the OAuth 2.0 `. .. important:: If you are developing on your localhost, remember to set the environment variable:: export AUTHLIB_INSECURE_TRANSPORT=true Looking for Django OAuth 2.0 client? Check out :ref:`django_client`. .. toctree:: :maxdepth: 2 authorization-server grants endpoints resource-server openid-connect api authlib-1.3.2/docs/django/2/openid-connect.rst000066400000000000000000000215121466226534200211520ustar00rootroot00000000000000.. _django_oidc_server: Django OIDC Provider ==================== .. meta:: :description: How to create an OpenID Connect server in Django with Authlib. And understand how OpenID Connect works. OpenID Connect 1.0 are built custom grant types and grant extensions. You need to read the Authorization Server chapter at first. .. module:: authlib.oauth2.rfc6749.grants :noindex: Looking for OpenID Connect Client? Head over to :ref:`django_client`. Understand JWT -------------- OpenID Connect 1.0 uses JWT a lot. Make sure you have the basic understanding of :ref:`jose`. For OpenID Connect, we need to understand at lease four concepts: 1. **alg**: Algorithm for JWT 2. **key**: Private key for JWT 3. **iss**: Issuer value for JWT 4. **exp**: JWT expires time alg ~~~ The algorithm to sign a JWT. This is the ``alg`` value defined in header part of a JWS: .. code-block:: json {"alg": "RS256"} The available algorithms are defined in :ref:`specs/rfc7518`, which are: - HS256: HMAC using SHA-256 - HS384: HMAC using SHA-384 - HS512: HMAC using SHA-512 - RS256: RSASSA-PKCS1-v1_5 using SHA-256 - RS384: RSASSA-PKCS1-v1_5 using SHA-384 - RS512: RSASSA-PKCS1-v1_5 using SHA-512 - ES256: ECDSA using P-256 and SHA-256 - ES384: ECDSA using P-384 and SHA-384 - ES512: ECDSA using P-521 and SHA-512 - PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 - PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 - PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 The HMAC using SHA algorithms are not suggested since you need to share secrets between server and client. Most OpenID Connect services are using ``RS256``. key ~~~ A private key is required to generate JWT. The key that you are going to use dependents on the ``alg`` you are using. For instance, the alg is ``RS256``, you need to use an RSA private key. It can be set with:: key = '''-----BEGIN RSA PRIVATE KEY-----\nMIIEog...''' # or in JWK format key = {"kty": "RSA", "n": ...} iss ~~~ The ``iss`` value in JWT payload. The value can be your website name or URL. For example, Google is using:: {"iss": "https://accounts.google.com"} Code Flow --------- OpenID Connect authorization code flow relies on the OAuth2 authorization code flow and extends it. In OpenID Connect, there will be a ``nonce`` parameter in request, we need to save it into database for later use. In this case, we have to rewrite our ``AuthorizationCode`` db model:: class AuthorizationCode(Model, AuthorizationCodeMixin): user = ForeignKey(User, on_delete=CASCADE) client_id = CharField(max_length=48, db_index=True) code = CharField(max_length=120, unique=True, null=False) redirect_uri = TextField(default='', null=True) response_type = TextField(default='') scope = TextField(default='', null=True) auth_time = IntegerField(null=False, default=now_timestamp) # add nonce nonce = CharField(max_length=120, default='', null=True) # ... other fields and methods ... OpenID Connect Code flow is the same as Authorization Code flow, but with extended features. We can apply the :class:`OpenIDCode` extension to ``AuthorizationCodeGrant``. First, we need to implement the missing methods for ``OpenIDCode``:: from authlib.oidc.core import grants, UserInfo class OpenIDCode(grants.OpenIDCode): def exists_nonce(self, nonce, request): try: AuthorizationCode.objects.get( client_id=request.client_id, nonce=nonce ) return True except AuthorizationCode.DoesNotExist: return False def get_jwt_config(self, grant): return { 'key': read_private_key_file(key_path), 'alg': 'RS512', 'iss': 'https://example.com', 'exp': 3600 } def generate_user_info(self, user, scope): user_info = UserInfo(sub=str(user.pk), name=user.name) if 'email' in scope: user_info['email'] = user.email return user_info Second, since there is one more ``nonce`` value in ``AuthorizationCode`` data, we need to save this value into database. In this case, we have to update our ``AuthorizationCodeGrant.save_authorization_code`` method:: class AuthorizationCodeGrant(_AuthorizationCodeGrant): def save_authorization_code(self, code, request): # openid request MAY have "nonce" parameter nonce = request.data.get('nonce') client = request.client auth_code = AuthorizationCode( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, user=request.user, nonce=nonce, ) auth_code.save() return auth_code Finally, you can register ``AuthorizationCodeGrant`` with ``OpenIDCode`` extension:: # register it to grant endpoint server.register_grant(AuthorizationCodeGrant, [OpenIDCode(require_nonce=True)]) The difference between OpenID Code flow and the standard code flow is that OpenID Connect request has a scope of "openid": .. code-block:: http GET /authorize? response_type=code &scope=openid%20profile%20email &client_id=s6BhdRkqt3 &state=af0ifjsldkj &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb HTTP/1.1 Host: server.example.com With the example above, you will also have to change the scope of your client in your application to something like ``openid profile email``. Now that you added the ``openid`` scope to your application, an OpenID token will be provided to this app whenever a client asks for a token with an ``openid`` scope. Implicit Flow ------------- The Implicit Flow is mainly used by Clients implemented in a browser using a scripting language. You need to implement the missing methods of :class:`OpenIDImplicitGrant` before register it:: from authlib.oidc.core import grants class OpenIDImplicitGrant(grants.OpenIDImplicitGrant): def exists_nonce(self, nonce, request): try: AuthorizationCode.objects.get( client_id=request.client_id, nonce=nonce) ) return True except AuthorizationCode.DoesNotExist: return False def get_jwt_config(self): return { 'key': read_private_key_file(key_path), 'alg': 'RS512', 'iss': 'https://example.com', 'exp': 3600 } def generate_user_info(self, user, scope): user_info = UserInfo(sub=user.id, name=user.name) if 'email' in scope: user_info['email'] = user.email return user_info server.register_grant(OpenIDImplicitGrant) Hybrid Flow ------------ Hybrid flow is a mix of the code flow and implicit flow. You only need to implement the authorization endpoint part, token endpoint will be handled by Authorization Code Flow. OpenIDHybridGrant is a subclass of OpenIDImplicitGrant, so the missing methods are the same, except that OpenIDHybridGrant has one more missing method, that is ``save_authorization_code``. You can implement it like this:: from authlib.oidc.core import grants class OpenIDHybridGrant(grants.OpenIDHybridGrant): def save_authorization_code(self, code, request): # openid request MAY have "nonce" parameter nonce = request.data.get('nonce') client = request.client auth_code = AuthorizationCode( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, user=request.user, nonce=nonce, ) auth_code.save() return auth_code def exists_nonce(self, nonce, request): try: AuthorizationCode.objects.get( client_id=request.client_id, nonce=nonce) ) return True except AuthorizationCode.DoesNotExist: return False def get_jwt_config(self): return { 'key': read_private_key_file(key_path), 'alg': 'RS512', 'iss': 'https://example.com', 'exp': 3600 } def generate_user_info(self, user, scope): user_info = UserInfo(sub=user.id, name=user.name) if 'email' in scope: user_info['email'] = user.email return user_info # register it to grant endpoint server.register_grant(OpenIDHybridGrant) Since all OpenID Connect Flow requires ``exists_nonce``, ``get_jwt_config`` and ``generate_user_info`` methods, you can create shared functions for them. authlib-1.3.2/docs/django/2/resource-server.rst000066400000000000000000000053171466226534200214050ustar00rootroot00000000000000Resource Server =============== Protect users resources, so that only the authorized clients with the authorized access token can access the given scope resources. A resource server can be a different server other than the authorization server. Here is the way to protect your users' resources in Django:: from authlib.integrations.django_oauth2 import ResourceProtector, BearerTokenValidator from django.http import JsonResponse require_oauth = ResourceProtector() require_oauth.register_token_validator(BearerTokenValidator(OAuth2Token)) @require_oauth('profile') def user_profile(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) If the resource is not protected by a scope, use ``None``:: @require_oauth() def user_profile(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) # or with None @require_oauth(None) def user_profile(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) The decorator ``require_oauth`` will add an ``oauth_token`` property on ``request``, which is the instance of current in-use Token. Multiple Scopes --------------- .. versionchanged:: v1.0 You can apply multiple scopes to one endpoint in **AND**, **OR** and mix modes. Here are some examples: .. code-block:: python @require_oauth(['profile email']) def user_profile(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) It requires the token containing both ``profile`` and ``email`` scope. .. code-block:: python @require_oauth(['profile', 'email']) def user_profile(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) It requires the token containing either ``profile`` or ``email`` scope. It is also possible to mix **AND** and **OR** logic. e.g.:: @app.route('/profile') @require_oauth(['profile email', 'user']) def user_profile(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) This means if the token will be valid if: 1. token contains both ``profile`` and ``email`` scope 2. or token contains ``user`` scope Optional ``require_oauth`` -------------------------- There is one more parameter for ``require_oauth`` which is used to serve public endpoints:: @require_oauth(optional=True) def timeline_api(request): if request.oauth_token: return get_user_timeline(request.oauth_token.user) return get_public_timeline(request) authlib-1.3.2/docs/django/index.rst000066400000000000000000000004261466226534200172140ustar00rootroot00000000000000Django OAuth Providers ====================== Authlib has built-in Django integrations for building OAuth 1.0 and OAuth 2.0 servers. It is best if developers can read :ref:`intro_oauth1` and :ref:`intro_oauth2` at first. .. toctree:: :maxdepth: 2 1/index 2/index authlib-1.3.2/docs/flask/000077500000000000000000000000001466226534200152075ustar00rootroot00000000000000authlib-1.3.2/docs/flask/1/000077500000000000000000000000001466226534200153475ustar00rootroot00000000000000authlib-1.3.2/docs/flask/1/api.rst000066400000000000000000000007271466226534200166600ustar00rootroot00000000000000API References of Flask OAuth 1.0 Server ======================================== This part of the documentation covers the interface of Flask OAuth 1.0 Server. .. module:: authlib.integrations.flask_oauth1 .. autoclass:: AuthorizationServer :members: .. autoclass:: ResourceProtector :member-order: bysource :members: .. data:: current_credential Routes protected by :class:`ResourceProtector` can access current credential with this variable. authlib-1.3.2/docs/flask/1/authorization-server.rst000066400000000000000000000303031466226534200223040ustar00rootroot00000000000000Authorization Server ==================== The Authorization Server provides several endpoints for temporary credentials, authorization, and issuing token credentials. When the resource owner (user) grants the authorization, this server will issue a token credential to the client. .. versionchanged:: v1.0.0 We have removed built-in SQLAlchemy integrations. Resource Owner -------------- Resource Owner is the user who is using your service. A resource owner can log in your website with username/email and password, or other methods. A resource owner MUST implement ``get_user_id()`` method:: class User(db.Model): id = db.Column(db.Integer, primary_key=True) def get_user_id(self): return self.id Client ------ A client is an application making protected resource requests on behalf of the resource owner and with its authorization. It contains at least three information: - Client Identifier, usually called **client_id** - Client Password, usually called **client_secret** - Client RSA Public Key (if RSA-SHA1 signature method supported) Developers MUST implement the missing methods of ``authlib.oauth1.ClientMixin``, take an example of Flask-SQAlchemy:: from authlib.oauth1 import ClientMixin class Client(ClientMixin, db.Model): id = db.Column(db.Integer, primary_key=True) client_id = db.Column(db.String(48), index=True) client_secret = db.Column(db.String(120), nullable=False) default_redirect_uri = db.Column(db.Text, nullable=False, default='') user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') def get_default_redirect_uri(self): return self.default_redirect_uri def get_client_secret(self): return self.client_secret def get_rsa_public_key(self): return None A client is registered by a user (developer) on your website. Get a deep inside with :class:`~authlib.oauth1.rfc5849.ClientMixin` API reference. Temporary Credentials --------------------- A temporary credential is used to exchange a token credential. It is also known as "request token and secret". Since it is temporary, it is better to save them into cache instead of database. A cache instance should have these methods: - ``.get(key)`` - ``.set(key, value, expires=None)`` - ``.delete(key)`` A cache can be a memcache, redis or something else. If cache is not available, developers can also implement it with database. For example, using SQLAlchemy:: from authlib.oauth1 import TemporaryCredentialMixin class TemporaryCredential(TemporaryCredentialMixin, db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') client_id = db.Column(db.String(48), index=True) oauth_token = db.Column(db.String(84), unique=True, index=True) oauth_token_secret = db.Column(db.String(84)) oauth_verifier = db.Column(db.String(84)) oauth_callback = db.Column(db.Text, default='') def get_client_id(self): return self.client_id def get_redirect_uri(self): return self.oauth_callback def check_verifier(self, verifier): return self.oauth_verifier == verifier def get_oauth_token(self): return self.oauth_token def get_oauth_token_secret(self): return self.oauth_token_secret Token Credentials ----------------- A token credential is used to access resource owners' resources. Unlike OAuth 2, the token credential will not expire in OAuth 1. This token credentials are supposed to be saved into a persist database rather than a cache. Developers MUST implement :class:`~authlib.oauth1.rfc5849.TokenCredentialMixin` missing methods. Here is an example of SQLAlchemy integration:: from authlib.oauth1 import TokenCredentialMixin class TokenCredential(TokenCredentialMixin, db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') client_id = db.Column(db.String(48), index=True) oauth_token = db.Column(db.String(84), unique=True, index=True) oauth_token_secret = db.Column(db.String(84)) def get_oauth_token(self): return self.oauth_token def get_oauth_token_secret(self): return self.oauth_token_secret Timestamp and Nonce ------------------- The nonce value MUST be unique across all requests with the same timestamp, client credentials, and token combinations. Authlib Flask integration has a built-in validation with cache. If cache is not available, developers can use a database, here is an example of using SQLAlchemy:: class TimestampNonce(db.Model): __table_args__ = ( db.UniqueConstraint( 'client_id', 'timestamp', 'nonce', 'oauth_token', name='unique_nonce' ), ) id = db.Column(db.Integer, primary_key=True) client_id = db.Column(db.String(48), nullable=False) timestamp = db.Column(db.Integer, nullable=False) nonce = db.Column(db.String(48), nullable=False) oauth_token = db.Column(db.String(84)) Define A Server --------------- Authlib provides a ready to use :class:`~authlib.integrations.flask_oauth1.AuthorizationServer` which has built-in tools to handle requests and responses:: from authlib.integrations.flask_oauth1 import AuthorizationServer def query_client(client_id): return Client.query.filter_by(client_id=client_id).first() server = AuthorizationServer(app, query_client=query_client) It can also be initialized lazily with init_app:: server = AuthorizationServer() server.init_app(app, query_client=query_client) It is strongly suggested that you use a cache. In this way, you don't have to re-implement a lot of the missing methods. There are other configurations. It works well without any changes. Here is a list of them: ================================== =============================================== OAUTH1_TOKEN_GENERATOR A string of module path for importing a function to generate ``oauth_token`` OAUTH1_TOKEN_SECRET_GENERATOR A string of module path for importing a function to generate ``oauth_token_secret``. OAUTH1_TOKEN_LENGTH If ``OAUTH1_TOKEN_GENERATOR`` is not configured, a random function will generate the given length of ``oauth_token``. Default value is ``42``. OAUTH1_TOKEN_SECRET_LENGTH A random function will generate the given length of ``oauth_token_secret``. Default value is ``48``. ================================== =============================================== These configurations are used to create the ``token_generator`` function. But you can pass the ``token_generator`` when initializing the AuthorizationServer:: def token_generator(): return { 'oauth_token': random_string(20), 'oauth_token_secret': random_string(46) } server = AuthorizationServer( app, query_client=query_client, token_generator=token_generator ) Server Hooks ------------ There are missing hooks that should be ``register_hook`` to AuthorizationServer. There are helper functions for registering hooks. If cache is available, you can take the advantage with:: from authlib.integrations.flask_oauth1.cache import ( register_nonce_hooks, register_temporary_credential_hooks ) register_nonce_hooks(server, cache) register_temporary_credential_hooks(server, cache) If cache is not available, developers MUST register the hooks with the database we defined above:: # check if nonce exists def exists_nonce(nonce, timestamp, client_id, oauth_token): q = TimestampNonce.query.filter_by( nonce=nonce, timestamp=timestamp, client_id=client_id, ) if oauth_token: q = q.filter_by(oauth_token=oauth_token) rv = q.first() if rv: return True item = TimestampNonce( nonce=nonce, timestamp=timestamp, client_id=client_id, oauth_token=oauth_token, ) db.session.add(item) db.session.commit() return False server.register_hook('exists_nonce', exists_nonce) # hooks for temporary credential def create_temporary_credential(token, client_id, redirect_uri): item = TemporaryCredential( client_id=client_id, oauth_token=token['oauth_token'], oauth_token_secret=token['oauth_token_secret'], oauth_callback=redirect_uri, ) db.session.add(item) db.session.commit() return item def get_temporary_credential(oauth_token): return TemporaryCredential.query.filter_by(oauth_token=oauth_token).first() def delete_temporary_credential(oauth_token): q = TemporaryCredential.query.filter_by(oauth_token=oauth_token) q.delete(synchronize_session=False) db.session.commit() def create_authorization_verifier(credential, grant_user, verifier): credential.user_id = grant_user.id # assuming your end user model has `.id` credential.oauth_verifier = verifier db.session.add(credential) db.session.commit() return credential server.register_hook('create_temporary_credential', create_temporary_credential) server.register_hook('get_temporary_credential', get_temporary_credential) server.register_hook('delete_temporary_credential', delete_temporary_credential) server.register_hook('create_authorization_verifier', create_authorization_verifier) For both cache and database temporary credential, Developers MUST register a ``create_token_credential`` hook:: def create_token_credential(token, temporary_credential): credential = TokenCredential( oauth_token=token['oauth_token'], oauth_token_secret=token['oauth_token_secret'], client_id=temporary_credential.get_client_id() ) credential.user_id = temporary_credential.user_id db.session.add(credential) db.session.commit() return credential server.register_hook('create_token_credential', create_token_credential) Server Implementation --------------------- It is ready to create the endpoints for authorization and issuing tokens. Let's start with the temporary credentials endpoint, which is used for clients to fetch a temporary credential:: @app.route('/initiate', methods=['POST']) def initiate_temporary_credential(): return server.create_temporary_credentials_response() The endpoint for resource owner authorization. OAuth 1 Client will redirect user to this authorization page, so that resource owner can grant or deny this request:: @app.route('/authorize', methods=['GET', 'POST']) def authorize(): # make sure that user is logged in for yourself if request.method == 'GET': try: req = server.check_authorization_request() return render_template('authorize.html', req=req) except OAuth1Error as error: return render_template('error.html', error=error) granted = request.form.get('granted') if granted: grant_user = current_user else: grant_user = None try: return server.create_authorization_response(grant_user=grant_user) except OAuth1Error as error: return render_template('error.html', error=error) Then the final token endpoint. OAuth 1 Client will use the given temporary credential and the ``oauth_verifier`` authorized by resource owner to exchange the token credential:: @app.route('/token', methods=['POST']) def issue_token(): return server.create_token_response() authlib-1.3.2/docs/flask/1/customize.rst000066400000000000000000000025551466226534200201320ustar00rootroot00000000000000Customize Signature Methods =========================== The ``AuthorizationServer`` and ``ResourceProtector`` only support **HMAC-SHA1** signature method by default. There are three signature methods built-in, which can be enabled with the configuration:: OAUTH1_SUPPORTED_SIGNATURE_METHODS = ['HMAC-SHA1', 'PLAINTEXT', 'RSA-SHA1'] It is also possible to extend the signature methods. For example, you want to create a **HMAC-SHA256** signature method:: import hmac from authlib.common.encoding import to_bytes from authlib.oauth1.rfc5849 import signature def verify_hmac_sha256(request): text = signature.generate_signature_base_string(request) key = escape(request.client_secret or '') key += '&' key += escape(request.token_secret or '') sig = hmac.new(to_bytes(key), to_bytes(text), hashlib.sha256) return binascii.b2a_base64(sig.digest())[:-1] AuthorizationServer.register_signature_method( 'HMAC-SHA256', verify_hmac_sha256 ) ResourceProtector.register_signature_method( 'HMAC-SHA256', verify_hmac_sha256 ) Then add this method into **SUPPORTED_SIGNATURE_METHODS**:: OAUTH1_SUPPORTED_SIGNATURE_METHODS = ['HMAC-SHA256'] With this configuration, your server will support **HMAC-SHA256** signature method only. If you want to support more methods, add them to the list. authlib-1.3.2/docs/flask/1/index.rst000066400000000000000000000014541466226534200172140ustar00rootroot00000000000000.. _flask_oauth1_server: Flask OAuth 1.0 Server ====================== .. meta:: :description: How to create an OAuth 1.0 server in Flask with Authlib. And understand how OAuth 1.0 works. Implement OAuth 1.0 provider in Flask. An OAuth 1 provider contains two servers: - Authorization Server: to issue access tokens - Resources Server: to serve your users' resources At the very beginning, we need to have some basic understanding of :ref:`the OAuth 1.0 `. .. important:: If you are developing on your localhost, remember to set the environment variable:: export AUTHLIB_INSECURE_TRANSPORT=true Looking for Flask OAuth 1.0 client? Check out :ref:`flask_client`. .. toctree:: :maxdepth: 2 authorization-server resource-server customize api authlib-1.3.2/docs/flask/1/resource-server.rst000066400000000000000000000060611466226534200212370ustar00rootroot00000000000000Resource Servers ================ .. versionchanged:: v1.0.0 We have removed built-in SQLAlchemy integrations. Protect users resources, so that only the authorized clients with the authorized access token can access the given scope resources. A resource server can be a different server other than the authorization server. Here is the way to protect your users' resources:: from flask import jsonify from authlib.integrations.flask_oauth1 import ResourceProtector, current_credential # we will define ``query_client``, ``query_token``, and ``exists_nonce`` later. require_oauth = ResourceProtector( app, query_client=query_client, query_token=query_token, exists_nonce=exists_nonce, ) # or initialize it lazily require_oauth = ResourceProtector() require_oauth.init_app( app, query_client=query_client, query_token=query_token, exists_nonce=exists_nonce, ) @app.route('/user') @require_oauth() def user_profile(): user = current_credential.user return jsonify(user) The ``current_credential`` is a proxy to the Token model you have defined above. Since there is a ``user`` relationship on the Token model, we can access this ``user`` with ``current_credential.user``. Initialize ---------- To initialize ``ResourceProtector``, we need three functions: 1. query_client 2. query_token 3. exists_nonce If using SQLAlchemy, the ``query_client`` could be:: def query_client(client_id): # assuming ``Client`` is the model return Client.query.filter_by(client_id=client_id).first() And ``query_token`` would be:: def query_token(client_id, oauth_token): return TokenCredential.query.filter_by(client_id=client_id, oauth_token=oauth_token).first() For ``exists_nonce``, if you are using cache now (as in authorization server), Authlib has a built-in tool function:: from authlib.integrations.flask_oauth1 import create_exists_nonce_func exists_nonce = create_exists_nonce_func(cache) If using database, with SQLAlchemy it would look like:: def exists_nonce(nonce, timestamp, client_id, oauth_token): q = db.session.query(TimestampNonce.nonce).filter_by( nonce=nonce, timestamp=timestamp, client_id=client_id, ) if oauth_token: q = q.filter_by(oauth_token=oauth_token) rv = q.first() if rv: return True tn = TimestampNonce( nonce=nonce, timestamp=timestamp, client_id=client_id, oauth_token=oauth_token, ) db.session.add(tn) db.session.commit() return False MethodView & Flask-Restful -------------------------- You can also use the ``require_oauth`` decorator in ``flask.views.MethodView`` and ``flask_restful.Resource``:: from flask.views import MethodView class UserAPI(MethodView): decorators = [require_oauth()] from flask_restful import Resource class UserAPI(Resource): method_decorators = [require_oauth()] authlib-1.3.2/docs/flask/2/000077500000000000000000000000001466226534200153505ustar00rootroot00000000000000authlib-1.3.2/docs/flask/2/api.rst000066400000000000000000000027731466226534200166640ustar00rootroot00000000000000API References of Flask OAuth 2.0 Server ======================================== This part of the documentation covers the interface of Flask OAuth 2.0 Server. .. module:: authlib.integrations.flask_oauth2 .. autoclass:: AuthorizationServer :members: register_grant, register_endpoint, create_bearer_token_generator, get_consent_grant, create_authorization_response, create_token_response, create_endpoint_response .. autoclass:: ResourceProtector :member-order: bysource :members: .. data:: current_token Routes protected by :class:`ResourceProtector` can access current token with this variable:: from authlib.integrations.flask_oauth2 import current_token @require_oauth() @app.route('/user_id') def user_id(): # current token instance of the OAuth Token model return current_token.user_id .. data:: client_authenticated Signal when client is authenticated .. data:: token_revoked Signal when token is revoked .. data:: token_authenticated Signal when token is authenticated SQLAlchemy Helper Functions --------------------------- .. warning:: We will drop ``sqla_oauth2`` module in version 1.0. .. module:: authlib.integrations.sqla_oauth2 .. autofunction:: create_query_client_func .. autofunction:: create_save_token_func .. autofunction:: create_query_token_func .. autofunction:: create_revocation_endpoint .. autofunction:: create_bearer_token_validator authlib-1.3.2/docs/flask/2/authorization-server.rst000066400000000000000000000202511466226534200223060ustar00rootroot00000000000000Authorization Server ==================== The Authorization Server provides several endpoints for authorization, issuing tokens, refreshing tokens and revoking tokens. When the resource owner (user) grants the authorization, this server will issue an access token to the client. Before creating the authorization server, we need to understand several concepts: Resource Owner -------------- Resource Owner is the user who is using your service. A resource owner can log in your website with username/email and password, or other methods. A resource owner SHOULD implement ``get_user_id()`` method, lets take SQLAlchemy models for example:: class User(Model): id = Column(Integer, primary_key=True) # other columns def get_user_id(self): return self.id Client ------ A client is an application making protected resource requests on behalf of the resource owner and with its authorization. It contains at least three information: - Client Identifier, usually called **client_id** - Client Password, usually called **client_secret** - Client Token Endpoint Authentication Method Authlib has provided a mixin for SQLAlchemy, define the client with this mixin:: from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin class Client(Model, OAuth2ClientMixin): id = Column(Integer, primary_key=True) user_id = Column( Integer, ForeignKey('user.id', ondelete='CASCADE') ) user = relationship('User') A client is registered by a user (developer) on your website. If you decide to implement all the missing methods by yourself, get a deep inside with :class:`~authlib.oauth2.rfc6749.ClientMixin` API reference. Token ----- .. note:: Only Bearer Token is supported for now. MAC Token is still under draft, it will be available when it goes into RFC. Tokens are used to access the users' resources. A token is issued with a valid duration, limited scopes and etc. It contains at least: - **access_token**: a token to authorize the http requests. - **refresh_token**: (optional) a token to exchange a new access token - **client_id**: this token is issued to which client - **expires_at**: when will this token expired - **scope**: a limited scope of resources that this token can access With the SQLAlchemy mixin provided by Authlib:: from authlib.integrations.sqla_oauth2 import OAuth2TokenMixin class Token(db.Model, OAuth2TokenMixin): id = db.Column(db.Integer, primary_key=True) user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') A token is associated with a resource owner. There is no certain name for it, here we call it ``user``, but it can be anything else. If you decide to implement all the missing methods by yourself, get a deep inside the :class:`~authlib.oauth2.rfc6749.TokenMixin` API reference. Server ------ Authlib provides a ready to use :class:`~authlib.integrations.flask_oauth2.AuthorizationServer` which has built-in tools to handle requests and responses:: from authlib.integrations.flask_oauth2 import AuthorizationServer def query_client(client_id): return Client.query.filter_by(client_id=client_id).first() def save_token(token_data, request): if request.user: user_id = request.user.get_user_id() else: # client_credentials grant_type user_id = request.client.user_id # or, depending on how you treat client_credentials user_id = None token = Token( client_id=request.client.client_id, user_id=user_id, **token_data ) db.session.add(token) db.session.commit() # or with the helper from authlib.integrations.sqla_oauth2 import ( create_query_client_func, create_save_token_func ) query_client = create_query_client_func(db.session, Client) save_token = create_save_token_func(db.session, Token) server = AuthorizationServer( app, query_client=query_client, save_token=save_token ) It can also be initialized lazily with init_app:: server = AuthorizationServer() server.init_app(app, query_client=query_client, save_token=save_token) It works well without configuration. However, it can be configured with these settings: ================================== ================================================== OAUTH2_TOKEN_EXPIRES_IN A dict to define ``expires_in`` for each grant OAUTH2_ACCESS_TOKEN_GENERATOR A function or string of module path for importing a function to generate ``access_token`` OAUTH2_REFRESH_TOKEN_GENERATOR A function or string of module path for importing a function to generate ``refresh_token``. It can also be ``True/False`` OAUTH2_ERROR_URIS A list of tuple for (``error``, ``error_uri``) ================================== ================================================== .. hint:: Here is an example of ``OAUTH2_TOKEN_EXPIRES_IN``:: OAUTH2_TOKEN_EXPIRES_IN = { 'authorization_code': 864000, 'implicit': 3600, 'password': 864000, 'client_credentials': 864000 } Here is an example of ``OAUTH2_ACCESS_TOKEN_GENERATOR``:: def gen_access_token(client, grant_type, user, scope): return create_some_random_string() ``OAUTH2_REFRESH_TOKEN_GENERATOR`` accepts the same parameters. Now define an endpoint for authorization. This endpoint is used by ``authorization_code`` and ``implicit`` grants:: from flask import request, render_template from your_project.auth import current_user @app.route('/oauth/authorize', methods=['GET', 'POST']) def authorize(): # Login is required since we need to know the current resource owner. # It can be done with a redirection to the login page, or a login # form on this authorization page. if request.method == 'GET': grant = server.get_consent_grant(end_user=current_user) client = grant.client scope = client.get_allowed_scope(grant.request.scope) # You may add a function to extract scope into a list of scopes # with rich information, e.g. scopes = describe_scope(scope) # returns [{'key': 'email', 'icon': '...'}] return render_template( 'authorize.html', grant=grant, user=current_user, client=client, scopes=scopes, ) confirmed = request.form['confirm'] if confirmed: # granted by resource owner return server.create_authorization_response(grant_user=current_user) # denied by resource owner return server.create_authorization_response(grant_user=None) This is a simple demo, the real case should be more complex. There is a little more complex demo in https://github.com/authlib/example-oauth2-server. The token endpoint is much easier:: @app.route('/oauth/token', methods=['POST']) def issue_token(): return server.create_token_response() However, the routes will not work properly. We need to register supported grants for them. Register Error URIs ------------------- To create a better developer experience for debugging, it is suggested that you create some documentation for errors. Here is a list of built-in :ref:`specs/rfc6949-errors`. You can design a documentation page with a description of each error. For instance, there is a web page for ``invalid_client``:: https://developer.your-company.com/errors#invalid-client In this case, you can register the error URI with ``OAUTH2_ERROR_URIS`` configuration:: OAUTH2_ERROR_URIS = [ ('invalid_client', 'https://developer.your-company.com/errors#invalid-client'), # other error URIs ] If there is no ``OAUTH2_ERROR_URIS``, the error response will not contain any ``error_uri`` data. I18N on Errors ~~~~~~~~~~~~~~ It is also possible to add i18n support to the ``error_description``. The feature has been implemented in version 0.8, but there is still work to do. authlib-1.3.2/docs/flask/2/endpoints.rst000066400000000000000000000004201466226534200201010ustar00rootroot00000000000000Token Endpoints =============== Flask OAuth 2.0 authorization server has a method to register other token endpoints: ``authorization_server.register_endpoint``. Find the available endpoints: - :ref:`register_revocation_endpoint` - :ref:`register_introspection_endpoint` authlib-1.3.2/docs/flask/2/grants.rst000066400000000000000000000201201466226534200173730ustar00rootroot00000000000000Register Grants =============== .. module:: authlib.oauth2.rfc6749.grants :noindex: There are four grant types defined by RFC6749, you can also create your own extended grant. Register the supported grant types to the authorization server. .. _flask_oauth2_code_grant: Authorization Code Grant ------------------------ Authorization Code Grant is a very common grant type, it is supported by almost every OAuth 2 providers. It uses an authorization code to exchange access tokens. In this case, we need a place to store the authorization code. It can be kept in a database or a cache like redis. Here is a SQLAlchemy mixin for **AuthorizationCode**:: from authlib.integrations.sqla_oauth2 import OAuth2AuthorizationCodeMixin class AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): id = db.Column(db.Integer, primary_key=True) user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') Implement this grant by subclassing :class:`AuthorizationCodeGrant`:: from authlib.oauth2.rfc6749 import grants class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): def save_authorization_code(self, code, request): client = request.client auth_code = AuthorizationCode( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, user_id=request.user.id, ) db.session.add(auth_code) db.session.commit() return auth_code def query_authorization_code(self, code, client): item = AuthorizationCode.query.filter_by( code=code, client_id=client.client_id).first() if item and not item.is_expired(): return item def delete_authorization_code(self, authorization_code): db.session.delete(authorization_code) db.session.commit() def authenticate_user(self, authorization_code): return User.query.get(authorization_code.user_id) # register it to grant endpoint server.register_grant(AuthorizationCodeGrant) .. note:: AuthorizationCodeGrant is the most complex grant. Default allowed :ref:`client_auth_methods` are: 1. client_secret_basic 2. client_secret_post 3. none You can change it in the subclass, e.g. remove the ``none`` authentication method:: class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post'] .. note:: This is important when you want to support OpenID Connect. Implicit Grant -------------- The implicit grant type is usually used in a browser, when resource owner granted the access, an access token is issued in the redirect URI, there is no missing implementation, which means it can be easily registered with:: from authlib.oauth2.rfc6749 import grants # register it to grant endpoint server.register_grant(grants.ImplicitGrant) Implicit Grant is used by **public** clients which have no **client_secret**. Default allowed :ref:`client_auth_methods`: ``none``. Resource Owner Password Credentials Grant ----------------------------------------- The resource owner uses its username and password to exchange an access token. This grant type should be used only when the client is trustworthy; implement it with a subclass of :class:`ResourceOwnerPasswordCredentialsGrant`:: from authlib.oauth2.rfc6749 import grants class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant): def authenticate_user(self, username, password): user = User.query.filter_by(username=username).first() if user.check_password(password): return user # register it to grant endpoint server.register_grant(PasswordGrant) Default allowed :ref:`client_auth_methods`: ``client_secret_basic``. You can add more in the subclass:: class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ 'client_secret_basic', 'client_secret_post' ] Client Credentials Grant ------------------------ Client credentials grant type can access public resources and MAYBE the client's creator's resources, depending on how you issue tokens to this grant type. It can be easily registered with:: from authlib.oauth2.rfc6749 import grants # register it to grant endpoint server.register_grant(grants.ClientCredentialsGrant) Default allowed :ref:`client_auth_methods`: ``client_secret_basic``. You can add more in the subclass:: class ClientCredentialsGrant(grants.ClientCredentialsGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ 'client_secret_basic', 'client_secret_post' ] Refresh Token Grant ------------------- Many OAuth 2 providers do not implement a refresh token endpoint. Authlib provides it as a grant type; implement it with a subclass of :class:`RefreshTokenGrant`:: from authlib.oauth2.rfc6749 import grants class RefreshTokenGrant(grants.RefreshTokenGrant): def authenticate_refresh_token(self, refresh_token): item = Token.query.filter_by(refresh_token=refresh_token).first() # define is_refresh_token_valid by yourself # usually, you should check if refresh token is expired and revoked if item and item.is_refresh_token_valid(): return item def authenticate_user(self, credential): return User.query.get(credential.user_id) def revoke_old_credential(self, credential): credential.revoked = True db.session.add(credential) db.session.commit() # register it to grant endpoint server.register_grant(RefreshTokenGrant) Default allowed :ref:`client_auth_methods`: ``client_secret_basic``. You can add more in the subclass:: class RefreshTokenGrant(grants.RefreshTokenGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ 'client_secret_basic', 'client_secret_post' ] By default, RefreshTokenGrant will not issue a ``refresh_token`` in the token response. Developers can change this behavior with:: class RefreshTokenGrant(grants.RefreshTokenGrant): INCLUDE_NEW_REFRESH_TOKEN = True .. _flask_oauth2_custom_grant_types: Custom Grant Types ------------------ It is also possible to create your own grant types. In Authlib, a **Grant** supports two endpoints: 1. Authorization Endpoint: which can handle requests with ``response_type``. 2. Token Endpoint: which is the endpoint to issue tokens. .. versionchanged:: v0.12 Using ``AuthorizationEndpointMixin`` and ``TokenEndpointMixin`` instead of ``AUTHORIZATION_ENDPOINT=True`` and ``TOKEN_ENDPOINT=True``. Creating a custom grant type with **BaseGrant**:: from authlib.oauth2.rfc6749.grants import ( BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin ) class MyCustomGrant(BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin): GRANT_TYPE = 'custom-grant-type-name' def validate_authorization_request(self): # only needed if using AuthorizationEndpointMixin def create_authorization_response(self, grant_user): # only needed if using AuthorizationEndpointMixin def validate_token_request(self): # only needed if using TokenEndpointMixin def create_token_response(self): # only needed if using TokenEndpointMixin For a better understanding, you can read the source code of the built-in grant types. And there are extended grant types defined by other specs: 1. :ref:`jwt_grant_type` .. _flask_oauth2_grant_extensions: Grant Extensions ---------------- .. versionadded:: 0.10 Grants can accept extensions. Developers can pass extensions when registering grants:: authorization_server.register_grant(AuthorizationCodeGrant, [extension]) For instance, there is the ``CodeChallenge`` extension in Authlib:: server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=False)]) Learn more about ``CodeChallenge`` at :ref:`specs/rfc7636`. authlib-1.3.2/docs/flask/2/index.rst000066400000000000000000000023651466226534200172170ustar00rootroot00000000000000.. _flask_oauth2_server: Flask OAuth 2.0 Server ====================== .. meta:: :description: How to create an OAuth 2.0 provider in Flask with Authlib. And understand how OAuth 2.0 works. Authlib has all built-in grant types for you. This section is not a step by step guide on how to create an OAuth 2.0 provider in Flask. Instead, we will learn how the Flask implementation works, and some technical details in an OAuth 2.0 provider. If you need a quick example, here are the official tutorial guide and examples on GitHub: 1. `Example of OAuth 2.0 server `_ 2. `Example of OpenID Connect server `_ 3. `On Demand Demo for your business `_ At the very beginning, we need to have some basic understanding of :ref:`the OAuth 2.0 `. .. important:: If you are developing on your localhost, remember to set the environment variable:: export AUTHLIB_INSECURE_TRANSPORT=true Looking for Flask OAuth 2.0 client? Check out :ref:`flask_client`. .. toctree:: :maxdepth: 2 authorization-server grants endpoints resource-server openid-connect api authlib-1.3.2/docs/flask/2/openid-connect.rst000066400000000000000000000205101466226534200210050ustar00rootroot00000000000000.. _flask_oidc_server: Flask OIDC Provider =================== .. meta:: :description: How to create an OpenID Connect 1.0 server in Flask with Authlib. And understand how OpenID Connect works. OpenID Connect 1.0 is supported since version 0.6. The integrations are built with :ref:`flask_oauth2_custom_grant_types` and :ref:`flask_oauth2_grant_extensions`. Since OpenID Connect is built on OAuth 2.0 frameworks, you need to read :ref:`flask_oauth2_server` at first. .. module:: authlib.oauth2.rfc6749.grants :noindex: .. versionchanged:: v0.12 The Grant system has been redesigned from v0.12. This documentation ONLY works for Authlib >=v0.12. Looking for OpenID Connect Client? Head over to :ref:`flask_client`. Understand JWT -------------- OpenID Connect 1.0 uses JWT a lot. Make sure you have the basic understanding of :ref:`jose`. For OpenID Connect, we need to understand at least four concepts: 1. **alg**: Algorithm for JWT 2. **key**: Private key for JWT 3. **iss**: Issuer value for JWT 4. **exp**: JWT expires time alg ~~~ The algorithm to sign a JWT. This is the ``alg`` value defined in header part of a JWS: .. code-block:: json {"alg": "RS256"} The available algorithms are defined in :ref:`specs/rfc7518`, which are: - HS256: HMAC using SHA-256 - HS384: HMAC using SHA-384 - HS512: HMAC using SHA-512 - RS256: RSASSA-PKCS1-v1_5 using SHA-256 - RS384: RSASSA-PKCS1-v1_5 using SHA-384 - RS512: RSASSA-PKCS1-v1_5 using SHA-512 - ES256: ECDSA using P-256 and SHA-256 - ES384: ECDSA using P-384 and SHA-384 - ES512: ECDSA using P-521 and SHA-512 - PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 - PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 - PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 The HMAC using SHA algorithms are not suggested since you need to share secrets between server and client. Most OpenID Connect services are using ``RS256``. key ~~~ A private key is required to generate a JWT. The key that you are going to use dependents on the ``alg`` you are using. For instance, the alg is ``RS256``, you need to use an RSA private key. It can be set with:: key = '''-----BEGIN RSA PRIVATE KEY-----\nMIIEog...''' # or in JWK format key = {"kty": "RSA", "n": ...} iss ~~~ The ``iss`` value in the JWT payload. The value can be your website name or URL. For example, Google is using:: {"iss": "https://accounts.google.com"} .. _flask_odic_code: Code Flow --------- OpenID Connect authorization code flow relies on the OAuth2 authorization code flow and extends it. OpenID Connect Code flow is the same as Authorization Code flow, but with extended features. We can apply the :class:`OpenIDCode` extension to :ref:`flask_oauth2_code_grant`. First, we need to implement the missing methods for ``OpenIDCode``:: from authlib.oidc.core import grants, UserInfo class OpenIDCode(grants.OpenIDCode): def exists_nonce(self, nonce, request): exists = AuthorizationCode.query.filter_by( client_id=request.client_id, nonce=nonce ).first() return bool(exists) def get_jwt_config(self, grant): return { 'key': read_private_key_file(key_path), 'alg': 'RS512', 'iss': 'https://example.com', 'exp': 3600 } def generate_user_info(self, user, scope): user_info = UserInfo(sub=user.id, name=user.name) if 'email' in scope: user_info['email'] = user.email return user_info Second, since there is one more ``nonce`` value in the ``AuthorizationCode`` data, we need to save this value into the database. In this case, we have to update our :ref:`flask_oauth2_code_grant` ``save_authorization_code`` method:: class AuthorizationCodeGrant(_AuthorizationCodeGrant): def save_authorization_code(self, code, request): # openid request MAY have "nonce" parameter nonce = request.data.get('nonce') auth_code = AuthorizationCode( code=code, client_id=request.client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, user_id=request.user.id, nonce=nonce, ) db.session.add(auth_code) db.session.commit() return auth_code # ... Finally, you can register ``AuthorizationCodeGrant`` with the ``OpenIDCode`` extension:: # register it to grant endpoint server.register_grant(AuthorizationCodeGrant, [OpenIDCode(require_nonce=True)]) The difference between OpenID Code flow and the standard code flow is that OpenID Connect requests have a scope of "openid": .. code-block:: http GET /authorize? response_type=code &scope=openid%20profile%20email &client_id=s6BhdRkqt3 &state=af0ifjsldkj &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb HTTP/1.1 Host: server.example.com With the example above, you will also have to change the scope of your client in your application to something like ``openid profile email``. Now that you added the ``openid`` scope to your application, an OpenID token will be provided to this app whenever a client asks for a token with an ``openid`` scope. .. _flask_odic_implicit: Implicit Flow ------------- The Implicit Flow is mainly used by Clients implemented in a browser using a scripting language. You need to implement the missing methods of :class:`OpenIDImplicitGrant` before registering it:: from authlib.oidc.core import grants class OpenIDImplicitGrant(grants.OpenIDImplicitGrant): def exists_nonce(self, nonce, request): exists = AuthorizationCode.query.filter_by( client_id=request.client_id, nonce=nonce ).first() return bool(exists) def get_jwt_config(self): return { 'key': read_private_key_file(key_path), 'alg': 'RS512', 'iss': 'https://example.com', 'exp': 3600 } def generate_user_info(self, user, scope): user_info = UserInfo(sub=user.id, name=user.name) if 'email' in scope: user_info['email'] = user.email return user_info server.register_grant(OpenIDImplicitGrant) .. _flask_odic_hybrid: Hybrid Flow ------------ The Hybrid flow is a mix of code flow and implicit flow. You only need to implement the authorization endpoint part, as token endpoint will be handled by Authorization Code Flow. OpenIDHybridGrant is a subclass of OpenIDImplicitGrant, so the missing methods are the same, except that OpenIDHybridGrant has one more missing method, that is ``save_authorization_code``. You can implement it like this:: from authlib.oidc.core import grants from authlib.common.security import generate_token class OpenIDHybridGrant(grants.OpenIDHybridGrant): def save_authorization_code(self, code, request): nonce = request.data.get('nonce') item = AuthorizationCode( code=code, client_id=request.client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, user_id=request.user.id, nonce=nonce, ) db.session.add(item) db.session.commit() return code def exists_nonce(self, nonce, request): exists = AuthorizationCode.query.filter_by( client_id=request.client_id, nonce=nonce ).first() return bool(exists) def get_jwt_config(self): return { 'key': read_private_key_file(key_path), 'alg': 'RS512', 'iss': 'https://example.com', 'exp': 3600 } def generate_user_info(self, user, scope): user_info = UserInfo(sub=user.id, name=user.name) if 'email' in scope: user_info['email'] = user.email return user_info # register it to grant endpoint server.register_grant(OpenIDHybridGrant) Since all OpenID Connect Flow require ``exists_nonce``, ``get_jwt_config`` and ``generate_user_info`` methods, you can create shared functions for them. Find the `example of OpenID Connect server `_. authlib-1.3.2/docs/flask/2/resource-server.rst000066400000000000000000000075161466226534200212460ustar00rootroot00000000000000.. _flask_oauth2_resource_protector: Resource Server =============== Protects users resources, so that only the authorized clients with the authorized access token can access the given scope resources. A resource server can be a different server other than the authorization server. Authlib offers a **decorator** to protect your API endpoints:: from flask import jsonify from authlib.integrations.flask_oauth2 import ResourceProtector, current_token from authlib.oauth2.rfc6750 import BearerTokenValidator class MyBearerTokenValidator(BearerTokenValidator): def authenticate_token(self, token_string): return Token.query.filter_by(access_token=token_string).first() require_oauth = ResourceProtector() # only bearer token is supported currently require_oauth.register_token_validator(MyBearerTokenValidator()) When the resource server has no access to the ``Token`` model (database), and there is an introspection token endpoint in authorization server, you can :ref:`require_oauth_introspection`. Here is the way to protect your users' resources:: @app.route('/user') @require_oauth('profile') def user_profile(): # if Token model has `.user` foreign key user = current_token.user return jsonify(user) If the resource is not protected by a scope, use ``None``:: @app.route('/user') @require_oauth() def user_profile(): user = current_token.user return jsonify(user) # or with None @app.route('/user') @require_oauth(None) def user_profile(): user = current_token.user return jsonify(user) The ``current_token`` is a proxy to the Token model you have defined above. Since there is a ``user`` relationship on the Token model, we can access this ``user`` with ``current_token.user``. If the decorator is not your favorite, there is a ``with`` statement for you:: @app.route('/user') def user_profile(): with require_oauth.acquire('profile') as token: user = token.user return jsonify(user) .. _flask_oauth2_multiple_scopes: Multiple Scopes --------------- .. versionchanged:: v1.0 You can apply multiple scopes to one endpoint in **AND**, **OR** and mix modes. Here are some examples: .. code-block:: python @app.route('/profile') @require_oauth(['profile email']) def user_profile(): user = current_token.user return jsonify(user) It requires the token containing both ``profile`` and ``email`` scope. .. code-block:: python @app.route('/profile') @require_oauth(['profile', 'email']') def user_profile(): user = current_token.user return jsonify(user) It requires the token containing either ``profile`` or ``email`` scope. It is also possible to mix **AND** and **OR** logic. e.g.:: @app.route('/profile') @require_oauth(['profile email', 'user']) def user_profile(): user = current_token.user return jsonify(user) This means if the token will be valid if: 1. token contains both ``profile`` and ``email`` scope 2. or token contains ``user`` scope Optional ``require_oauth`` -------------------------- There is one more parameter for ``require_oauth`` which is used to serve public endpoints:: @app.route('/timeline') @require_oauth(optional=True) def timeline_api(): if current_token: return get_user_timeline(current_token.user) return get_public_timeline() MethodView & Flask-Restful -------------------------- You can also use the ``require_oauth`` decorator in ``flask.views.MethodView`` and ``flask_restful.Resource``:: from flask.views import MethodView class UserAPI(MethodView): decorators = [require_oauth('profile')] from flask_restful import Resource class UserAPI(Resource): method_decorators = [require_oauth('profile')] authlib-1.3.2/docs/flask/index.rst000066400000000000000000000004431466226534200170510ustar00rootroot00000000000000Flask OAuth Providers ===================== Authlib has built-in Flask integrations for building OAuth 1.0, OAuth 2.0 and OpenID Connect servers. It is best if developers can read :ref:`intro_oauth1` and :ref:`intro_oauth2` at first. .. toctree:: :maxdepth: 2 1/index 2/index authlib-1.3.2/docs/index.rst000066400000000000000000000027361466226534200157600ustar00rootroot00000000000000.. meta:: :description: The ultimate Python library in building OAuth 1.0, OAuth 2.0, OpenID Connect providers and clients, with built-in JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), and JSON Web Token (JWT) support. :image: https://repository-images.githubusercontent.com/108510280/cab09300-d0ad-11e9-8113-ed9e6ba76927 Authlib: Python Authentication ============================== Release v\ |version|. (:ref:`Installation `) The ultimate Python library in building OAuth and OpenID Connect servers. It is designed from low level specifications implementations to high level frameworks integrations, to meet the needs of everyone. Authlib is compatible with Python3.6+. User's Guide ------------ This part of the documentation begins with some background information about Authlib, and installation of Authlib. Then it will explain OAuth 1.0, OAuth 2.0, and JOSE. At last, it shows the implementation in frameworks, and libraries such as Flask, Django, Requests, HTTPX, Starlette, FastAPI, and etc. .. toctree:: :maxdepth: 2 basic/index client/index jose/index oauth/index flask/index django/index specs/index community/index Get Updates ----------- Stay tuned with Authlib, here is a history of Authlib changes. .. toctree:: :maxdepth: 2 changelog Consider to follow `Authlib on Twitter `_, and subscribe `Authlib Blog `_. authlib-1.3.2/docs/jose/000077500000000000000000000000001466226534200150475ustar00rootroot00000000000000authlib-1.3.2/docs/jose/index.rst000066400000000000000000000015161466226534200167130ustar00rootroot00000000000000.. _jose: JOSE Guide ========== This part of the documentation contains information on the JOSE implementation. It includes: 1. JSON Web Signature (JWS) 2. JSON Web Encryption (JWE) 3. JSON Web Key (JWK) 4. JSON Web Algorithm (JWA) 5. JSON Web Token (JWT) .. important:: We are splitting the ``jose`` module into a separated package. You may be interested in joserfc_. .. _joserfc: https://jose.authlib.org/ Usage ----- A simple example on how to use JWT with Authlib:: from authlib.jose import jwt with open('private.pem', 'rb') as f: key = f.read() payload = {'iss': 'Authlib', 'sub': '123', ...} header = {'alg': 'RS256'} s = jwt.encode(header, payload, key) Guide ----- Follow the documentation below to find out more in detail. .. toctree:: :maxdepth: 2 jws jwe jwk jwt authlib-1.3.2/docs/jose/jwe.rst000066400000000000000000000061131466226534200163670ustar00rootroot00000000000000.. _jwe_guide: JSON Web Encryption (JWE) ========================= .. module:: authlib.jose :noindex: JSON Web Encryption (JWE) represents encrypted content using JSON-based data structures. .. important:: We are splitting the ``jose`` module into a separated package. You may be interested in joserfc_. .. _joserfc: https://jose.authlib.org/en/dev/guide/jwe/ There are two types of JWE Serializations: 1. JWE Compact Serialization 2. JWE JSON Serialization Authlib has only implemented the **Compact Serialization**. This feature is not mature yet, use at your own risk. The JWE Compact Serialization represents encrypted content as a compact, URL-safe string. This string is: BASE64URL(UTF8(JWE Protected Header)) || '.' || BASE64URL(JWE Encrypted Key) || '.' || BASE64URL(JWE Initialization Vector) || '.' || BASE64URL(JWE Ciphertext) || '.' || BASE64URL(JWE Authentication Tag) An example (with line breaks for display purposes only):: eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ . OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8 1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi 6UklfCpIMfIjf7iGdXKHzg . 48V1_ALb6US04U3b . 5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji SdiwkIr3ajwQzaBtQD_A . XFBoMYUZodetZdvTiFvSkQ A JWE requires JWA to work properly. The algorithms for JWE are provided in :ref:`specs/rfc7518`. Compact Serialize and Deserialize --------------------------------- Generate a JWE compact serialization would be easy with :meth:`JsonWebEncryption.serialize_compact`, build a JWE instance with JWA:: from authlib.jose import JsonWebEncryption jwe = JsonWebEncryption() protected = {'alg': 'RSA-OAEP', 'enc': 'A256GCM'} payload = b'hello' with open('rsa_public.pem', 'rb') as f: key = f.read() s = jwe.serialize_compact(protected, payload, key) There are two required algorithms in protected header: ``alg`` and ``enc``. The available ``alg`` list: 1. RSA1_5, RSA-OAEP, RSA-OAEP-256 2. A128KW, A192KW, A256KW 3. A128GCMKW, A192GCMKW, A256GCMKW The available ``enc`` list: 1. A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 2. A128GCM, A192GCM, A256GCM More ``alg`` and ``enc`` will be added in the future. It is also available to compress the payload with ``zip`` header:: protected = {'alg': 'RSA-OAEP', 'enc': 'A256GCM', 'zip': 'DEF'} s = jwe.serialize_compact(protected, payload, key) To deserialize a JWE Compact Serialization, use :meth:`JsonWebEncryption.deserialize_compact`:: with open('rsa_private.pem', 'rb') as f: key = f.read() data = jwe.deserialize_compact(s, key) jwe_header = data['header'] payload = data['payload'] The result of the ``deserialize_compact`` is a dict, which contains ``header`` and ``payload``. Using **JWK** for keys? Find how to use JWK with :ref:`jwk_guide`. authlib-1.3.2/docs/jose/jwk.rst000066400000000000000000000024041466226534200163740ustar00rootroot00000000000000.. _jwk_guide: JSON Web Key (JWK) ================== .. important:: We are splitting the ``jose`` module into a separated package. You may be interested in joserfc_. .. _joserfc: https://jose.authlib.org/en/dev/guide/jwk/ .. module:: authlib.jose :noindex: A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key. An example would help a lot:: { "kty": "EC", "crv": "P-256", "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", "kid": "iss-a" } This is an Elliptic Curve Public Key represented by JSON data structure. :meth:`JsonWebKey.import_key` will convert PEM, JSON, bytes into these keys: 1. :class:`OctKey` 2. :class:`RSAKey` 3. :class:`ECKey` 4. :class:`OKPKey` Algorithms for ``kty`` (Key Type) is defined by :ref:`specs/rfc7518`. Import a key with:: from authlib.jose import JsonWebKey key_data = read_file('public.pem') key = JsonWebKey.import_key(key_data, {'kty': 'RSA'}) key.as_dict() key.as_json() You may pass extra parameters into ``import_key`` method, available parameters can be found on RFC7517 `Section 4`_. .. _`Section 4`: https://tools.ietf.org/html/rfc7517#section-4 authlib-1.3.2/docs/jose/jws.rst000066400000000000000000000165531466226534200164160ustar00rootroot00000000000000.. _jws_guide: JSON Web Signature (JWS) ======================== .. module:: authlib.jose :noindex: JSON Web Signature (JWS) represents content secured with digital signatures or Message Authentication Codes (MACs) using JSON-based data structures. .. important:: We are splitting the ``jose`` module into a separated package. You may be interested in joserfc_. .. _joserfc: https://jose.authlib.org/en/dev/guide/jws/ There are two types of JWS Serializations: 1. JWS Compact Serialization 2. JWS JSON Serialization The JWS Compact Serialization represents digitally signed or MACed content as a compact, URL-safe string. An example (with line breaks for display purposes only):: eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 . eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt cGxlLmNvbS9pc19yb290Ijp0cnVlfQ . dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk There are two types of JWS JSON Serialization syntax: 1. General JWS JSON Serialization Syntax 2. Flattened JWS JSON Serialization Syntax An example on General JWS JSON Serialization Syntax (with line breaks within values for display purposes only):: { "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGF tcGxlLmNvbS9pc19yb290Ijp0cnVlfQ", "signatures":[ {"protected":"eyJhbGciOiJSUzI1NiJ9", "header":{"kid":"2010-12-29"}, "signature": "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZ mh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjb KBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHl b1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZES c6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AX LIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"}, {"protected":"eyJhbGciOiJFUzI1NiJ9", "header":{"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"}, "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8IS lSApmWQxfKTUJqPP3-Kg6NU1Q"}] } An example on Flattened JWS JSON Serialization Syntax (with line breaks within values for display purposes only):: { "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGF tcGxlLmNvbS9pc19yb290Ijp0cnVlfQ", "protected":"eyJhbGciOiJFUzI1NiJ9", "header": {"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"}, "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8IS lSApmWQxfKTUJqPP3-Kg6NU1Q" } A JWS requires JWA to work properly. The algorithms for JWS are provided in :ref:`specs/rfc7518`. Compact Serialize and Deserialize --------------------------------- Generate a JWS compact serialization would be easy with :meth:`JsonWebSignature.serialize_compact`, build a JWS instance with JWA:: from authlib.jose import JsonWebSignature jws = JsonWebSignature() # alg is a required parameter name protected = {'alg': 'HS256'} payload = b'example' secret = b'secret' jws.serialize_compact(protected, payload, secret) There are other ``alg`` that you could use. Here is a full list of available algorithms: 1. HS256, HS384, HS512 2. RS256, RS384, RS512 3. ES256, ES384, ES512, ES256K 4. PS256, PS384, PS512 5. EdDSA For example, a JWS with RS256 requires a private PEM key to sign the JWS:: jws = JsonWebSignature(algorithms=['RS256']) protected = {'alg': 'RS256'} payload = b'example' with open('private.pem', 'rb') as f: secret = f.read() jws.serialize_compact(protected, payload, secret) To deserialize a JWS Compact Serialization, use :meth:`JsonWebSignature.deserialize_compact`:: # if it is a RS256, we use public RSA key with open('public.pem', 'rb') as f: key = f.read() data = jws.deserialize_compact(s, key) jws_header = data['header'] payload = data['payload'] .. important:: The above method is susceptible to a signature bypass described in CVE-2016-10555. It allows mixing symmetric algorithms and asymmetric algorithms. You should never combine symmetric (HS) and asymmetric (RS, ES, PS) signature schemes. If you must support both protocols use a custom key loader which provides a different keys for different methods. Load a different ``key`` for symmetric and asymmetric signatures:: def load_key(header, payload): if header['alg'] == 'RS256': return rsa_pub_key elif header['alg'] == 'HS256': return shared_secret else: raise UnsupportedAlgorithmError() claims = jws.deserialize_compact(token, load_key) A ``key`` can be dynamically loaded, if you don't know which key to be used:: def load_key(header, payload): kid = header['kid'] return get_key_by_kid(kid) jws.deserialize_compact(s, load_key) The result of the ``deserialize_compact`` is a dict, which contains ``header`` and ``payload``. The value of the ``header`` is a :class:`JWSHeader`. Using **JWK** for keys? Find how to use JWK with :ref:`jwk_guide`. JSON Serialize and Deserialize ------------------------------ :meth:`JsonWebSignature.serialize_json` is used to generate a JWS JSON Serialization, :meth:`JsonWebSignature.deserialize_json` is used to extract a JWS JSON Serialization. The usage is the same as "Compact Serialize and Deserialize", the only difference is the "header":: # Flattened JSON serialization header syntax header = {'protected': {'alg': 'HS256'}, 'header': {'cty': 'JWT'}} key = b'secret' payload = b'example' jws.serialize_json(header, payload, key) # General JSON serialization header syntax header = [{'protected': {'alg': 'HS256'}, 'header': {'cty': 'JWT'}}] jws.serialize_json(header, payload, key) For general JSON Serialization, there may be many signatures, each signature can use its own key, in this case the dynamical key would be useful:: def load_private_key(header, payload): kid = header['kid'] return get_private_key(kid) header = [ {'protected': {'alg': 'HS256'}, 'header': {'kid': 'foo'}}, {'protected': {'alg': 'RS256'}, 'header': {'kid': 'bar'}}, ] data = jws.serialize_json(header, payload, load_private_key) # data is a dict def load_public_key(header, payload): kid = header['kid'] return get_public_key(kid) jws.deserialize_json(data, load_public_key) Actually, there is a :meth:`JsonWebSignature.serialize` and :meth:`JsonWebSignature.deserialize`, which can automatically serialize and deserialize Compact and JSON Serializations. The result of the ``deserialize_json`` is a dict, which contains ``header`` and ``payload``. The value of the ``header`` is a :class:`JWSHeader`. Using **JWK** for keys? Find how to use JWK with :ref:`jwk_guide`. Header Parameter Names ~~~~~~~~~~~~~~~~~~~~~~ :class:`JsonWebSignature` has a validation on header parameter names. It will first check if the parameter name is in "Registered Header Parameter Names" defined by RFC7515 `Section 4.1`_. Then it will check if the parameter name is in your defined private headers. In this case, if there are header parameter names out of the registered header parameter names scope, you can pass the names:: private_headers = ['h1', 'h2'] jws = JsonWebSignature(private_headers=private_headers) .. _`Section 4.1`: https://tools.ietf.org/html/rfc7515#section-4.1 authlib-1.3.2/docs/jose/jwt.rst000066400000000000000000000134411466226534200164100ustar00rootroot00000000000000.. _jwt_guide: JSON Web Token (JWT) ==================== .. important:: We are splitting the ``jose`` module into a separated package. You may be interested in joserfc_. .. _joserfc: https://jose.authlib.org/en/dev/guide/jwt/ .. module:: authlib.jose :noindex: JSON Web Token (JWT) is structured by :ref:`specs/rfc7515` or :ref:`specs/rfc7516` with certain payload claims. The JWT implementation in Authlib has all built-in algorithms via :ref:`specs/rfc7518`, it can also load private/public keys of :ref:`specs/rfc7517`:: >>> from authlib.jose import jwt >>> header = {'alg': 'RS256'} >>> payload = {'iss': 'Authlib', 'sub': '123', ...} >>> private_key = read_file('private.pem') >>> s = jwt.encode(header, payload, private_key) >>> public_key = read_file('public.pem') >>> claims = jwt.decode(s, public_key) >>> print(claims) {'iss': 'Authlib', 'sub': '123', ...} >>> print(claims.header) {'alg': 'RS256', 'typ': 'JWT'} >>> claims.validate() The imported ``jwt`` is an instance of :class:`JsonWebToken`. It has all supported JWS algorithms, and it can handle JWK automatically. When :meth:`JsonWebToken.encode` a payload, JWT will check payload claims for security, if you really want to expose them, you can always turn it off via ``check=False``. .. important:: JWT payload with JWS is not encrypted, it is just signed. Anyone can extract the payload without any private or public keys. Adding sensitive data like passwords, social security numbers in JWT payload is not safe if you are going to send them in a non-secure connection. You can also use JWT with JWE which is encrypted. But this feature is not mature, documentation is not provided yet. JWT Encode ---------- ``jwt.encode`` is the method to create a JSON Web Token string. It encodes the payload with the given ``alg`` in header:: >>> from authlib.jose import jwt >>> header = {'alg': 'RS256'} >>> payload = {'iss': 'Authlib', 'sub': '123', ...} >>> private_key = read_file('private.pem') >>> s = jwt.encode(header, payload, private_key) The available keys in headers are defined by :ref:`specs/rfc7515`. JWT Decode ---------- ``jwt.decode`` is the method to translate a JSON Web Token string into the dict of the payload:: >>> from authlib.jose import jwt >>> public_key = read_file('public.pem') >>> claims = jwt.decode(s, public_key) .. important:: This decoding method is insecure. By default ``jwt.decode`` parses the alg header. This allows symmetric macs and asymmetric signatures. If both are allowed a signature bypass described in CVE-2016-10555 is possible. See the following section for a mitigation. The returned value is a :class:`JWTClaims`, check the next section to validate claims value. JWT with limited Algorithms --------------------------- There are cases that we don't want to support all the ``alg`` values, especially when decoding a token. In this case, we can pass a list of supported ``alg`` into :class:`JsonWebToken`:: >>> from authlib.jose import JsonWebToken >>> jwt = JsonWebToken(['RS256']) .. important:: You should never combine symmetric (HS) and asymmetric (RS, ES, PS) signature schemes. When both are allowed a signature bypass described in CVE-2016-10555 is possible. If you must support both protocols use a custom key loader which provides a different keys for different methods. Load a different ``key`` for symmetric and asymmetric signatures:: def load_key(header, payload): if header['alg'] == 'RS256': return rsa_pub_key elif header['alg'] == 'HS256': return shared_secret else: raise UnsupportedAlgorithmError() claims = jwt.decode(token, load_key) JWT Payload Claims Validation ----------------------------- :meth:`JsonWebToken.decode` accepts 3 claims-related parameters: ``claims_cls``, ``claims_option`` and ``claims_params``. The default ``claims_cls`` is :class:`JWTClaims`. The ``decode`` method returns:: >>> JWTClaims(payload, header, options=claims_options, params=claims_params) Claims validation is actually handled by :meth:`JWTClaims.validate`, which validates payload claims with ``claims_option`` and ``claims_params``. For standard JWTClaims, ``claims_params`` value is not used, but it is used in :class:`~authlib.oidc.core.IDToken`. Here is an example of ``claims_option``:: { "iss": { "essential": True, "values": ["https://example.com", "https://example.org"] }, "sub": { "essential": True "value": "248289761001" }, "jti": { "validate": validate_jti } } It is a dict configuration, the option key is the name of a claim. - **essential**: this value is REQUIRED. - **values**: claim value can be any one in the values list. - **value**: claim value MUST be the same value. - **validate**: a function to validate the claim value. Use dynamic keys ---------------- When ``.encode`` and ``.decode`` a token, there is a ``key`` parameter to use. This ``key`` can be the bytes of your PEM key, a JWK set, and a function. There ara cases that you don't know which key to use to ``.decode`` the token. For instance, you have a JWK set:: jwks = { "keys": [ { "kid": "k1", ...}, { "kid": "k2", ...}, ] } And in the token, it has a ``kid=k2`` in the header part, if you pass ``jwks`` to the ``key`` parameter, Authlib will auto resolve the correct key:: jwt.decode(s, key=jwks, ...) It is also possible to resolve the correct key by yourself:: def resolve_key(header, payload): return my_keys[header['kid']] jwt.decode(s, key=resolve_key) For ``.encode``, if you pass a JWK set, it will randomly pick a key and assign its ``kid`` into the header. authlib-1.3.2/docs/oauth/000077500000000000000000000000001466226534200152275ustar00rootroot00000000000000authlib-1.3.2/docs/oauth/1/000077500000000000000000000000001466226534200153675ustar00rootroot00000000000000authlib-1.3.2/docs/oauth/1/index.rst000066400000000000000000000007041466226534200172310ustar00rootroot00000000000000OAuth 1.0 ========= OAuth 1.0 is the standardization and combined wisdom of many well established industry protocols at its creation time. It was first introduced as Twitter's open protocol. It is similar to other protocols at that time in use (Google AuthSub, AOL OpenAuth, Yahoo BBAuth, Upcoming API, Flickr API, etc). If you are creating an open platform, AUTHLIB ENCOURAGES YOU TO USE OAUTH 2.0 INSTEAD. .. toctree:: :maxdepth: 2 intro authlib-1.3.2/docs/oauth/1/intro.rst000066400000000000000000000123721466226534200172610ustar00rootroot00000000000000.. meta:: :description: Understand the concepts in OAuth 1.0, the authorization flow, roles, signatures, and etc. :image: https://user-images.githubusercontent.com/290496/48671968-2c316080-eb73-11e8-9e6a-9e895cd67262.png .. _intro_oauth1: Introduce OAuth 1.0 =================== OAuth 1.0 is the standardization and combined wisdom of many well established industry protocols at its creation time. It was first introduced as Twitter's open protocol. It is similar to other protocols at that time in use (Google AuthSub, AOL OpenAuth, Yahoo BBAuth, Upcoming API, Flickr API, etc). Authlib implemented OAuth 1.0 according to RFC5849_, this section will help developers understand the concepts in OAuth 1.0, the authorization flow of OAuth 1.0, and etc. OAuth provides a method for clients to access server resources on behalf of a resource owner (such as a different client or an end- user). It also provides a process for end-users to authorize third- party access to their server resources without sharing their credentials (typically, a username and password pair), using user- agent redirection. .. _RFC5849: https://tools.ietf.org/html/rfc5849 Here is an overview of a typical OAuth 1.0 authorization flow: .. figure:: https://user-images.githubusercontent.com/290496/48671968-2c316080-eb73-11e8-9e6a-9e895cd67262.png :alt: OAuth 1.0 Flow OAuth 1.0 Flow -------------- Let's take your mobile Twitter app as an example. When a user wants to send a tweet through your application, he/she needs to authenticate at first. When the app is opened, and the login button is clicked: 1. **Client** uses its **client credentials** to make a request to server, asking the server for a temporary credential. 2. **Server** responds with a **temporary credential** if it verified your client credential. 3. **Client** saves temporary credential for later use, then open a web view (browser) for **resource owner** to grant the access. 4. When access is granted, **Server** responds with a **verifier** to client. 5. **Client** uses this **verifier and temporary credential** to make a request to the server asking for **token credentials**. 6. **Server** responds with access token if it verified everything. And then **Client** can send tweets with the **token credentials**. Roles in OAuth 1.0 ------------------ To understand above flow, you need to know the roles in OAuth 1.0. There are usually three roles in an OAuth 1.0 flow. Take the above example, imagining that you are building a mobile app to send tweets: - **Client**: a client is a third-party application. In this case, it is your Twitter application. - **Resource Owner**: the users on Twitter are the resource owners, since they own their tweets (resources). - **Server**: authorization and resource server. In this case, it is Twitter. OAuth 1.0 in HTTP ----------------- Let's explain OAuth 1.0 in HTTP one more time. The first step is: **Client** uses its **client credentials** to make a request to server, asking the server for a temporary credential. It means we need to ask a temporary credential from Twitter. A temporary credential is called **request token** in Twitter. The first request is (line breaks are for display purposes only): .. code-block:: http POST /oauth/request_token HTTP/1.1 Host: api.twitter.com Authorization: OAuth oauth_consumer_key="dpf43f3p2l4k3l03", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131200", oauth_nonce="wIjqoS", oauth_callback="https%3A%2F%.example.com%2Fauth", oauth_signature="74KNZJeDHnMBp0EMJ9ZHt%2FXKycU%3D", oauth_version="1.0" And Twitter will response with a temporary credential like: .. code-block:: http HTTP/1.1 200 OK Content-Type: application/x-www-form-urlencoded oauth_token=Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik &oauth_token_secret=Kd75W4OQfb2oJTV0vzGzeXftVAwgMnEK9MumzYcM &oauth_callback_confirmed=true Our Twitter client will then redirect user to the authorization page:: https://api.twitter.com/oauth/authenticate?oauth_token=Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik On this authorization page, if user granted access to your Twitter client, it will redirect back to your application page, e.g.:: https://example.com/auth?oauth_token=Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik&oauth_verifier=hfdp7dh39dks9884 And the final step is here, use the temporary credential to exchange access token: .. code-block:: http POST /oauth/access_token HTTP/1.1 Host: api.twitter.com Authorization: OAuth oauth_consumer_key="dpf43f3p2l4k3l03", oauth_token="Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131201", oauth_nonce="walatlh", oauth_verifier="hfdp7dh39dks9884", oauth_signature=".....", oauth_version="1.0" If everything works well, Twitter would response with the final access token now: .. code-block:: http HTTP/1.1 200 OK Content-Type: application/x-www-form-urlencoded oauth_token=6253282-eWudHldSbIaelX7swmsiHImEL4KinwaGloHANdrY &oauth_token_secret=2EEfA6BG5ly3sR3XjE0IBSnlQu4ZrUzPiYTmrkVU &user_id=6253282 You can use the ``oauth_token`` and ``oauth_token_secret`` for later use. authlib-1.3.2/docs/oauth/2/000077500000000000000000000000001466226534200153705ustar00rootroot00000000000000authlib-1.3.2/docs/oauth/2/index.rst000066400000000000000000000000761466226534200172340ustar00rootroot00000000000000OAuth 2.0 ========= .. toctree:: :maxdepth: 2 intro authlib-1.3.2/docs/oauth/2/intro.rst000066400000000000000000000133161466226534200172610ustar00rootroot00000000000000.. meta:: :description: Understand the concepts in OAuth 2.0, the authorization flow, grant types, roles, authentication methods and etc. :image: https://user-images.githubusercontent.com/290496/48670041-e5803e00-eb53-11e8-91a9-3776276d6bf6.png .. _intro_oauth2: Introduce OAuth 2.0 =================== The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf. This section will help developers understand the concepts in OAuth 2.0, but not in deep of OAuth 2.0. Here is an overview of a very simple OAuth 2.0 flow: .. figure:: https://user-images.githubusercontent.com/290496/48670041-e5803e00-eb53-11e8-91a9-3776276d6bf6.png :alt: OAuth 2.0 Flow OAuth 2.0 Roles --------------- There are usually four roles in an OAuth 2.0 flow. Let's take GitHub as an example, you are building an application to analyze one's code on GitHub: - **Client**: a client is a third-party application, in this case, it is your application. - **Resource Owner**: the users and orgs on GitHub are the resource owners, since they own their source code (resources). - **Resource Server**: The API servers of GitHub. Your **client** will make requests to the resource server to fetch source code. The server serves resources. - **Authorization Server**: The server for **client** to obtain an access token. OAuth 2.0 Flow -------------- The above image is a simplified version of an OAuth 2.0 authorization. Let's take GitHub as an example. A user wants to use your application to analyze his/her source code on GitHub. It usually takes these steps: 1. Your application (**client**) prompts the user to log in. 2. The user clicks the *login* button, your application will redirect to GitHub's authorize page (**Authorization Server**). 3. The user (he/she is a GitHub user, which means he/she is a **Resource Owner**) clicks the *allow* button to tell GitHub that he/she granted the access. 4. The **Authorization Server** issues an **access token** to your application. (This step can contain several sub-steps) 5. Your application uses the **access token** to fetch source code from GitHub's **Resource Server**, analyze the source code and return the result to your application user. But there are more details inside the flow. The most important thing in OAuth 2.0 is the authorization. A client obtains an access token from the authorization server with the grant of the resource owner. Grant Types ----------- .. module:: authlib.oauth2.rfc6749.grants :noindex: Authorization server MAY supports several **grant types** during the **authorization**, step 1 and 2. A grant type defines a way of how the authorization server will verify the request and issue the token. There are lots of built-in grant types in Authlib, including: - :class:`AuthorizationCodeGrant` - :class:`ImplicitGrant` - :class:`ResourceOwnerPasswordCredentialsGrant` - :class:`ClientCredentialsGrant` - :class:`RefreshTokenGrant` - :class:`JWTBearerGrant` Take ``authorization_code`` as an example, in step 2, when the resource owner granted the access, **Authorization Server** will return a ``code`` to the client. The client can use this ``code`` to exchange an access token: .. code-block:: http :emphasize-lines: 3,6 POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA .. _client_auth_methods: Client Authentication Methods ----------------------------- In the above code, there is an ``Authorization`` header; it contains the information of the client. A client MUST provide its client information to obtain an access token. There are several ways to provide this data, for instance: - ``none``: The client is a public client which means it has no client_secret .. code-block:: http :emphasize-lines: 6 POST /token HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &client_id=s6BhdRkqt3 - ``client_secret_post``: The client uses the HTTP POST parameters .. code-block:: http :emphasize-lines: 6 POST /token HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &client_id=s6BhdRkqt3&client_secret=gX1fBat3bV - ``client_secret_basic``: The client uses HTTP Basic Authorization .. code-block:: http :emphasize-lines: 3 POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA There are more client authentication methods defined by OAuth 2.0 extensions, including ``client_secret_jwt``, ``private_key_jwt``. They can be found in section :ref:`jwt_client_authentication`. Token Scopes ------------ Scope is a very important concept in OAuth 2.0. An access token is usually issued with limited scopes. For instance, your "source code analyzer" application MAY only have access to the public repositories of a GitHub user. Endpoints --------- The above example only shows one endpoint, which is **token endpoint**. There are more endpoints in OAuth 2.0. For example: - :ref:`Token Revocation Endpoint ` - :ref:`Dynamic Client Registration Endpoint ` - :ref:`Token Introspection Endpoint ` authlib-1.3.2/docs/oauth/index.rst000066400000000000000000000003461466226534200170730ustar00rootroot00000000000000OAuth & OpenID Connect ====================== This section contains introduction and implementation of Authlib core OAuth 1.0, OAuth 2.0, and OpenID Connect. .. toctree:: :maxdepth: 2 1/index 2/index oidc/index authlib-1.3.2/docs/oauth/oidc/000077500000000000000000000000001466226534200161455ustar00rootroot00000000000000authlib-1.3.2/docs/oauth/oidc/core.rst000066400000000000000000000011451466226534200176300ustar00rootroot00000000000000OpenID Connect Core =================== This section is about the core part of OpenID Connect. Authlib implemented `OpenID Connect Core 1.0`_ on top of OAuth 2.0. It enhanced OAuth 2.0 with: .. module:: authlib.oidc.core.grants :noindex: 1. :class:`OpenIDCode` extension for Authorization code flow 2. :class:`OpenIDImplicitGrant` grant type for implicit flow 3. :class:`OpenIDHybridGrant` grant type for hybrid flow .. _`OpenID Connect Core 1.0`: https://openid.net/specs/openid-connect-core-1_0.html Authorization Code Flow ----------------------- Implicit Flow ------------- Hybrid Flow ----------- authlib-1.3.2/docs/oauth/oidc/discovery.rst000066400000000000000000000053371466226534200207160ustar00rootroot00000000000000OpenID Connect Discovery ======================== This section is about OpenID Provider Discovery. OpenID Providers have metadata describing their configuration. The endpoint is usually located at:: /.well-known/openid-configuration The metadata is formatted in JSON. Here is an example of how it looks like: .. code-block:: http HTTP/1.1 200 OK Content-Type: application/json { "issuer": "https://server.example.com", "authorization_endpoint": "https://server.example.com/connect/authorize", "token_endpoint": "https://server.example.com/connect/token", "token_endpoint_auth_methods_supported": ["client_secret_basic", "private_key_jwt"], "token_endpoint_auth_signing_alg_values_supported": ["RS256", "ES256"], "userinfo_endpoint": "https://server.example.com/connect/userinfo", "check_session_iframe": "https://server.example.com/connect/check_session", "end_session_endpoint": "https://server.example.com/connect/end_session", "jwks_uri": "https://server.example.com/jwks.json", "registration_endpoint": "https://server.example.com/connect/register", "scopes_supported": ["openid", "profile", "email", "address", "phone", "offline_access"], "response_types_supported": ["code", "code id_token", "id_token", "token id_token"], "acr_values_supported": ["urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"], "subject_types_supported": ["public", "pairwise"], "userinfo_signing_alg_values_supported": ["RS256", "ES256", "HS256"], "userinfo_encryption_alg_values_supported": ["RSA1_5", "A128KW"], "userinfo_encryption_enc_values_supported": ["A128CBC-HS256", "A128GCM"], "id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"], "id_token_encryption_alg_values_supported": ["RSA1_5", "A128KW"], "id_token_encryption_enc_values_supported": ["A128CBC-HS256", "A128GCM"], "request_object_signing_alg_values_supported": ["none", "RS256", "ES256"], "display_values_supported": ["page", "popup"], "claim_types_supported": ["normal", "distributed"], "claims_supported": ["sub", "iss", "auth_time", "acr", "name", "given_name", "family_name", "nickname", "profile", "picture", "website", "email", "email_verified", "locale", "zoneinfo", "http://example.info/claims/groups"], "claims_parameter_supported": true, "service_documentation": "http://server.example.com/connect/service_documentation.html", "ui_locales_supported": ["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"] } authlib-1.3.2/docs/oauth/oidc/index.rst000066400000000000000000000001371466226534200200070ustar00rootroot00000000000000OpenID Connect ============== .. toctree:: :maxdepth: 2 intro core discovery authlib-1.3.2/docs/oauth/oidc/intro.rst000066400000000000000000000002011466226534200200230ustar00rootroot00000000000000Introduce OpenID Connect ======================== OpenID Connect is an identity layer on top of the OAuth 2.0 framework. (TBD) authlib-1.3.2/docs/requirements.txt000066400000000000000000000002151466226534200173710ustar00rootroot00000000000000cryptography pycryptodomex>=3.10,<4 Flask Django SQLAlchemy requests httpx>=0.18.2 starlette sphinx sphinx-design sphinx-copybutton shibuya authlib-1.3.2/docs/specs/000077500000000000000000000000001466226534200152245ustar00rootroot00000000000000authlib-1.3.2/docs/specs/index.rst000066400000000000000000000007061466226534200170700ustar00rootroot00000000000000Specifications ============== Guide on specifications. You don't have to read this section if you are just using Authlib. But it would be good for you to understand how Authlib works. .. toctree:: :maxdepth: 2 rfc5849 rfc6749 rfc6750 rfc7009 rfc7515 rfc7516 rfc7517 rfc7518 rfc7519 rfc7523 rfc7591 rfc7592 rfc7636 rfc7638 rfc7662 rfc8037 rfc8414 rfc8628 rfc9068 oidc authlib-1.3.2/docs/specs/oidc.rst000066400000000000000000000020501466226534200166710ustar00rootroot00000000000000.. _specs/oidc: OpenID Connect 1.0 ================== .. meta:: :description: General implementation of OpenID Connect 1.0 in Python. Learn how to create a OpenID Connect provider in Python. This part of the documentation covers the specification of OpenID Connect. Learn how to use it in :ref:`flask_oidc_server` and :ref:`django_oidc_server`. OpenID Grants ------------- .. module:: authlib.oidc.core.grants .. autoclass:: OpenIDToken :show-inheritance: :members: .. autoclass:: OpenIDCode :show-inheritance: :members: .. autoclass:: OpenIDImplicitGrant :show-inheritance: :members: .. autoclass:: OpenIDHybridGrant :show-inheritance: :members: OpenID Claims ------------- .. module:: authlib.oidc.core .. autoclass:: IDToken :show-inheritance: :members: .. autoclass:: CodeIDToken :show-inheritance: :members: .. autoclass:: ImplicitIDToken :show-inheritance: :members: .. autoclass:: HybridIDToken :show-inheritance: :members: .. autoclass:: UserInfo :members: authlib-1.3.2/docs/specs/rfc5849.rst000066400000000000000000000015671466226534200170730ustar00rootroot00000000000000RFC5849: The OAuth 1.0 Protocol =============================== .. meta:: :description: API references of RFC5849 in Python implementation. Learn how to create an OAuth 1.0 provider in Python. This section contains the generic implementation of RFC5849_. Learn how to create an OAuth 1.0 provider in these frameworks: 1. Flask: :ref:`flask_oauth1_server`. 2. Django: :ref:`django_oauth1_server`. .. _RFC5849: https://tools.ietf.org/html/rfc5849 .. module:: authlib.oauth1.rfc5849 Servers ------- .. autoclass:: AuthorizationServer :members: :inherited-members: .. autoclass:: ResourceProtector :members: :inherited-members: Models Mixin ------------ .. autoclass:: ClientMixin :members: .. autoclass:: TemporaryCredentialMixin :members: :inherited-members: .. autoclass:: TokenCredentialMixin :members: :inherited-members: authlib-1.3.2/docs/specs/rfc6749.rst000066400000000000000000000040001466226534200170540ustar00rootroot00000000000000.. _specs/rfc6749: RFC6749: The OAuth 2.0 Authorization Framework ============================================== .. meta:: :description: API references of RFC6749 in Python implementation. Learn how to create an OAuth 2.0 provider in Python. This section contains the generic implementation of RFC6749_. You should read :ref:`intro_oauth2` at first. Here are some tips: 1. Have a better understanding of :ref:`OAuth 2.0 ` 2. How to use :ref:`oauth_2_session` for Requests 3. How to implement :ref:`flask_client` 4. How to implement :ref:`flask_oauth2_server` 5. How to implement :ref:`django_client` 6. How to implement :ref:`django_oauth2_server` .. _RFC6749: https://tools.ietf.org/html/rfc6749 API References -------------- Here are the API references for developers. For framework level interfaces, check: Servers ~~~~~~~ .. module:: authlib.oauth2.rfc6749 .. autoclass:: AuthorizationServer :members: .. autoclass:: ResourceProtector :members: Client Model ~~~~~~~~~~~~ .. autoclass:: ClientMixin :members: .. _specs/rfc6949-errors: Token Model ~~~~~~~~~~~ .. autoclass:: AuthorizationCodeMixin :members: .. autoclass:: TokenMixin :members: Errors ~~~~~~ .. autoclass:: OAuth2Error :members: .. autoclass:: InsecureTransportError :members: .. autoclass:: InvalidRequestError .. autoclass:: InvalidClientError .. autoclass:: InvalidGrantError .. autoclass:: UnauthorizedClientError .. autoclass:: UnsupportedGrantTypeError .. autoclass:: InvalidScopeError .. autoclass:: AccessDeniedError Grant Types ~~~~~~~~~~~ .. module:: authlib.oauth2.rfc6749.grants .. autoclass:: AuthorizationCodeGrant :member-order: bysource :members: .. autoclass:: ImplicitGrant :member-order: bysource :members: .. autoclass:: ResourceOwnerPasswordCredentialsGrant :member-order: bysource :members: .. autoclass:: ClientCredentialsGrant :member-order: bysource :members: .. autoclass:: RefreshTokenGrant :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc6750.rst000066400000000000000000000015011466226534200170470ustar00rootroot00000000000000.. _specs/rfc6750: RFC6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage ================================================================== .. meta:: :description: API references of RFC6750 in Python implementation. Guide on how to use Bearer Token and its validator. This section contains the generic implementation of RFC6750_. .. _RFC6750: https://tools.ietf.org/html/rfc6750 Guide on Bearer Token --------------------- Bearer token is used in OAuth 2.0 framework to protect resources. You need to implement the missing methods of :class:`BearerTokenValidator` before using it. Learn how to use it in :ref:`flask_oauth2_resource_protector`. API Reference ------------- .. module:: authlib.oauth2.rfc6750 .. autoclass:: BearerTokenValidator :members: .. autoclass:: BearerToken :members: authlib-1.3.2/docs/specs/rfc7009.rst000066400000000000000000000035221466226534200170520ustar00rootroot00000000000000.. _specs/rfc7009: RFC7009: OAuth 2.0 Token Revocation =================================== .. meta:: :description: API references on RFC7009 OAuth 2.0 Token Revocation Authlib implementation. This section contains the generic implementation of RFC7009_. .. _RFC7009: https://tools.ietf.org/html/rfc7009 .. module:: authlib.oauth2.rfc7009 .. _register_revocation_endpoint: Register Revocation Endpoint ---------------------------- The revocation endpoint can be easily registered to :ref:`flask_oauth2_server` or :ref:`django_oauth2_server`. But there are missing methods to be implemented:: from authlib.oauth2.rfc7009 import RevocationEndpoint class MyRevocationEndpoint(RevocationEndpoint): def query_token(self, token, token_type_hint, client): q = Token.query.filter_by(client_id=client.client_id) if token_type_hint == 'access_token': return q.filter_by(access_token=token).first() elif token_type_hint == 'refresh_token': return q.filter_by(refresh_token=token).first() # without token_type_hint item = q.filter_by(access_token=token).first() if item: return item return q.filter_by(refresh_token=token).first() def revoke_token(self, token): token.revoked = True db.session.add(token) db.session.commit() # register it to authorization server authorization_server.register_endpoint(MyRevocationEndpoint) After the registration, you can create a response with:: @app.route('/oauth/revoke', methods=['POST']) def revoke_token(): return server.create_endpoint_response(MyRevocationEndpoint.ENDPOINT_NAME) API Reference ------------- .. autoclass:: RevocationEndpoint :member-order: bysource :members: :inherited-members: authlib-1.3.2/docs/specs/rfc7515.rst000066400000000000000000000011621466226534200170520ustar00rootroot00000000000000.. _specs/rfc7515: RFC7515: JSON Web Signature =========================== .. meta:: :description: API references on RFC7515 JSON Web Signature (JWS) Authlib implementation. This section contains the generic implementation of RFC7515_. Find how to use it in :ref:`JWS Guide `. .. _RFC7515: https://tools.ietf.org/html/rfc7515 API Reference ------------- .. autoclass:: authlib.jose.JsonWebSignature :member-order: bysource :members: .. autoclass:: authlib.jose.JWSHeader .. autoclass:: authlib.jose.JWSObject .. autoclass:: authlib.jose.JWSAlgorithm :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc7516.rst000066400000000000000000000014151466226534200170540ustar00rootroot00000000000000.. _specs/rfc7516: RFC7516: JSON Web Encryption ============================ .. meta:: :description: API references on RFC7516 JSON Web Encryption (JWE) Python implementation. Guide on JWE serialization and deserialization. This section contains the generic implementation of RFC7516_. Find how to use it in :ref:`JWE Guide `. .. _RFC7516: https://tools.ietf.org/html/rfc7516 API Reference ------------- .. autoclass:: authlib.jose.JsonWebEncryption :member-order: bysource :members: .. autoclass:: authlib.jose.JWEAlgorithm :member-order: bysource :members: .. autoclass:: authlib.jose.JWEEncAlgorithm :member-order: bysource :members: .. autoclass:: authlib.jose.JWEZipAlgorithm :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc7517.rst000066400000000000000000000014561466226534200170620ustar00rootroot00000000000000.. _specs/rfc7517: RFC7517: JSON Web Key ===================== .. meta:: :description: API references on RFC7517 JSON Web Key (JWK) Authlib implementation. This section contains the generic implementation of RFC7517_. Find how to use it in :ref:`JWK Guide `. .. _RFC7517: https://tools.ietf.org/html/rfc7517 API Reference ------------- .. autoclass:: authlib.jose.JsonWebKey :member-order: bysource :members: .. autoclass:: authlib.jose.Key :member-order: bysource :members: .. autoclass:: authlib.jose.KeySet :member-order: bysource :members: .. autoclass:: authlib.jose.OctKey :member-order: bysource :members: .. autoclass:: authlib.jose.RSAKey :member-order: bysource :members: .. autoclass:: authlib.jose.ECKey :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc7518.rst000066400000000000000000000065701466226534200170650ustar00rootroot00000000000000.. _specs/rfc7518: RFC7518: JSON Web Algorithms ============================ .. meta:: :description: API references on RFC7518 JSON Web Algorithms (JWA) Authlib implementation. This section contains the generic implementation of RFC7518_. .. _RFC7518: https://tools.ietf.org/html/rfc7518 Algorithms for JWS ------------------ The interface for JWS Algorithms are all inherit from :class:`authlib.jose.JWSAlgorithm`. Find how to use them in :ref:`jws_guide`. HMAC with SHA-2 Functions ~~~~~~~~~~~~~~~~~~~~~~~~~ This section is defined by RFC7518 `Section 3.2`_. .. _`Section 3.2`: https://tools.ietf.org/html/rfc7518#section-3.2 1. HS256: HMAC using SHA-256 2. HS384: HMAC using SHA-384 3. HS512: HMAC using SHA-512 Digital Signature with RSASSA-PKCS1-v1_5 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Algorithms in this section requires extra crypto backends. This section is defined by RFC7518 `Section 3.3`_. .. _`Section 3.3`: https://tools.ietf.org/html/rfc7518#section-3.3 1. RS256: RSASSA-PKCS1-v1_5 using SHA-256 2. RS384: RSASSA-PKCS1-v1_5 using SHA-384 3. RS512: RSASSA-PKCS1-v1_5 using SHA-384 Digital Signature with ECDSA ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Algorithms in this section requires extra crypto backends. This section is defined by RFC7518 `Section 3.4`_. .. _`Section 3.4`: https://tools.ietf.org/html/rfc7518#section-3.4 1. ES256: ECDSA using P-256 and SHA-256 2. ES384: ECDSA using P-384 and SHA-384 3. ES512: ECDSA using P-521 and SHA-512 Digital Signature with RSASSA-PSS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Algorithms in this section requires extra crypto backends. This section is defined by RFC7518 `Section 3.5`_. .. _`Section 3.5`: https://tools.ietf.org/html/rfc7518#section-3.5 1. PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 2. PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 3. PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 Algorithms for JWE ------------------ This section contains algorithms for JWE ``alg`` and ``enc`` header. For ``alg`` the interface are all inherited from :class:`authlib.jose.JWEAlgorithm`. For ``enc``, the interface are inherited from :class:`authlib.jose.JWEEncAlgorithm`. Current available algorithms for ``alg``: 0. dir: Direct use of a shared symmetric key 1. RSA1_5: RSAES-PKCS1-v1_5 2. RSA-OAEP: RSAES OAEP using default parameters 3. RSA-OAEP-256: RSAES OAEP using SHA-256 and MGF1 with SHA-256 4. A128KW: AES Key Wrap with default initial value using 128-bit key 5. A192KW: AES Key Wrap with default initial value using 192-bit key 6. A256KW: AES Key Wrap with default initial value using 256-bit key 7. A128GCMKW: Key wrapping with AES GCM using 128-bit key 8. A192GCMKW: Key wrapping with AES GCM using 192-bit key 9. A256GCMKW: Key wrapping with AES GCM using 256-bit key 10. ECDH-ES: In the Direct Key Agreement mode 11. ECDH-ES+A128KW: using Concat KDF and CEK wrapped with A128KW 12. ECDH-ES+A192KW: using Concat KDF and CEK wrapped with A192KW 13. ECDH-ES+A256KW: using Concat KDF and CEK wrapped with A256KW Current available algorithms for ``enc``: 1. A128CBC-HS256 2. A192CBC-HS384 3. A256CBC-HS512 4. A128GCM 5. A192GCM 6. A256GCM Current available algorithms for ``zip``: 1. DEF Algorithms for JWK ------------------ This section defines the parameters for keys using the algorithms via RFC7518 `Section 6`_. Find how to use them in :ref:`jwk_guide`. .. _`Section 6`: https://tools.ietf.org/html/rfc7518#section-6 authlib-1.3.2/docs/specs/rfc7519.rst000066400000000000000000000011621466226534200170560ustar00rootroot00000000000000.. _specs/rfc7519: RFC7519: JSON Web Token ======================= .. meta:: :description: API references on RFC7519 JSON Web Token (JWT) Python implementation, guide on JWT encoding and decoding, guide on JWT payload claims validation. This section contains the generic Python implementation of RFC7519_. Find how to use it in :ref:`JWT Guide `. .. _RFC7519: https://tools.ietf.org/html/rfc7519 API Reference ------------- .. autoclass:: authlib.jose.JsonWebToken :member-order: bysource :members: .. autoclass:: authlib.jose.JWTClaims :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc7523.rst000066400000000000000000000167251466226534200170640ustar00rootroot00000000000000.. _specs/rfc7523: RFC7523: JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants ================================================================================= .. meta:: :description: API references on RFC7523 JWT Bearer Grant of Python implementation, guide on how to create a server that support JWT profile for OAuth 2.0 client authentication and authorization grants. This section contains the generic Python implementation of RFC7523_. .. _RFC7523: https://tools.ietf.org/html/rfc7523 .. module:: authlib.oauth2.rfc7523 .. _jwt_grant_type: Using JWTs as Authorization Grants ---------------------------------- .. versionchanged:: v1.0.0 Please note that all not-implemented methods are changed. JWT Profile for OAuth 2.0 Authorization Grants works in the same way with :ref:`RFC6749 ` built-in grants. Which means it can be registered with :meth:`~authlib.oauth2.rfc6749.AuthorizationServer.register_grant`. The base class is :class:`JWTBearerGrant`, you need to implement the missing methods in order to use it. Here is an example:: from authlib.jose import JsonWebKey from authlib.oauth2.rfc7523 import JWTBearerGrant as _JWTBearerGrant class JWTBearerGrant(_JWTBearerGrant): def resolve_issuer_client(self, issuer): # if using client_id as issuer return Client.objects.get(client_id=issuer) def resolve_client_key(self, client, headers, payload): # if client has `jwks` column key_set = JsonWebKey.import_key_set(client.jwks) return key_set.find_by_kid(headers['kid']) def authenticate_user(self, subject): # when assertion contains `sub` value, if this `sub` is email return User.objects.get(email=subject) def has_granted_permission(self, client, user): # check if the client has access to user's resource. # for instance, we have a table `UserGrant`, which user can add client # to this table to record that client has granted permission grant = UserGrant.objects.get(client_id=client.client_id, user_id=user.id) if grant: return grant.enabled return False # register grant to authorization server authorization_server.register_grant(JWTBearerGrant) When creating a client, authorization server will generate several key pairs. The server itself can only keep the public keys, which will be used to decode assertion value. For **client implementation**, check out: 1. :class:`~authlib.integrations.requests_client.AssertionSession`. 2. :class:`~authlib.integrations.httpx_client.AssertionSession`. 3. :class:`~authlib.integrations.httpx_client.AsyncAssertionSession`. .. _jwt_client_authentication: Using JWTs for Client Authentication ------------------------------------ In :ref:`specs/rfc6749`, Authlib provided three built-in client authentication methods, which are ``none``, ``client_secret_post`` and ``client_secret_basic``. With the power of Assertion Framework, we can add more client authentication methods. In this section, Authlib provides two more options: ``client_secret_jwt`` and ``private_key_jwt``. RFC7523 itself doesn't define any names, these two names are defined by OpenID Connect in ClientAuthentication_. The :class:`~authlib.oauth2.rfc6749.AuthorizationServer` has provided a method :meth:`~authlib.oauth2.rfc6749.AuthorizationServer.register_client_auth_method` to add more client authentication methods. In Authlib, ``client_secret_jwt`` and ``private_key_jwt`` share the same API, using :class:`JWTBearerClientAssertion` to create a new client authentication:: class JWTClientAuth(JWTBearerClientAssertion): def validate_jti(self, claims, jti): # validate_jti is required by OpenID Connect # but it is optional by RFC7523 # use cache to validate jti value key = 'jti:{}-{}'.format(claims['sub'], jti) if cache.get(key): return False cache.set(key, 1, timeout=3600) return True def resolve_client_public_key(self, client, headers): if headers['alg'] == 'HS256': return client.client_secret if headers['alg'] == 'RS256': return client.public_key # you may support other ``alg`` value authorization_server.register_client_auth_method( JWTClientAuth.CLIENT_AUTH_METHOD, JWTClientAuth('https://example.com/oauth/token') ) The value ``https://example.com/oauth/token`` is your authorization server's token endpoint, which is used as ``aud`` value in JWT. Now we have added this client auth method to authorization server, but no grant types support this authentication method, you need to add it to the supported grant types too, e.g. we want to support this in authorization code grant:: from authlib.oauth2.rfc6749 import grants class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ 'client_secret_basic', JWTClientAuth.CLIENT_AUTH_METHOD, ] # ... You may noticed that the value of ``CLIENT_AUTH_METHOD`` is ``client_assertion_jwt``. It is not ``client_secret_jwt`` or ``private_key_jwt``, because they have the same logic. In the above implementation:: def resolve_client_public_key(self, client, headers): alg = headers['alg'] If this ``alg`` is a MAC SHA like ``HS256``, it is called ``client_secret_jwt``, because the key used to sign a JWT is the client's ``client_secret`` value. If this ``alg`` is ``RS256`` or something else, it is called ``private_key_jwt``, because client will use its private key to sign the JWT. You can set a limitation in the implementation of ``resolve_client_public_key`` to accept only ``HS256`` alg, in this case, you can also alter ``CLIENT_AUTH_METHOD = 'client_secret_jwt'``. .. _ClientAuthentication: http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication .. _jwt_oauth2session: Using JWTs Client Assertion in OAuth2Session -------------------------------------------- Authlib RFC7523 provides two more client authentication methods for :ref:`oauth_2_session`: 1. ``client_secret_jwt`` 2. ``private_key_jwt`` Here is an example of how to register ``client_secret_jwt`` for ``OAuth2Session``:: from authlib.oauth2.rfc7523 import ClientSecretJWT from authlib.integrations.requests_client import OAuth2Session session = OAuth2Session( 'your-client-id', 'your-client-secret', token_endpoint_auth_method='client_secret_jwt' ) token_endpoint = 'https://example.com/oauth/token' session.register_client_auth_method(ClientSecretJWT(token_endpoint)) session.fetch_token(token_endpoint) How about ``private_key_jwt``? It is the same as ``client_secret_jwt``:: from authlib.oauth2.rfc7523 import PrivateKeyJWT with open('your-private-key.pem', 'rb') as f: private_key = f.read() session = OAuth2Session( 'your-client-id', private_key, token_endpoint_auth_method='private_key_jwt' # NOTICE HERE ) token_endpoint = 'https://example.com/oauth/token' session.register_client_auth_method(PrivateKeyJWT(token_endpoint)) session.fetch_token(token_endpoint) API Reference ------------- .. autoclass:: JWTBearerGrant :member-order: bysource :members: .. autoclass:: JWTBearerClientAssertion :member-order: bysource :members: .. autoclass:: ClientSecretJWT .. autoclass:: PrivateKeyJWT authlib-1.3.2/docs/specs/rfc7591.rst000066400000000000000000000066261466226534200170700ustar00rootroot00000000000000.. _specs/rfc7591: RFC7591: OAuth 2.0 Dynamic Client Registration Protocol ======================================================= .. meta:: :description: Python API references on RFC7591 OAuth 2.0 Dynamic Client Registration Protocol in Python with Authlib implementation. .. module:: authlib.oauth2.rfc7591 This section contains the generic implementation of RFC7591_. OAuth 2.0 Dynamic Client Registration Protocol allows developers creating OAuth client via API through Authorization Server. To integrate with Authlib :ref:`flask_oauth2_server` or :ref:`django_oauth2_server`, developers MUST implement the missing methods of :class:`ClientRegistrationEndpoint`. .. _RFC7591: https://tools.ietf.org/html/rfc7591 Client Registration Endpoint ---------------------------- The client registration endpoint accepts client metadata as JSON payload via POST request. The metadata may contain a :ref:`JWT ` ``software_statement`` value. Endpoint can choose if it support ``software_statement``, it is not enabled by default. .. versionchanged:: v0.15 ClientRegistrationEndpoint has a breaking change in v0.15. Method of ``authenticate_user`` is replaced by ``authenticate_token``, and parameters in ``save_client`` is also changed. Before register the endpoint, developers MUST implement the missing methods:: from authlib.oauth2.rfc7591 import ClientRegistrationEndpoint class MyClientRegistrationEndpoint(ClientRegistrationEndpoint): def authenticate_token(self, request): # this method is used to find who is going to create the client auth_header = request.headers.get('Authorization') # bearer a-token-string bearer_token = auth_header.split()[1] token = Token.query.get(bearer_token) return token def save_client(self, client_info, client_metadata, request): client = OAuthClient( user_id=request.credential.user_id, client_id=client_info['client_id'], client_secret=client_info['client_secret'], **client_metadata, ) client.save() return client If developers want to support ``software_statement``, additional methods should be implemented:: class MyClientRegistrationEndpoint(ClientRegistrationEndpoint): # adding this to support JWT with RS256 alg, you may change it to other alg values software_statement_alg_values_supported = ['RS256'] def resolve_public_key(self, request): # the authenticated user's public key # can be a string, bytes, jwk and jwk set return request.user.public_jwk_set Register this endpoint and use this endpoint in routes:: authorization_server.register_endpoint(MyClientRegistrationEndpoint) # for Flask @app.route('/register', methods=['POST']) def client_registration(): return authorization_server.create_endpoint_response('client_registration') # for Django from django.views.decorators.http import require_http_methods @require_http_methods(["POST"]) def client_registration(request): return authorization_server.create_endpoint_response('client_registration', request) API Reference ------------- .. autoclass:: ClientRegistrationEndpoint :member-order: bysource :members: .. autoclass:: ClientMetadataClaims :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc7592.rst000066400000000000000000000072321466226534200170630ustar00rootroot00000000000000.. _specs/rfc7592: RFC7592: OAuth 2.0 Dynamic Client Registration Management Protocol ================================================================== This section contains the generic implementation of RFC7592_. OAuth 2.0 Dynamic Client Registration Management Protocol allows developers edit and delete OAuth client via API through Authorization Server. This specification is an extension of :ref:`specs/rfc7591`. .. meta:: :description: Python API references on RFC7592 OAuth 2.0 Dynamic Client Registration Management Protocol in Python with Authlib implementation. .. module:: authlib.oauth2.rfc7592 .. _RFC7592: https://tools.ietf.org/html/rfc7592 Client Configuration Endpoint ----------------------------- Before register the endpoint, developers MUST implement the missing methods:: from authlib.oauth2.rfc7592 import ClientConfigurationEndpoint class MyClientConfigurationEndpoint(ClientConfigurationEndpoint): def authenticate_token(self, request): # this method is used to authenticate the registration access # token returned by the RFC7591 registration endpoint auth_header = request.headers.get('Authorization') bearer_token = auth_header.split()[1] token = Token.get(bearer_token) return token def authenticate_client(self, request): client_id = request.data.get('client_id') return Client.get(client_id=client_id) def revoke_access_token(self, token, request): token.revoked = True token.save() def check_permission(self, client, request): return client.editable def delete_client(self, client, request): client.delete() def save_client(self, client_info, client_metadata, request): client = OAuthClient( user_id=request.credential.user_id, client_id=client_info['client_id'], client_secret=client_info['client_secret'], **client_metadata, ) client.save() return client def generate_client_registration_info(self, client, request): access_token = request.headers['Authorization'].split(' ')[1] return { 'registration_client_uri': request.uri, 'registration_access_token': access_token, } def get_server_metadata(self): return { 'issuer': ..., 'authorization_endpoint': ..., 'token_endpoint': ..., 'jwks_uri': ..., 'registration_endpoint': ..., 'scopes_supported': ..., 'response_types_supported': ..., 'response_modes_supported': ..., 'grant_types_supported': ..., 'token_endpoint_auth_methods_supported': ..., 'token_endpoint_auth_signing_alg_values_supported': ..., 'service_documentation': ..., 'ui_locales_supported': ..., 'op_policy_uri': ..., 'op_tos_uri': ..., 'revocation_endpoint': ..., 'revocation_endpoint_auth_methods_supported': ..., 'revocation_endpoint_auth_signing_alg_values_supported': ..., 'introspection_endpoint': ..., 'introspection_endpoint_auth_methods_supported': ..., 'introspection_endpoint_auth_signing_alg_values_supported': ..., 'code_challenge_methods_supported': ..., } API Reference ------------- .. autoclass:: ClientConfigurationEndpoint :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc7636.rst000066400000000000000000000067521466226534200170700ustar00rootroot00000000000000.. _specs/rfc7636: RFC7636: Proof Key for Code Exchange by OAuth Public Clients ============================================================ .. meta:: :description: API references on RFC76736 Proof Key for Code Exchange by OAuth Public Clients implementation, guide on how to add it to OAuth 2.0 authorization server. This RFC7636_ is used to improve the security of Authorization Code flow for public clients by sending extra "code_challenge" and "code_verifier" to the authorization server. .. _RFC7636: https://tools.ietf.org/html/rfc7636 .. module:: authlib.oauth2.rfc7636 Using RFC7636 in Authorization Code Grant ----------------------------------------- In order to apply proof key for code exchange, you need to register the :class:`CodeChallenge` extension to ``AuthorizationCodeGrant``. But before that, we need to re-design our AuthorizationCode database. * For Flask Developers, check the section :ref:`flask_oauth2_code_grant`. * For Django Developers, check the section :ref:`django_oauth2_code_grant`. The new database SHOULD contain two more columns: 1. code_challenge: A VARCHAR 2. code_challenge_method: A VARCHAR And the ``AuthorizationCodeGrant`` should record the ``code_challenge`` and ``code_challenge_method`` into database in ``save_authorization_code`` method:: class MyAuthorizationCodeGrant(AuthorizationCodeGrant): # YOU MAY NEED TO ADD "none" METHOD FOR AUTHORIZATION WITHOUT CLIENT SECRET TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none'] def save_authorization_code(self, code, request): # NOTICE BELOW code_challenge = request.data.get('code_challenge') code_challenge_method = request.data.get('code_challenge_method') auth_code = AuthorizationCode( code=code, client_id=request.client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, user_id=request.user.id, code_challenge=code_challenge, code_challenge_method=code_challenge_method, ) auth_code.save() return auth_code Now you can register your ``AuthorizationCodeGrant`` with the extension:: from authlib.oauth2.rfc7636 import CodeChallenge server.register_grant(MyAuthorizationCodeGrant, [CodeChallenge(required=True)]) If ``required=True``, code challenge is required for authorization code flow from public clients. If ``required=False``, it is optional, it will only valid the code challenge when clients send these parameters. Using ``code_challenge`` in Client ---------------------------------- Read the **Code Challenge** section in the :ref:`frameworks_clients`. It is also possible to add ``code_challenge`` in ``OAuth2Session``, consider that we already have a ``session``:: >>> from authlib.oauth2.rfc7636 import create_s256_code_challenge >>> code_verifier = generate_token(48) >>> code_challenge = create_s256_code_challenge(code_verifier) >>> uri, state = session.create_authorization_url(authorize_url, code_challenge=code_challenge, code_challenge_method='S256') >>> # visit uri, get the response >>> authorization_response = 'https://example.com/auth?code=42..e9&state=d..t' >>> token = session.fetch_token(token_endpoint, authorization_response=authorization_response, code_verifier=code_verifier) API Reference ------------- .. autoclass:: CodeChallenge :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc7638.rst000066400000000000000000000014021466226534200170550ustar00rootroot00000000000000RFC7638: JSON Web Key (JWK) Thumbprint ====================================== .. versionadded:: v0.15 .. automodule:: authlib.jose This RFC7638_ is used for computing a hash value over a JSON Web Key (JWK). The value can be used as an identity of the JWK. .. _RFC7638: https://tools.ietf.org/html/rfc7638 The ``.thumbprint`` method is defined on the ``Key`` class, you can use it directly:: from authlib.jose import JsonWebKey raw = read_file('rsa.pem') key = JsonWebKey.import_key(raw) key.thumbprint() If a key has no ``kid``, you can add the value of ``.thumbprint()`` as a kid:: key['kid'] = key.thumbprint() This method is available on every Key class, including :class:`OctKey`, :class:`RSAKey`, :class:`ECKey`, and :class:`OKPKey`. authlib-1.3.2/docs/specs/rfc7662.rst000066400000000000000000000076131466226534200170640ustar00rootroot00000000000000.. _specs/rfc7662: RFC7662: OAuth 2.0 Token Introspection ====================================== .. meta:: :description: API references on RFC7009, OAuth 2.0 Token Introspection, in Python via Authlib implementation. This section contains the generic implementation of RFC7662_. OAuth 2.0 Token Introspection is usually designed to let resource servers to know content of a token. .. _RFC7662: https://tools.ietf.org/html/rfc7662 .. module:: authlib.oauth2.rfc7662 .. _register_introspection_endpoint: Register Introspection Endpoint ------------------------------- .. versionchanged:: v1.0 Authlib is designed to be very extendable, with the method of ``.register_endpoint`` on ``AuthorizationServer``, it is easy to add the introspection endpoint to the authorization server. It works on both :ref:`flask_oauth2_server` and :ref:`django_oauth2_server`. But first, we need to implement the missing methods:: from authlib.oauth2.rfc7662 import IntrospectionEndpoint class MyIntrospectionEndpoint(IntrospectionEndpoint): def query_token(self, token, token_type_hint): if token_type_hint == 'access_token': tok = Token.query.filter_by(access_token=token).first() elif token_type_hint == 'refresh_token': tok = Token.query.filter_by(refresh_token=token).first() else: # without token_type_hint tok = Token.query.filter_by(access_token=token).first() if not tok: tok = Token.query.filter_by(refresh_token=token).first() return tok def introspect_token(self, token): return { 'active': True, 'client_id': token.client_id, 'token_type': token.token_type, 'username': get_token_username(token), 'scope': token.get_scope(), 'sub': get_token_user_sub(token), 'aud': token.client_id, 'iss': 'https://server.example.com/', 'exp': token.expires_at, 'iat': token.issued_at, } def check_permission(self, token, client, request): # for example, we only allow internal client to access introspection endpoint return client.client_type == 'internal' # register it to authorization server server.register_endpoint(MyIntrospectionEndpoint) After the registration, we can create a response with:: @app.route('/oauth/introspect', methods=['POST']) def introspect_token(): return server.create_endpoint_response(MyIntrospectionEndpoint.ENDPOINT_NAME) .. _require_oauth_introspection: Use Introspection in Resource Server ------------------------------------ .. versionadded:: v1.0 When resource server has no access to token database, it can use introspection endpoint to validate the given token. Here is how:: import requests from authlib.oauth2.rfc7662 import IntrospectTokenValidator from your_project import secrets class MyIntrospectTokenValidator(IntrospectTokenValidator): def introspect_token(self, token_string): url = 'https://example.com/oauth/introspect' data = {'token': token_string, 'token_type_hint': 'access_token'} auth = (secrets.internal_client_id, secrets.internal_client_secret) resp = requests.post(url, data=data, auth=auth) resp.raise_for_status() return resp.json() We can then register this token validator in to resource protector:: require_oauth = ResourceProtector() require_oauth.register_token_validator(MyIntrospectTokenValidator()) Please note, when using ``IntrospectTokenValidator``, the ``current_token`` will be a dict. API Reference ------------- .. autoclass:: IntrospectionEndpoint :member-order: bysource :members: :inherited-members: .. autoclass:: IntrospectTokenValidator :members: authlib-1.3.2/docs/specs/rfc8037.rst000066400000000000000000000045421466226534200170570ustar00rootroot00000000000000RFC8037: CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in JSON Object Signing and Encryption (JOSE) ============================================================================================================== .. meta:: :description: API references on "X25519", "X448", "Ed25519", and "Ed448" algorithms for JWK. And "EdDSA" signing data using "JSON Web Signature (JWS)". .. versionadded:: v0.15 Algorithms for JWS ------------------ In RFC8037, algorithm "EdDSA" is defined for JWS. Use Edwards-curve Digital Signature Algorithm (EdDSA) for signing data using "JSON Web Signature (JWS)":: from authlib.jose import JsonWebSignature # only allow "EdDSA" alg value jws = JsonWebSignature(algorithms=['EdDSA']) protected = {'alg': 'EdDSA'} payload = b'example' with open('ed25519-pkcs8.pem', 'rb') as f: secret = f.read() jws.serialize_compact(protected, payload, secret) Learn how to use other JWS functions at :ref:`jws_guide`. It can also be used in JSON Web Token (JWT):: from authlib.jose import JsonWebToken jwt = JsonWebToken(algorithms=['EdDSA']) with open('ed25519-pkcs8.pem', 'rb') as f: key = f.read() header = {'alg': 'EdDSA'} payload = {'iss': 'Authlib', 'sub': '123', ...} s = jwt.encode(header, payload, key) Algorithms for JWK ------------------ In RFC8037, algorithms "Ed25519", "Ed448", "X25519", "X448" are defined for JWK. Loads and dumps Json Web Keys with:: from authlib.jose import JsonWebKey with open('ed25519-pkcs8.pem', 'rb') as f: key = f.read() # MUST use "OKP" as "kty" value JsonWebKey.import_key(key, {'kty': 'OKP'}) Learn how to use other JWK functions at :ref:`jwk_guide`. Algorithms for JWE ------------------ "X25519", "X448" keys are used in "epk" for ECDH-ES algorithms. Just use the ``X25519`` and ``X448`` key for ``ECDH-ES`` in JWE:: from authlib.jose import OKPKey from authlib.jose import JsonWebEncryption jwe = JsonWebEncryption() with open('X25519.pem', 'rb') as f: key = OKPKey.import_key(f.read()) protected = { "alg": "ECDH-ES", "enc": "A128GCM", "apu": "QWxpY2U", "apv": "Qm9i", } jwe.serialize_compact(protected, b'hello', key) API Reference ------------- .. autoclass:: authlib.jose.OKPKey :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc8414.rst000066400000000000000000000004161466226534200170520ustar00rootroot00000000000000.. _specs/rfc8414: RFC8414: OAuth 2.0 Authorization Server Metadata ================================================ .. module:: authlib.oauth2.rfc8414 API Reference ------------- .. autoclass:: AuthorizationServerMetadata :member-order: bysource :members: authlib-1.3.2/docs/specs/rfc8628.rst000066400000000000000000000074731466226534200170730ustar00rootroot00000000000000.. _specs/rfc8628: RFC8628: OAuth 2.0 Device Authorization Grant ============================================= .. meta:: :description: Python API references on RFC8628 DeviceAuthorizationEndpoint and DeviceCodeGrant with Authlib implementation. .. module:: authlib.oauth2.rfc8628 This section contains the generic implementation of RFC8628_. OAuth 2.0 Device Authorization Grant is usually used when devices have limited input capabilities or lack a suitable browser, such as smart TVs, media consoles, picture frames, printers and etc. To integrate with Authlib :ref:`flask_oauth2_server` or :ref:`django_oauth2_server`, developers MUST implement the missing methods of the two classes: 1. :class:`DeviceAuthorizationEndpoint` 2. :class:`DeviceCodeGrant` Device Authorization Endpoint ----------------------------- There are two missing methods that developers MUST implement:: from authlib.oauth2.rfc8628 import DeviceAuthorizationEndpoint class MyDeviceAuthorizationEndpoint(DeviceAuthorizationEndpoint): def get_verification_uri(self): return 'https://example.com/active' def save_device_credential(self, client_id, scope, data): credential = DeviceCredential( client_id=client_id, scope=scope, **data ) credential.save() # register it to authorization server authorization_server.register_endpoint(MyDeviceAuthorizationEndpoint) ``get_verification_uri`` is the URL that end user will use their browser to log in and authenticate. See below "Verification Endpoint". After the registration, you can create a response with:: @app.route('/device_authorization', methods=['POST']) def device_authorization(): return server.create_endpoint_response('device_authorization') Device Code Grant ----------------- With Authlib ``.register_grant``, we can add ``DeviceCodeGrant`` easily. But first, we need to implement the missing methods:: from authlib.oauth2.rfc8628 import DeviceCodeGrant class MyDeviceCodeGrant(DeviceCodeGrant): def query_device_credential(self, device_code): return DeviceCredential.query(device_code=device_code) def query_user_grant(self, user_code): data = redis.get('oauth_user_grant:' + user_code) if not data: return None user_id, allowed = data.split() user = User.query.get(user_id) return user, bool(allowed) def should_slow_down(self, credential, now): # developers can return True/False based on credential and now return False authorization_server.register_grant(MyDeviceCodeGrant) Note ``query_user_grant``, we are fetching data from redis. This data was saved from verification endpoint when end user granted the request. Verification Endpoint --------------------- Developers MUST implement this part by themselves. Here is a hint on how to implement this endpoint:: @app.route('/active', methods=['GET', 'POST']) @login_required def verify_device_code(): if request.method == 'GET': return render_template('verification.html') allowed = request.form['allowed'] user_code = request.form['user_code'] key = 'oauth_user_grant:' + user_code redis.set(key, f'{current_user.id} {allowed}', 12) return render_template('verification.html') Check points: 1. route should match ``get_verification_uri`` in Device Authorization Endpoint 2. user grant should match ``query_user_grant`` in Device Code Grant .. _RFC8628: https://tools.ietf.org/html/rfc8628 API Reference ------------- .. autoclass:: DeviceAuthorizationEndpoint :member-order: bysource :members: .. autoclass:: DeviceCodeGrant :member-order: bysource :members: :inherited-members: authlib-1.3.2/docs/specs/rfc9068.rst000066400000000000000000000051561466226534200170660ustar00rootroot00000000000000.. _specs/rfc9068: RFC9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens ================================================================= This section contains the generic implementation of RFC9068_. JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens allows developpers to generate JWT access tokens. Using JWT instead of plain text for access tokens result in different possibilities: - User information can be filled in the JWT claims, similar to the :ref:`specs/oidc` ``id_token``, possibly making the economy of requests to the ``userinfo_endpoint``. - Resource servers do not *need* to reach the authorization server :ref:`specs/rfc7662` endpoint to verify each incoming tokens, as the JWT signature is a proof of its validity. This brings the economy of one network request at each resource access. - Consequently, the authorization server do not need to store access tokens in a database. If a resource server does not implement this spec and still need to reach the authorization server introspection endpoint to check the token validation, then the authorization server can simply validate the JWT without requesting its database. - If the authorization server do not store access tokens in a database, it won't have the possibility to revoke the tokens. The produced access tokens will be valid until the timestamp defined in its ``exp`` claim is reached. This specification is just about **access** tokens. Other kinds of tokens like refresh tokens are not covered. RFC9068_ define a few optional JWT claims inspired from RFC7643_ that can can be used to determine if the token bearer is authorized to access a resource: ``groups``, ``roles`` and ``entitlements``. This module brings tools to: - generate JWT access tokens with :class:`~authlib.oauth2.rfc9068.JWTBearerTokenGenerator` - protected resources endpoints and validate JWT access tokens with :class:`~authlib.oauth2.rfc9068.JWTBearerTokenValidator` - introspect JWT access tokens with :class:`~authlib.oauth2.rfc9068.JWTIntrospectionEndpoint` - deny JWT access tokens revokation attempts with :class:`~authlib.oauth2.rfc9068.JWTRevocationEndpoint` .. _RFC9068: https://www.rfc-editor.org/rfc/rfc9068.html .. _RFC7643: https://tools.ietf.org/html/rfc7643 API Reference ------------- .. module:: authlib.oauth2.rfc9068 .. autoclass:: JWTBearerTokenGenerator :member-order: bysource :members: .. autoclass:: JWTBearerTokenValidator :member-order: bysource :members: .. autoclass:: JWTIntrospectionEndpoint :member-order: bysource :members: .. autoclass:: JWTRevocationEndpoint :member-order: bysource :members: authlib-1.3.2/pyproject.toml000066400000000000000000000032451466226534200160770ustar00rootroot00000000000000[project] name = "Authlib" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." authors = [{name = "Hsiaoming Yang", email="me@lepture.com"}] dependencies = [ "cryptography", ] license = {text = "BSD-3-Clause"} requires-python = ">=3.8" dynamic = ["version"] readme = "README.rst" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Security", "Topic :: Security :: Cryptography", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ] [project.urls] Documentation = "https://docs.authlib.org/" Purchase = "https://authlib.org/plans" Issues = "https://github.com/lepture/authlib/issues" Source = "https://github.com/lepture/authlib" Donate = "https://github.com/sponsors/lepture" Blog = "https://blog.authlib.org/" [build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools.dynamic] version = {attr = "authlib.__version__"} [tool.setuptools.packages.find] where = ["."] include = ["authlib", "authlib.*"] authlib-1.3.2/serve.py000066400000000000000000000002761466226534200146620ustar00rootroot00000000000000from livereload import Server, shell app = Server() # app.watch("src", shell("make build-docs"), delay=2) app.watch("docs", shell("make build-docs"), delay=2) app.serve(root="build/_html") authlib-1.3.2/setup.cfg000066400000000000000000000002321466226534200147750ustar00rootroot00000000000000[bdist_wheel] universal = 1 [check-manifest] ignore = tox.ini [tool:pytest] python_files = test*.py norecursedirs = authlib build dist docs htmlcov authlib-1.3.2/setup.py000066400000000000000000000003031466226534200146650ustar00rootroot00000000000000from setuptools import setup # Metadata goes in setup.cfg. These are here for GitHub's dependency graph. setup( name="Authlib", install_requires=[ "cryptography>=3.2", ], ) authlib-1.3.2/tests/000077500000000000000000000000001466226534200143215ustar00rootroot00000000000000authlib-1.3.2/tests/__init__.py000066400000000000000000000000001466226534200164200ustar00rootroot00000000000000authlib-1.3.2/tests/clients/000077500000000000000000000000001466226534200157625ustar00rootroot00000000000000authlib-1.3.2/tests/clients/__init__.py000066400000000000000000000000001466226534200200610ustar00rootroot00000000000000authlib-1.3.2/tests/clients/asgi_helper.py000066400000000000000000000036611466226534200206240ustar00rootroot00000000000000import json from starlette.requests import Request as ASGIRequest from starlette.responses import Response as ASGIResponse class AsyncMockDispatch: def __init__(self, body=b'', status_code=200, headers=None, assert_func=None): if headers is None: headers = {} if isinstance(body, dict): body = json.dumps(body).encode() headers['Content-Type'] = 'application/json' else: if isinstance(body, str): body = body.encode() headers['Content-Type'] = 'application/x-www-form-urlencoded' self.body = body self.status_code = status_code self.headers = headers self.assert_func = assert_func async def __call__(self, scope, receive, send): request = ASGIRequest(scope, receive=receive) if self.assert_func: await self.assert_func(request) response = ASGIResponse( status_code=self.status_code, content=self.body, headers=self.headers, ) await response(scope, receive, send) class AsyncPathMapDispatch: def __init__(self, path_maps): self.path_maps = path_maps async def __call__(self, scope, receive, send): request = ASGIRequest(scope, receive=receive) rv = self.path_maps[request.url.path] status_code = rv.get('status_code', 200) body = rv.get('body') headers = rv.get('headers', {}) if isinstance(body, dict): body = json.dumps(body).encode() headers['Content-Type'] = 'application/json' else: if isinstance(body, str): body = body.encode() headers['Content-Type'] = 'application/x-www-form-urlencoded' response = ASGIResponse( status_code=status_code, content=body, headers=headers, ) await response(scope, receive, send) authlib-1.3.2/tests/clients/keys/000077500000000000000000000000001466226534200167355ustar00rootroot00000000000000authlib-1.3.2/tests/clients/keys/jwks_private.json000066400000000000000000000064441466226534200223500ustar00rootroot00000000000000{ "keys": [ {"kty": "RSA", "kid": "abc", "n": "pF1JaMSN8TEsh4N4O_5SpEAVLivJyLH-Cgl3OQBPGgJkt8cg49oasl-5iJS-VdrILxWM9_JCJyURpUuslX4Eb4eUBtQ0x5BaPa8-S2NLdGTaL7nBOO8o8n0C5FEUU-qlEip79KE8aqOj-OC44VsIquSmOvWIQD26n3fCVlgwoRBD1gzzsDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMsRNACmBomzgA4M1TTsisSUO87444pe35Z4_n5c735o2fZMrGgMwiJNh7rT8SYxtIkxngioiGnwkxGQxQ4NzPAHg-XSY0J04pNm7KqTkgtxyrqOANJLIjXlR-U9SQ90NjHVQ", "e": "AQAB", "d": "G4E84ppZwm3fLMI0YZ26iJ_sq3BKcRpQD6_r0o8ZrZmO7y4Uc-ywoP7h1lhFzaox66cokuloZpKOdGHIfK-84EkI3WeveWHPqBjmTMlN_ClQVcI48mUbLhD7Zeenhi9y9ipD2fkNWi8OJny8k4GfXrGqm50w8schrsPksnxJjvocGMT6KZNfDURKF2HlM5X1uY8VCofokXOjBEeHIfYM8e7IcmPpyXwXKonDmVVbMbefo-u-TttgeyOYaO6s3flSy6Y0CnpWi43JQ_VEARxQl6Brj1oizr8UnQQ0nNCOWwDNVtOV4eSl7PZoiiT7CxYkYnhJXECMAM5YBpm4Qk9zdQ", "p": "1g4ZGrXOuo75p9_MRIepXGpBWxip4V7B9XmO9WzPCv8nMorJntWBmsYV1I01aITxadHatO4Gl2xLniNkDyrEQzJ7w38RQgsVK-CqbnC0K9N77QPbHeC1YQd9RCNyUohOimKvb7jyv798FBU1GO5QI2eNgfnnfteSVXhD2iOoTOs", "q": "xJJ-8toxJdnLa0uUsAbql6zeNXGbUBMzu3FomKlyuWuq841jS2kIalaO_TRj5hbnE45jmCjeLgTVO6Ach3Wfk4zrqajqfFJ0zUg_Wexp49lC3RWiV4icBb85Q6bzeJD9Dn9vhjpfWVkczf_NeA1fGH_pcgfkT6Dm706GFFttLL8", "dp": "Zfx3l5NR-O8QIhzuHSSp279Afl_E6P0V2phdNa_vAaVKDrmzkHrXcl-4nPnenXrh7vIuiw_xkgnmCWWBUfylYALYlu-e0GGpZ6t2aIJIRa1QmT_CEX0zzhQcae-dk5cgHK0iO0_aUOOyAXuNPeClzAiVknz4ACZDsXdIlNFyaZs", "dq": "Z9DG4xOBKXBhEoWUPXMpqnlN0gPx9tRtWe2HRDkZsfu_CWn-qvEJ1L9qPSfSKs6ls5pb1xyeWseKpjblWlUwtgiS3cOsM4SI03H4o1FMi11PBtxKJNitLgvT_nrJ0z8fpux-xfFGMjXyFImoxmKpepLzg5nPZo6f6HscLNwsSJk", "qi": "Sk20wFvilpRKHq79xxFWiDUPHi0x0pp82dYIEntGQkKUWkbSlhgf3MAi5NEQTDmXdnB-rVeWIvEi-BXfdnNgdn8eC4zSdtF4sIAhYr5VWZo0WVWDhT7u2ccvZBFymiz8lo3gN57wGUCi9pbZqzV1-ZppX6YTNDdDCE0q-KO3Cec"}, {"kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB", "d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ", "p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nRaO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmGpeNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0k", "q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc", "dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn-RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX59ehik", "dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pErAMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJKbi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdKT1cYF8", "qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-NZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDhjJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpPz8aaI4"} ] } authlib-1.3.2/tests/clients/keys/jwks_public.json000066400000000000000000000015301466226534200221430ustar00rootroot00000000000000{ "keys": [ {"kty": "RSA", "kid": "abc", "n": "pF1JaMSN8TEsh4N4O_5SpEAVLivJyLH-Cgl3OQBPGgJkt8cg49oasl-5iJS-VdrILxWM9_JCJyURpUuslX4Eb4eUBtQ0x5BaPa8-S2NLdGTaL7nBOO8o8n0C5FEUU-qlEip79KE8aqOj-OC44VsIquSmOvWIQD26n3fCVlgwoRBD1gzzsDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMsRNACmBomzgA4M1TTsisSUO87444pe35Z4_n5c735o2fZMrGgMwiJNh7rT8SYxtIkxngioiGnwkxGQxQ4NzPAHg-XSY0J04pNm7KqTkgtxyrqOANJLIjXlR-U9SQ90NjHVQ", "e": "AQAB"}, {"kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB"} ] } authlib-1.3.2/tests/clients/keys/rsa_private.pem000066400000000000000000000032131466226534200217560ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEApF1JaMSN8TEsh4N4O/5SpEAVLivJyLH+Cgl3OQBPGgJkt8cg 49oasl+5iJS+VdrILxWM9/JCJyURpUuslX4Eb4eUBtQ0x5BaPa8+S2NLdGTaL7nB OO8o8n0C5FEUU+qlEip79KE8aqOj+OC44VsIquSmOvWIQD26n3fCVlgwoRBD1gzz sDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMsRNACmBomzgA4M1TTsisSUO87444p e35Z4/n5c735o2fZMrGgMwiJNh7rT8SYxtIkxngioiGnwkxGQxQ4NzPAHg+XSY0J 04pNm7KqTkgtxyrqOANJLIjXlR+U9SQ90NjHVQIDAQABAoIBABuBPOKaWcJt3yzC NGGduoif7KtwSnEaUA+v69KPGa2Zju8uFHPssKD+4dZYRc2qMeunKJLpaGaSjnRh yHyvvOBJCN1nr3lhz6gY5kzJTfwpUFXCOPJlGy4Q+2Xnp4YvcvYqQ9n5DVovDiZ8 vJOBn16xqpudMPLHIa7D5LJ8SY76HBjE+imTXw1EShdh5TOV9bmPFQqH6JFzowRH hyH2DPHuyHJj6cl8FyqJw5lVWzG3n6Prvk7bYHsjmGjurN35UsumNAp6VouNyUP1 RAEcUJega49aIs6/FJ0ENJzQjlsAzVbTleHkpez2aIok+wsWJGJ4SVxAjADOWAaZ uEJPc3UCgYEA1g4ZGrXOuo75p9/MRIepXGpBWxip4V7B9XmO9WzPCv8nMorJntWB msYV1I01aITxadHatO4Gl2xLniNkDyrEQzJ7w38RQgsVK+CqbnC0K9N77QPbHeC1 YQd9RCNyUohOimKvb7jyv798FBU1GO5QI2eNgfnnfteSVXhD2iOoTOsCgYEAxJJ+ 8toxJdnLa0uUsAbql6zeNXGbUBMzu3FomKlyuWuq841jS2kIalaO/TRj5hbnE45j mCjeLgTVO6Ach3Wfk4zrqajqfFJ0zUg/Wexp49lC3RWiV4icBb85Q6bzeJD9Dn9v hjpfWVkczf/NeA1fGH/pcgfkT6Dm706GFFttLL8CgYBl/HeXk1H47xAiHO4dJKnb v0B+X8To/RXamF01r+8BpUoOubOQetdyX7ic+d6deuHu8i6LD/GSCeYJZYFR/KVg AtiW757QYalnq3ZogkhFrVCZP8IRfTPOFBxp752TlyAcrSI7T9pQ47IBe4094KXM CJWSfPgAJkOxd0iU0XJpmwKBgGfQxuMTgSlwYRKFlD1zKap5TdID8fbUbVnth0Q5 GbH7vwlp/qrxCdS/aj0n0irOpbOaW9ccnlrHiqY25VpVMLYIkt3DrDOEiNNx+KNR TItdTwbcSiTYrS4L0/56ydM/H6bsfsXxRjI18hSJqMZiqXqS84OZz2aOn+h7HCzc LEiZAoGASk20wFvilpRKHq79xxFWiDUPHi0x0pp82dYIEntGQkKUWkbSlhgf3MAi 5NEQTDmXdnB+rVeWIvEi+BXfdnNgdn8eC4zSdtF4sIAhYr5VWZo0WVWDhT7u2ccv ZBFymiz8lo3gN57wGUCi9pbZqzV1+ZppX6YTNDdDCE0q+KO3Cec= -----END RSA PRIVATE KEY----- authlib-1.3.2/tests/clients/test_django/000077500000000000000000000000001466226534200202635ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_django/__init__.py000066400000000000000000000000001466226534200223620ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_django/settings.py000066400000000000000000000012741466226534200225010ustar00rootroot00000000000000SECRET_KEY = 'django-secret' DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:", } } MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware' ] SESSION_ENGINE = 'django.contrib.sessions.backends.cache' CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'LOCATION': 'unique-snowflake', } } INSTALLED_APPS=[] AUTHLIB_OAUTH_CLIENTS = { 'dev_overwrite': { 'client_id': 'dev-client-id', 'client_secret': 'dev-client-secret', 'access_token_params': { 'foo': 'foo-1', 'bar': 'bar-2' } } } USE_TZ = True authlib-1.3.2/tests/clients/test_django/test_oauth_client.py000066400000000000000000000300001466226534200243430ustar00rootroot00000000000000from unittest import mock from authlib.jose import JsonWebKey from authlib.oidc.core.grants.util import generate_id_token from authlib.integrations.django_client import OAuth, OAuthError from authlib.common.urls import urlparse, url_decode from django.test import override_settings from tests.django_helper import TestCase from ..util import ( mock_send_value, get_bearer_token ) dev_client = { 'client_id': 'dev-key', 'client_secret': 'dev-secret' } class DjangoOAuthTest(TestCase): def test_register_remote_app(self): oauth = OAuth() self.assertRaises(AttributeError, lambda: oauth.dev) oauth.register( 'dev', client_id='dev', client_secret='dev', request_token_url='https://i.b/reqeust-token', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) self.assertEqual(oauth.dev.name, 'dev') self.assertEqual(oauth.dev.client_id, 'dev') def test_register_with_overwrite(self): oauth = OAuth() oauth.register( 'dev_overwrite', overwrite=True, client_id='dev', client_secret='dev', request_token_url='https://i.b/reqeust-token', api_base_url='https://i.b/api', access_token_url='https://i.b/token', access_token_params={ 'foo': 'foo' }, authorize_url='https://i.b/authorize' ) self.assertEqual(oauth.dev_overwrite.client_id, 'dev-client-id') self.assertEqual( oauth.dev_overwrite.access_token_params['foo'], 'foo-1') @override_settings(AUTHLIB_OAUTH_CLIENTS={'dev': dev_client}) def test_register_from_settings(self): oauth = OAuth() oauth.register('dev') self.assertEqual(oauth.dev.client_id, 'dev-key') self.assertEqual(oauth.dev.client_secret, 'dev-secret') def test_oauth1_authorize(self): request = self.factory.get('/login') request.session = self.factory.session oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', request_token_url='https://i.b/reqeust-token', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', ) with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value('oauth_token=foo&oauth_verifier=baz') resp = client.authorize_redirect(request) self.assertEqual(resp.status_code, 302) url = resp.get('Location') self.assertIn('oauth_token=foo', url) request2 = self.factory.get(url) request2.session = request.session with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value('oauth_token=a&oauth_token_secret=b') token = client.authorize_access_token(request2) self.assertEqual(token['oauth_token'], 'a') def test_oauth2_authorize(self): request = self.factory.get('/login') request.session = self.factory.session oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', ) rv = client.authorize_redirect(request, 'https://a.b/c') self.assertEqual(rv.status_code, 302) url = rv.get('Location') self.assertIn('state=', url) state = dict(url_decode(urlparse.urlparse(url).query))['state'] with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value(get_bearer_token()) request2 = self.factory.get(f'/authorize?state={state}') request2.session = request.session token = client.authorize_access_token(request2) self.assertEqual(token['access_token'], 'a') def test_oauth2_authorize_access_denied(self): oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', ) with mock.patch('requests.sessions.Session.send'): request = self.factory.get('/?error=access_denied&error_description=Not+Allowed') request.session = self.factory.session self.assertRaises(OAuthError, client.authorize_access_token, request) def test_oauth2_authorize_code_challenge(self): request = self.factory.get('/login') request.session = self.factory.session oauth = OAuth() client = oauth.register( 'dev', client_id='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={'code_challenge_method': 'S256'}, ) rv = client.authorize_redirect(request, 'https://a.b/c') self.assertEqual(rv.status_code, 302) url = rv.get('Location') self.assertIn('state=', url) self.assertIn('code_challenge=', url) state = dict(url_decode(urlparse.urlparse(url).query))['state'] state_data = request.session[f'_state_dev_{state}']['data'] verifier = state_data['code_verifier'] def fake_send(sess, req, **kwargs): self.assertIn(f'code_verifier={verifier}', req.body) return mock_send_value(get_bearer_token()) with mock.patch('requests.sessions.Session.send', fake_send): request2 = self.factory.get(f'/authorize?state={state}') request2.session = request.session token = client.authorize_access_token(request2) self.assertEqual(token['access_token'], 'a') def test_oauth2_authorize_code_verifier(self): request = self.factory.get('/login') request.session = self.factory.session oauth = OAuth() client = oauth.register( 'dev', client_id='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={'code_challenge_method': 'S256'}, ) state = 'foo' code_verifier = 'bar' rv = client.authorize_redirect( request, 'https://a.b/c', state=state, code_verifier=code_verifier ) self.assertEqual(rv.status_code, 302) url = rv.get('Location') self.assertIn('state=', url) self.assertIn('code_challenge=', url) with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value(get_bearer_token()) request2 = self.factory.get(f'/authorize?state={state}') request2.session = request.session token = client.authorize_access_token(request2) self.assertEqual(token['access_token'], 'a') def test_openid_authorize(self): request = self.factory.get('/login') request.session = self.factory.session secret_key = JsonWebKey.import_key('secret', {'kty': 'oct', 'kid': 'f'}) oauth = OAuth() client = oauth.register( 'dev', client_id='dev', jwks={'keys': [secret_key.as_dict()]}, api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={'scope': 'openid profile'}, ) resp = client.authorize_redirect(request, 'https://b.com/bar') self.assertEqual(resp.status_code, 302) url = resp.get('Location') self.assertIn('nonce=', url) query_data = dict(url_decode(urlparse.urlparse(url).query)) token = get_bearer_token() token['id_token'] = generate_id_token( token, {'sub': '123'}, secret_key, alg='HS256', iss='https://i.b', aud='dev', exp=3600, nonce=query_data['nonce'], ) state = query_data['state'] with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value(token) request2 = self.factory.get(f'/authorize?state={state}&code=foo') request2.session = request.session token = client.authorize_access_token(request2) self.assertEqual(token['access_token'], 'a') self.assertIn('userinfo', token) self.assertEqual(token['userinfo']['sub'], '123') def test_oauth2_access_token_with_post(self): oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', ) payload = {'code': 'a', 'state': 'b'} with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value(get_bearer_token()) request = self.factory.post('/token', data=payload) request.session = self.factory.session request.session['_state_dev_b'] = {'data': {}} token = client.authorize_access_token(request) self.assertEqual(token['access_token'], 'a') def test_with_fetch_token_in_oauth(self): def fetch_token(name, request): return {'access_token': name, 'token_type': 'bearer'} oauth = OAuth(fetch_token=fetch_token) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) def fake_send(sess, req, **kwargs): self.assertEqual(sess.token['access_token'], 'dev') return mock_send_value(get_bearer_token()) with mock.patch('requests.sessions.Session.send', fake_send): request = self.factory.get('/login') client.get('/user', request=request) def test_with_fetch_token_in_register(self): def fetch_token(request): return {'access_token': 'dev', 'token_type': 'bearer'} oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', fetch_token=fetch_token, ) def fake_send(sess, req, **kwargs): self.assertEqual(sess.token['access_token'], 'dev') return mock_send_value(get_bearer_token()) with mock.patch('requests.sessions.Session.send', fake_send): request = self.factory.get('/login') client.get('/user', request=request) def test_request_without_token(self): oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) def fake_send(sess, req, **kwargs): auth = req.headers.get('Authorization') self.assertIsNone(auth) resp = mock.MagicMock() resp.text = 'hi' resp.status_code = 200 return resp with mock.patch('requests.sessions.Session.send', fake_send): resp = client.get('/api/user', withhold_token=True) self.assertEqual(resp.text, 'hi') self.assertRaises(OAuthError, client.get, 'https://i.b/api/user') authlib-1.3.2/tests/clients/test_flask/000077500000000000000000000000001466226534200201215ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_flask/__init__.py000066400000000000000000000000001466226534200222200ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_flask/test_oauth_client.py000066400000000000000000000442301466226534200242130ustar00rootroot00000000000000from unittest import TestCase, mock from flask import Flask, session from authlib.jose import jwk from authlib.oidc.core.grants.util import generate_id_token from authlib.integrations.flask_client import OAuth, OAuthError from authlib.integrations.flask_client import FlaskOAuth2App from authlib.common.urls import urlparse, url_decode from cachelib import SimpleCache from ..util import ( mock_send_value, get_bearer_token ) class FlaskOAuthTest(TestCase): def test_register_remote_app(self): app = Flask(__name__) oauth = OAuth(app) self.assertRaises(AttributeError, lambda: oauth.dev) oauth.register( 'dev', client_id='dev', client_secret='dev', ) self.assertEqual(oauth.dev.name, 'dev') self.assertEqual(oauth.dev.client_id, 'dev') def test_register_conf_from_app(self): app = Flask(__name__) app.config.update({ 'DEV_CLIENT_ID': 'dev', 'DEV_CLIENT_SECRET': 'dev', }) oauth = OAuth(app) oauth.register('dev') self.assertEqual(oauth.dev.client_id, 'dev') def test_register_with_overwrite(self): app = Flask(__name__) app.config.update({ 'DEV_CLIENT_ID': 'dev-1', 'DEV_CLIENT_SECRET': 'dev', 'DEV_ACCESS_TOKEN_PARAMS': {'foo': 'foo-1'} }) oauth = OAuth(app) oauth.register( 'dev', overwrite=True, client_id='dev', access_token_params={'foo': 'foo'} ) self.assertEqual(oauth.dev.client_id, 'dev-1') self.assertEqual(oauth.dev.client_secret, 'dev') self.assertEqual(oauth.dev.access_token_params['foo'], 'foo-1') def test_init_app_later(self): app = Flask(__name__) app.config.update({ 'DEV_CLIENT_ID': 'dev', 'DEV_CLIENT_SECRET': 'dev', }) oauth = OAuth() remote = oauth.register('dev') self.assertRaises(RuntimeError, lambda: oauth.dev.client_id) oauth.init_app(app) self.assertEqual(oauth.dev.client_id, 'dev') self.assertEqual(remote.client_id, 'dev') self.assertIsNone(oauth.cache) self.assertIsNone(oauth.fetch_token) self.assertIsNone(oauth.update_token) def test_init_app_params(self): app = Flask(__name__) oauth = OAuth() oauth.init_app(app, SimpleCache()) self.assertIsNotNone(oauth.cache) self.assertIsNone(oauth.update_token) oauth.init_app(app, update_token=lambda o: o) self.assertIsNotNone(oauth.update_token) def test_create_client(self): app = Flask(__name__) oauth = OAuth(app) self.assertIsNone(oauth.create_client('dev')) oauth.register('dev', client_id='dev') self.assertIsNotNone(oauth.create_client('dev')) def test_register_oauth1_remote_app(self): app = Flask(__name__) oauth = OAuth(app) client_kwargs = dict( client_id='dev', client_secret='dev', request_token_url='https://i.b/reqeust-token', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', fetch_request_token=lambda: None, save_request_token=lambda token: token, ) oauth.register('dev', **client_kwargs) self.assertEqual(oauth.dev.name, 'dev') self.assertEqual(oauth.dev.client_id, 'dev') oauth = OAuth(app, cache=SimpleCache()) oauth.register('dev', **client_kwargs) self.assertEqual(oauth.dev.name, 'dev') self.assertEqual(oauth.dev.client_id, 'dev') def test_oauth1_authorize_cache(self): app = Flask(__name__) app.secret_key = '!' cache = SimpleCache() oauth = OAuth(app, cache=cache) client = oauth.register( 'dev', client_id='dev', client_secret='dev', request_token_url='https://i.b/reqeust-token', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) with app.test_request_context(): with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value('oauth_token=foo&oauth_verifier=baz') resp = client.authorize_redirect('https://b.com/bar') self.assertEqual(resp.status_code, 302) url = resp.headers.get('Location') self.assertIn('oauth_token=foo', url) with app.test_request_context('/?oauth_token=foo'): with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value('oauth_token=a&oauth_token_secret=b') token = client.authorize_access_token() self.assertEqual(token['oauth_token'], 'a') def test_oauth1_authorize_session(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', request_token_url='https://i.b/reqeust-token', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) with app.test_request_context(): with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value('oauth_token=foo&oauth_verifier=baz') resp = client.authorize_redirect('https://b.com/bar') self.assertEqual(resp.status_code, 302) url = resp.headers.get('Location') self.assertIn('oauth_token=foo', url) data = session['_state_dev_foo'] with app.test_request_context('/?oauth_token=foo'): session['_state_dev_foo'] = data with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value('oauth_token=a&oauth_token_secret=b') token = client.authorize_access_token() self.assertEqual(token['oauth_token'], 'a') def test_register_oauth2_remote_app(self): app = Flask(__name__) oauth = OAuth(app) oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', refresh_token_url='https://i.b/token', authorize_url='https://i.b/authorize', update_token=lambda name: 'hi' ) self.assertEqual(oauth.dev.name, 'dev') session = oauth.dev._get_oauth_client() self.assertIsNotNone(session.update_token) def test_oauth2_authorize(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) with app.test_request_context(): resp = client.authorize_redirect('https://b.com/bar') self.assertEqual(resp.status_code, 302) url = resp.headers.get('Location') self.assertIn('state=', url) state = dict(url_decode(urlparse.urlparse(url).query))['state'] self.assertIsNotNone(state) data = session[f'_state_dev_{state}'] with app.test_request_context(path=f'/?code=a&state={state}'): # session is cleared in tests session[f'_state_dev_{state}'] = data with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value(get_bearer_token()) token = client.authorize_access_token() self.assertEqual(token['access_token'], 'a') with app.test_request_context(): self.assertEqual(client.token, None) def test_oauth2_authorize_access_denied(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) with app.test_request_context(path='/?error=access_denied&error_description=Not+Allowed'): # session is cleared in tests with mock.patch('requests.sessions.Session.send'): self.assertRaises(OAuthError, client.authorize_access_token) def test_oauth2_authorize_via_custom_client(self): class CustomRemoteApp(FlaskOAuth2App): OAUTH_APP_CONFIG = {'authorize_url': 'https://i.b/custom'} app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', client_cls=CustomRemoteApp, ) with app.test_request_context(): resp = client.authorize_redirect('https://b.com/bar') self.assertEqual(resp.status_code, 302) url = resp.headers.get('Location') self.assertTrue(url.startswith('https://i.b/custom?')) def test_oauth2_authorize_with_metadata(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', ) self.assertRaises(RuntimeError, lambda: client.create_authorization_url(None)) client = oauth.register( 'dev2', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', server_metadata_url='https://i.b/.well-known/openid-configuration' ) with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value({ 'authorization_endpoint': 'https://i.b/authorize' }) with app.test_request_context(): resp = client.authorize_redirect('https://b.com/bar') self.assertEqual(resp.status_code, 302) def test_oauth2_authorize_code_challenge(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={'code_challenge_method': 'S256'}, ) with app.test_request_context(): resp = client.authorize_redirect('https://b.com/bar') self.assertEqual(resp.status_code, 302) url = resp.headers.get('Location') self.assertIn('code_challenge=', url) self.assertIn('code_challenge_method=S256', url) state = dict(url_decode(urlparse.urlparse(url).query))['state'] self.assertIsNotNone(state) data = session[f'_state_dev_{state}'] verifier = data['data']['code_verifier'] self.assertIsNotNone(verifier) def fake_send(sess, req, **kwargs): self.assertIn(f'code_verifier={verifier}', req.body) return mock_send_value(get_bearer_token()) path = f'/?code=a&state={state}' with app.test_request_context(path=path): # session is cleared in tests session[f'_state_dev_{state}'] = data with mock.patch('requests.sessions.Session.send', fake_send): token = client.authorize_access_token() self.assertEqual(token['access_token'], 'a') def test_openid_authorize(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) key = jwk.dumps('secret', 'oct', kid='f') client = oauth.register( 'dev', client_id='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={'scope': 'openid profile'}, jwks={'keys': [key]}, ) with app.test_request_context(): resp = client.authorize_redirect('https://b.com/bar') self.assertEqual(resp.status_code, 302) url = resp.headers['Location'] query_data = dict(url_decode(urlparse.urlparse(url).query)) state = query_data['state'] self.assertIsNotNone(state) session_data = session[f'_state_dev_{state}'] nonce = session_data['data']['nonce'] self.assertIsNotNone(nonce) self.assertEqual(nonce, query_data['nonce']) token = get_bearer_token() token['id_token'] = generate_id_token( token, {'sub': '123'}, key, alg='HS256', iss='https://i.b', aud='dev', exp=3600, nonce=query_data['nonce'], ) path = f'/?code=a&state={state}' with app.test_request_context(path=path): session[f'_state_dev_{state}'] = session_data with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value(token) token = client.authorize_access_token() self.assertEqual(token['access_token'], 'a') self.assertIn('userinfo', token) def test_oauth2_access_token_with_post(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) payload = {'code': 'a', 'state': 'b'} with app.test_request_context(data=payload, method='POST'): session['_state_dev_b'] = {'data': payload} with mock.patch('requests.sessions.Session.send') as send: send.return_value = mock_send_value(get_bearer_token()) token = client.authorize_access_token() self.assertEqual(token['access_token'], 'a') def test_access_token_with_fetch_token(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth() token = get_bearer_token() oauth.init_app(app, fetch_token=lambda name: token) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) def fake_send(sess, req, **kwargs): auth = req.headers['Authorization'] self.assertEqual(auth, 'Bearer {}'.format(token['access_token'])) resp = mock.MagicMock() resp.text = 'hi' resp.status_code = 200 return resp with app.test_request_context(): with mock.patch('requests.sessions.Session.send', fake_send): resp = client.get('/api/user') self.assertEqual(resp.text, 'hi') # trigger ctx.authlib_client_oauth_token resp = client.get('/api/user') self.assertEqual(resp.text, 'hi') def test_request_with_refresh_token(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth() expired_token = { 'token_type': 'Bearer', 'access_token': 'expired-a', 'refresh_token': 'expired-b', 'expires_in': '3600', 'expires_at': 1566465749, } oauth.init_app(app, fetch_token=lambda name: expired_token) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', refresh_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) def fake_send(sess, req, **kwargs): if req.url == 'https://i.b/token': auth = req.headers['Authorization'] self.assertIn('Basic', auth) resp = mock.MagicMock() resp.json = get_bearer_token resp.status_code = 200 return resp resp = mock.MagicMock() resp.text = 'hi' resp.status_code = 200 return resp with app.test_request_context(): with mock.patch('requests.sessions.Session.send', fake_send): resp = client.get('/api/user', token=expired_token) self.assertEqual(resp.text, 'hi') def test_request_without_token(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize' ) def fake_send(sess, req, **kwargs): auth = req.headers.get('Authorization') self.assertIsNone(auth) resp = mock.MagicMock() resp.text = 'hi' resp.status_code = 200 return resp with app.test_request_context(): with mock.patch('requests.sessions.Session.send', fake_send): resp = client.get('/api/user', withhold_token=True) self.assertEqual(resp.text, 'hi') self.assertRaises(OAuthError, client.get, 'https://i.b/api/user') authlib-1.3.2/tests/clients/test_flask/test_user_mixin.py000066400000000000000000000125731466226534200237240ustar00rootroot00000000000000from unittest import TestCase, mock from flask import Flask from authlib.jose import JsonWebKey from authlib.jose.errors import InvalidClaimError from authlib.integrations.flask_client import OAuth from authlib.oidc.core.grants.util import generate_id_token from ..util import get_bearer_token, read_key_file secret_key = JsonWebKey.import_key('secret', {'kty': 'oct', 'kid': 'f'}) class FlaskUserMixinTest(TestCase): def test_fetch_userinfo(self): app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', fetch_token=get_bearer_token, userinfo_endpoint='https://i.b/userinfo', ) def fake_send(sess, req, **kwargs): resp = mock.MagicMock() resp.json = lambda: {'sub': '123'} resp.status_code = 200 return resp with app.test_request_context(): with mock.patch('requests.sessions.Session.send', fake_send): user = client.userinfo() self.assertEqual(user.sub, '123') def test_parse_id_token(self): token = get_bearer_token() id_token = generate_id_token( token, {'sub': '123'}, secret_key, alg='HS256', iss='https://i.b', aud='dev', exp=3600, nonce='n', ) app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', fetch_token=get_bearer_token, jwks={'keys': [secret_key.as_dict()]}, issuer='https://i.b', id_token_signing_alg_values_supported=['HS256', 'RS256'], ) with app.test_request_context(): self.assertIsNone(client.parse_id_token(token, nonce='n')) token['id_token'] = id_token user = client.parse_id_token(token, nonce='n') self.assertEqual(user.sub, '123') claims_options = {'iss': {'value': 'https://i.b'}} user = client.parse_id_token(token, nonce='n', claims_options=claims_options) self.assertEqual(user.sub, '123') claims_options = {'iss': {'value': 'https://i.c'}} self.assertRaises( InvalidClaimError, client.parse_id_token, token, 'n', claims_options ) def test_parse_id_token_nonce_supported(self): token = get_bearer_token() id_token = generate_id_token( token, {'sub': '123', 'nonce_supported': False}, secret_key, alg='HS256', iss='https://i.b', aud='dev', exp=3600, ) app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', fetch_token=get_bearer_token, jwks={'keys': [secret_key.as_dict()]}, issuer='https://i.b', id_token_signing_alg_values_supported=['HS256', 'RS256'], ) with app.test_request_context(): token['id_token'] = id_token user = client.parse_id_token(token, nonce='n') self.assertEqual(user.sub, '123') def test_runtime_error_fetch_jwks_uri(self): token = get_bearer_token() id_token = generate_id_token( token, {'sub': '123'}, secret_key, alg='HS256', iss='https://i.b', aud='dev', exp=3600, nonce='n', ) app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) alt_key = secret_key.as_dict() alt_key['kid'] = 'b' client = oauth.register( 'dev', client_id='dev', client_secret='dev', fetch_token=get_bearer_token, jwks={'keys': [alt_key]}, issuer='https://i.b', id_token_signing_alg_values_supported=['HS256'], ) with app.test_request_context(): token['id_token'] = id_token self.assertRaises(RuntimeError, client.parse_id_token, token, 'n') def test_force_fetch_jwks_uri(self): secret_keys = read_key_file('jwks_private.json') token = get_bearer_token() id_token = generate_id_token( token, {'sub': '123'}, secret_keys, alg='RS256', iss='https://i.b', aud='dev', exp=3600, nonce='n', ) app = Flask(__name__) app.secret_key = '!' oauth = OAuth(app) client = oauth.register( 'dev', client_id='dev', client_secret='dev', fetch_token=get_bearer_token, jwks={'keys': [secret_key.as_dict()]}, jwks_uri='https://i.b/jwks', issuer='https://i.b', ) def fake_send(sess, req, **kwargs): resp = mock.MagicMock() resp.json = lambda: read_key_file('jwks_public.json') resp.status_code = 200 return resp with app.test_request_context(): self.assertIsNone(client.parse_id_token(token, nonce='n')) with mock.patch('requests.sessions.Session.send', fake_send): token['id_token'] = id_token user = client.parse_id_token(token, nonce='n') self.assertEqual(user.sub, '123') authlib-1.3.2/tests/clients/test_httpx/000077500000000000000000000000001466226534200201705ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_httpx/__init__.py000066400000000000000000000000001466226534200222670ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_httpx/test_assertion_client.py000066400000000000000000000030251466226534200251460ustar00rootroot00000000000000import time import pytest from authlib.integrations.httpx_client import AssertionClient from ..wsgi_helper import MockDispatch default_token = { 'token_type': 'Bearer', 'access_token': 'a', 'refresh_token': 'b', 'expires_in': '3600', 'expires_at': int(time.time()) + 3600, } def test_refresh_token(): def verifier(request): content = request.form if str(request.url) == 'https://i.b/token': assert 'assertion' in content with AssertionClient( 'https://i.b/token', issuer='foo', subject='foo', audience='foo', alg='HS256', key='secret', app=MockDispatch(default_token, assert_func=verifier) ) as client: client.get('https://i.b') # trigger more case now = int(time.time()) with AssertionClient( 'https://i.b/token', issuer='foo', subject=None, audience='foo', issued_at=now, expires_at=now + 3600, header={'alg': 'HS256'}, key='secret', scope='email', claims={'test_mode': 'true'}, app=MockDispatch(default_token, assert_func=verifier) ) as client: client.get('https://i.b') client.get('https://i.b') def test_without_alg(): with AssertionClient( 'https://i.b/token', issuer='foo', subject='foo', audience='foo', key='secret', app=MockDispatch(default_token) ) as client: with pytest.raises(ValueError): client.get('https://i.b') authlib-1.3.2/tests/clients/test_httpx/test_async_assertion_client.py000066400000000000000000000033371466226534200263510ustar00rootroot00000000000000import time import pytest from authlib.integrations.httpx_client import AsyncAssertionClient from ..asgi_helper import AsyncMockDispatch default_token = { 'token_type': 'Bearer', 'access_token': 'a', 'refresh_token': 'b', 'expires_in': '3600', 'expires_at': int(time.time()) + 3600, } @pytest.mark.asyncio async def test_refresh_token(): async def verifier(request): content = await request.body() if str(request.url) == 'https://i.b/token': assert b'assertion=' in content async with AsyncAssertionClient( 'https://i.b/token', grant_type=AsyncAssertionClient.JWT_BEARER_GRANT_TYPE, issuer='foo', subject='foo', audience='foo', alg='HS256', key='secret', app=AsyncMockDispatch(default_token, assert_func=verifier) ) as client: await client.get('https://i.b') # trigger more case now = int(time.time()) async with AsyncAssertionClient( 'https://i.b/token', issuer='foo', subject=None, audience='foo', issued_at=now, expires_at=now + 3600, header={'alg': 'HS256'}, key='secret', scope='email', claims={'test_mode': 'true'}, app=AsyncMockDispatch(default_token, assert_func=verifier) ) as client: await client.get('https://i.b') await client.get('https://i.b') @pytest.mark.asyncio async def test_without_alg(): async with AsyncAssertionClient( 'https://i.b/token', issuer='foo', subject='foo', audience='foo', key='secret', app=AsyncMockDispatch() ) as client: with pytest.raises(ValueError): await client.get('https://i.b') authlib-1.3.2/tests/clients/test_httpx/test_async_oauth1_client.py000066400000000000000000000117121466226534200255370ustar00rootroot00000000000000import pytest from authlib.integrations.httpx_client import ( OAuthError, AsyncOAuth1Client, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, ) from ..asgi_helper import AsyncMockDispatch oauth_url = 'https://example.com/oauth' @pytest.mark.asyncio async def test_fetch_request_token_via_header(): request_token = {'oauth_token': '1', 'oauth_token_secret': '2'} async def assert_func(request): auth_header = request.headers.get('authorization') assert 'oauth_consumer_key="id"' in auth_header assert 'oauth_signature=' in auth_header app = AsyncMockDispatch(request_token, assert_func=assert_func) async with AsyncOAuth1Client('id', 'secret', app=app) as client: response = await client.fetch_request_token(oauth_url) assert response == request_token @pytest.mark.asyncio async def test_fetch_request_token_via_body(): request_token = {'oauth_token': '1', 'oauth_token_secret': '2'} async def assert_func(request): auth_header = request.headers.get('authorization') assert auth_header is None content = await request.body() assert b'oauth_consumer_key=id' in content assert b'&oauth_signature=' in content mock_response = AsyncMockDispatch(request_token, assert_func=assert_func) async with AsyncOAuth1Client( 'id', 'secret', signature_type=SIGNATURE_TYPE_BODY, app=mock_response, ) as client: response = await client.fetch_request_token(oauth_url) assert response == request_token @pytest.mark.asyncio async def test_fetch_request_token_via_query(): request_token = {'oauth_token': '1', 'oauth_token_secret': '2'} async def assert_func(request): auth_header = request.headers.get('authorization') assert auth_header is None url = str(request.url) assert 'oauth_consumer_key=id' in url assert '&oauth_signature=' in url mock_response = AsyncMockDispatch(request_token, assert_func=assert_func) async with AsyncOAuth1Client( 'id', 'secret', signature_type=SIGNATURE_TYPE_QUERY, app=mock_response, ) as client: response = await client.fetch_request_token(oauth_url) assert response == request_token @pytest.mark.asyncio async def test_fetch_access_token(): request_token = {'oauth_token': '1', 'oauth_token_secret': '2'} async def assert_func(request): auth_header = request.headers.get('authorization') assert 'oauth_verifier="d"' in auth_header assert 'oauth_token="foo"' in auth_header assert 'oauth_consumer_key="id"' in auth_header assert 'oauth_signature=' in auth_header mock_response = AsyncMockDispatch(request_token, assert_func=assert_func) async with AsyncOAuth1Client( 'id', 'secret', token='foo', token_secret='bar', app=mock_response, ) as client: with pytest.raises(OAuthError): await client.fetch_access_token(oauth_url) response = await client.fetch_access_token(oauth_url, verifier='d') assert response == request_token @pytest.mark.asyncio async def test_get_via_header(): mock_response = AsyncMockDispatch(b'hello') async with AsyncOAuth1Client( 'id', 'secret', token='foo', token_secret='bar', app=mock_response, ) as client: response = await client.get('https://example.com/') assert response.content == b'hello' request = response.request auth_header = request.headers.get('authorization') assert 'oauth_token="foo"' in auth_header assert 'oauth_consumer_key="id"' in auth_header assert 'oauth_signature=' in auth_header @pytest.mark.asyncio async def test_get_via_body(): async def assert_func(request): content = await request.body() assert b'oauth_token=foo' in content assert b'oauth_consumer_key=id' in content assert b'oauth_signature=' in content mock_response = AsyncMockDispatch(b'hello', assert_func=assert_func) async with AsyncOAuth1Client( 'id', 'secret', token='foo', token_secret='bar', signature_type=SIGNATURE_TYPE_BODY, app=mock_response, ) as client: response = await client.post('https://example.com/') assert response.content == b'hello' request = response.request auth_header = request.headers.get('authorization') assert auth_header is None @pytest.mark.asyncio async def test_get_via_query(): mock_response = AsyncMockDispatch(b'hello') async with AsyncOAuth1Client( 'id', 'secret', token='foo', token_secret='bar', signature_type=SIGNATURE_TYPE_QUERY, app=mock_response, ) as client: response = await client.get('https://example.com/') assert response.content == b'hello' request = response.request auth_header = request.headers.get('authorization') assert auth_header is None url = str(request.url) assert 'oauth_token=foo' in url assert 'oauth_consumer_key=id' in url assert 'oauth_signature=' in url authlib-1.3.2/tests/clients/test_httpx/test_async_oauth2_client.py000066400000000000000000000335021466226534200255410ustar00rootroot00000000000000import asyncio import time import pytest from unittest import mock from copy import deepcopy from httpx import AsyncClient from authlib.common.security import generate_token from authlib.common.urls import url_encode from authlib.integrations.httpx_client import ( OAuthError, AsyncOAuth2Client, ) from ..asgi_helper import AsyncMockDispatch default_token = { 'token_type': 'Bearer', 'access_token': 'a', 'refresh_token': 'b', 'expires_in': '3600', 'expires_at': int(time.time()) + 3600, } @pytest.mark.asyncio async def assert_token_in_header(request): token = 'Bearer ' + default_token['access_token'] auth_header = request.headers.get('authorization') assert auth_header == token @pytest.mark.asyncio async def assert_token_in_body(request): content = await request.body() assert default_token['access_token'] in content.decode() @pytest.mark.asyncio async def assert_token_in_uri(request): assert default_token['access_token'] in str(request.url) @pytest.mark.asyncio @pytest.mark.parametrize( "assert_func, token_placement", [ (assert_token_in_header, "header"), (assert_token_in_body, "body"), (assert_token_in_uri, "uri") ] ) async def test_add_token_get_request(assert_func, token_placement): mock_response = AsyncMockDispatch({'a': 'a'}, assert_func=assert_func) async with AsyncOAuth2Client( 'foo', token=default_token, token_placement=token_placement, app=mock_response ) as client: resp = await client.get('https://i.b') data = resp.json() assert data['a'] == 'a' @pytest.mark.asyncio @pytest.mark.parametrize( "assert_func, token_placement", [ (assert_token_in_header, "header"), (assert_token_in_body, "body"), (assert_token_in_uri, "uri") ] ) async def test_add_token_to_streaming_request(assert_func, token_placement): mock_response = AsyncMockDispatch({'a': 'a'}, assert_func=assert_func) async with AsyncOAuth2Client( 'foo', token=default_token, token_placement=token_placement, app=mock_response ) as client: async with client.stream("GET", 'https://i.b') as stream: await stream.aread() data = stream.json() assert data['a'] == 'a' @pytest.mark.parametrize("client", [ AsyncOAuth2Client( 'foo', token=default_token, token_placement="header", app=AsyncMockDispatch({'a': 'a'}, assert_func=assert_token_in_header) ), AsyncClient(app=AsyncMockDispatch({'a': 'a'})) ]) async def test_httpx_client_stream_match(client): async with client as client_entered: async with client_entered.stream("GET", 'https://i.b') as stream: assert stream.status_code == 200 def test_create_authorization_url(): url = 'https://example.com/authorize?foo=bar' sess = AsyncOAuth2Client(client_id='foo') auth_url, state = sess.create_authorization_url(url) assert state in auth_url assert 'client_id=foo' in auth_url assert 'response_type=code' in auth_url sess = AsyncOAuth2Client(client_id='foo', prompt='none') auth_url, state = sess.create_authorization_url( url, state='foo', redirect_uri='https://i.b', scope='profile') assert state == 'foo' assert 'i.b' in auth_url assert 'profile' in auth_url assert 'prompt=none' in auth_url def test_code_challenge(): sess = AsyncOAuth2Client('foo', code_challenge_method='S256') url = 'https://example.com/authorize' auth_url, _ = sess.create_authorization_url( url, code_verifier=generate_token(48)) assert 'code_challenge=' in auth_url assert 'code_challenge_method=S256' in auth_url def test_token_from_fragment(): sess = AsyncOAuth2Client('foo') response_url = 'https://i.b/callback#' + url_encode(default_token.items()) assert sess.token_from_fragment(response_url) == default_token token = sess.fetch_token(authorization_response=response_url) assert token == default_token @pytest.mark.asyncio async def test_fetch_token_post(): url = 'https://example.com/token' async def assert_func(request): content = await request.body() content = content.decode() assert 'code=v' in content assert 'client_id=' in content assert 'grant_type=authorization_code' in content mock_response = AsyncMockDispatch(default_token, assert_func=assert_func) async with AsyncOAuth2Client('foo', app=mock_response) as client: token = await client.fetch_token(url, authorization_response='https://i.b/?code=v') assert token == default_token async with AsyncOAuth2Client( 'foo', token_endpoint_auth_method='none', app=mock_response ) as client: token = await client.fetch_token(url, code='v') assert token == default_token mock_response = AsyncMockDispatch({'error': 'invalid_request'}) async with AsyncOAuth2Client('foo', app=mock_response) as client: with pytest.raises(OAuthError): await client.fetch_token(url) @pytest.mark.asyncio async def test_fetch_token_get(): url = 'https://example.com/token' async def assert_func(request): url = str(request.url) assert 'code=v' in url assert 'client_id=' in url assert 'grant_type=authorization_code' in url mock_response = AsyncMockDispatch(default_token, assert_func=assert_func) async with AsyncOAuth2Client('foo', app=mock_response) as client: authorization_response = 'https://i.b/?code=v' token = await client.fetch_token( url, authorization_response=authorization_response, method='GET') assert token == default_token async with AsyncOAuth2Client( 'foo', token_endpoint_auth_method='none', app=mock_response ) as client: token = await client.fetch_token(url, code='v', method='GET') assert token == default_token token = await client.fetch_token(url + '?q=a', code='v', method='GET') assert token == default_token @pytest.mark.asyncio async def test_token_auth_method_client_secret_post(): url = 'https://example.com/token' async def assert_func(request): content = await request.body() content = content.decode() assert 'code=v' in content assert 'client_id=' in content assert 'client_secret=bar' in content assert 'grant_type=authorization_code' in content mock_response = AsyncMockDispatch(default_token, assert_func=assert_func) async with AsyncOAuth2Client( 'foo', 'bar', token_endpoint_auth_method='client_secret_post', app=mock_response ) as client: token = await client.fetch_token(url, code='v') assert token == default_token @pytest.mark.asyncio async def test_access_token_response_hook(): url = 'https://example.com/token' def _access_token_response_hook(resp): assert resp.json() == default_token return resp access_token_response_hook = mock.Mock(side_effect=_access_token_response_hook) app = AsyncMockDispatch(default_token) async with AsyncOAuth2Client('foo', token=default_token, app=app) as sess: sess.register_compliance_hook( 'access_token_response', access_token_response_hook ) assert await sess.fetch_token(url) == default_token assert access_token_response_hook.called is True @pytest.mark.asyncio async def test_password_grant_type(): url = 'https://example.com/token' async def assert_func(request): content = await request.body() content = content.decode() assert 'username=v' in content assert 'scope=profile' in content assert 'grant_type=password' in content app = AsyncMockDispatch(default_token, assert_func=assert_func) async with AsyncOAuth2Client('foo', scope='profile', app=app) as sess: token = await sess.fetch_token(url, username='v', password='v') assert token == default_token token = await sess.fetch_token( url, username='v', password='v', grant_type='password') assert token == default_token @pytest.mark.asyncio async def test_client_credentials_type(): url = 'https://example.com/token' async def assert_func(request): content = await request.body() content = content.decode() assert 'scope=profile' in content assert 'grant_type=client_credentials' in content app = AsyncMockDispatch(default_token, assert_func=assert_func) async with AsyncOAuth2Client('foo', scope='profile', app=app) as sess: token = await sess.fetch_token(url) assert token == default_token token = await sess.fetch_token(url, grant_type='client_credentials') assert token == default_token @pytest.mark.asyncio async def test_cleans_previous_token_before_fetching_new_one(): now = int(time.time()) new_token = deepcopy(default_token) past = now - 7200 default_token['expires_at'] = past new_token['expires_at'] = now + 3600 url = 'https://example.com/token' app = AsyncMockDispatch(new_token) with mock.patch('time.time', lambda: now): async with AsyncOAuth2Client('foo', token=default_token, app=app) as sess: assert await sess.fetch_token(url) == new_token def test_token_status(): token = dict(access_token='a', token_type='bearer', expires_at=100) sess = AsyncOAuth2Client('foo', token=token) assert sess.token.is_expired() is True @pytest.mark.asyncio async def test_auto_refresh_token(): async def _update_token(token, refresh_token=None, access_token=None): assert refresh_token == 'b' assert token == default_token update_token = mock.Mock(side_effect=_update_token) old_token = dict( access_token='a', refresh_token='b', token_type='bearer', expires_at=100 ) app = AsyncMockDispatch(default_token) async with AsyncOAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, app=app ) as sess: await sess.get('https://i.b/user') assert update_token.called is True old_token = dict( access_token='a', token_type='bearer', expires_at=100 ) async with AsyncOAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, app=app ) as sess: with pytest.raises(OAuthError): await sess.get('https://i.b/user') @pytest.mark.asyncio async def test_auto_refresh_token2(): async def _update_token(token, refresh_token=None, access_token=None): assert access_token == 'a' assert token == default_token update_token = mock.Mock(side_effect=_update_token) old_token = dict( access_token='a', token_type='bearer', expires_at=100 ) app = AsyncMockDispatch(default_token) async with AsyncOAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', grant_type='client_credentials', app=app, ) as client: await client.get('https://i.b/user') assert update_token.called is False async with AsyncOAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, grant_type='client_credentials', app=app, ) as client: await client.get('https://i.b/user') assert update_token.called is True @pytest.mark.asyncio async def test_auto_refresh_token3(): async def _update_token(token, refresh_token=None, access_token=None): assert access_token == 'a' assert token == default_token update_token = mock.Mock(side_effect=_update_token) old_token = dict( access_token='a', token_type='bearer', expires_at=100 ) app = AsyncMockDispatch(default_token) async with AsyncOAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, grant_type='client_credentials', app=app, ) as client: await client.post('https://i.b/user', json={'foo': 'bar'}) assert update_token.called is True @pytest.mark.asyncio async def test_auto_refresh_token4(): async def _update_token(token, refresh_token=None, access_token=None): # This test only makes sense if the expired token is refreshed token["expires_at"] = int(time.time()) + 3600 # artificial sleep to force other coroutines to wake await asyncio.sleep(0.1) update_token = mock.Mock(side_effect=_update_token) old_token = dict( access_token='old', token_type='bearer', expires_at=100 ) app = AsyncMockDispatch(default_token) async with AsyncOAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, grant_type='client_credentials', app=app, ) as client: coroutines = [client.get('https://i.b/user') for x in range(10)] await asyncio.gather(*coroutines) update_token.assert_called_once() @pytest.mark.asyncio async def test_revoke_token(): answer = {'status': 'ok'} app = AsyncMockDispatch(answer) async with AsyncOAuth2Client('a', app=app) as sess: resp = await sess.revoke_token('https://i.b/token', 'hi') assert resp.json() == answer resp = await sess.revoke_token( 'https://i.b/token', 'hi', token_type_hint='access_token' ) assert resp.json() == answer @pytest.mark.asyncio async def test_request_without_token(): async with AsyncOAuth2Client('a', app=AsyncMockDispatch()) as client: with pytest.raises(OAuthError): await client.get('https://i.b/token') authlib-1.3.2/tests/clients/test_httpx/test_oauth1_client.py000066400000000000000000000111021466226534200243330ustar00rootroot00000000000000import pytest from authlib.integrations.httpx_client import ( OAuthError, OAuth1Client, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, ) from ..wsgi_helper import MockDispatch oauth_url = 'https://example.com/oauth' def test_fetch_request_token_via_header(): request_token = {'oauth_token': '1', 'oauth_token_secret': '2'} def assert_func(request): auth_header = request.headers.get('authorization') assert 'oauth_consumer_key="id"' in auth_header assert 'oauth_signature=' in auth_header app = MockDispatch(request_token, assert_func=assert_func) with OAuth1Client('id', 'secret', app=app) as client: response = client.fetch_request_token(oauth_url) assert response == request_token def test_fetch_request_token_via_body(): request_token = {'oauth_token': '1', 'oauth_token_secret': '2'} def assert_func(request): auth_header = request.headers.get('authorization') assert auth_header is None content = request.form assert content.get('oauth_consumer_key') == 'id' assert 'oauth_signature' in content mock_response = MockDispatch(request_token, assert_func=assert_func) with OAuth1Client( 'id', 'secret', signature_type=SIGNATURE_TYPE_BODY, app=mock_response, ) as client: response = client.fetch_request_token(oauth_url) assert response == request_token def test_fetch_request_token_via_query(): request_token = {'oauth_token': '1', 'oauth_token_secret': '2'} def assert_func(request): auth_header = request.headers.get('authorization') assert auth_header is None url = str(request.url) assert 'oauth_consumer_key=id' in url assert '&oauth_signature=' in url mock_response = MockDispatch(request_token, assert_func=assert_func) with OAuth1Client( 'id', 'secret', signature_type=SIGNATURE_TYPE_QUERY, app=mock_response, ) as client: response = client.fetch_request_token(oauth_url) assert response == request_token def test_fetch_access_token(): request_token = {'oauth_token': '1', 'oauth_token_secret': '2'} def assert_func(request): auth_header = request.headers.get('authorization') assert 'oauth_verifier="d"' in auth_header assert 'oauth_token="foo"' in auth_header assert 'oauth_consumer_key="id"' in auth_header assert 'oauth_signature=' in auth_header mock_response = MockDispatch(request_token, assert_func=assert_func) with OAuth1Client( 'id', 'secret', token='foo', token_secret='bar', app=mock_response, ) as client: with pytest.raises(OAuthError): client.fetch_access_token(oauth_url) response = client.fetch_access_token(oauth_url, verifier='d') assert response == request_token def test_get_via_header(): mock_response = MockDispatch(b'hello') with OAuth1Client( 'id', 'secret', token='foo', token_secret='bar', app=mock_response, ) as client: response = client.get('https://example.com/') assert response.content == b'hello' request = response.request auth_header = request.headers.get('authorization') assert 'oauth_token="foo"' in auth_header assert 'oauth_consumer_key="id"' in auth_header assert 'oauth_signature=' in auth_header def test_get_via_body(): def assert_func(request): content = request.form assert content.get('oauth_token') == 'foo' assert content.get('oauth_consumer_key') == 'id' assert 'oauth_signature' in content mock_response = MockDispatch(b'hello', assert_func=assert_func) with OAuth1Client( 'id', 'secret', token='foo', token_secret='bar', signature_type=SIGNATURE_TYPE_BODY, app=mock_response, ) as client: response = client.post('https://example.com/') assert response.content == b'hello' request = response.request auth_header = request.headers.get('authorization') assert auth_header is None def test_get_via_query(): mock_response = MockDispatch(b'hello') with OAuth1Client( 'id', 'secret', token='foo', token_secret='bar', signature_type=SIGNATURE_TYPE_QUERY, app=mock_response, ) as client: response = client.get('https://example.com/') assert response.content == b'hello' request = response.request auth_header = request.headers.get('authorization') assert auth_header is None url = str(request.url) assert 'oauth_token=foo' in url assert 'oauth_consumer_key=id' in url assert 'oauth_signature=' in url authlib-1.3.2/tests/clients/test_httpx/test_oauth2_client.py000066400000000000000000000266051466226534200243520ustar00rootroot00000000000000import time import pytest from unittest import mock from copy import deepcopy from authlib.common.security import generate_token from authlib.common.urls import url_encode from authlib.integrations.httpx_client import ( OAuthError, OAuth2Client, ) from ..wsgi_helper import MockDispatch default_token = { 'token_type': 'Bearer', 'access_token': 'a', 'refresh_token': 'b', 'expires_in': '3600', 'expires_at': int(time.time()) + 3600, } def assert_token_in_header(request): token = 'Bearer ' + default_token['access_token'] auth_header = request.headers.get('authorization') assert auth_header == token def assert_token_in_body(request): content = request.data content = content.decode() assert content == 'access_token=%s' % default_token['access_token'] def assert_token_in_uri(request): assert default_token['access_token'] in str(request.url) @pytest.mark.parametrize( "assert_func, token_placement", [ (assert_token_in_header, "header"), (assert_token_in_body, "body"), (assert_token_in_uri, "uri") ] ) def test_add_token_get_request(assert_func, token_placement): mock_response = MockDispatch({'a': 'a'}, assert_func=assert_func) with OAuth2Client( 'foo', token=default_token, token_placement=token_placement, app=mock_response ) as client: resp = client.get('https://i.b') data = resp.json() assert data['a'] == 'a' @pytest.mark.parametrize( "assert_func, token_placement", [ (assert_token_in_header, "header"), (assert_token_in_body, "body"), (assert_token_in_uri, "uri") ] ) def test_add_token_to_streaming_request(assert_func, token_placement): mock_response = MockDispatch({'a': 'a'}, assert_func=assert_func) with OAuth2Client( 'foo', token=default_token, token_placement=token_placement, app=mock_response ) as client: with client.stream("GET", 'https://i.b') as stream: stream.read() data = stream.json() assert data['a'] == 'a' def test_create_authorization_url(): url = 'https://example.com/authorize?foo=bar' sess = OAuth2Client(client_id='foo') auth_url, state = sess.create_authorization_url(url) assert state in auth_url assert 'client_id=foo' in auth_url assert 'response_type=code' in auth_url sess = OAuth2Client(client_id='foo', prompt='none') auth_url, state = sess.create_authorization_url( url, state='foo', redirect_uri='https://i.b', scope='profile') assert state == 'foo' assert 'i.b' in auth_url assert 'profile' in auth_url assert 'prompt=none' in auth_url def test_code_challenge(): sess = OAuth2Client('foo', code_challenge_method='S256') url = 'https://example.com/authorize' auth_url, _ = sess.create_authorization_url( url, code_verifier=generate_token(48)) assert 'code_challenge=' in auth_url assert 'code_challenge_method=S256' in auth_url def test_token_from_fragment(): sess = OAuth2Client('foo') response_url = 'https://i.b/callback#' + url_encode(default_token.items()) assert sess.token_from_fragment(response_url) == default_token token = sess.fetch_token(authorization_response=response_url) assert token == default_token def test_fetch_token_post(): url = 'https://example.com/token' def assert_func(request): content = request.form assert content.get('code') == 'v' assert content.get('client_id') == 'foo' assert content.get('grant_type') == 'authorization_code' mock_response = MockDispatch(default_token, assert_func=assert_func) with OAuth2Client('foo', app=mock_response) as client: token = client.fetch_token(url, authorization_response='https://i.b/?code=v') assert token == default_token with OAuth2Client( 'foo', token_endpoint_auth_method='none', app=mock_response ) as client: token = client.fetch_token(url, code='v') assert token == default_token mock_response = MockDispatch({'error': 'invalid_request'}) with OAuth2Client('foo', app=mock_response) as client: with pytest.raises(OAuthError): client.fetch_token(url) def test_fetch_token_get(): url = 'https://example.com/token' def assert_func(request): url = str(request.url) assert 'code=v' in url assert 'client_id=' in url assert 'grant_type=authorization_code' in url mock_response = MockDispatch(default_token, assert_func=assert_func) with OAuth2Client('foo', app=mock_response) as client: authorization_response = 'https://i.b/?code=v' token = client.fetch_token( url, authorization_response=authorization_response, method='GET') assert token == default_token with OAuth2Client( 'foo', token_endpoint_auth_method='none', app=mock_response ) as client: token = client.fetch_token(url, code='v', method='GET') assert token == default_token token = client.fetch_token(url + '?q=a', code='v', method='GET') assert token == default_token def test_token_auth_method_client_secret_post(): url = 'https://example.com/token' def assert_func(request): content = request.form assert content.get('code') == 'v' assert content.get('client_id') == 'foo' assert content.get('client_secret') == 'bar' assert content.get('grant_type') == 'authorization_code' mock_response = MockDispatch(default_token, assert_func=assert_func) with OAuth2Client( 'foo', 'bar', token_endpoint_auth_method='client_secret_post', app=mock_response ) as client: token = client.fetch_token(url, code='v') assert token == default_token def test_access_token_response_hook(): url = 'https://example.com/token' def _access_token_response_hook(resp): assert resp.json() == default_token return resp access_token_response_hook = mock.Mock(side_effect=_access_token_response_hook) app = MockDispatch(default_token) with OAuth2Client('foo', token=default_token, app=app) as sess: sess.register_compliance_hook( 'access_token_response', access_token_response_hook ) assert sess.fetch_token(url) == default_token assert access_token_response_hook.called is True def test_password_grant_type(): url = 'https://example.com/token' def assert_func(request): content = request.form assert content.get('username') == 'v' assert content.get('scope') == 'profile' assert content.get('grant_type') == 'password' app = MockDispatch(default_token, assert_func=assert_func) with OAuth2Client('foo', scope='profile', app=app) as sess: token = sess.fetch_token(url, username='v', password='v') assert token == default_token token = sess.fetch_token( url, username='v', password='v', grant_type='password') assert token == default_token def test_client_credentials_type(): url = 'https://example.com/token' def assert_func(request): content = request.form assert content.get('scope') == 'profile' assert content.get('grant_type') == 'client_credentials' app = MockDispatch(default_token, assert_func=assert_func) with OAuth2Client('foo', scope='profile', app=app) as sess: token = sess.fetch_token(url) assert token == default_token token = sess.fetch_token(url, grant_type='client_credentials') assert token == default_token def test_cleans_previous_token_before_fetching_new_one(): now = int(time.time()) new_token = deepcopy(default_token) past = now - 7200 default_token['expires_at'] = past new_token['expires_at'] = now + 3600 url = 'https://example.com/token' app = MockDispatch(new_token) with mock.patch('time.time', lambda: now): with OAuth2Client('foo', token=default_token, app=app) as sess: assert sess.fetch_token(url) == new_token def test_token_status(): token = dict(access_token='a', token_type='bearer', expires_at=100) sess = OAuth2Client('foo', token=token) assert sess.token.is_expired() is True def test_auto_refresh_token(): def _update_token(token, refresh_token=None, access_token=None): assert refresh_token == 'b' assert token == default_token update_token = mock.Mock(side_effect=_update_token) old_token = dict( access_token='a', refresh_token='b', token_type='bearer', expires_at=100 ) app = MockDispatch(default_token) with OAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, app=app ) as sess: sess.get('https://i.b/user') assert update_token.called is True old_token = dict( access_token='a', token_type='bearer', expires_at=100 ) with OAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, app=app ) as sess: with pytest.raises(OAuthError): sess.get('https://i.b/user') def test_auto_refresh_token2(): def _update_token(token, refresh_token=None, access_token=None): assert access_token == 'a' assert token == default_token update_token = mock.Mock(side_effect=_update_token) old_token = dict( access_token='a', token_type='bearer', expires_at=100 ) app = MockDispatch(default_token) with OAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', grant_type='client_credentials', app=app, ) as client: client.get('https://i.b/user') assert update_token.called is False with OAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, grant_type='client_credentials', app=app, ) as client: client.get('https://i.b/user') assert update_token.called is True def test_auto_refresh_token3(): def _update_token(token, refresh_token=None, access_token=None): assert access_token == 'a' assert token == default_token update_token = mock.Mock(side_effect=_update_token) old_token = dict( access_token='a', token_type='bearer', expires_at=100 ) app = MockDispatch(default_token) with OAuth2Client( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, grant_type='client_credentials', app=app, ) as client: client.post('https://i.b/user', json={'foo': 'bar'}) assert update_token.called is True def test_revoke_token(): answer = {'status': 'ok'} app = MockDispatch(answer) with OAuth2Client('a', app=app) as sess: resp = sess.revoke_token('https://i.b/token', 'hi') assert resp.json() == answer resp = sess.revoke_token( 'https://i.b/token', 'hi', token_type_hint='access_token' ) assert resp.json() == answer def test_request_without_token(): with OAuth2Client('a', app=MockDispatch()) as client: with pytest.raises(OAuthError): client.get('https://i.b/token') authlib-1.3.2/tests/clients/test_requests/000077500000000000000000000000001466226534200206745ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_requests/__init__.py000066400000000000000000000000001466226534200227730ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_requests/test_assertion_session.py000066400000000000000000000035031466226534200260600ustar00rootroot00000000000000import time from unittest import TestCase, mock from authlib.integrations.requests_client import AssertionSession class AssertionSessionTest(TestCase): def setUp(self): self.token = { 'token_type': 'Bearer', 'access_token': 'a', 'refresh_token': 'b', 'expires_in': '3600', 'expires_at': int(time.time()) + 3600, } def test_refresh_token(self): def verifier(r, **kwargs): resp = mock.MagicMock() resp.status_code = 200 if r.url == 'https://i.b/token': self.assertIn('assertion=', r.body) resp.json = lambda: self.token return resp sess = AssertionSession( 'https://i.b/token', issuer='foo', subject='foo', audience='foo', alg='HS256', key='secret', ) sess.send = verifier sess.get('https://i.b') # trigger more case now = int(time.time()) sess = AssertionSession( 'https://i.b/token', issuer='foo', subject=None, audience='foo', issued_at=now, expires_at=now + 3600, header={'alg': 'HS256'}, key='secret', scope='email', claims={'test_mode': 'true'} ) sess.send = verifier sess.get('https://i.b') # trigger for branch test case sess.get('https://i.b') def test_without_alg(self): sess = AssertionSession( 'https://i.b/token', grant_type=AssertionSession.JWT_BEARER_GRANT_TYPE, issuer='foo', subject='foo', audience='foo', key='secret', ) self.assertRaises(ValueError, sess.get, 'https://i.b') authlib-1.3.2/tests/clients/test_requests/test_oauth1_session.py000066400000000000000000000262001466226534200252510ustar00rootroot00000000000000import requests from unittest import TestCase, mock from io import StringIO from authlib.oauth1 import ( SIGNATURE_PLAINTEXT, SIGNATURE_RSA_SHA1, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, ) from authlib.oauth1.rfc5849.util import escape from authlib.common.encoding import to_unicode from authlib.integrations.requests_client import OAuth1Session, OAuthError from ..util import mock_text_response, read_key_file TEST_RSA_OAUTH_SIGNATURE = ( "j8WF8PGjojT82aUDd2EL%2Bz7HCoHInFzWUpiEKMCy%2BJ2cYHWcBS7mXlmFDLgAKV0" "P%2FyX4TrpXODYnJ6dRWdfghqwDpi%2FlQmB2jxCiGMdJoYxh3c5zDf26gEbGdP6D7O" "Ssp5HUnzH6sNkmVjuE%2FxoJcHJdc23H6GhOs7VJ2LWNdbhKWP%2FMMlTrcoQDn8lz" "%2Fb24WsJ6ae1txkUzpFOOlLM8aTdNtGL4OtsubOlRhNqnAFq93FyhXg0KjzUyIZzmMX" "9Vx90jTks5QeBGYcLE0Op2iHb2u%2FO%2BEgdwFchgEwE5LgMUyHUI4F3Wglp28yHOAM" "jPkI%2FkWMvpxtMrU3Z3KN31WQ%3D%3D" ) class OAuth1SessionTest(TestCase): def test_no_client_id(self): self.assertRaises(ValueError, lambda: OAuth1Session(None)) def test_signature_types(self): def verify_signature(getter): def fake_send(r, **kwargs): signature = to_unicode(getter(r)) self.assertIn('oauth_signature', signature) resp = mock.MagicMock(spec=requests.Response) resp.cookies = [] return resp return fake_send header = OAuth1Session('foo') header.send = verify_signature(lambda r: r.headers['Authorization']) header.post('https://i.b') query = OAuth1Session('foo', signature_type=SIGNATURE_TYPE_QUERY) query.send = verify_signature(lambda r: r.url) query.post('https://i.b') body = OAuth1Session('foo', signature_type=SIGNATURE_TYPE_BODY) headers = {'Content-Type': 'application/x-www-form-urlencoded'} body.send = verify_signature(lambda r: r.body) body.post('https://i.b', headers=headers, data='') @mock.patch('authlib.oauth1.rfc5849.client_auth.generate_timestamp') @mock.patch('authlib.oauth1.rfc5849.client_auth.generate_nonce') def test_signature_methods(self, generate_nonce, generate_timestamp): generate_nonce.return_value = 'abc' generate_timestamp.return_value = '123' signature = ', '.join([ 'OAuth oauth_nonce="abc"', 'oauth_timestamp="123"', 'oauth_version="1.0"', 'oauth_signature_method="HMAC-SHA1"', 'oauth_consumer_key="foo"', 'oauth_signature="h2sRqLArjhlc5p3FTkuNogVHlKE%3D"' ]) auth = OAuth1Session('foo') auth.send = self.verify_signature(signature) auth.post('https://i.b') signature = ( 'OAuth ' 'oauth_nonce="abc", oauth_timestamp="123", oauth_version="1.0", ' 'oauth_signature_method="PLAINTEXT", oauth_consumer_key="foo", ' 'oauth_signature="%26"' ) auth = OAuth1Session('foo', signature_method=SIGNATURE_PLAINTEXT) auth.send = self.verify_signature(signature) auth.post('https://i.b') signature = ( 'OAuth ' 'oauth_nonce="abc", oauth_timestamp="123", oauth_version="1.0", ' 'oauth_signature_method="RSA-SHA1", oauth_consumer_key="foo", ' 'oauth_signature="{sig}"' ).format(sig=TEST_RSA_OAUTH_SIGNATURE) rsa_key = read_key_file('rsa_private.pem') auth = OAuth1Session( 'foo', signature_method=SIGNATURE_RSA_SHA1, rsa_key=rsa_key) auth.send = self.verify_signature(signature) auth.post('https://i.b') @mock.patch('authlib.oauth1.rfc5849.client_auth.generate_timestamp') @mock.patch('authlib.oauth1.rfc5849.client_auth.generate_nonce') def test_binary_upload(self, generate_nonce, generate_timestamp): generate_nonce.return_value = 'abc' generate_timestamp.return_value = '123' fake_xml = StringIO('hello world') headers = {'Content-Type': 'application/xml'} def fake_send(r, **kwargs): auth_header = r.headers['Authorization'] self.assertIn('oauth_body_hash', auth_header) auth = OAuth1Session('foo', force_include_body=True) auth.send = fake_send auth.post('https://i.b', headers=headers, files=[('fake', fake_xml)]) @mock.patch('authlib.oauth1.rfc5849.client_auth.generate_timestamp') @mock.patch('authlib.oauth1.rfc5849.client_auth.generate_nonce') def test_nonascii(self, generate_nonce, generate_timestamp): generate_nonce.return_value = 'abc' generate_timestamp.return_value = '123' signature = ( 'OAuth oauth_nonce="abc", oauth_timestamp="123", oauth_version="1.0", ' 'oauth_signature_method="HMAC-SHA1", oauth_consumer_key="foo", ' 'oauth_signature="W0haoue5IZAZoaJiYCtfqwMf8x8%3D"' ) auth = OAuth1Session('foo') auth.send = self.verify_signature(signature) auth.post('https://i.b?cjk=%E5%95%A6%E5%95%A6') def test_redirect_uri(self): sess = OAuth1Session('foo') self.assertIsNone(sess.redirect_uri) url = 'https://i.b' sess.redirect_uri = url self.assertEqual(sess.redirect_uri, url) def test_set_token(self): sess = OAuth1Session('foo') try: sess.token = {} except OAuthError as exc: self.assertEqual(exc.error, 'missing_token') sess.token = {'oauth_token': 'a', 'oauth_token_secret': 'b'} self.assertIsNone(sess.token['oauth_verifier']) sess.token = {'oauth_token': 'a', 'oauth_verifier': 'c'} self.assertEqual(sess.token['oauth_token_secret'], 'b') self.assertEqual(sess.token['oauth_verifier'], 'c') sess.token = None self.assertIsNone(sess.token['oauth_token']) self.assertIsNone(sess.token['oauth_token_secret']) self.assertIsNone(sess.token['oauth_verifier']) def test_create_authorization_url(self): auth = OAuth1Session('foo') url = 'https://example.comm/authorize' token = 'asluif023sf' auth_url = auth.create_authorization_url(url, request_token=token) self.assertEqual(auth_url, url + '?oauth_token=' + token) redirect_uri = 'https://c.b' auth = OAuth1Session('foo', redirect_uri=redirect_uri) auth_url = auth.create_authorization_url(url, request_token=token) self.assertIn(escape(redirect_uri), auth_url) def test_parse_response_url(self): url = 'https://i.b/callback?oauth_token=foo&oauth_verifier=bar' auth = OAuth1Session('foo') resp = auth.parse_authorization_response(url) self.assertEqual(resp['oauth_token'], 'foo') self.assertEqual(resp['oauth_verifier'], 'bar') for k, v in resp.items(): self.assertTrue(isinstance(k, str)) self.assertTrue(isinstance(v, str)) def test_fetch_request_token(self): auth = OAuth1Session('foo', realm='A') auth.send = mock_text_response('oauth_token=foo') resp = auth.fetch_request_token('https://example.com/token') self.assertEqual(resp['oauth_token'], 'foo') for k, v in resp.items(): self.assertTrue(isinstance(k, str)) self.assertTrue(isinstance(v, str)) resp = auth.fetch_request_token('https://example.com/token') self.assertEqual(resp['oauth_token'], 'foo') def test_fetch_request_token_with_optional_arguments(self): auth = OAuth1Session('foo') auth.send = mock_text_response('oauth_token=foo') resp = auth.fetch_request_token('https://example.com/token', verify=False, stream=True) self.assertEqual(resp['oauth_token'], 'foo') for k, v in resp.items(): self.assertTrue(isinstance(k, str)) self.assertTrue(isinstance(v, str)) def test_fetch_access_token(self): auth = OAuth1Session('foo', verifier='bar') auth.send = mock_text_response('oauth_token=foo') resp = auth.fetch_access_token('https://example.com/token') self.assertEqual(resp['oauth_token'], 'foo') for k, v in resp.items(): self.assertTrue(isinstance(k, str)) self.assertTrue(isinstance(v, str)) auth = OAuth1Session('foo', verifier='bar') auth.send = mock_text_response('{"oauth_token":"foo"}') resp = auth.fetch_access_token('https://example.com/token') self.assertEqual(resp['oauth_token'], 'foo') auth = OAuth1Session('foo') auth.send = mock_text_response('oauth_token=foo') resp = auth.fetch_access_token( 'https://example.com/token', verifier='bar') self.assertEqual(resp['oauth_token'], 'foo') def test_fetch_access_token_with_optional_arguments(self): auth = OAuth1Session('foo', verifier='bar') auth.send = mock_text_response('oauth_token=foo') resp = auth.fetch_access_token('https://example.com/token', verify=False, stream=True) self.assertEqual(resp['oauth_token'], 'foo') for k, v in resp.items(): self.assertTrue(isinstance(k, str)) self.assertTrue(isinstance(v, str)) def _test_fetch_access_token_raises_error(self, session): """Assert that an error is being raised whenever there's no verifier passed in to the client. """ session.send = mock_text_response('oauth_token=foo') # Use a try-except block so that we can assert on the exception message # being raised and also keep the Python2.6 compatibility where # assertRaises is not a context manager. try: session.fetch_access_token('https://example.com/token') except OAuthError as exc: self.assertEqual(exc.error, 'missing_verifier') def test_fetch_token_invalid_response(self): auth = OAuth1Session('foo') auth.send = mock_text_response('not valid urlencoded response!') self.assertRaises( ValueError, auth.fetch_request_token, 'https://example.com/token') for code in (400, 401, 403): auth.send = mock_text_response('valid=response', code) # use try/catch rather than self.assertRaises, so we can # assert on the properties of the exception try: auth.fetch_request_token('https://example.com/token') except OAuthError as err: self.assertEqual(err.error, 'fetch_token_denied') else: # no exception raised self.fail("ValueError not raised") def test_fetch_access_token_missing_verifier(self): self._test_fetch_access_token_raises_error(OAuth1Session('foo')) def test_fetch_access_token_has_verifier_is_none(self): session = OAuth1Session('foo') session.auth.verifier = None self._test_fetch_access_token_raises_error(session) def verify_signature(self, signature): def fake_send(r, **kwargs): auth_header = to_unicode(r.headers['Authorization']) self.assertEqual(auth_header, signature) resp = mock.MagicMock(spec=requests.Response) resp.cookies = [] return resp return fake_send authlib-1.3.2/tests/clients/test_requests/test_oauth2_session.py000066400000000000000000000447611466226534200252660ustar00rootroot00000000000000import time from copy import deepcopy from unittest import TestCase, mock from authlib.common.security import generate_token from authlib.common.urls import url_encode, add_params_to_uri from authlib.integrations.requests_client import OAuth2Session, OAuthError from authlib.oauth2.rfc6749 import MismatchingStateException from authlib.oauth2.rfc7523 import ClientSecretJWT, PrivateKeyJWT from ..util import read_key_file def mock_json_response(payload): def fake_send(r, **kwargs): resp = mock.MagicMock() resp.status_code = 200 resp.json = lambda: payload return resp return fake_send def mock_assertion_response(ctx, session): def fake_send(r, **kwargs): ctx.assertIn('client_assertion=', r.body) ctx.assertIn('client_assertion_type=', r.body) resp = mock.MagicMock() resp.status_code = 200 resp.json = lambda: ctx.token return resp session.send = fake_send class OAuth2SessionTest(TestCase): def setUp(self): self.token = { 'token_type': 'Bearer', 'access_token': 'a', 'refresh_token': 'b', 'expires_in': '3600', 'expires_at': int(time.time()) + 3600, } self.client_id = 'foo' def test_invalid_token_type(self): token = { 'token_type': 'invalid', 'access_token': 'a', 'refresh_token': 'b', 'expires_in': '3600', 'expires_at': int(time.time()) + 3600, } with OAuth2Session(self.client_id, token=token) as sess: self.assertRaises(OAuthError, sess.get, 'https://i.b') def test_add_token_to_header(self): token = 'Bearer ' + self.token['access_token'] def verifier(r, **kwargs): auth_header = r.headers.get('Authorization', None) self.assertEqual(auth_header, token) resp = mock.MagicMock() return resp sess = OAuth2Session(client_id=self.client_id, token=self.token) sess.send = verifier sess.get('https://i.b') def test_add_token_to_body(self): def verifier(r, **kwargs): self.assertIn(self.token['access_token'], r.body) resp = mock.MagicMock() return resp sess = OAuth2Session( client_id=self.client_id, token=self.token, token_placement='body' ) sess.send = verifier sess.post('https://i.b') def test_add_token_to_uri(self): def verifier(r, **kwargs): self.assertIn(self.token['access_token'], r.url) resp = mock.MagicMock() return resp sess = OAuth2Session( client_id=self.client_id, token=self.token, token_placement='uri' ) sess.send = verifier sess.get('https://i.b') def test_create_authorization_url(self): url = 'https://example.com/authorize?foo=bar' sess = OAuth2Session(client_id=self.client_id) auth_url, state = sess.create_authorization_url(url) self.assertIn(state, auth_url) self.assertIn(self.client_id, auth_url) self.assertIn('response_type=code', auth_url) sess = OAuth2Session(client_id=self.client_id, prompt='none') auth_url, state = sess.create_authorization_url( url, state='foo', redirect_uri='https://i.b', scope='profile') self.assertEqual(state, 'foo') self.assertIn('i.b', auth_url) self.assertIn('profile', auth_url) self.assertIn('prompt=none', auth_url) def test_code_challenge(self): sess = OAuth2Session(client_id=self.client_id, code_challenge_method='S256') url = 'https://example.com/authorize' auth_url, _ = sess.create_authorization_url( url, code_verifier=generate_token(48)) self.assertIn('code_challenge', auth_url) self.assertIn('code_challenge_method=S256', auth_url) def test_token_from_fragment(self): sess = OAuth2Session(self.client_id) response_url = 'https://i.b/callback#' + url_encode(self.token.items()) self.assertEqual(sess.token_from_fragment(response_url), self.token) token = sess.fetch_token(authorization_response=response_url) self.assertEqual(token, self.token) def test_fetch_token_post(self): url = 'https://example.com/token' def fake_send(r, **kwargs): self.assertIn('code=v', r.body) self.assertIn('client_id=', r.body) self.assertIn('grant_type=authorization_code', r.body) resp = mock.MagicMock() resp.status_code = 200 resp.json = lambda: self.token return resp sess = OAuth2Session(client_id=self.client_id) sess.send = fake_send self.assertEqual( sess.fetch_token( url, authorization_response='https://i.b/?code=v'), self.token) sess = OAuth2Session( client_id=self.client_id, token_endpoint_auth_method='none', ) sess.send = fake_send token = sess.fetch_token(url, code='v') self.assertEqual(token, self.token) error = {'error': 'invalid_request'} sess = OAuth2Session(client_id=self.client_id, token=self.token) sess.send = mock_json_response(error) self.assertRaises(OAuthError, sess.fetch_access_token, url) def test_fetch_token_get(self): url = 'https://example.com/token' def fake_send(r, **kwargs): self.assertIn('code=v', r.url) self.assertIn('grant_type=authorization_code', r.url) resp = mock.MagicMock() resp.status_code = 200 resp.json = lambda: self.token return resp sess = OAuth2Session(client_id=self.client_id) sess.send = fake_send token = sess.fetch_token( url, authorization_response='https://i.b/?code=v', method='GET') self.assertEqual(token, self.token) sess = OAuth2Session( client_id=self.client_id, token_endpoint_auth_method='none', ) sess.send = fake_send token = sess.fetch_token(url, code='v', method='GET') self.assertEqual(token, self.token) token = sess.fetch_token(url + '?q=a', code='v', method='GET') self.assertEqual(token, self.token) def test_token_auth_method_client_secret_post(self): url = 'https://example.com/token' def fake_send(r, **kwargs): self.assertIn('code=v', r.body) self.assertIn('client_id=', r.body) self.assertIn('client_secret=bar', r.body) self.assertIn('grant_type=authorization_code', r.body) resp = mock.MagicMock() resp.status_code = 200 resp.json = lambda: self.token return resp sess = OAuth2Session( client_id=self.client_id, client_secret='bar', token_endpoint_auth_method='client_secret_post', ) sess.send = fake_send token = sess.fetch_token(url, code='v') self.assertEqual(token, self.token) def test_access_token_response_hook(self): url = 'https://example.com/token' def access_token_response_hook(resp): self.assertEqual(resp.json(), self.token) return resp sess = OAuth2Session(client_id=self.client_id, token=self.token) sess.register_compliance_hook( 'access_token_response', access_token_response_hook ) sess.send = mock_json_response(self.token) self.assertEqual(sess.fetch_token(url), self.token) def test_password_grant_type(self): url = 'https://example.com/token' def fake_send(r, **kwargs): self.assertIn('username=v', r.body) self.assertIn('grant_type=password', r.body) self.assertIn('scope=profile', r.body) resp = mock.MagicMock() resp.status_code = 200 resp.json = lambda: self.token return resp sess = OAuth2Session(client_id=self.client_id, scope='profile') sess.send = fake_send token = sess.fetch_token(url, username='v', password='v') self.assertEqual(token, self.token) def test_client_credentials_type(self): url = 'https://example.com/token' def fake_send(r, **kwargs): self.assertIn('grant_type=client_credentials', r.body) self.assertIn('scope=profile', r.body) resp = mock.MagicMock() resp.status_code = 200 resp.json = lambda: self.token return resp sess = OAuth2Session( client_id=self.client_id, client_secret='v', scope='profile', ) sess.send = fake_send token = sess.fetch_token(url) self.assertEqual(token, self.token) def test_cleans_previous_token_before_fetching_new_one(self): """Makes sure the previous token is cleaned before fetching a new one. The reason behind it is that, if the previous token is expired, this method shouldn't fail with a TokenExpiredError, since it's attempting to get a new one (which shouldn't be expired). """ now = int(time.time()) new_token = deepcopy(self.token) past = now - 7200 self.token['expires_at'] = past new_token['expires_at'] = now + 3600 url = 'https://example.com/token' with mock.patch('time.time', lambda: now): sess = OAuth2Session(client_id=self.client_id, token=self.token) sess.send = mock_json_response(new_token) self.assertEqual(sess.fetch_token(url), new_token) def test_mis_match_state(self): sess = OAuth2Session('foo') self.assertRaises( MismatchingStateException, sess.fetch_token, 'https://i.b/token', authorization_response='https://i.b/no-state?code=abc', state='somestate', ) def test_token_status(self): token = dict(access_token='a', token_type='bearer', expires_at=100) sess = OAuth2Session('foo', token=token) self.assertTrue(sess.token.is_expired) def test_token_status2(self): token = dict(access_token='a', token_type='bearer', expires_in=10) sess = OAuth2Session('foo', token=token, leeway=15) self.assertTrue(sess.token.is_expired(sess.leeway)) def test_token_status3(self): token = dict(access_token='a', token_type='bearer', expires_in=10) sess = OAuth2Session('foo', token=token, leeway=5) self.assertFalse(sess.token.is_expired(sess.leeway)) def test_token_expired(self): token = dict(access_token='a', token_type='bearer', expires_at=100) sess = OAuth2Session('foo', token=token) self.assertRaises( OAuthError, sess.get, 'https://i.b/token', ) def test_missing_token(self): sess = OAuth2Session('foo') self.assertRaises( OAuthError, sess.get, 'https://i.b/token', ) def test_register_compliance_hook(self): sess = OAuth2Session('foo') self.assertRaises( ValueError, sess.register_compliance_hook, 'invalid_hook', lambda o: o, ) def protected_request(url, headers, data): self.assertIn('Authorization', headers) return url, headers, data sess = OAuth2Session('foo', token=self.token) sess.register_compliance_hook( 'protected_request', protected_request, ) sess.send = mock_json_response({'name': 'a'}) sess.get('https://i.b/user') def test_auto_refresh_token(self): def _update_token(token, refresh_token=None, access_token=None): self.assertEqual(refresh_token, 'b') self.assertEqual(token, self.token) update_token = mock.Mock(side_effect=_update_token) old_token = dict( access_token='a', refresh_token='b', token_type='bearer', expires_at=100 ) sess = OAuth2Session( 'foo', token=old_token, token_endpoint='https://i.b/token', update_token=update_token, ) sess.send = mock_json_response(self.token) sess.get('https://i.b/user') self.assertTrue(update_token.called) def test_auto_refresh_token2(self): def _update_token(token, refresh_token=None, access_token=None): self.assertEqual(access_token, 'a') self.assertEqual(token, self.token) update_token = mock.Mock(side_effect=_update_token) old_token = dict( access_token='a', token_type='bearer', expires_at=100 ) sess = OAuth2Session( 'foo', token=old_token, token_endpoint='https://i.b/token', grant_type='client_credentials', ) sess.send = mock_json_response(self.token) sess.get('https://i.b/user') self.assertFalse(update_token.called) sess = OAuth2Session( 'foo', token=old_token, token_endpoint='https://i.b/token', grant_type='client_credentials', update_token=update_token, ) sess.send = mock_json_response(self.token) sess.get('https://i.b/user') self.assertTrue(update_token.called) def test_revoke_token(self): sess = OAuth2Session('a') answer = {'status': 'ok'} sess.send = mock_json_response(answer) resp = sess.revoke_token('https://i.b/token', 'hi') self.assertEqual(resp.json(), answer) resp = sess.revoke_token( 'https://i.b/token', 'hi', token_type_hint='access_token' ) self.assertEqual(resp.json(), answer) def revoke_token_request(url, headers, data): self.assertEqual(url, 'https://i.b/token') return url, headers, data sess.register_compliance_hook( 'revoke_token_request', revoke_token_request, ) sess.revoke_token( 'https://i.b/token', 'hi', body='', token_type_hint='access_token' ) def test_introspect_token(self): sess = OAuth2Session('a') answer = { "active": True, "client_id": "l238j323ds-23ij4", "username": "jdoe", "scope": "read write dolphin", "sub": "Z5O3upPC88QrAjx00dis", "aud": "https://protected.example.net/resource", "iss": "https://server.example.com/", "exp": 1419356238, "iat": 1419350238 } sess.send = mock_json_response(answer) resp = sess.introspect_token('https://i.b/token', 'hi') self.assertEqual(resp.json(), answer) def test_client_secret_jwt(self): sess = OAuth2Session( 'id', 'secret', token_endpoint_auth_method='client_secret_jwt' ) sess.register_client_auth_method(ClientSecretJWT()) mock_assertion_response(self, sess) token = sess.fetch_token('https://i.b/token') self.assertEqual(token, self.token) def test_client_secret_jwt2(self): sess = OAuth2Session( 'id', 'secret', token_endpoint_auth_method=ClientSecretJWT(), ) mock_assertion_response(self, sess) token = sess.fetch_token('https://i.b/token') self.assertEqual(token, self.token) def test_private_key_jwt(self): client_secret = read_key_file('rsa_private.pem') sess = OAuth2Session( 'id', client_secret, token_endpoint_auth_method='private_key_jwt' ) sess.register_client_auth_method(PrivateKeyJWT()) mock_assertion_response(self, sess) token = sess.fetch_token('https://i.b/token') self.assertEqual(token, self.token) def test_custom_client_auth_method(self): def auth_client(client, method, uri, headers, body): uri = add_params_to_uri(uri, [ ('client_id', client.client_id), ('client_secret', client.client_secret), ]) uri = uri + '&' + body body = '' return uri, headers, body sess = OAuth2Session( 'id', 'secret', token_endpoint_auth_method='client_secret_uri' ) sess.register_client_auth_method(('client_secret_uri', auth_client)) def fake_send(r, **kwargs): self.assertIn('client_id=', r.url) self.assertIn('client_secret=', r.url) resp = mock.MagicMock() resp.status_code = 200 resp.json = lambda: self.token return resp sess.send = fake_send token = sess.fetch_token('https://i.b/token') self.assertEqual(token, self.token) def test_use_client_token_auth(self): import requests token = 'Bearer ' + self.token['access_token'] def verifier(r, **kwargs): auth_header = r.headers.get('Authorization', None) self.assertEqual(auth_header, token) resp = mock.MagicMock() return resp client = OAuth2Session( client_id=self.client_id, token=self.token ) sess = requests.Session() sess.send = verifier sess.get('https://i.b', auth=client.token_auth) def test_use_default_request_timeout(self): expected_timeout = 15 def verifier(r, **kwargs): timeout = kwargs.get('timeout') self.assertEqual(timeout, expected_timeout) resp = mock.MagicMock() return resp client = OAuth2Session( client_id=self.client_id, token=self.token, default_timeout=expected_timeout, ) client.send = verifier client.request('GET', 'https://i.b', withhold_token=False) def test_override_default_request_timeout(self): default_timeout = 15 expected_timeout = 10 def verifier(r, **kwargs): timeout = kwargs.get('timeout') self.assertEqual(timeout, expected_timeout) resp = mock.MagicMock() return resp client = OAuth2Session( client_id=self.client_id, token=self.token, default_timeout=default_timeout, ) client.send = verifier client.request('GET', 'https://i.b', withhold_token=False, timeout=expected_timeout) authlib-1.3.2/tests/clients/test_starlette/000077500000000000000000000000001466226534200210305ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_starlette/__init__.py000066400000000000000000000000001466226534200231270ustar00rootroot00000000000000authlib-1.3.2/tests/clients/test_starlette/test_oauth_client.py000066400000000000000000000207411466226534200251230ustar00rootroot00000000000000import pytest from starlette.config import Config from starlette.requests import Request from authlib.common.urls import urlparse, url_decode from authlib.integrations.starlette_client import OAuth, OAuthError from ..asgi_helper import AsyncPathMapDispatch from ..util import get_bearer_token def test_register_remote_app(): oauth = OAuth() with pytest.raises(AttributeError): assert oauth.dev.name == 'dev' oauth.register( 'dev', client_id='dev', client_secret='dev', ) assert oauth.dev.name == 'dev' assert oauth.dev.client_id == 'dev' def test_register_with_config(): config = Config(environ={'DEV_CLIENT_ID': 'dev'}) oauth = OAuth(config) oauth.register('dev') assert oauth.dev.name == 'dev' assert oauth.dev.client_id == 'dev' def test_register_with_overwrite(): config = Config(environ={'DEV_CLIENT_ID': 'dev'}) oauth = OAuth(config) oauth.register('dev', client_id='not-dev', overwrite=True) assert oauth.dev.name == 'dev' assert oauth.dev.client_id == 'dev' @pytest.mark.asyncio async def test_oauth1_authorize(): oauth = OAuth() app = AsyncPathMapDispatch({ '/request-token': {'body': 'oauth_token=foo&oauth_verifier=baz'}, '/token': {'body': 'oauth_token=a&oauth_token_secret=b'}, }) client = oauth.register( 'dev', client_id='dev', client_secret='dev', request_token_url='https://i.b/request-token', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={ 'app': app, } ) req_scope = {'type': 'http', 'session': {}} req = Request(req_scope) resp = await client.authorize_redirect(req, 'https://b.com/bar') assert resp.status_code == 302 url = resp.headers.get('Location') assert 'oauth_token=foo' in url assert '_state_dev_foo' in req.session req.scope['query_string'] = 'oauth_token=foo&oauth_verifier=baz' token = await client.authorize_access_token(req) assert token['oauth_token'] == 'a' @pytest.mark.asyncio async def test_oauth2_authorize(): oauth = OAuth() app = AsyncPathMapDispatch({ '/token': {'body': get_bearer_token()} }) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={ 'app': app, } ) req_scope = {'type': 'http', 'session': {}} req = Request(req_scope) resp = await client.authorize_redirect(req, 'https://b.com/bar') assert resp.status_code == 302 url = resp.headers.get('Location') assert 'state=' in url state = dict(url_decode(urlparse.urlparse(url).query))['state'] assert f'_state_dev_{state}' in req.session req_scope.update( { 'path': '/', 'query_string': f'code=a&state={state}', 'session': req.session, } ) req = Request(req_scope) token = await client.authorize_access_token(req) assert token['access_token'] == 'a' @pytest.mark.asyncio async def test_oauth2_authorize_access_denied(): oauth = OAuth() app = AsyncPathMapDispatch({ '/token': {'body': get_bearer_token()} }) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={ 'app': app, } ) req = Request({ 'type': 'http', 'session': {}, 'path': '/', 'query_string': 'error=access_denied&error_description=Not+Allowed', }) with pytest.raises(OAuthError): await client.authorize_access_token(req) @pytest.mark.asyncio async def test_oauth2_authorize_code_challenge(): app = AsyncPathMapDispatch({ '/token': {'body': get_bearer_token()} }) oauth = OAuth() client = oauth.register( 'dev', client_id='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={ 'code_challenge_method': 'S256', 'app': app, }, ) req_scope = {'type': 'http', 'session': {}} req = Request(req_scope) resp = await client.authorize_redirect(req, redirect_uri='https://b.com/bar') assert resp.status_code == 302 url = resp.headers.get('Location') assert 'code_challenge=' in url assert 'code_challenge_method=S256' in url state = dict(url_decode(urlparse.urlparse(url).query))['state'] state_data = req.session[f'_state_dev_{state}']['data'] verifier = state_data['code_verifier'] assert verifier is not None req_scope.update( { 'path': '/', 'query_string': f'code=a&state={state}'.encode(), 'session': req.session, } ) req = Request(req_scope) token = await client.authorize_access_token(req) assert token['access_token'] == 'a' @pytest.mark.asyncio async def test_with_fetch_token_in_register(): async def fetch_token(request): return {'access_token': 'dev', 'token_type': 'bearer'} app = AsyncPathMapDispatch({ '/user': {'body': {'sub': '123'}} }) oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', fetch_token=fetch_token, client_kwargs={ 'app': app, } ) req_scope = {'type': 'http', 'session': {}} req = Request(req_scope) resp = await client.get('/user', request=req) assert resp.json()['sub'] == '123' @pytest.mark.asyncio async def test_with_fetch_token_in_oauth(): async def fetch_token(name, request): return {'access_token': 'dev', 'token_type': 'bearer'} app = AsyncPathMapDispatch({ '/user': {'body': {'sub': '123'}} }) oauth = OAuth(fetch_token=fetch_token) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', authorize_url='https://i.b/authorize', client_kwargs={ 'app': app, } ) req_scope = {'type': 'http', 'session': {}} req = Request(req_scope) resp = await client.get('/user', request=req) assert resp.json()['sub'] == '123' @pytest.mark.asyncio async def test_request_withhold_token(): oauth = OAuth() app = AsyncPathMapDispatch({ '/user': {'body': {'sub': '123'}} }) client = oauth.register( "dev", client_id="dev", client_secret="dev", api_base_url="https://i.b/api", access_token_url="https://i.b/token", authorize_url="https://i.b/authorize", client_kwargs={ 'app': app, } ) req_scope = {'type': 'http', 'session': {}} req = Request(req_scope) resp = await client.get('/user', request=req, withhold_token=True) assert resp.json()['sub'] == '123' @pytest.mark.asyncio async def test_oauth2_authorize_no_url(): oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', ) req_scope = {'type': 'http', 'session': {}} req = Request(req_scope) with pytest.raises(RuntimeError): await client.create_authorization_url(req) @pytest.mark.asyncio async def test_oauth2_authorize_with_metadata(): oauth = OAuth() app = AsyncPathMapDispatch({ '/.well-known/openid-configuration': {'body': { 'authorization_endpoint': 'https://i.b/authorize' }} }) client = oauth.register( 'dev', client_id='dev', client_secret='dev', api_base_url='https://i.b/api', access_token_url='https://i.b/token', server_metadata_url='https://i.b/.well-known/openid-configuration', client_kwargs={ 'app': app, } ) req_scope = {'type': 'http', 'session': {}} req = Request(req_scope) resp = await client.authorize_redirect(req, 'https://b.com/bar') assert resp.status_code == 302 authlib-1.3.2/tests/clients/test_starlette/test_user_mixin.py000066400000000000000000000072221466226534200246260ustar00rootroot00000000000000import pytest from starlette.requests import Request from authlib.integrations.starlette_client import OAuth from authlib.jose import JsonWebKey from authlib.jose.errors import InvalidClaimError from authlib.oidc.core.grants.util import generate_id_token from ..util import get_bearer_token, read_key_file from ..asgi_helper import AsyncPathMapDispatch secret_key = JsonWebKey.import_key('secret', {'kty': 'oct', 'kid': 'f'}) async def run_fetch_userinfo(payload): oauth = OAuth() async def fetch_token(request): return get_bearer_token() app = AsyncPathMapDispatch({ '/userinfo': {'body': payload} }) client = oauth.register( 'dev', client_id='dev', client_secret='dev', fetch_token=fetch_token, userinfo_endpoint='https://i.b/userinfo', client_kwargs={ 'app': app, } ) req_scope = {'type': 'http', 'session': {}} req = Request(req_scope) user = await client.userinfo(request=req) assert user.sub == '123' @pytest.mark.asyncio async def test_fetch_userinfo(): await run_fetch_userinfo({'sub': '123'}) @pytest.mark.asyncio async def test_parse_id_token(): token = get_bearer_token() id_token = generate_id_token( token, {'sub': '123'}, secret_key, alg='HS256', iss='https://i.b', aud='dev', exp=3600, nonce='n', ) token['id_token'] = id_token oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', fetch_token=get_bearer_token, jwks={'keys': [secret_key.as_dict()]}, issuer='https://i.b', id_token_signing_alg_values_supported=['HS256', 'RS256'], ) user = await client.parse_id_token(token, nonce='n') assert user.sub == '123' claims_options = {'iss': {'value': 'https://i.b'}} user = await client.parse_id_token(token, nonce='n', claims_options=claims_options) assert user.sub == '123' with pytest.raises(InvalidClaimError): claims_options = {'iss': {'value': 'https://i.c'}} await client.parse_id_token(token, nonce='n', claims_options=claims_options) @pytest.mark.asyncio async def test_runtime_error_fetch_jwks_uri(): token = get_bearer_token() id_token = generate_id_token( token, {'sub': '123'}, secret_key, alg='HS256', iss='https://i.b', aud='dev', exp=3600, nonce='n', ) oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', fetch_token=get_bearer_token, issuer='https://i.b', id_token_signing_alg_values_supported=['HS256'], ) req_scope = {'type': 'http', 'session': {'_dev_authlib_nonce_': 'n'}} req = Request(req_scope) token['id_token'] = id_token with pytest.raises(RuntimeError): await client.parse_id_token(req, token) @pytest.mark.asyncio async def test_force_fetch_jwks_uri(): secret_keys = read_key_file('jwks_private.json') token = get_bearer_token() id_token = generate_id_token( token, {'sub': '123'}, secret_keys, alg='RS256', iss='https://i.b', aud='dev', exp=3600, nonce='n', ) token['id_token'] = id_token app = AsyncPathMapDispatch({ '/jwks': {'body': read_key_file('jwks_public.json')} }) oauth = OAuth() client = oauth.register( 'dev', client_id='dev', client_secret='dev', fetch_token=get_bearer_token, jwks_uri='https://i.b/jwks', issuer='https://i.b', client_kwargs={ 'app': app, } ) user = await client.parse_id_token(token, nonce='n') assert user.sub == '123' authlib-1.3.2/tests/clients/util.py000066400000000000000000000020611466226534200173100ustar00rootroot00000000000000import os import time import json import requests from unittest import mock ROOT = os.path.abspath(os.path.dirname(__file__)) def read_key_file(name): file_path = os.path.join(ROOT, 'keys', name) with open(file_path) as f: if name.endswith('.json'): return json.load(f) return f.read() def mock_text_response(body, status_code=200): def fake_send(r, **kwargs): resp = mock.MagicMock(spec=requests.Response) resp.cookies = [] resp.text = body resp.status_code = status_code return resp return fake_send def mock_send_value(body, status_code=200): resp = mock.MagicMock(spec=requests.Response) resp.cookies = [] if isinstance(body, dict): resp.json = lambda: body else: resp.text = body resp.status_code = status_code return resp def get_bearer_token(): return { 'token_type': 'Bearer', 'access_token': 'a', 'refresh_token': 'b', 'expires_in': '3600', 'expires_at': int(time.time()) + 3600, } authlib-1.3.2/tests/clients/wsgi_helper.py000066400000000000000000000020731466226534200206460ustar00rootroot00000000000000import json from werkzeug.wrappers import Request as WSGIRequest from werkzeug.wrappers import Response as WSGIResponse class MockDispatch: def __init__(self, body=b'', status_code=200, headers=None, assert_func=None): if headers is None: headers = {} if isinstance(body, dict): body = json.dumps(body).encode() headers['Content-Type'] = 'application/json' else: if isinstance(body, str): body = body.encode() headers['Content-Type'] = 'application/x-www-form-urlencoded' self.body = body self.status_code = status_code self.headers = headers self.assert_func = assert_func def __call__(self, environ, start_response): request = WSGIRequest(environ) if self.assert_func: self.assert_func(request) response = WSGIResponse( status=self.status_code, response=self.body, headers=self.headers, ) return response(environ, start_response) authlib-1.3.2/tests/core/000077500000000000000000000000001466226534200152515ustar00rootroot00000000000000authlib-1.3.2/tests/core/__init__.py000066400000000000000000000000001466226534200173500ustar00rootroot00000000000000authlib-1.3.2/tests/core/test_oauth2/000077500000000000000000000000001466226534200175125ustar00rootroot00000000000000authlib-1.3.2/tests/core/test_oauth2/__init__.py000066400000000000000000000000001466226534200216110ustar00rootroot00000000000000authlib-1.3.2/tests/core/test_oauth2/test_rfc6749_misc.py000066400000000000000000000062451466226534200232510ustar00rootroot00000000000000import unittest import base64 from authlib.oauth2.rfc6749 import parameters from authlib.oauth2.rfc6749 import util from authlib.oauth2.rfc6749 import errors class OAuth2ParametersTest(unittest.TestCase): def test_parse_authorization_code_response(self): self.assertRaises( errors.MissingCodeException, parameters.parse_authorization_code_response, 'https://i.b/?state=c' ) self.assertRaises( errors.MismatchingStateException, parameters.parse_authorization_code_response, 'https://i.b/?code=a&state=c', 'b' ) url = 'https://i.b/?code=a&state=c' rv = parameters.parse_authorization_code_response(url, 'c') self.assertEqual(rv, {'code': 'a', 'state': 'c'}) def test_parse_implicit_response(self): self.assertRaises( errors.MissingTokenException, parameters.parse_implicit_response, 'https://i.b/#a=b' ) self.assertRaises( errors.MissingTokenTypeException, parameters.parse_implicit_response, 'https://i.b/#access_token=a' ) self.assertRaises( errors.MismatchingStateException, parameters.parse_implicit_response, 'https://i.b/#access_token=a&token_type=bearer&state=c', 'abc' ) url = 'https://i.b/#access_token=a&token_type=bearer&state=c' rv = parameters.parse_implicit_response(url, 'c') self.assertEqual( rv, {'access_token': 'a', 'token_type': 'bearer', 'state': 'c'} ) def test_prepare_grant_uri(self): grant_uri = parameters.prepare_grant_uri('https://i.b/authorize', 'dev', 'code', max_age=0) self.assertEqual( grant_uri, "https://i.b/authorize?response_type=code&client_id=dev&max_age=0" ) class OAuth2UtilTest(unittest.TestCase): def test_list_to_scope(self): self.assertEqual(util.list_to_scope(['a', 'b']), 'a b') self.assertEqual(util.list_to_scope('a b'), 'a b') self.assertIsNone(util.list_to_scope(None)) def test_scope_to_list(self): self.assertEqual(util.scope_to_list('a b'), ['a', 'b']) self.assertEqual(util.scope_to_list(['a', 'b']), ['a', 'b']) self.assertIsNone(util.scope_to_list(None)) def test_extract_basic_authorization(self): self.assertEqual(util.extract_basic_authorization({}), (None, None)) self.assertEqual( util.extract_basic_authorization({'Authorization': 'invalid'}), (None, None) ) text = 'Basic invalid-base64' self.assertEqual( util.extract_basic_authorization({'Authorization': text}), (None, None) ) text = 'Basic {}'.format(base64.b64encode(b'a').decode()) self.assertEqual( util.extract_basic_authorization({'Authorization': text}), ('a', None) ) text = 'Basic {}'.format(base64.b64encode(b'a:b').decode()) self.assertEqual( util.extract_basic_authorization({'Authorization': text}), ('a', 'b') ) authlib-1.3.2/tests/core/test_oauth2/test_rfc7523.py000066400000000000000000000354251466226534200222270ustar00rootroot00000000000000import time from unittest import TestCase, mock from authlib.jose import jwt from authlib.oauth2.rfc7523 import ClientSecretJWT, PrivateKeyJWT from tests.util import read_file_path class ClientSecretJWTTest(TestCase): def test_nothing_set(self): jwt_signer = ClientSecretJWT() self.assertEqual(jwt_signer.token_endpoint, None) self.assertEqual(jwt_signer.claims, None) self.assertEqual(jwt_signer.headers, None) self.assertEqual(jwt_signer.alg, "HS256") def test_endpoint_set(self): jwt_signer = ClientSecretJWT(token_endpoint="https://example.com/oauth/access_token") self.assertEqual(jwt_signer.token_endpoint, "https://example.com/oauth/access_token") self.assertEqual(jwt_signer.claims, None) self.assertEqual(jwt_signer.headers, None) self.assertEqual(jwt_signer.alg, "HS256") def test_alg_set(self): jwt_signer = ClientSecretJWT(alg="HS512") self.assertEqual(jwt_signer.token_endpoint, None) self.assertEqual(jwt_signer.claims, None) self.assertEqual(jwt_signer.headers, None) self.assertEqual(jwt_signer.alg, "HS512") def test_claims_set(self): jwt_signer = ClientSecretJWT(claims={"foo1": "bar1"}) self.assertEqual(jwt_signer.token_endpoint, None) self.assertEqual(jwt_signer.claims, {"foo1": "bar1"}) self.assertEqual(jwt_signer.headers, None) self.assertEqual(jwt_signer.alg, "HS256") def test_headers_set(self): jwt_signer = ClientSecretJWT(headers={"foo1": "bar1"}) self.assertEqual(jwt_signer.token_endpoint, None) self.assertEqual(jwt_signer.claims, None) self.assertEqual(jwt_signer.headers, {"foo1": "bar1"}) self.assertEqual(jwt_signer.alg, "HS256") def test_all_set(self): jwt_signer = ClientSecretJWT( token_endpoint="https://example.com/oauth/access_token", claims={"foo1a": "bar1a"}, headers={"foo1b": "bar1b"}, alg="HS512" ) self.assertEqual(jwt_signer.token_endpoint, "https://example.com/oauth/access_token") self.assertEqual(jwt_signer.claims, {"foo1a": "bar1a"}) self.assertEqual(jwt_signer.headers, {"foo1b": "bar1b"}) self.assertEqual(jwt_signer.alg, "HS512") @staticmethod def sign_and_decode(jwt_signer, client_id, client_secret, token_endpoint): auth = mock.MagicMock() auth.client_id = client_id auth.client_secret = client_secret pre_sign_time = int(time.time()) data = jwt_signer.sign(auth, token_endpoint).decode("utf-8") decoded = jwt.decode(data, client_secret) # , claims_cls=None, claims_options=None, claims_params=None): iat = decoded.pop("iat") exp = decoded.pop("exp") jti = decoded.pop("jti") return decoded, pre_sign_time, iat, exp, jti def test_sign_nothing_set(self): jwt_signer = ClientSecretJWT() decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", "client_secret_1", "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", }, decoded ) self.assertEqual( {"alg": "HS256", "typ": "JWT"}, decoded.header ) def test_sign_custom_jti(self): jwt_signer = ClientSecretJWT(claims={"jti": "custom_jti"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", "client_secret_1", "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertEqual("custom_jti", jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", } ) self.assertEqual( {"alg": "HS256", "typ": "JWT"}, decoded.header ) def test_sign_with_additional_header(self): jwt_signer = ClientSecretJWT(headers={"kid": "custom_kid"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", "client_secret_1", "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", } ) self.assertEqual( {"alg": "HS256", "typ": "JWT", "kid": "custom_kid"}, decoded.header ) def test_sign_with_additional_headers(self): jwt_signer = ClientSecretJWT(headers={"kid": "custom_kid", "jku": "https://example.com/oauth/jwks"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", "client_secret_1", "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", } ) self.assertEqual( {"alg": "HS256", "typ": "JWT", "kid": "custom_kid", "jku": "https://example.com/oauth/jwks"}, decoded.header ) def test_sign_with_additional_claim(self): jwt_signer = ClientSecretJWT(claims={"name": "Foo"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", "client_secret_1", "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", "name": "Foo"} ) self.assertEqual( {"alg": "HS256", "typ": "JWT"}, decoded.header ) def test_sign_with_additional_claims(self): jwt_signer = ClientSecretJWT(claims={"name": "Foo", "role": "bar"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", "client_secret_1", "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", "name": "Foo", "role": "bar"} ) self.assertEqual( {"alg": "HS256", "typ": "JWT"}, decoded.header ) class PrivateKeyJWTTest(TestCase): @classmethod def setUpClass(cls): cls.public_key = read_file_path("rsa_public.pem") cls.private_key = read_file_path("rsa_private.pem") def test_nothing_set(self): jwt_signer = PrivateKeyJWT() self.assertEqual(jwt_signer.token_endpoint, None) self.assertEqual(jwt_signer.claims, None) self.assertEqual(jwt_signer.headers, None) self.assertEqual(jwt_signer.alg, "RS256") def test_endpoint_set(self): jwt_signer = PrivateKeyJWT(token_endpoint="https://example.com/oauth/access_token") self.assertEqual(jwt_signer.token_endpoint, "https://example.com/oauth/access_token") self.assertEqual(jwt_signer.claims, None) self.assertEqual(jwt_signer.headers, None) self.assertEqual(jwt_signer.alg, "RS256") def test_alg_set(self): jwt_signer = PrivateKeyJWT(alg="RS512") self.assertEqual(jwt_signer.token_endpoint, None) self.assertEqual(jwt_signer.claims, None) self.assertEqual(jwt_signer.headers, None) self.assertEqual(jwt_signer.alg, "RS512") def test_claims_set(self): jwt_signer = PrivateKeyJWT(claims={"foo1": "bar1"}) self.assertEqual(jwt_signer.token_endpoint, None) self.assertEqual(jwt_signer.claims, {"foo1": "bar1"}) self.assertEqual(jwt_signer.headers, None) self.assertEqual(jwt_signer.alg, "RS256") def test_headers_set(self): jwt_signer = PrivateKeyJWT(headers={"foo1": "bar1"}) self.assertEqual(jwt_signer.token_endpoint, None) self.assertEqual(jwt_signer.claims, None) self.assertEqual(jwt_signer.headers, {"foo1": "bar1"}) self.assertEqual(jwt_signer.alg, "RS256") def test_all_set(self): jwt_signer = PrivateKeyJWT( token_endpoint="https://example.com/oauth/access_token", claims={"foo1a": "bar1a"}, headers={"foo1b": "bar1b"}, alg="RS512" ) self.assertEqual(jwt_signer.token_endpoint, "https://example.com/oauth/access_token") self.assertEqual(jwt_signer.claims, {"foo1a": "bar1a"}) self.assertEqual(jwt_signer.headers, {"foo1b": "bar1b"}) self.assertEqual(jwt_signer.alg, "RS512") @staticmethod def sign_and_decode(jwt_signer, client_id, public_key, private_key, token_endpoint): auth = mock.MagicMock() auth.client_id = client_id auth.client_secret = private_key pre_sign_time = int(time.time()) data = jwt_signer.sign(auth, token_endpoint).decode("utf-8") decoded = jwt.decode(data, public_key) # , claims_cls=None, claims_options=None, claims_params=None): iat = decoded.pop("iat") exp = decoded.pop("exp") jti = decoded.pop("jti") return decoded, pre_sign_time, iat, exp, jti def test_sign_nothing_set(self): jwt_signer = PrivateKeyJWT() decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", self.public_key, self.private_key, "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", }, decoded ) self.assertEqual( {"alg": "RS256", "typ": "JWT"}, decoded.header ) def test_sign_custom_jti(self): jwt_signer = PrivateKeyJWT(claims={"jti": "custom_jti"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", self.public_key, self.private_key, "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertEqual("custom_jti", jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", } ) self.assertEqual( {"alg": "RS256", "typ": "JWT"}, decoded.header ) def test_sign_with_additional_header(self): jwt_signer = PrivateKeyJWT(headers={"kid": "custom_kid"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", self.public_key, self.private_key, "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", } ) self.assertEqual( {"alg": "RS256", "typ": "JWT", "kid": "custom_kid"}, decoded.header ) def test_sign_with_additional_headers(self): jwt_signer = PrivateKeyJWT(headers={"kid": "custom_kid", "jku": "https://example.com/oauth/jwks"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", self.public_key, self.private_key, "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", } ) self.assertEqual( {"alg": "RS256", "typ": "JWT", "kid": "custom_kid", "jku": "https://example.com/oauth/jwks"}, decoded.header ) def test_sign_with_additional_claim(self): jwt_signer = PrivateKeyJWT(claims={"name": "Foo"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", self.public_key, self.private_key, "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", "name": "Foo"} ) self.assertEqual( {"alg": "RS256", "typ": "JWT"}, decoded.header ) def test_sign_with_additional_claims(self): jwt_signer = PrivateKeyJWT(claims={"name": "Foo", "role": "bar"}) decoded, pre_sign_time, iat, exp, jti = self.sign_and_decode( jwt_signer, "client_id_1", self.public_key, self.private_key, "https://example.com/oauth/access_token" ) self.assertGreaterEqual(iat, pre_sign_time) self.assertGreaterEqual(exp, iat + 3600) self.assertLessEqual(exp, iat + 3600 + 2) self.assertIsNotNone(jti) self.assertEqual( decoded, {"iss": "client_id_1", "aud": "https://example.com/oauth/access_token", "sub": "client_id_1", "name": "Foo", "role": "bar"} ) self.assertEqual( {"alg": "RS256", "typ": "JWT"}, decoded.header ) authlib-1.3.2/tests/core/test_oauth2/test_rfc7591.py000066400000000000000000000022351466226534200222250ustar00rootroot00000000000000from unittest import TestCase from authlib.oauth2.rfc7591 import ClientMetadataClaims from authlib.jose.errors import InvalidClaimError class ClientMetadataClaimsTest(TestCase): def test_validate_redirect_uris(self): claims = ClientMetadataClaims({'redirect_uris': ['foo']}, {}) self.assertRaises(InvalidClaimError, claims.validate) def test_validate_client_uri(self): claims = ClientMetadataClaims({'client_uri': 'foo'}, {}) self.assertRaises(InvalidClaimError, claims.validate) def test_validate_logo_uri(self): claims = ClientMetadataClaims({'logo_uri': 'foo'}, {}) self.assertRaises(InvalidClaimError, claims.validate) def test_validate_tos_uri(self): claims = ClientMetadataClaims({'tos_uri': 'foo'}, {}) self.assertRaises(InvalidClaimError, claims.validate) def test_validate_policy_uri(self): claims = ClientMetadataClaims({'policy_uri': 'foo'}, {}) self.assertRaises(InvalidClaimError, claims.validate) def test_validate_jwks_uri(self): claims = ClientMetadataClaims({'jwks_uri': 'foo'}, {}) self.assertRaises(InvalidClaimError, claims.validate) authlib-1.3.2/tests/core/test_oauth2/test_rfc7662.py000066400000000000000000000035551466226534200222320ustar00rootroot00000000000000import unittest from authlib.oauth2.rfc7662 import IntrospectionToken class IntrospectionTokenTest(unittest.TestCase): def test_client_id(self): token = IntrospectionToken() self.assertIsNone(token.client_id) self.assertIsNone(token.get_client_id()) token = IntrospectionToken({'client_id': 'foo'}) self.assertEqual(token.client_id, 'foo') self.assertEqual(token.get_client_id(), 'foo') def test_scope(self): token = IntrospectionToken() self.assertIsNone(token.scope) self.assertIsNone(token.get_scope()) token = IntrospectionToken({'scope': 'foo'}) self.assertEqual(token.scope, 'foo') self.assertEqual(token.get_scope(), 'foo') def test_expires_in(self): token = IntrospectionToken() self.assertEqual(token.get_expires_in(), 0) def test_expires_at(self): token = IntrospectionToken() self.assertIsNone(token.exp) self.assertEqual(token.get_expires_at(), 0) token = IntrospectionToken({'exp': 3600}) self.assertEqual(token.exp, 3600) self.assertEqual(token.get_expires_at(), 3600) def test_all_attributes(self): # https://tools.ietf.org/html/rfc7662#section-2.2 token = IntrospectionToken() self.assertIsNone(token.active) self.assertIsNone(token.scope) self.assertIsNone(token.client_id) self.assertIsNone(token.username) self.assertIsNone(token.token_type) self.assertIsNone(token.exp) self.assertIsNone(token.iat) self.assertIsNone(token.nbf) self.assertIsNone(token.sub) self.assertIsNone(token.aud) self.assertIsNone(token.iss) self.assertIsNone(token.jti) def test_invalid_attr(self): token = IntrospectionToken() self.assertRaises(AttributeError, lambda: token.invalid) authlib-1.3.2/tests/core/test_oauth2/test_rfc8414.py000066400000000000000000000442441466226534200222260ustar00rootroot00000000000000import unittest from authlib.oauth2.rfc8414 import get_well_known_url from authlib.oauth2.rfc8414 import AuthorizationServerMetadata WELL_KNOWN_URL = '/.well-known/oauth-authorization-server' class WellKnownTest(unittest.TestCase): def test_no_suffix_issuer(self): self.assertEqual( get_well_known_url('https://authlib.org'), WELL_KNOWN_URL ) self.assertEqual( get_well_known_url('https://authlib.org/'), WELL_KNOWN_URL ) def test_with_suffix_issuer(self): self.assertEqual( get_well_known_url('https://authlib.org/issuer1'), WELL_KNOWN_URL + '/issuer1' ) self.assertEqual( get_well_known_url('https://authlib.org/a/b/c'), WELL_KNOWN_URL + '/a/b/c' ) def test_with_external(self): self.assertEqual( get_well_known_url('https://authlib.org', external=True), 'https://authlib.org' + WELL_KNOWN_URL ) def test_with_changed_suffix(self): url = get_well_known_url( 'https://authlib.org', suffix='openid-configuration') self.assertEqual(url, '/.well-known/openid-configuration') url = get_well_known_url( 'https://authlib.org', external=True, suffix='openid-configuration' ) self.assertEqual(url, 'https://authlib.org/.well-known/openid-configuration') class AuthorizationServerMetadataTest(unittest.TestCase): def test_validate_issuer(self): #: missing metadata = AuthorizationServerMetadata({}) with self.assertRaises(ValueError) as cm: metadata.validate() self.assertEqual('"issuer" is required', str(cm.exception)) #: https metadata = AuthorizationServerMetadata({ 'issuer': 'http://authlib.org/' }) with self.assertRaises(ValueError) as cm: metadata.validate_issuer() self.assertIn('https', str(cm.exception)) #: query metadata = AuthorizationServerMetadata({ 'issuer': 'https://authlib.org/?a=b' }) with self.assertRaises(ValueError) as cm: metadata.validate_issuer() self.assertIn('query', str(cm.exception)) #: fragment metadata = AuthorizationServerMetadata({ 'issuer': 'https://authlib.org/#a=b' }) with self.assertRaises(ValueError) as cm: metadata.validate_issuer() self.assertIn('fragment', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'issuer': 'https://authlib.org/' }) metadata.validate_issuer() def test_validate_authorization_endpoint(self): # https metadata = AuthorizationServerMetadata({ 'authorization_endpoint': 'http://authlib.org/' }) with self.assertRaises(ValueError) as cm: metadata.validate_authorization_endpoint() self.assertIn('https', str(cm.exception)) # valid https metadata = AuthorizationServerMetadata({ 'authorization_endpoint': 'https://authlib.org/' }) metadata.validate_authorization_endpoint() # missing metadata = AuthorizationServerMetadata() with self.assertRaises(ValueError) as cm: metadata.validate_authorization_endpoint() self.assertIn('required', str(cm.exception)) # valid missing metadata = AuthorizationServerMetadata({ 'grant_types_supported': ['password'] }) metadata.validate_authorization_endpoint() def test_validate_token_endpoint(self): # implicit metadata = AuthorizationServerMetadata({ 'grant_types_supported': ['implicit'] }) metadata.validate_token_endpoint() # missing metadata = AuthorizationServerMetadata() with self.assertRaises(ValueError) as cm: metadata.validate_token_endpoint() self.assertIn('required', str(cm.exception)) # https metadata = AuthorizationServerMetadata({ 'token_endpoint': 'http://authlib.org/' }) with self.assertRaises(ValueError) as cm: metadata.validate_token_endpoint() self.assertIn('https', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'token_endpoint': 'https://authlib.org/' }) metadata.validate_token_endpoint() def test_validate_jwks_uri(self): # can missing metadata = AuthorizationServerMetadata() metadata.validate_jwks_uri() metadata = AuthorizationServerMetadata({ 'jwks_uri': 'http://authlib.org/jwks.json' }) with self.assertRaises(ValueError) as cm: metadata.validate_jwks_uri() self.assertIn('https', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'jwks_uri': 'https://authlib.org/jwks.json' }) metadata.validate_jwks_uri() def test_validate_registration_endpoint(self): metadata = AuthorizationServerMetadata() metadata.validate_registration_endpoint() metadata = AuthorizationServerMetadata({ 'registration_endpoint': 'http://authlib.org/' }) with self.assertRaises(ValueError) as cm: metadata.validate_registration_endpoint() self.assertIn('https', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'registration_endpoint': 'https://authlib.org/' }) metadata.validate_registration_endpoint() def test_validate_scopes_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_scopes_supported() # not array metadata = AuthorizationServerMetadata({ 'scopes_supported': 'foo' }) with self.assertRaises(ValueError) as cm: metadata.validate_scopes_supported() self.assertIn('JSON array', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'scopes_supported': ['foo'] }) metadata.validate_scopes_supported() def test_validate_response_types_supported(self): # missing metadata = AuthorizationServerMetadata() with self.assertRaises(ValueError) as cm: metadata.validate_response_types_supported() self.assertIn('required', str(cm.exception)) # not array metadata = AuthorizationServerMetadata({ 'response_types_supported': 'code' }) with self.assertRaises(ValueError) as cm: metadata.validate_response_types_supported() self.assertIn('JSON array', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'response_types_supported': ['code'] }) metadata.validate_response_types_supported() def test_validate_response_modes_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_response_modes_supported() # not array metadata = AuthorizationServerMetadata({ 'response_modes_supported': 'query' }) with self.assertRaises(ValueError) as cm: metadata.validate_response_modes_supported() self.assertIn('JSON array', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'response_modes_supported': ['query'] }) metadata.validate_response_modes_supported() def test_validate_grant_types_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_grant_types_supported() # not array metadata = AuthorizationServerMetadata({ 'grant_types_supported': 'password' }) with self.assertRaises(ValueError) as cm: metadata.validate_grant_types_supported() self.assertIn('JSON array', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'grant_types_supported': ['password'] }) metadata.validate_grant_types_supported() def test_validate_token_endpoint_auth_methods_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_token_endpoint_auth_methods_supported() # not array metadata = AuthorizationServerMetadata({ 'token_endpoint_auth_methods_supported': 'client_secret_basic' }) with self.assertRaises(ValueError) as cm: metadata.validate_token_endpoint_auth_methods_supported() self.assertIn('JSON array', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'token_endpoint_auth_methods_supported': ['client_secret_basic'] }) metadata.validate_token_endpoint_auth_methods_supported() def test_validate_token_endpoint_auth_signing_alg_values_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_token_endpoint_auth_signing_alg_values_supported() metadata = AuthorizationServerMetadata({ 'token_endpoint_auth_methods_supported': ['client_secret_jwt'] }) with self.assertRaises(ValueError) as cm: metadata.validate_token_endpoint_auth_signing_alg_values_supported() self.assertIn('required', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'token_endpoint_auth_signing_alg_values_supported': 'RS256' }) with self.assertRaises(ValueError) as cm: metadata.validate_token_endpoint_auth_signing_alg_values_supported() self.assertIn('JSON array', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'token_endpoint_auth_methods_supported': ['client_secret_jwt'], 'token_endpoint_auth_signing_alg_values_supported': ['RS256', 'none'] }) with self.assertRaises(ValueError) as cm: metadata.validate_token_endpoint_auth_signing_alg_values_supported() self.assertIn('none', str(cm.exception)) def test_validate_service_documentation(self): metadata = AuthorizationServerMetadata() metadata.validate_service_documentation() metadata = AuthorizationServerMetadata({ 'service_documentation': 'invalid' }) with self.assertRaises(ValueError) as cm: metadata.validate_service_documentation() self.assertIn('MUST be a URL', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'service_documentation': 'https://authlib.org/' }) metadata.validate_service_documentation() def test_validate_ui_locales_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_ui_locales_supported() # not array metadata = AuthorizationServerMetadata({ 'ui_locales_supported': 'en' }) with self.assertRaises(ValueError) as cm: metadata.validate_ui_locales_supported() self.assertIn('JSON array', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'ui_locales_supported': ['en'] }) metadata.validate_ui_locales_supported() def test_validate_op_policy_uri(self): metadata = AuthorizationServerMetadata() metadata.validate_op_policy_uri() metadata = AuthorizationServerMetadata({ 'op_policy_uri': 'invalid' }) with self.assertRaises(ValueError) as cm: metadata.validate_op_policy_uri() self.assertIn('MUST be a URL', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'op_policy_uri': 'https://authlib.org/' }) metadata.validate_op_policy_uri() def test_validate_op_tos_uri(self): metadata = AuthorizationServerMetadata() metadata.validate_op_tos_uri() metadata = AuthorizationServerMetadata({ 'op_tos_uri': 'invalid' }) with self.assertRaises(ValueError) as cm: metadata.validate_op_tos_uri() self.assertIn('MUST be a URL', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'op_tos_uri': 'https://authlib.org/' }) metadata.validate_op_tos_uri() def test_validate_revocation_endpoint(self): metadata = AuthorizationServerMetadata() metadata.validate_revocation_endpoint() # https metadata = AuthorizationServerMetadata({ 'revocation_endpoint': 'http://authlib.org/' }) with self.assertRaises(ValueError) as cm: metadata.validate_revocation_endpoint() self.assertIn('https', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'revocation_endpoint': 'https://authlib.org/' }) metadata.validate_revocation_endpoint() def test_validate_revocation_endpoint_auth_methods_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_revocation_endpoint_auth_methods_supported() # not array metadata = AuthorizationServerMetadata({ 'revocation_endpoint_auth_methods_supported': 'client_secret_basic' }) with self.assertRaises(ValueError) as cm: metadata.validate_revocation_endpoint_auth_methods_supported() self.assertIn('JSON array', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'revocation_endpoint_auth_methods_supported': ['client_secret_basic'] }) metadata.validate_revocation_endpoint_auth_methods_supported() def test_validate_revocation_endpoint_auth_signing_alg_values_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_revocation_endpoint_auth_signing_alg_values_supported() metadata = AuthorizationServerMetadata({ 'revocation_endpoint_auth_methods_supported': ['client_secret_jwt'] }) with self.assertRaises(ValueError) as cm: metadata.validate_revocation_endpoint_auth_signing_alg_values_supported() self.assertIn('required', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'revocation_endpoint_auth_signing_alg_values_supported': 'RS256' }) with self.assertRaises(ValueError) as cm: metadata.validate_revocation_endpoint_auth_signing_alg_values_supported() self.assertIn('JSON array', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'revocation_endpoint_auth_methods_supported': ['client_secret_jwt'], 'revocation_endpoint_auth_signing_alg_values_supported': ['RS256', 'none'] }) with self.assertRaises(ValueError) as cm: metadata.validate_revocation_endpoint_auth_signing_alg_values_supported() self.assertIn('none', str(cm.exception)) def test_validate_introspection_endpoint(self): metadata = AuthorizationServerMetadata() metadata.validate_introspection_endpoint() # https metadata = AuthorizationServerMetadata({ 'introspection_endpoint': 'http://authlib.org/' }) with self.assertRaises(ValueError) as cm: metadata.validate_introspection_endpoint() self.assertIn('https', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'introspection_endpoint': 'https://authlib.org/' }) metadata.validate_introspection_endpoint() def test_validate_introspection_endpoint_auth_methods_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_introspection_endpoint_auth_methods_supported() # not array metadata = AuthorizationServerMetadata({ 'introspection_endpoint_auth_methods_supported': 'client_secret_basic' }) with self.assertRaises(ValueError) as cm: metadata.validate_introspection_endpoint_auth_methods_supported() self.assertIn('JSON array', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'introspection_endpoint_auth_methods_supported': ['client_secret_basic'] }) metadata.validate_introspection_endpoint_auth_methods_supported() def test_validate_introspection_endpoint_auth_signing_alg_values_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_introspection_endpoint_auth_signing_alg_values_supported() metadata = AuthorizationServerMetadata({ 'introspection_endpoint_auth_methods_supported': ['client_secret_jwt'] }) with self.assertRaises(ValueError) as cm: metadata.validate_introspection_endpoint_auth_signing_alg_values_supported() self.assertIn('required', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'introspection_endpoint_auth_signing_alg_values_supported': 'RS256' }) with self.assertRaises(ValueError) as cm: metadata.validate_introspection_endpoint_auth_signing_alg_values_supported() self.assertIn('JSON array', str(cm.exception)) metadata = AuthorizationServerMetadata({ 'introspection_endpoint_auth_methods_supported': ['client_secret_jwt'], 'introspection_endpoint_auth_signing_alg_values_supported': ['RS256', 'none'] }) with self.assertRaises(ValueError) as cm: metadata.validate_introspection_endpoint_auth_signing_alg_values_supported() self.assertIn('none', str(cm.exception)) def test_validate_code_challenge_methods_supported(self): metadata = AuthorizationServerMetadata() metadata.validate_code_challenge_methods_supported() # not array metadata = AuthorizationServerMetadata({ 'code_challenge_methods_supported': 'S256' }) with self.assertRaises(ValueError) as cm: metadata.validate_code_challenge_methods_supported() self.assertIn('JSON array', str(cm.exception)) # valid metadata = AuthorizationServerMetadata({ 'code_challenge_methods_supported': ['S256'] }) metadata.validate_code_challenge_methods_supported() authlib-1.3.2/tests/core/test_oidc/000077500000000000000000000000001466226534200172265ustar00rootroot00000000000000authlib-1.3.2/tests/core/test_oidc/__init__.py000066400000000000000000000000001466226534200213250ustar00rootroot00000000000000authlib-1.3.2/tests/core/test_oidc/test_core.py000066400000000000000000000105161466226534200215720ustar00rootroot00000000000000import unittest from authlib.jose.errors import MissingClaimError, InvalidClaimError from authlib.oidc.core import CodeIDToken, ImplicitIDToken, HybridIDToken from authlib.oidc.core import UserInfo, get_claim_cls_by_response_type class IDTokenTest(unittest.TestCase): def test_essential_claims(self): claims = CodeIDToken({}, {}) self.assertRaises(MissingClaimError, claims.validate) claims = CodeIDToken({ 'iss': '1', 'sub': '1', 'aud': '1', 'exp': 10000, 'iat': 100 }, {}) claims.validate(1000) def test_validate_auth_time(self): claims = CodeIDToken({ 'iss': '1', 'sub': '1', 'aud': '1', 'exp': 10000, 'iat': 100 }, {}) claims.params = {'max_age': 100} self.assertRaises(MissingClaimError, claims.validate, 1000) claims['auth_time'] = 'foo' self.assertRaises(InvalidClaimError, claims.validate, 1000) def test_validate_nonce(self): claims = CodeIDToken({ 'iss': '1', 'sub': '1', 'aud': '1', 'exp': 10000, 'iat': 100 }, {}) claims.params = {'nonce': 'foo'} self.assertRaises(MissingClaimError, claims.validate, 1000) claims['nonce'] = 'bar' self.assertRaises(InvalidClaimError, claims.validate, 1000) claims['nonce'] = 'foo' claims.validate(1000) def test_validate_amr(self): claims = CodeIDToken({ 'iss': '1', 'sub': '1', 'aud': '1', 'exp': 10000, 'iat': 100, 'amr': 'invalid' }, {}) self.assertRaises(InvalidClaimError, claims.validate, 1000) def test_validate_azp(self): claims = CodeIDToken({ 'iss': '1', 'sub': '1', 'aud': '1', 'exp': 10000, 'iat': 100, }, {}) claims.params = {'client_id': '2'} self.assertRaises(MissingClaimError, claims.validate, 1000) claims['azp'] = '1' self.assertRaises(InvalidClaimError, claims.validate, 1000) claims['azp'] = '2' claims.validate(1000) def test_validate_at_hash(self): claims = CodeIDToken({ 'iss': '1', 'sub': '1', 'aud': '1', 'exp': 10000, 'iat': 100, 'at_hash': 'a' }, {}) claims.params = {'access_token': 'a'} # invalid alg won't raise claims.header = {'alg': 'HS222'} claims.validate(1000) claims.header = {'alg': 'HS256'} self.assertRaises(InvalidClaimError, claims.validate, 1000) def test_implicit_id_token(self): claims = ImplicitIDToken({ 'iss': '1', 'sub': '1', 'aud': '1', 'exp': 10000, 'iat': 100, 'nonce': 'a' }, {}) claims.params = {'access_token': 'a'} self.assertRaises(MissingClaimError, claims.validate, 1000) def test_hybrid_id_token(self): claims = HybridIDToken({ 'iss': '1', 'sub': '1', 'aud': '1', 'exp': 10000, 'iat': 100, 'nonce': 'a' }, {}) claims.validate(1000) claims.params = {'code': 'a'} self.assertRaises(MissingClaimError, claims.validate, 1000) # invalid alg won't raise claims.header = {'alg': 'HS222'} claims['c_hash'] = 'a' claims.validate(1000) claims.header = {'alg': 'HS256'} self.assertRaises(InvalidClaimError, claims.validate, 1000) def test_get_claim_cls_by_response_type(self): cls = get_claim_cls_by_response_type('id_token') self.assertEqual(cls, ImplicitIDToken) cls = get_claim_cls_by_response_type('code') self.assertEqual(cls, CodeIDToken) cls = get_claim_cls_by_response_type('code id_token') self.assertEqual(cls, HybridIDToken) cls = get_claim_cls_by_response_type('none') self.assertIsNone(cls) class UserInfoTest(unittest.TestCase): def test_getattribute(self): user = UserInfo({'sub': '1'}) self.assertEqual(user.sub, '1') self.assertIsNone(user.email, None) self.assertRaises(AttributeError, lambda: user.invalid) authlib-1.3.2/tests/core/test_oidc/test_discovery.py000066400000000000000000000171061466226534200226530ustar00rootroot00000000000000import unittest from authlib.oidc.discovery import get_well_known_url, OpenIDProviderMetadata WELL_KNOWN_URL = '/.well-known/openid-configuration' class WellKnownTest(unittest.TestCase): def test_no_suffix_issuer(self): self.assertEqual( get_well_known_url('https://authlib.org'), WELL_KNOWN_URL ) self.assertEqual( get_well_known_url('https://authlib.org/'), WELL_KNOWN_URL ) def test_with_suffix_issuer(self): self.assertEqual( get_well_known_url('https://authlib.org/issuer1'), '/issuer1' + WELL_KNOWN_URL ) self.assertEqual( get_well_known_url('https://authlib.org/a/b/c'), '/a/b/c' + WELL_KNOWN_URL ) def test_with_external(self): self.assertEqual( get_well_known_url('https://authlib.org', external=True), 'https://authlib.org' + WELL_KNOWN_URL ) class OpenIDProviderMetadataTest(unittest.TestCase): def test_validate_jwks_uri(self): # required metadata = OpenIDProviderMetadata() with self.assertRaises(ValueError) as cm: metadata.validate_jwks_uri() self.assertEqual('"jwks_uri" is required', str(cm.exception)) metadata = OpenIDProviderMetadata({ 'jwks_uri': 'http://authlib.org/jwks.json' }) with self.assertRaises(ValueError) as cm: metadata.validate_jwks_uri() self.assertIn('https', str(cm.exception)) metadata = OpenIDProviderMetadata({ 'jwks_uri': 'https://authlib.org/jwks.json' }) metadata.validate_jwks_uri() def test_validate_acr_values_supported(self): self._call_validate_array( 'acr_values_supported', ['urn:mace:incommon:iap:silver'] ) def test_validate_subject_types_supported(self): self._call_validate_array( 'subject_types_supported', ['pairwise', 'public'], required=True ) self._call_contains_invalid_value( 'subject_types_supported', ['invalid'] ) def test_validate_id_token_signing_alg_values_supported(self): self._call_validate_array( 'id_token_signing_alg_values_supported', ['RS256'], required=True, ) metadata = OpenIDProviderMetadata({ 'id_token_signing_alg_values_supported': ['none'] }) with self.assertRaises(ValueError) as cm: metadata.validate_id_token_signing_alg_values_supported() self.assertIn('RS256', str(cm.exception)) def test_validate_id_token_encryption_alg_values_supported(self): self._call_validate_array( 'id_token_encryption_alg_values_supported', ['A128KW'] ) def test_validate_id_token_encryption_enc_values_supported(self): self._call_validate_array( 'id_token_encryption_enc_values_supported', ['A128GCM'] ) def test_validate_userinfo_signing_alg_values_supported(self): self._call_validate_array( 'userinfo_signing_alg_values_supported', ['RS256'] ) def test_validate_userinfo_encryption_alg_values_supported(self): self._call_validate_array( 'userinfo_encryption_alg_values_supported', ['A128KW'] ) def test_validate_userinfo_encryption_enc_values_supported(self): self._call_validate_array( 'userinfo_encryption_enc_values_supported', ['A128GCM'] ) def test_validate_request_object_signing_alg_values_supported(self): self._call_validate_array( 'request_object_signing_alg_values_supported', ['none', 'RS256'] ) metadata = OpenIDProviderMetadata({ 'request_object_signing_alg_values_supported': ['RS512'] }) with self.assertRaises(ValueError) as cm: metadata.validate_request_object_signing_alg_values_supported() self.assertIn('SHOULD support none and RS256', str(cm.exception)) def test_validate_request_object_encryption_alg_values_supported(self): self._call_validate_array( 'request_object_encryption_alg_values_supported', ['A128KW'] ) def test_validate_request_object_encryption_enc_values_supported(self): self._call_validate_array( 'request_object_encryption_enc_values_supported', ['A128GCM'] ) def test_validate_display_values_supported(self): self._call_validate_array( 'display_values_supported', ['page', 'touch'] ) self._call_contains_invalid_value( 'display_values_supported', ['invalid'] ) def test_validate_claim_types_supported(self): self._call_validate_array( 'claim_types_supported', ['normal'] ) self._call_contains_invalid_value( 'claim_types_supported', ['invalid'] ) metadata = OpenIDProviderMetadata() self.assertEqual(metadata.claim_types_supported, ['normal']) def test_validate_claims_supported(self): self._call_validate_array( 'claims_supported', ['sub'] ) def test_validate_claims_locales_supported(self): self._call_validate_array( 'claims_locales_supported', ['en-US'] ) def test_validate_claims_parameter_supported(self): self._call_validate_boolean('claims_parameter_supported') def test_validate_request_parameter_supported(self): self._call_validate_boolean('request_parameter_supported') def test_validate_request_uri_parameter_supported(self): self._call_validate_boolean('request_uri_parameter_supported', True) def test_validate_require_request_uri_registration(self): self._call_validate_boolean('require_request_uri_registration') def _call_validate_boolean(self, key, default_value=False): def _validate(metadata): getattr(metadata, 'validate_' + key)() metadata = OpenIDProviderMetadata() _validate(metadata) self.assertEqual(getattr(metadata, key), default_value) metadata = OpenIDProviderMetadata({key: 'str'}) with self.assertRaises(ValueError) as cm: _validate(metadata) self.assertIn('MUST be boolean', str(cm.exception)) metadata = OpenIDProviderMetadata({key: True}) _validate(metadata) def _call_validate_array(self, key, valid_value, required=False): def _validate(metadata): getattr(metadata, 'validate_' + key)() metadata = OpenIDProviderMetadata() if required: with self.assertRaises(ValueError) as cm: _validate(metadata) self.assertEqual(f'"{key}" is required', str(cm.exception)) else: _validate(metadata) # not array metadata = OpenIDProviderMetadata({key: 'foo'}) with self.assertRaises(ValueError) as cm: _validate(metadata) self.assertIn('JSON array', str(cm.exception)) # valid metadata = OpenIDProviderMetadata({key: valid_value}) _validate(metadata) def _call_contains_invalid_value(self, key, invalid_value): metadata = OpenIDProviderMetadata({key: invalid_value}) with self.assertRaises(ValueError) as cm: getattr(metadata, 'validate_' + key)() self.assertEqual( f'"{key}" contains invalid values', str(cm.exception) ) authlib-1.3.2/tests/django/000077500000000000000000000000001466226534200155635ustar00rootroot00000000000000authlib-1.3.2/tests/django/__init__.py000066400000000000000000000000001466226534200176620ustar00rootroot00000000000000authlib-1.3.2/tests/django/settings.py000066400000000000000000000011131466226534200177710ustar00rootroot00000000000000SECRET_KEY = 'django-secret' DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:", } } MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware' ] SESSION_ENGINE = 'django.contrib.sessions.backends.cache' CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'LOCATION': 'unique-snowflake', } } INSTALLED_APPS=[ 'django.contrib.contenttypes', 'django.contrib.auth', 'tests.django.test_oauth1', 'tests.django.test_oauth2', ] USE_TZ = True authlib-1.3.2/tests/django/test_oauth1/000077500000000000000000000000001466226534200200235ustar00rootroot00000000000000authlib-1.3.2/tests/django/test_oauth1/__init__.py000066400000000000000000000000001466226534200221220ustar00rootroot00000000000000authlib-1.3.2/tests/django/test_oauth1/models.py000066400000000000000000000020651466226534200216630ustar00rootroot00000000000000from django.db.models import Model, CharField, TextField from django.db.models import ForeignKey, CASCADE from django.contrib.auth.models import User from tests.util import read_file_path class Client(Model): user = ForeignKey(User, on_delete=CASCADE) client_id = CharField(max_length=48, unique=True, db_index=True) client_secret = CharField(max_length=48, blank=True) default_redirect_uri = TextField(blank=False, default='') def get_default_redirect_uri(self): return self.default_redirect_uri def get_client_secret(self): return self.client_secret def get_rsa_public_key(self): return read_file_path('rsa_public.pem') class TokenCredential(Model): user = ForeignKey(User, on_delete=CASCADE) client_id = CharField(max_length=48, db_index=True) oauth_token = CharField(max_length=84, unique=True, db_index=True) oauth_token_secret = CharField(max_length=84) def get_oauth_token(self): return self.oauth_token def get_oauth_token_secret(self): return self.oauth_token_secret authlib-1.3.2/tests/django/test_oauth1/oauth1_server.py000066400000000000000000000010111466226534200231550ustar00rootroot00000000000000import os from authlib.integrations.django_oauth1 import ( CacheAuthorizationServer, ) from tests.django_helper import TestCase as _TestCase from .models import Client, TokenCredential class TestCase(_TestCase): def setUp(self): super().setUp() os.environ['AUTHLIB_INSECURE_TRANSPORT'] = 'true' def tearDown(self): os.environ.pop('AUTHLIB_INSECURE_TRANSPORT') super().tearDown() def create_server(self): return CacheAuthorizationServer(Client, TokenCredential) authlib-1.3.2/tests/django/test_oauth1/test_authorize.py000066400000000000000000000121451466226534200234510ustar00rootroot00000000000000from authlib.oauth1.rfc5849 import errors from django.test import override_settings from tests.util import decode_response from .models import User, Client from .oauth1_server import TestCase class AuthorizationTest(TestCase): def prepare_data(self): user = User(username='foo') user.save() client = Client( user_id=user.pk, client_id='client', client_secret='secret', default_redirect_uri='https://a.b', ) client.save() def test_invalid_authorization(self): server = self.create_server() url = '/oauth/authorize' request = self.factory.post(url) self.assertRaises( errors.MissingRequiredParameterError, server.check_authorization_request, request ) request = self.factory.post(url, data={'oauth_token': 'a'}) self.assertRaises( errors.InvalidTokenError, server.check_authorization_request, request ) def test_invalid_initiate(self): server = self.create_server() url = '/oauth/initiate' request = self.factory.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) resp = server.create_temporary_credentials_response(request) data = decode_response(resp.content) self.assertEqual(data['error'], 'invalid_client') @override_settings( AUTHLIB_OAUTH1_PROVIDER={'signature_methods': ['PLAINTEXT']}) def test_authorize_denied(self): self.prepare_data() server = self.create_server() initiate_url = '/oauth/initiate' authorize_url = '/oauth/authorize' # case 1 request = self.factory.post(initiate_url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) resp = server.create_temporary_credentials_response(request) data = decode_response(resp.content) self.assertIn('oauth_token', data) request = self.factory.post(authorize_url, data={ 'oauth_token': data['oauth_token'] }) resp = server.create_authorization_response(request) self.assertEqual(resp.status_code, 302) self.assertIn('access_denied', resp['Location']) self.assertIn('https://a.b', resp['Location']) # case 2 request = self.factory.post(initiate_url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'https://i.test', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) resp = server.create_temporary_credentials_response(request) data = decode_response(resp.content) self.assertIn('oauth_token', data) request = self.factory.post(authorize_url, data={ 'oauth_token': data['oauth_token'] }) resp = server.create_authorization_response(request) self.assertEqual(resp.status_code, 302) self.assertIn('access_denied', resp['Location']) self.assertIn('https://i.test', resp['Location']) @override_settings( AUTHLIB_OAUTH1_PROVIDER={'signature_methods': ['PLAINTEXT']}) def test_authorize_granted(self): self.prepare_data() server = self.create_server() user = User.objects.get(username='foo') initiate_url = '/oauth/initiate' authorize_url = '/oauth/authorize' # case 1 request = self.factory.post(initiate_url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) resp = server.create_temporary_credentials_response(request) data = decode_response(resp.content) self.assertIn('oauth_token', data) request = self.factory.post(authorize_url, data={ 'oauth_token': data['oauth_token'] }) resp = server.create_authorization_response(request, user) self.assertEqual(resp.status_code, 302) self.assertIn('oauth_verifier', resp['Location']) self.assertIn('https://a.b', resp['Location']) # case 2 request = self.factory.post(initiate_url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'https://i.test', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) resp = server.create_temporary_credentials_response(request) data = decode_response(resp.content) self.assertIn('oauth_token', data) request = self.factory.post(authorize_url, data={ 'oauth_token': data['oauth_token'] }) resp = server.create_authorization_response(request, user) self.assertEqual(resp.status_code, 302) self.assertIn('oauth_verifier', resp['Location']) self.assertIn('https://i.test', resp['Location']) authlib-1.3.2/tests/django/test_oauth1/test_resource_protector.py000066400000000000000000000151231466226534200253660ustar00rootroot00000000000000import json import time from authlib.common.encoding import to_unicode from authlib.oauth1.rfc5849 import signature from authlib.common.urls import add_params_to_uri from authlib.integrations.django_oauth1 import ResourceProtector from django.http import JsonResponse from django.test import override_settings from tests.util import read_file_path from .models import User, Client, TokenCredential from .oauth1_server import TestCase class ResourceTest(TestCase): def create_route(self): require_oauth = ResourceProtector(Client, TokenCredential) @require_oauth() def handle(request): user = request.oauth1_credential.user return JsonResponse(dict(username=user.username)) return handle def prepare_data(self): user = User(username='foo') user.save() client = Client( user_id=user.pk, client_id='client', client_secret='secret', default_redirect_uri='https://a.b', ) client.save() tok = TokenCredential( user_id=user.pk, client_id=client.client_id, oauth_token='valid-token', oauth_token_secret='valid-token-secret' ) tok.save() def test_invalid_request_parameters(self): self.prepare_data() handle = self.create_route() url = '/user' # case 1 request = self.factory.get(url) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_consumer_key', data['error_description']) # case 2 request = self.factory.get( add_params_to_uri(url, {'oauth_consumer_key': 'a'})) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_client') # case 3 request = self.factory.get( add_params_to_uri(url, {'oauth_consumer_key': 'client'})) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_token', data['error_description']) # case 4 request = self.factory.get( add_params_to_uri(url, { 'oauth_consumer_key': 'client', 'oauth_token': 'a' }) ) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_token') # case 5 request = self.factory.get( add_params_to_uri(url, { 'oauth_consumer_key': 'client', 'oauth_token': 'valid-token' }) ) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_timestamp', data['error_description']) @override_settings( AUTHLIB_OAUTH1_PROVIDER={'signature_methods': ['PLAINTEXT']}) def test_plaintext_signature(self): self.prepare_data() handle = self.create_route() url = '/user' # case 1: success auth_header = ( 'OAuth oauth_consumer_key="client",' 'oauth_signature_method="PLAINTEXT",' 'oauth_token="valid-token",' 'oauth_signature="secret&valid-token-secret"' ) request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertIn('username', data) # case 2: invalid signature auth_header = auth_header.replace('valid-token-secret', 'invalid') request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_signature') def test_hmac_sha1_signature(self): self.prepare_data() handle = self.create_route() url = '/user' params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'valid-token'), ('oauth_signature_method', 'HMAC-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'hmac-sha1-nonce'), ] base_string = signature.construct_base_string( 'GET', 'http://testserver/user', params ) sig = signature.hmac_sha1_signature( base_string, 'secret', 'valid-token-secret') params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param # case 1: success request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertIn('username', data) # case 2: exists nonce request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_nonce') @override_settings( AUTHLIB_OAUTH1_PROVIDER={'signature_methods': ['RSA-SHA1']}) def test_rsa_sha1_signature(self): self.prepare_data() handle = self.create_route() url = '/user' params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'valid-token'), ('oauth_signature_method', 'RSA-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'rsa-sha1-nonce'), ] base_string = signature.construct_base_string( 'GET', 'http://testserver/user', params ) sig = signature.rsa_sha1_signature( base_string, read_file_path('rsa_private.pem')) params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertIn('username', data) # case: invalid signature auth_param = auth_param.replace('rsa-sha1-nonce', 'alt-sha1-nonce') auth_header = 'OAuth ' + auth_param request = self.factory.get(url, HTTP_AUTHORIZATION=auth_header) resp = handle(request) data = json.loads(to_unicode(resp.content)) self.assertEqual(data['error'], 'invalid_signature') authlib-1.3.2/tests/django/test_oauth1/test_token_credentials.py000066400000000000000000000161271466226534200251400ustar00rootroot00000000000000import time from authlib.oauth1.rfc5849 import signature from tests.util import read_file_path, decode_response from django.test import override_settings from django.core.cache import cache from .models import User, Client from .oauth1_server import TestCase class AuthorizationTest(TestCase): def prepare_data(self): user = User(username='foo') user.save() client = Client( user_id=user.pk, client_id='client', client_secret='secret', default_redirect_uri='https://a.b', ) client.save() def prepare_temporary_credential(self, server): token = { 'oauth_token': 'abc', 'oauth_token_secret': 'abc-secret', 'oauth_verifier': 'abc-verifier', 'client_id': 'client', 'user_id': 1 } key_prefix = server._temporary_credential_key_prefix key = key_prefix + token['oauth_token'] cache.set(key, token, timeout=server._temporary_expires_in) def test_invalid_token_request_parameters(self): self.prepare_data() server = self.create_server() url = '/oauth/token' # case 1 request = self.factory.post(url) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_consumer_key', data['error_description']) # case 2 request = self.factory.post(url, data={'oauth_consumer_key': 'a'}) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertEqual(data['error'], 'invalid_client') # case 3 request = self.factory.post(url, data={'oauth_consumer_key': 'client'}) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_token', data['error_description']) # case 4 request = self.factory.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_token': 'a' }) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertEqual(data['error'], 'invalid_token') def test_duplicated_oauth_parameters(self): self.prepare_data() server = self.create_server() url = '/oauth/token?oauth_consumer_key=client' request = self.factory.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_token': 'abc', 'oauth_verifier': 'abc' }) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertEqual(data['error'], 'duplicated_oauth_protocol_parameter') @override_settings( AUTHLIB_OAUTH1_PROVIDER={'signature_methods': ['PLAINTEXT']}) def test_plaintext_signature(self): self.prepare_data() server = self.create_server() url = '/oauth/token' # case 1: success self.prepare_temporary_credential(server) auth_header = ( 'OAuth oauth_consumer_key="client",' 'oauth_signature_method="PLAINTEXT",' 'oauth_token="abc",' 'oauth_verifier="abc-verifier",' 'oauth_signature="secret&abc-secret"' ) request = self.factory.post(url, HTTP_AUTHORIZATION=auth_header) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertIn('oauth_token', data) # case 2: invalid signature self.prepare_temporary_credential(server) request = self.factory.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_signature_method': 'PLAINTEXT', 'oauth_token': 'abc', 'oauth_verifier': 'abc-verifier', 'oauth_signature': 'invalid-signature' }) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertEqual(data['error'], 'invalid_signature') def test_hmac_sha1_signature(self): self.prepare_data() server = self.create_server() url = '/oauth/token' params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'abc'), ('oauth_verifier', 'abc-verifier'), ('oauth_signature_method', 'HMAC-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'hmac-sha1-nonce'), ] base_string = signature.construct_base_string( 'POST', 'http://testserver/oauth/token', params ) sig = signature.hmac_sha1_signature( base_string, 'secret', 'abc-secret') params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param # case 1: success self.prepare_temporary_credential(server) request = self.factory.post(url, HTTP_AUTHORIZATION=auth_header) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertIn('oauth_token', data) # case 2: exists nonce self.prepare_temporary_credential(server) request = self.factory.post(url, HTTP_AUTHORIZATION=auth_header) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertEqual(data['error'], 'invalid_nonce') @override_settings( AUTHLIB_OAUTH1_PROVIDER={'signature_methods': ['RSA-SHA1']}) def test_rsa_sha1_signature(self): self.prepare_data() server = self.create_server() url = '/oauth/token' self.prepare_temporary_credential(server) params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'abc'), ('oauth_verifier', 'abc-verifier'), ('oauth_signature_method', 'RSA-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'rsa-sha1-nonce'), ] base_string = signature.construct_base_string( 'POST', 'http://testserver/oauth/token', params ) sig = signature.rsa_sha1_signature( base_string, read_file_path('rsa_private.pem')) params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param request = self.factory.post(url, HTTP_AUTHORIZATION=auth_header) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertIn('oauth_token', data) # case: invalid signature self.prepare_temporary_credential(server) auth_param = auth_param.replace('rsa-sha1-nonce', 'alt-sha1-nonce') auth_header = 'OAuth ' + auth_param request = self.factory.post(url, HTTP_AUTHORIZATION=auth_header) resp = server.create_token_response(request) data = decode_response(resp.content) self.assertEqual(data['error'], 'invalid_signature') authlib-1.3.2/tests/django/test_oauth2/000077500000000000000000000000001466226534200200245ustar00rootroot00000000000000authlib-1.3.2/tests/django/test_oauth2/__init__.py000066400000000000000000000000001466226534200221230ustar00rootroot00000000000000authlib-1.3.2/tests/django/test_oauth2/models.py000066400000000000000000000112041466226534200216570ustar00rootroot00000000000000import time from django.db.models import ( Model, CharField, TextField, BooleanField, IntegerField, ) from django.db.models import ForeignKey, CASCADE from django.contrib.auth.models import User from authlib.common.security import generate_token from authlib.oauth2.rfc6749 import ( ClientMixin, TokenMixin, AuthorizationCodeMixin, ) from authlib.oauth2.rfc6749.util import scope_to_list, list_to_scope def now_timestamp(): return int(time.time()) class Client(Model, ClientMixin): user = ForeignKey(User, on_delete=CASCADE) client_id = CharField(max_length=48, unique=True, db_index=True) client_secret = CharField(max_length=48, blank=True) redirect_uris = TextField(default='') default_redirect_uri = TextField(blank=False, default='') scope = TextField(default='') response_type = TextField(default='') grant_type = TextField(default='') token_endpoint_auth_method = CharField(max_length=120, default='') def get_client_id(self): return self.client_id def get_default_redirect_uri(self): return self.default_redirect_uri def get_allowed_scope(self, scope): if not scope: return '' allowed = set(scope_to_list(self.scope)) return list_to_scope([s for s in scope.split() if s in allowed]) def check_redirect_uri(self, redirect_uri): if redirect_uri == self.default_redirect_uri: return True return redirect_uri in self.redirect_uris def check_client_secret(self, client_secret): return self.client_secret == client_secret def check_endpoint_auth_method(self, method, endpoint): if endpoint == 'token': return self.token_endpoint_auth_method == method return True def check_response_type(self, response_type): allowed = self.response_type.split() return response_type in allowed def check_grant_type(self, grant_type): allowed = self.grant_type.split() return grant_type in allowed class OAuth2Token(Model, TokenMixin): user = ForeignKey(User, on_delete=CASCADE) client_id = CharField(max_length=48, db_index=True) token_type = CharField(max_length=40) access_token = CharField(max_length=255, unique=True, null=False) refresh_token = CharField(max_length=255, db_index=True) scope = TextField(default='') issued_at = IntegerField(null=False, default=now_timestamp) expires_in = IntegerField(null=False, default=0) access_token_revoked_at = IntegerField(default=0) refresh_token_revoked_at = IntegerField(default=0) def check_client(self, client): return self.client_id == client.client_id def get_scope(self): return self.scope def get_expires_in(self): return self.expires_in def is_revoked(self): return self.access_token_revoked_at or self.refresh_token_revoked_at def is_expired(self): if not self.expires_in: return False expires_at = self.issued_at + self.expires_in return expires_at < time.time() def is_refresh_token_active(self): return not self.refresh_token_revoked_at class OAuth2Code(Model, AuthorizationCodeMixin): user = ForeignKey(User, on_delete=CASCADE) client_id = CharField(max_length=48, db_index=True) code = CharField(max_length=120, unique=True, null=False) redirect_uri = TextField(default='', null=True) response_type = TextField(default='') scope = TextField(default='', null=True) auth_time = IntegerField(null=False, default=now_timestamp) def is_expired(self): return self.auth_time + 300 < time.time() def get_redirect_uri(self): return self.redirect_uri def get_scope(self): return self.scope or '' def get_auth_time(self): return self.auth_time class CodeGrantMixin: def query_authorization_code(self, code, client): try: item = OAuth2Code.objects.get(code=code, client_id=client.client_id) except OAuth2Code.DoesNotExist: return None if not item.is_expired(): return item def delete_authorization_code(self, authorization_code): authorization_code.delete() def authenticate_user(self, authorization_code): return authorization_code.user def generate_authorization_code(client, grant_user, request, **extra): code = generate_token(48) item = OAuth2Code( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, response_type=request.response_type, scope=request.scope, user=grant_user, **extra ) item.save() return code authlib-1.3.2/tests/django/test_oauth2/oauth2_server.py000066400000000000000000000013561466226534200231730ustar00rootroot00000000000000import os import base64 from authlib.common.encoding import to_bytes, to_unicode from authlib.integrations.django_oauth2 import AuthorizationServer from tests.django_helper import TestCase as _TestCase from .models import Client, OAuth2Token class TestCase(_TestCase): def setUp(self): super().setUp() os.environ['AUTHLIB_INSECURE_TRANSPORT'] = 'true' def tearDown(self): super().tearDown() os.environ.pop('AUTHLIB_INSECURE_TRANSPORT') def create_server(self): return AuthorizationServer(Client, OAuth2Token) def create_basic_auth(self, username, password): text = f'{username}:{password}' auth = to_unicode(base64.b64encode(to_bytes(text))) return 'Basic ' + auth authlib-1.3.2/tests/django/test_oauth2/test_authorization_code_grant.py000066400000000000000000000156301466226534200265270ustar00rootroot00000000000000import json from authlib.oauth2.rfc6749 import grants, errors from authlib.common.urls import urlparse, url_decode from django.test import override_settings from .models import User, Client, OAuth2Code from .models import CodeGrantMixin from .oauth2_server import TestCase class AuthorizationCodeGrant(CodeGrantMixin, grants.AuthorizationCodeGrant): TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none'] def save_authorization_code(self, code, request): auth_code = OAuth2Code( code=code, client_id=request.client.client_id, redirect_uri=request.redirect_uri, response_type=request.response_type, scope=request.scope, user=request.user, ) auth_code.save() class AuthorizationCodeTest(TestCase): def create_server(self): server = super().create_server() server.register_grant(AuthorizationCodeGrant) return server def prepare_data(self, response_type='code', grant_type='authorization_code', scope=''): user = User(username='foo') user.save() client = Client( user_id=user.pk, client_id='client', client_secret='secret', response_type=response_type, grant_type=grant_type, scope=scope, token_endpoint_auth_method='client_secret_basic', default_redirect_uri='https://a.b', ) client.save() def test_get_consent_grant_client(self): server = self.create_server() url = '/authorize?response_type=code' request = self.factory.get(url) self.assertRaises( errors.InvalidClientError, server.get_consent_grant, request ) url = '/authorize?response_type=code&client_id=client' request = self.factory.get(url) self.assertRaises( errors.InvalidClientError, server.get_consent_grant, request ) self.prepare_data(response_type='') self.assertRaises( errors.UnauthorizedClientError, server.get_consent_grant, request ) url = '/authorize?response_type=code&client_id=client&scope=profile&state=bar&redirect_uri=https%3A%2F%2Fa.b&response_type=code' request = self.factory.get(url) self.assertRaises( errors.InvalidRequestError, server.get_consent_grant, request ) def test_get_consent_grant_redirect_uri(self): server = self.create_server() self.prepare_data() base_url = '/authorize?response_type=code&client_id=client' url = base_url + '&redirect_uri=https%3A%2F%2Fa.c' request = self.factory.get(url) self.assertRaises( errors.InvalidRequestError, server.get_consent_grant, request ) url = base_url + '&redirect_uri=https%3A%2F%2Fa.b' request = self.factory.get(url) grant = server.get_consent_grant(request) self.assertIsInstance(grant, AuthorizationCodeGrant) def test_get_consent_grant_scope(self): server = self.create_server() server.scopes_supported = ['profile'] self.prepare_data() base_url = '/authorize?response_type=code&client_id=client' url = base_url + '&scope=invalid' request = self.factory.get(url) self.assertRaises( errors.InvalidScopeError, server.get_consent_grant, request ) def test_create_authorization_response(self): server = self.create_server() self.prepare_data() data = {'response_type': 'code', 'client_id': 'client'} request = self.factory.post('/authorize', data=data) server.get_consent_grant(request) resp = server.create_authorization_response(request) self.assertEqual(resp.status_code, 302) self.assertIn('error=access_denied', resp['Location']) grant_user = User.objects.get(username='foo') resp = server.create_authorization_response(request, grant_user=grant_user) self.assertEqual(resp.status_code, 302) self.assertIn('code=', resp['Location']) def test_create_token_response_invalid(self): server = self.create_server() self.prepare_data() # case: no auth request = self.factory.post('/oauth/token', data={'grant_type': 'authorization_code'}) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') auth_header = self.create_basic_auth('client', 'secret') # case: no code request = self.factory.post( '/oauth/token', data={'grant_type': 'authorization_code'}, HTTP_AUTHORIZATION=auth_header, ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_request') # case: invalid code request = self.factory.post( '/oauth/token', data={'grant_type': 'authorization_code', 'code': 'invalid'}, HTTP_AUTHORIZATION=auth_header, ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_grant') def test_create_token_response_success(self): self.prepare_data() data = self.get_token_response() self.assertIn('access_token', data) self.assertNotIn('refresh_token', data) @override_settings( AUTHLIB_OAUTH2_PROVIDER={'refresh_token_generator': True}) def test_create_token_response_with_refresh_token(self): self.prepare_data(grant_type='authorization_code\nrefresh_token') data = self.get_token_response() self.assertIn('access_token', data) self.assertIn('refresh_token', data) def get_token_response(self): server = self.create_server() data = {'response_type': 'code', 'client_id': 'client'} request = self.factory.post('/authorize', data=data) grant_user = User.objects.get(username='foo') resp = server.create_authorization_response(request, grant_user=grant_user) self.assertEqual(resp.status_code, 302) params = dict(url_decode(urlparse.urlparse(resp['Location']).query)) code = params['code'] request = self.factory.post( '/oauth/token', data={'grant_type': 'authorization_code', 'code': code}, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 200) data = json.loads(resp.content) return data authlib-1.3.2/tests/django/test_oauth2/test_client_credentials_grant.py000066400000000000000000000071461466226534200264730ustar00rootroot00000000000000import json from authlib.oauth2.rfc6749 import grants from .oauth2_server import TestCase from .models import User, Client class PasswordTest(TestCase): def create_server(self): server = super().create_server() server.register_grant(grants.ClientCredentialsGrant) return server def prepare_data(self, grant_type='client_credentials', scope=''): user = User(username='foo') user.save() client = Client( user_id=user.pk, client_id='client', client_secret='secret', scope=scope, grant_type=grant_type, token_endpoint_auth_method='client_secret_basic', default_redirect_uri='https://a.b', ) client.save() def test_invalid_client(self): server = self.create_server() self.prepare_data() request = self.factory.post( '/oauth/token', data={'grant_type': 'client_credentials'}, ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') request = self.factory.post( '/oauth/token', data={'grant_type': 'client_credentials'}, HTTP_AUTHORIZATION=self.create_basic_auth('invalid', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') def test_invalid_scope(self): server = self.create_server() server.scopes_supported = ['profile'] self.prepare_data() request = self.factory.post( '/oauth/token', data={'grant_type': 'client_credentials', 'scope': 'invalid'}, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_scope') def test_invalid_request(self): server = self.create_server() self.prepare_data() request = self.factory.get( '/oauth/token?grant_type=client_credentials', HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'unsupported_grant_type') def test_unauthorized_client(self): server = self.create_server() self.prepare_data(grant_type='invalid') request = self.factory.post( '/oauth/token', data={'grant_type': 'client_credentials'}, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'unauthorized_client') def test_authorize_token(self): server = self.create_server() self.prepare_data() request = self.factory.post( '/oauth/token', data={'grant_type': 'client_credentials'}, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 200) data = json.loads(resp.content) self.assertIn('access_token', data) authlib-1.3.2/tests/django/test_oauth2/test_implicit_grant.py000066400000000000000000000053121466226534200244430ustar00rootroot00000000000000from authlib.oauth2.rfc6749 import grants, errors from authlib.common.urls import urlparse, url_decode from .oauth2_server import TestCase from .models import User, Client class ImplicitTest(TestCase): def create_server(self): server = super().create_server() server.register_grant(grants.ImplicitGrant) return server def prepare_data(self, response_type='token', scope=''): user = User(username='foo') user.save() client = Client( user_id=user.pk, client_id='client', response_type=response_type, scope=scope, token_endpoint_auth_method='none', default_redirect_uri='https://a.b', ) client.save() def test_get_consent_grant_client(self): server = self.create_server() url = '/authorize?response_type=token' request = self.factory.get(url) self.assertRaises( errors.InvalidClientError, server.get_consent_grant, request ) url = '/authorize?response_type=token&client_id=client' request = self.factory.get(url) self.assertRaises( errors.InvalidClientError, server.get_consent_grant, request ) self.prepare_data(response_type='') self.assertRaises( errors.UnauthorizedClientError, server.get_consent_grant, request ) def test_get_consent_grant_scope(self): server = self.create_server() server.scopes_supported = ['profile'] self.prepare_data() base_url = '/authorize?response_type=token&client_id=client' url = base_url + '&scope=invalid' request = self.factory.get(url) self.assertRaises( errors.InvalidScopeError, server.get_consent_grant, request ) def test_create_authorization_response(self): server = self.create_server() self.prepare_data() data = {'response_type': 'token', 'client_id': 'client'} request = self.factory.post('/authorize', data=data) server.get_consent_grant(request) resp = server.create_authorization_response(request) self.assertEqual(resp.status_code, 302) params = dict(url_decode(urlparse.urlparse(resp['Location']).fragment)) self.assertEqual(params['error'], 'access_denied') grant_user = User.objects.get(username='foo') resp = server.create_authorization_response(request, grant_user=grant_user) self.assertEqual(resp.status_code, 302) params = dict(url_decode(urlparse.urlparse(resp['Location']).fragment)) self.assertIn('access_token', params) authlib-1.3.2/tests/django/test_oauth2/test_password_grant.py000066400000000000000000000130531466226534200244740ustar00rootroot00000000000000import json from authlib.oauth2.rfc6749.grants import ( ResourceOwnerPasswordCredentialsGrant as _PasswordGrant, ) from .oauth2_server import TestCase from .models import User, Client class PasswordGrant(_PasswordGrant): def authenticate_user(self, username, password): try: user = User.objects.get(username=username) if user.check_password(password): return user except User.DoesNotExist: return None class PasswordTest(TestCase): def create_server(self): server = super().create_server() server.register_grant(PasswordGrant) return server def prepare_data(self, grant_type='password', scope=''): user = User(username='foo') user.set_password('ok') user.save() client = Client( user_id=user.pk, client_id='client', client_secret='secret', scope=scope, grant_type=grant_type, token_endpoint_auth_method='client_secret_basic', default_redirect_uri='https://a.b', ) client.save() def test_invalid_client(self): server = self.create_server() self.prepare_data() request = self.factory.post( '/oauth/token', data={'grant_type': 'password', 'username': 'foo', 'password': 'ok'}, ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') request = self.factory.post( '/oauth/token', data={'grant_type': 'password', 'username': 'foo', 'password': 'ok'}, HTTP_AUTHORIZATION=self.create_basic_auth('invalid', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') def test_invalid_scope(self): server = self.create_server() server.scopes_supported = ['profile'] self.prepare_data() request = self.factory.post( '/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', 'scope': 'invalid', }, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_scope') def test_invalid_request(self): server = self.create_server() self.prepare_data() auth_header = self.create_basic_auth('client', 'secret') # case 1 request = self.factory.get( '/oauth/token?grant_type=password', HTTP_AUTHORIZATION=auth_header, ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'unsupported_grant_type') # case 2 request = self.factory.post( '/oauth/token', data={'grant_type': 'password'}, HTTP_AUTHORIZATION=auth_header, ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_request') # case 3 request = self.factory.post( '/oauth/token', data={'grant_type': 'password', 'username': 'foo'}, HTTP_AUTHORIZATION=auth_header, ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_request') # case 4 request = self.factory.post( '/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'wrong', }, HTTP_AUTHORIZATION=auth_header, ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_request') def test_unauthorized_client(self): server = self.create_server() self.prepare_data(grant_type='invalid') request = self.factory.post( '/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', }, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'unauthorized_client') def test_authorize_token(self): server = self.create_server() self.prepare_data() request = self.factory.post( '/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', }, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 200) data = json.loads(resp.content) self.assertIn('access_token', data) authlib-1.3.2/tests/django/test_oauth2/test_refresh_token.py000066400000000000000000000142371466226534200243020ustar00rootroot00000000000000import json import time from authlib.oauth2.rfc6749.grants import ( RefreshTokenGrant as _RefreshTokenGrant, ) from .models import User, Client, OAuth2Token from .oauth2_server import TestCase class RefreshTokenGrant(_RefreshTokenGrant): def authenticate_refresh_token(self, refresh_token): try: item = OAuth2Token.objects.get(refresh_token=refresh_token) if item.is_refresh_token_active(): return item except OAuth2Token.DoesNotExist: return None def authenticate_user(self, credential): return credential.user def revoke_old_credential(self, credential): now = int(time.time()) credential.access_token_revoked_at = now credential.refresh_token_revoked_at = now credential.save() return credential class RefreshTokenTest(TestCase): def create_server(self): server = super().create_server() server.register_grant(RefreshTokenGrant) return server def prepare_client(self, grant_type='refresh_token', scope=''): user = User(username='foo') user.save() client = Client( user_id=user.pk, client_id='client', client_secret='secret', scope=scope, grant_type=grant_type, token_endpoint_auth_method='client_secret_basic', default_redirect_uri='https://a.b', ) client.save() def prepare_token(self, scope='profile', user_id=1): token = OAuth2Token( user_id=user_id, client_id='client', token_type='bearer', access_token='a1', refresh_token='r1', scope=scope, expires_in=3600, ) token.save() def test_invalid_client(self): server = self.create_server() self.prepare_client() request = self.factory.post( '/oauth/token', data={'grant_type': 'refresh_token', 'refresh_token': 'foo'}, ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') request = self.factory.post( '/oauth/token', data={'grant_type': 'refresh_token', 'refresh_token': 'foo'}, HTTP_AUTHORIZATION=self.create_basic_auth('invalid', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') def test_invalid_refresh_token(self): self.prepare_client() server = self.create_server() auth_header = self.create_basic_auth('client', 'secret') request = self.factory.post( '/oauth/token', data={'grant_type': 'refresh_token'}, HTTP_AUTHORIZATION=auth_header ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_request') self.assertIn('Missing', data['error_description']) request = self.factory.post( '/oauth/token', data={'grant_type': 'refresh_token', 'refresh_token': 'invalid'}, HTTP_AUTHORIZATION=auth_header ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_grant') def test_invalid_scope(self): server = self.create_server() server.scopes_supported = ['profile'] self.prepare_client() self.prepare_token() request = self.factory.post( '/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'invalid', }, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_scope') def test_authorize_tno_scope(self): server = self.create_server() self.prepare_client() self.prepare_token() request = self.factory.post( '/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', }, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 200) data = json.loads(resp.content) self.assertIn('access_token', data) def test_authorize_token_scope(self): server = self.create_server() self.prepare_client() self.prepare_token() request = self.factory.post( '/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'profile', }, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 200) data = json.loads(resp.content) self.assertIn('access_token', data) def test_revoke_old_token(self): server = self.create_server() self.prepare_client() self.prepare_token() request = self.factory.post( '/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'profile', }, HTTP_AUTHORIZATION=self.create_basic_auth('client', 'secret'), ) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 200) data = json.loads(resp.content) self.assertIn('access_token', data) resp = server.create_token_response(request) self.assertEqual(resp.status_code, 400) authlib-1.3.2/tests/django/test_oauth2/test_resource_protector.py000066400000000000000000000112341466226534200253660ustar00rootroot00000000000000import json from authlib.integrations.django_oauth2 import ResourceProtector, BearerTokenValidator from django.http import JsonResponse from .models import User, Client, OAuth2Token from .oauth2_server import TestCase require_oauth = ResourceProtector() require_oauth.register_token_validator(BearerTokenValidator(OAuth2Token)) class ResourceProtectorTest(TestCase): def prepare_data(self, expires_in=3600, scope='profile'): user = User(username='foo') user.save() client = Client( user_id=user.pk, client_id='client', client_secret='secret', scope='profile', ) client.save() token = OAuth2Token( user_id=user.pk, client_id=client.client_id, token_type='bearer', access_token='a1', scope=scope, expires_in=expires_in, ) token.save() def test_invalid_token(self): @require_oauth('profile') def get_user_profile(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) self.prepare_data() request = self.factory.get('/user') resp = get_user_profile(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'missing_authorization') request = self.factory.get('/user', HTTP_AUTHORIZATION='invalid token') resp = get_user_profile(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'unsupported_token_type') request = self.factory.get('/user', HTTP_AUTHORIZATION='bearer token') resp = get_user_profile(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_token') def test_expired_token(self): self.prepare_data(-10) @require_oauth('profile') def get_user_profile(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) request = self.factory.get('/user', HTTP_AUTHORIZATION='bearer a1') resp = get_user_profile(request) self.assertEqual(resp.status_code, 401) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_token') def test_insufficient_token(self): self.prepare_data() @require_oauth('email') def get_user_email(request): user = request.oauth_token.user return JsonResponse(dict(email=user.email)) request = self.factory.get('/user/email', HTTP_AUTHORIZATION='bearer a1') resp = get_user_email(request) self.assertEqual(resp.status_code, 403) data = json.loads(resp.content) self.assertEqual(data['error'], 'insufficient_scope') def test_access_resource(self): self.prepare_data() @require_oauth('profile', optional=True) def get_user_profile(request): if request.oauth_token: user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) return JsonResponse(dict(sub=0, username='anonymous')) request = self.factory.get('/user') resp = get_user_profile(request) self.assertEqual(resp.status_code, 200) data = json.loads(resp.content) self.assertEqual(data['username'], 'anonymous') request = self.factory.get('/user', HTTP_AUTHORIZATION='bearer a1') resp = get_user_profile(request) self.assertEqual(resp.status_code, 200) data = json.loads(resp.content) self.assertEqual(data['username'], 'foo') def test_scope_operator(self): self.prepare_data() @require_oauth(['profile email']) def operator_and(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) @require_oauth(['profile', 'email']) def operator_or(request): user = request.oauth_token.user return JsonResponse(dict(sub=user.pk, username=user.username)) request = self.factory.get('/user', HTTP_AUTHORIZATION='bearer a1') resp = operator_and(request) self.assertEqual(resp.status_code, 403) data = json.loads(resp.content) self.assertEqual(data['error'], 'insufficient_scope') resp = operator_or(request) self.assertEqual(resp.status_code, 200) data = json.loads(resp.content) self.assertEqual(data['username'], 'foo') authlib-1.3.2/tests/django/test_oauth2/test_revocation_endpoint.py000066400000000000000000000111341466226534200255060ustar00rootroot00000000000000import json from authlib.integrations.django_oauth2 import RevocationEndpoint from .oauth2_server import TestCase from .models import User, OAuth2Token, Client ENDPOINT_NAME = RevocationEndpoint.ENDPOINT_NAME class RevocationEndpointTest(TestCase): def create_server(self): server = super().create_server() server.register_endpoint(RevocationEndpoint) return server def prepare_client(self): user = User(username='foo') user.save() client = Client( user_id=user.pk, client_id='client', client_secret='secret', token_endpoint_auth_method='client_secret_basic', default_redirect_uri='https://a.b', ) client.save() def prepare_token(self, scope='profile', user_id=1): token = OAuth2Token( user_id=user_id, client_id='client', token_type='bearer', access_token='a1', refresh_token='r1', scope=scope, expires_in=3600, ) token.save() def test_invalid_client(self): server = self.create_server() request = self.factory.post('/oauth/revoke') resp = server.create_endpoint_response(ENDPOINT_NAME, request) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') request = self.factory.post('/oauth/revoke', HTTP_AUTHORIZATION='invalid token') resp = server.create_endpoint_response(ENDPOINT_NAME, request) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') request = self.factory.post( '/oauth/revoke', HTTP_AUTHORIZATION=self.create_basic_auth('invalid', 'secret'), ) resp = server.create_endpoint_response(ENDPOINT_NAME, request) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') request = self.factory.post( '/oauth/revoke', HTTP_AUTHORIZATION=self.create_basic_auth('client', 'invalid'), ) resp = server.create_endpoint_response(ENDPOINT_NAME, request) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_client') def test_invalid_token(self): server = self.create_server() self.prepare_client() self.prepare_token() auth_header = self.create_basic_auth('client', 'secret') request = self.factory.post('/oauth/revoke', HTTP_AUTHORIZATION=auth_header) resp = server.create_endpoint_response(ENDPOINT_NAME, request) data = json.loads(resp.content) self.assertEqual(data['error'], 'invalid_request') # case 1 request = self.factory.post( '/oauth/revoke', data={'token': 'invalid-token'}, HTTP_AUTHORIZATION=auth_header, ) resp = server.create_endpoint_response(ENDPOINT_NAME, request) self.assertEqual(resp.status_code, 200) # case 2 request = self.factory.post( '/oauth/revoke', data={ 'token': 'a1', 'token_type_hint': 'unsupported_token_type', }, HTTP_AUTHORIZATION=auth_header, ) resp = server.create_endpoint_response(ENDPOINT_NAME, request) data = json.loads(resp.content) self.assertEqual(data['error'], 'unsupported_token_type') # case 3 request = self.factory.post( '/oauth/revoke', data={ 'token': 'a1', 'token_type_hint': 'refresh_token', }, HTTP_AUTHORIZATION=auth_header, ) resp = server.create_endpoint_response(ENDPOINT_NAME, request) self.assertEqual(resp.status_code, 200) def test_revoke_token_with_hint(self): self.prepare_client() self.prepare_token() self.revoke_token({'token': 'a1', 'token_type_hint': 'access_token'}) self.revoke_token({'token': 'r1', 'token_type_hint': 'refresh_token'}) def test_revoke_token_without_hint(self): self.prepare_client() self.prepare_token() self.revoke_token({'token': 'a1'}) self.revoke_token({'token': 'r1'}) def revoke_token(self, data): server = self.create_server() auth_header = self.create_basic_auth('client', 'secret') request = self.factory.post( '/oauth/revoke', data=data, HTTP_AUTHORIZATION=auth_header, ) resp = server.create_endpoint_response(ENDPOINT_NAME, request) self.assertEqual(resp.status_code, 200) authlib-1.3.2/tests/django_helper.py000066400000000000000000000012321466226534200174720ustar00rootroot00000000000000from django.test import TestCase as _TestCase, RequestFactory from django.conf import settings from django.utils.module_loading import import_module class RequestClient(RequestFactory): @property def session(self): engine = import_module(settings.SESSION_ENGINE) cookie = self.cookies.get(settings.SESSION_COOKIE_NAME) if cookie: return engine.SessionStore(cookie.value) session = engine.SessionStore() session.save() self.cookies[settings.SESSION_COOKIE_NAME] = session.session_key return session class TestCase(_TestCase): def setUp(self): self.factory = RequestClient() authlib-1.3.2/tests/files/000077500000000000000000000000001466226534200154235ustar00rootroot00000000000000authlib-1.3.2/tests/files/ed25519-pkcs8.pem000066400000000000000000000001671466226534200201560ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIJ1hsZ3v/VpguoRK9JLsLMREScVpezJpGXA7rAMcrn9g -----END PRIVATE KEY----- authlib-1.3.2/tests/files/ed25519-pub.pem000066400000000000000000000001611466226534200177060ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo= -----END PUBLIC KEY----- authlib-1.3.2/tests/files/ed25519-ssh.pub000066400000000000000000000001421466226534200177210ustar00rootroot00000000000000ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAA/RNCWJ6VfjsWW3fGjdbeM+9VbX+iXCQu02B09Bw24 demo@authlib.org authlib-1.3.2/tests/files/jwk_private.json000066400000000000000000000032301466226534200206410ustar00rootroot00000000000000{"kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB", "d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ", "p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nRaO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmGpeNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0k", "q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc", "dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn-RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX59ehik", "dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pErAMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJKbi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdKT1cYF8", "qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-NZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDhjJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpPz8aaI4"} authlib-1.3.2/tests/files/jwk_public.json000066400000000000000000000006621466226534200204530ustar00rootroot00000000000000{"kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB"} authlib-1.3.2/tests/files/jwks_private.json000066400000000000000000000064441466226534200210360ustar00rootroot00000000000000{ "keys": [ {"kty": "RSA", "kid": "abc", "n": "pF1JaMSN8TEsh4N4O_5SpEAVLivJyLH-Cgl3OQBPGgJkt8cg49oasl-5iJS-VdrILxWM9_JCJyURpUuslX4Eb4eUBtQ0x5BaPa8-S2NLdGTaL7nBOO8o8n0C5FEUU-qlEip79KE8aqOj-OC44VsIquSmOvWIQD26n3fCVlgwoRBD1gzzsDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMsRNACmBomzgA4M1TTsisSUO87444pe35Z4_n5c735o2fZMrGgMwiJNh7rT8SYxtIkxngioiGnwkxGQxQ4NzPAHg-XSY0J04pNm7KqTkgtxyrqOANJLIjXlR-U9SQ90NjHVQ", "e": "AQAB", "d": "G4E84ppZwm3fLMI0YZ26iJ_sq3BKcRpQD6_r0o8ZrZmO7y4Uc-ywoP7h1lhFzaox66cokuloZpKOdGHIfK-84EkI3WeveWHPqBjmTMlN_ClQVcI48mUbLhD7Zeenhi9y9ipD2fkNWi8OJny8k4GfXrGqm50w8schrsPksnxJjvocGMT6KZNfDURKF2HlM5X1uY8VCofokXOjBEeHIfYM8e7IcmPpyXwXKonDmVVbMbefo-u-TttgeyOYaO6s3flSy6Y0CnpWi43JQ_VEARxQl6Brj1oizr8UnQQ0nNCOWwDNVtOV4eSl7PZoiiT7CxYkYnhJXECMAM5YBpm4Qk9zdQ", "p": "1g4ZGrXOuo75p9_MRIepXGpBWxip4V7B9XmO9WzPCv8nMorJntWBmsYV1I01aITxadHatO4Gl2xLniNkDyrEQzJ7w38RQgsVK-CqbnC0K9N77QPbHeC1YQd9RCNyUohOimKvb7jyv798FBU1GO5QI2eNgfnnfteSVXhD2iOoTOs", "q": "xJJ-8toxJdnLa0uUsAbql6zeNXGbUBMzu3FomKlyuWuq841jS2kIalaO_TRj5hbnE45jmCjeLgTVO6Ach3Wfk4zrqajqfFJ0zUg_Wexp49lC3RWiV4icBb85Q6bzeJD9Dn9vhjpfWVkczf_NeA1fGH_pcgfkT6Dm706GFFttLL8", "dp": "Zfx3l5NR-O8QIhzuHSSp279Afl_E6P0V2phdNa_vAaVKDrmzkHrXcl-4nPnenXrh7vIuiw_xkgnmCWWBUfylYALYlu-e0GGpZ6t2aIJIRa1QmT_CEX0zzhQcae-dk5cgHK0iO0_aUOOyAXuNPeClzAiVknz4ACZDsXdIlNFyaZs", "dq": "Z9DG4xOBKXBhEoWUPXMpqnlN0gPx9tRtWe2HRDkZsfu_CWn-qvEJ1L9qPSfSKs6ls5pb1xyeWseKpjblWlUwtgiS3cOsM4SI03H4o1FMi11PBtxKJNitLgvT_nrJ0z8fpux-xfFGMjXyFImoxmKpepLzg5nPZo6f6HscLNwsSJk", "qi": "Sk20wFvilpRKHq79xxFWiDUPHi0x0pp82dYIEntGQkKUWkbSlhgf3MAi5NEQTDmXdnB-rVeWIvEi-BXfdnNgdn8eC4zSdtF4sIAhYr5VWZo0WVWDhT7u2ccvZBFymiz8lo3gN57wGUCi9pbZqzV1-ZppX6YTNDdDCE0q-KO3Cec"}, {"kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB", "d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ", "p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nRaO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmGpeNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0k", "q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc", "dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn-RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX59ehik", "dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pErAMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJKbi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdKT1cYF8", "qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-NZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDhjJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpPz8aaI4"} ] } authlib-1.3.2/tests/files/jwks_public.json000066400000000000000000000015301466226534200206310ustar00rootroot00000000000000{ "keys": [ {"kty": "RSA", "kid": "abc", "n": "pF1JaMSN8TEsh4N4O_5SpEAVLivJyLH-Cgl3OQBPGgJkt8cg49oasl-5iJS-VdrILxWM9_JCJyURpUuslX4Eb4eUBtQ0x5BaPa8-S2NLdGTaL7nBOO8o8n0C5FEUU-qlEip79KE8aqOj-OC44VsIquSmOvWIQD26n3fCVlgwoRBD1gzzsDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMsRNACmBomzgA4M1TTsisSUO87444pe35Z4_n5c735o2fZMrGgMwiJNh7rT8SYxtIkxngioiGnwkxGQxQ4NzPAHg-XSY0J04pNm7KqTkgtxyrqOANJLIjXlR-U9SQ90NjHVQ", "e": "AQAB"}, {"kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB"} ] } authlib-1.3.2/tests/files/jwks_single_private.json000066400000000000000000000031711466226534200223710ustar00rootroot00000000000000{ "keys": [ {"kty": "RSA", "n": "pF1JaMSN8TEsh4N4O_5SpEAVLivJyLH-Cgl3OQBPGgJkt8cg49oasl-5iJS-VdrILxWM9_JCJyURpUuslX4Eb4eUBtQ0x5BaPa8-S2NLdGTaL7nBOO8o8n0C5FEUU-qlEip79KE8aqOj-OC44VsIquSmOvWIQD26n3fCVlgwoRBD1gzzsDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMsRNACmBomzgA4M1TTsisSUO87444pe35Z4_n5c735o2fZMrGgMwiJNh7rT8SYxtIkxngioiGnwkxGQxQ4NzPAHg-XSY0J04pNm7KqTkgtxyrqOANJLIjXlR-U9SQ90NjHVQ", "e": "AQAB", "d": "G4E84ppZwm3fLMI0YZ26iJ_sq3BKcRpQD6_r0o8ZrZmO7y4Uc-ywoP7h1lhFzaox66cokuloZpKOdGHIfK-84EkI3WeveWHPqBjmTMlN_ClQVcI48mUbLhD7Zeenhi9y9ipD2fkNWi8OJny8k4GfXrGqm50w8schrsPksnxJjvocGMT6KZNfDURKF2HlM5X1uY8VCofokXOjBEeHIfYM8e7IcmPpyXwXKonDmVVbMbefo-u-TttgeyOYaO6s3flSy6Y0CnpWi43JQ_VEARxQl6Brj1oizr8UnQQ0nNCOWwDNVtOV4eSl7PZoiiT7CxYkYnhJXECMAM5YBpm4Qk9zdQ", "p": "1g4ZGrXOuo75p9_MRIepXGpBWxip4V7B9XmO9WzPCv8nMorJntWBmsYV1I01aITxadHatO4Gl2xLniNkDyrEQzJ7w38RQgsVK-CqbnC0K9N77QPbHeC1YQd9RCNyUohOimKvb7jyv798FBU1GO5QI2eNgfnnfteSVXhD2iOoTOs", "q": "xJJ-8toxJdnLa0uUsAbql6zeNXGbUBMzu3FomKlyuWuq841jS2kIalaO_TRj5hbnE45jmCjeLgTVO6Ach3Wfk4zrqajqfFJ0zUg_Wexp49lC3RWiV4icBb85Q6bzeJD9Dn9vhjpfWVkczf_NeA1fGH_pcgfkT6Dm706GFFttLL8", "dp": "Zfx3l5NR-O8QIhzuHSSp279Afl_E6P0V2phdNa_vAaVKDrmzkHrXcl-4nPnenXrh7vIuiw_xkgnmCWWBUfylYALYlu-e0GGpZ6t2aIJIRa1QmT_CEX0zzhQcae-dk5cgHK0iO0_aUOOyAXuNPeClzAiVknz4ACZDsXdIlNFyaZs", "dq": "Z9DG4xOBKXBhEoWUPXMpqnlN0gPx9tRtWe2HRDkZsfu_CWn-qvEJ1L9qPSfSKs6ls5pb1xyeWseKpjblWlUwtgiS3cOsM4SI03H4o1FMi11PBtxKJNitLgvT_nrJ0z8fpux-xfFGMjXyFImoxmKpepLzg5nPZo6f6HscLNwsSJk", "qi": "Sk20wFvilpRKHq79xxFWiDUPHi0x0pp82dYIEntGQkKUWkbSlhgf3MAi5NEQTDmXdnB-rVeWIvEi-BXfdnNgdn8eC4zSdtF4sIAhYr5VWZo0WVWDhT7u2ccvZBFymiz8lo3gN57wGUCi9pbZqzV1-ZppX6YTNDdDCE0q-KO3Cec"} ] } authlib-1.3.2/tests/files/jwks_single_public.json000066400000000000000000000006411466226534200221740ustar00rootroot00000000000000{ "keys": [ {"kty": "RSA", "kid": "abc", "n": "pF1JaMSN8TEsh4N4O_5SpEAVLivJyLH-Cgl3OQBPGgJkt8cg49oasl-5iJS-VdrILxWM9_JCJyURpUuslX4Eb4eUBtQ0x5BaPa8-S2NLdGTaL7nBOO8o8n0C5FEUU-qlEip79KE8aqOj-OC44VsIquSmOvWIQD26n3fCVlgwoRBD1gzzsDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMsRNACmBomzgA4M1TTsisSUO87444pe35Z4_n5c735o2fZMrGgMwiJNh7rT8SYxtIkxngioiGnwkxGQxQ4NzPAHg-XSY0J04pNm7KqTkgtxyrqOANJLIjXlR-U9SQ90NjHVQ", "e": "AQAB"} ] } authlib-1.3.2/tests/files/rsa_private.pem000066400000000000000000000032131466226534200204440ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEApF1JaMSN8TEsh4N4O/5SpEAVLivJyLH+Cgl3OQBPGgJkt8cg 49oasl+5iJS+VdrILxWM9/JCJyURpUuslX4Eb4eUBtQ0x5BaPa8+S2NLdGTaL7nB OO8o8n0C5FEUU+qlEip79KE8aqOj+OC44VsIquSmOvWIQD26n3fCVlgwoRBD1gzz sDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMsRNACmBomzgA4M1TTsisSUO87444p e35Z4/n5c735o2fZMrGgMwiJNh7rT8SYxtIkxngioiGnwkxGQxQ4NzPAHg+XSY0J 04pNm7KqTkgtxyrqOANJLIjXlR+U9SQ90NjHVQIDAQABAoIBABuBPOKaWcJt3yzC NGGduoif7KtwSnEaUA+v69KPGa2Zju8uFHPssKD+4dZYRc2qMeunKJLpaGaSjnRh yHyvvOBJCN1nr3lhz6gY5kzJTfwpUFXCOPJlGy4Q+2Xnp4YvcvYqQ9n5DVovDiZ8 vJOBn16xqpudMPLHIa7D5LJ8SY76HBjE+imTXw1EShdh5TOV9bmPFQqH6JFzowRH hyH2DPHuyHJj6cl8FyqJw5lVWzG3n6Prvk7bYHsjmGjurN35UsumNAp6VouNyUP1 RAEcUJega49aIs6/FJ0ENJzQjlsAzVbTleHkpez2aIok+wsWJGJ4SVxAjADOWAaZ uEJPc3UCgYEA1g4ZGrXOuo75p9/MRIepXGpBWxip4V7B9XmO9WzPCv8nMorJntWB msYV1I01aITxadHatO4Gl2xLniNkDyrEQzJ7w38RQgsVK+CqbnC0K9N77QPbHeC1 YQd9RCNyUohOimKvb7jyv798FBU1GO5QI2eNgfnnfteSVXhD2iOoTOsCgYEAxJJ+ 8toxJdnLa0uUsAbql6zeNXGbUBMzu3FomKlyuWuq841jS2kIalaO/TRj5hbnE45j mCjeLgTVO6Ach3Wfk4zrqajqfFJ0zUg/Wexp49lC3RWiV4icBb85Q6bzeJD9Dn9v hjpfWVkczf/NeA1fGH/pcgfkT6Dm706GFFttLL8CgYBl/HeXk1H47xAiHO4dJKnb v0B+X8To/RXamF01r+8BpUoOubOQetdyX7ic+d6deuHu8i6LD/GSCeYJZYFR/KVg AtiW757QYalnq3ZogkhFrVCZP8IRfTPOFBxp752TlyAcrSI7T9pQ47IBe4094KXM CJWSfPgAJkOxd0iU0XJpmwKBgGfQxuMTgSlwYRKFlD1zKap5TdID8fbUbVnth0Q5 GbH7vwlp/qrxCdS/aj0n0irOpbOaW9ccnlrHiqY25VpVMLYIkt3DrDOEiNNx+KNR TItdTwbcSiTYrS4L0/56ydM/H6bsfsXxRjI18hSJqMZiqXqS84OZz2aOn+h7HCzc LEiZAoGASk20wFvilpRKHq79xxFWiDUPHi0x0pp82dYIEntGQkKUWkbSlhgf3MAi 5NEQTDmXdnB+rVeWIvEi+BXfdnNgdn8eC4zSdtF4sIAhYr5VWZo0WVWDhT7u2ccv ZBFymiz8lo3gN57wGUCi9pbZqzV1+ZppX6YTNDdDCE0q+KO3Cec= -----END RSA PRIVATE KEY----- authlib-1.3.2/tests/files/rsa_public.pem000066400000000000000000000007031466226534200202510ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApF1JaMSN8TEsh4N4O/5S pEAVLivJyLH+Cgl3OQBPGgJkt8cg49oasl+5iJS+VdrILxWM9/JCJyURpUuslX4E b4eUBtQ0x5BaPa8+S2NLdGTaL7nBOO8o8n0C5FEUU+qlEip79KE8aqOj+OC44VsI quSmOvWIQD26n3fCVlgwoRBD1gzzsDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMs RNACmBomzgA4M1TTsisSUO87444pe35Z4/n5c735o2fZMrGgMwiJNh7rT8SYxtIk xngioiGnwkxGQxQ4NzPAHg+XSY0J04pNm7KqTkgtxyrqOANJLIjXlR+U9SQ90NjH VQIDAQAB -----END PUBLIC KEY----- authlib-1.3.2/tests/files/secp256k1-private.pem000066400000000000000000000003551466226534200212240ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgTHXBopHraQcg1U8bPK63 eO5tNMt5ZcHo/1RsJkSnLAahRANCAAROhceIcao7c/9Ei6PgBLr3+UgDbkxSCJ0d KDtXgKipXfrI1mVHys/FJ0TzvNPCEZNpPPeWYd/sr5V6ADhdQsHe -----END PRIVATE KEY----- authlib-1.3.2/tests/files/secp256k1-pub.pem000066400000000000000000000002561466226534200203400ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEToXHiHGqO3P/RIuj4AS69/lIA25MUgid HSg7V4CoqV36yNZlR8rPxSdE87zTwhGTaTz3lmHf7K+VegA4XULB3g== -----END PUBLIC KEY----- authlib-1.3.2/tests/files/secp521r1-private.json000066400000000000000000000005701466226534200214150ustar00rootroot00000000000000{"kty": "EC", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "crv": "P-521", "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1", "d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt"} authlib-1.3.2/tests/files/secp521r1-public.json000066400000000000000000000004271466226534200212220ustar00rootroot00000000000000{"kty": "EC", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "crv": "P-521", "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1"} authlib-1.3.2/tests/files/ssh_private.pem000066400000000000000000000034371466226534200204640ustar00rootroot00000000000000-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn NhAAAAAwEAAQAAAQEApvV61Iw9YMrS8pAC9NAPf6ESkqNSpROGu+T0/hR7jSohlUpQk/C3 65Zjlw+ivCY0WQeTrYMmqixx1UgSn4b6BDehsMAG72LugMO1edsXqhvhbXxEjZiogEHpkl A8u2O9fqaSaA2OcmQs+v7ptGAwYTxIPIM/5A4l8NKfwQy4qNrjPbFaDUQF8v6Y6/PgmoIm sT9eLKxcb2qZ0inBh7IdeeNoeJD25vql7qGrZrNizxf2QrmdvHLOk8a38jsVVnxf48/0pc H8jy+wMaeBEwlB1W68joFndKdSr+Nf8uYSCULeQRE3yTKPsNEJsODvtAWipWtA73UAc5fZ kTH69QXf7QAAA8jT7CeP0+wnjwAAAAdzc2gtcnNhAAABAQCm9XrUjD1gytLykAL00A9/oR KSo1KlE4a75PT+FHuNKiGVSlCT8LfrlmOXD6K8JjRZB5OtgyaqLHHVSBKfhvoEN6GwwAbv Yu6Aw7V52xeqG+FtfESNmKiAQemSUDy7Y71+ppJoDY5yZCz6/um0YDBhPEg8gz/kDiXw0p /BDLio2uM9sVoNRAXy/pjr8+CagiaxP14srFxvapnSKcGHsh1542h4kPbm+qXuoatms2LP F/ZCuZ28cs6TxrfyOxVWfF/jz/SlwfyPL7Axp4ETCUHVbryOgWd0p1Kv41/y5hIJQt5BET fJMo+w0Qmw4O+0BaKla0DvdQBzl9mRMfr1Bd/tAAAAAwEAAQAAAQAHB1iXcBv5YjCFQ6jM M6IjZl2IzNVi27KVYGsr8yLMa5SkW0+PGtgzU853gpIAR792bAo8iMPs8BgOuY0HKECIQu tMrJjeJRUTWKngKmgRokDYQh4EtAOL+rphjX0xCl7k4RBSzxdLG4qFOowOKup+fXIxEflz mWDhhYxyLR3tBdCR64jm36oYuGZLv3UAH3p8CWrXW/EiWfHmiiEEtVsgVXZtX/QMKezJh0 2BNBIGOa2bXV+I4sdrq/f49vLO0YiGaQ/Zx85fmzfoBMlQexqKWW/zbtxebf0sE9r5HAtv Tb3LqXyb5Dqy1JI00Sm77QoZgiz524sRRwVTUyN0os9hAAAAgDEID/fmFnPcdHtX6Gnlkr fyivhrWUJGt1jmZLrXrCTB80XXJOF5yhym2p6Hwr3JDQKIIu6+eVUpnsMHeiU8WTBSKbnP BhtJFi/1uTLpJgTfLOZS7CewCkGqAIiUlVybz2GdIeyEKFwOkKwRSN7OqdanIoIX8zi7P8 n/24xflap7AAAAgQDavsAywdgi96CdC7FIZK4X5nZuI7C7rrgPUkEb9KPnfNlT+N0QNIFK xyFfB0L3m2eGpJRKTtMGl1zKuD+Ecl729bnHqKKnbprd+jUlR8Bg9+Aq/rJtUue3nLBg08 0bBUNewDmTp9R96uNr4ZiGYV6d3+ampgQ6+mukKKP6H8D6JQAAAIEAw2Tb6/c1+uKjwiYP HvnvpupXeFw7srnqp2eAxB3UmvhAqUB6tH0H+YADCFATbMB5nUPdnFW/q+wj0AE0R7Qcun 7HvwztlZN9ZCYfvzy5hSZN3jdOk/I1TAObv6iDhuV0ZplP3kXZDPFT3VU7vcBBUkp04G5S WBgEKVBjECn1kCkAAAAQZGVtb0BhdXRobGliLm9yZwECAw== -----END OPENSSH PRIVATE KEY----- authlib-1.3.2/tests/files/ssh_public.pem000066400000000000000000000006161466226534200202640ustar00rootroot00000000000000ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCm9XrUjD1gytLykAL00A9/oRKSo1KlE4a75PT+FHuNKiGVSlCT8LfrlmOXD6K8JjRZB5OtgyaqLHHVSBKfhvoEN6GwwAbvYu6Aw7V52xeqG+FtfESNmKiAQemSUDy7Y71+ppJoDY5yZCz6/um0YDBhPEg8gz/kDiXw0p/BDLio2uM9sVoNRAXy/pjr8+CagiaxP14srFxvapnSKcGHsh1542h4kPbm+qXuoatms2LPF/ZCuZ28cs6TxrfyOxVWfF/jz/SlwfyPL7Axp4ETCUHVbryOgWd0p1Kv41/y5hIJQt5BETfJMo+w0Qmw4O+0BaKla0DvdQBzl9mRMfr1Bd/t demo@authlib.org authlib-1.3.2/tests/files/thumbprint_example.json000066400000000000000000000006541466226534200222320ustar00rootroot00000000000000{ "kty": "RSA", "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", "e": "AQAB", "alg": "RS256", "kid": "2011-04-29" } authlib-1.3.2/tests/flask/000077500000000000000000000000001466226534200154215ustar00rootroot00000000000000authlib-1.3.2/tests/flask/__init__.py000066400000000000000000000000001466226534200175200ustar00rootroot00000000000000authlib-1.3.2/tests/flask/cache.py000066400000000000000000000033101466226534200170330ustar00rootroot00000000000000import time try: import cPickle as pickle except ImportError: import pickle class SimpleCache: """A SimpleCache for testing. Copied from Werkzeug.""" def __init__(self, threshold=500, default_timeout=300): self.default_timeout = default_timeout self._cache = {} self.clear = self._cache.clear self._threshold = threshold def _prune(self): if len(self._cache) > self._threshold: now = time.time() toremove = [] for idx, (key, (expires, _)) in enumerate(self._cache.items()): if (expires != 0 and expires <= now) or idx % 3 == 0: toremove.append(key) for key in toremove: self._cache.pop(key, None) def _normalize_timeout(self, timeout): if timeout is None: timeout = self.default_timeout if timeout > 0: timeout = time.time() + timeout return timeout def get(self, key): try: expires, value = self._cache[key] if expires == 0 or expires > time.time(): return pickle.loads(value) except (KeyError, pickle.PickleError): return None def set(self, key, value, timeout=None): expires = self._normalize_timeout(timeout) self._prune() self._cache[key] = ( expires, pickle.dumps(value, pickle.HIGHEST_PROTOCOL) ) return True def delete(self, key): return self._cache.pop(key, None) is not None def has(self, key): try: expires, value = self._cache[key] return expires == 0 or expires > time.time() except KeyError: return False authlib-1.3.2/tests/flask/test_oauth1/000077500000000000000000000000001466226534200176615ustar00rootroot00000000000000authlib-1.3.2/tests/flask/test_oauth1/__init__.py000066400000000000000000000000001466226534200217600ustar00rootroot00000000000000authlib-1.3.2/tests/flask/test_oauth1/oauth1_server.py000066400000000000000000000225731466226534200230330ustar00rootroot00000000000000import os import unittest from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy from authlib.oauth1 import ( ClientMixin, TokenCredentialMixin, TemporaryCredentialMixin, ) from authlib.integrations.flask_oauth1 import ( AuthorizationServer, ResourceProtector, current_credential ) from authlib.integrations.flask_oauth1 import ( register_temporary_credential_hooks, register_nonce_hooks, create_exists_nonce_func as create_cache_exists_nonce_func, ) from authlib.oauth1.errors import OAuth1Error from authlib.common.urls import url_encode from tests.util import read_file_path from ..cache import SimpleCache db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(40), unique=True, nullable=False) def get_user_id(self): return self.id class Client(ClientMixin, db.Model): id = db.Column(db.Integer, primary_key=True) client_id = db.Column(db.String(48), index=True) client_secret = db.Column(db.String(120), nullable=False) default_redirect_uri = db.Column(db.Text, nullable=False, default='') user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') def get_default_redirect_uri(self): return self.default_redirect_uri def get_client_secret(self): return self.client_secret def get_rsa_public_key(self): return read_file_path('rsa_public.pem') class TokenCredential(TokenCredentialMixin, db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') client_id = db.Column(db.String(48), index=True) oauth_token = db.Column(db.String(84), unique=True, index=True) oauth_token_secret = db.Column(db.String(84)) def get_oauth_token(self): return self.oauth_token def get_oauth_token_secret(self): return self.oauth_token_secret class TemporaryCredential(TemporaryCredentialMixin, db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') client_id = db.Column(db.String(48), index=True) oauth_token = db.Column(db.String(84), unique=True, index=True) oauth_token_secret = db.Column(db.String(84)) oauth_verifier = db.Column(db.String(84)) oauth_callback = db.Column(db.Text, default='') def get_user_id(self): return self.user_id def get_client_id(self): return self.client_id def get_redirect_uri(self): return self.oauth_callback def check_verifier(self, verifier): return self.oauth_verifier == verifier def get_oauth_token(self): return self.oauth_token def get_oauth_token_secret(self): return self.oauth_token_secret class TimestampNonce(db.Model): __table_args__ = ( db.UniqueConstraint( 'client_id', 'timestamp', 'nonce', 'oauth_token', name='unique_nonce' ), ) id = db.Column(db.Integer, primary_key=True) client_id = db.Column(db.String(48), nullable=False) timestamp = db.Column(db.Integer, nullable=False) nonce = db.Column(db.String(48), nullable=False) oauth_token = db.Column(db.String(84)) def exists_nonce(nonce, timestamp, client_id, oauth_token): q = TimestampNonce.query.filter_by( nonce=nonce, timestamp=timestamp, client_id=client_id, ) if oauth_token: q = q.filter_by(oauth_token=oauth_token) rv = q.first() if rv: return True item = TimestampNonce( nonce=nonce, timestamp=timestamp, client_id=client_id, oauth_token=oauth_token, ) db.session.add(item) db.session.commit() return False def create_temporary_credential(token, client_id, redirect_uri): item = TemporaryCredential( client_id=client_id, oauth_token=token['oauth_token'], oauth_token_secret=token['oauth_token_secret'], oauth_callback=redirect_uri, ) db.session.add(item) db.session.commit() return item def get_temporary_credential(oauth_token): return TemporaryCredential.query.filter_by(oauth_token=oauth_token).first() def delete_temporary_credential(oauth_token): q = TemporaryCredential.query.filter_by(oauth_token=oauth_token) q.delete(synchronize_session=False) db.session.commit() def create_authorization_verifier(credential, grant_user, verifier): credential.user_id = grant_user.id # assuming your end user model has `.id` credential.oauth_verifier = verifier db.session.add(credential) db.session.commit() return credential def create_token_credential(token, temporary_credential): credential = TokenCredential( oauth_token=token['oauth_token'], oauth_token_secret=token['oauth_token_secret'], client_id=temporary_credential.get_client_id() ) credential.user_id = temporary_credential.get_user_id() db.session.add(credential) db.session.commit() return credential def create_authorization_server(app, use_cache=False, lazy=False): def query_client(client_id): return Client.query.filter_by(client_id=client_id).first() if lazy: server = AuthorizationServer() server.init_app(app, query_client) else: server = AuthorizationServer(app, query_client=query_client) if use_cache: cache = SimpleCache() register_nonce_hooks(server, cache) register_temporary_credential_hooks(server, cache) server.register_hook('create_token_credential', create_token_credential) else: server.register_hook('exists_nonce', exists_nonce) server.register_hook('create_temporary_credential', create_temporary_credential) server.register_hook('get_temporary_credential', get_temporary_credential) server.register_hook('delete_temporary_credential', delete_temporary_credential) server.register_hook('create_authorization_verifier', create_authorization_verifier) server.register_hook('create_token_credential', create_token_credential) @app.route('/oauth/initiate', methods=['GET', 'POST']) def initiate(): return server.create_temporary_credentials_response() @app.route('/oauth/authorize', methods=['GET', 'POST']) def authorize(): if request.method == 'GET': try: server.check_authorization_request() return 'ok' except OAuth1Error: return 'error' user_id = request.form.get('user_id') if user_id: grant_user = db.session.get(User, int(user_id)) else: grant_user = None try: return server.create_authorization_response(grant_user=grant_user) except OAuth1Error as error: return url_encode(error.get_body()) @app.route('/oauth/token', methods=['POST']) def issue_token(): return server.create_token_response() return server def create_resource_server(app, use_cache=False, lazy=False): if use_cache: cache = SimpleCache() exists_nonce = create_cache_exists_nonce_func(cache) else: def exists_nonce(nonce, timestamp, client_id, oauth_token): q = db.session.query(TimestampNonce.nonce).filter_by( nonce=nonce, timestamp=timestamp, client_id=client_id, ) if oauth_token: q = q.filter_by(oauth_token=oauth_token) rv = q.first() if rv: return True tn = TimestampNonce( nonce=nonce, timestamp=timestamp, client_id=client_id, oauth_token=oauth_token, ) db.session.add(tn) db.session.commit() return False def query_client(client_id): return Client.query.filter_by(client_id=client_id).first() def query_token(client_id, oauth_token): return TokenCredential.query.filter_by(client_id=client_id, oauth_token=oauth_token).first() if lazy: require_oauth = ResourceProtector() require_oauth.init_app(app, query_client, query_token, exists_nonce) else: require_oauth = ResourceProtector( app, query_client, query_token, exists_nonce) @app.route('/user') @require_oauth() def user_profile(): user = current_credential.user return jsonify(id=user.id, username=user.username) def create_flask_app(): app = Flask(__name__) app.debug = True app.testing = True app.secret_key = 'testing' app.config.update({ 'OAUTH1_SUPPORTED_SIGNATURE_METHODS': ['PLAINTEXT', 'HMAC-SHA1', 'RSA-SHA1'], 'SQLALCHEMY_TRACK_MODIFICATIONS': False, 'SQLALCHEMY_DATABASE_URI': 'sqlite://' }) return app class TestCase(unittest.TestCase): def setUp(self): os.environ['AUTHLIB_INSECURE_TRANSPORT'] = 'true' app = create_flask_app() self._ctx = app.app_context() self._ctx.push() db.init_app(app) db.create_all() self.app = app self.client = app.test_client() def tearDown(self): db.drop_all() self._ctx.pop() os.environ.pop('AUTHLIB_INSECURE_TRANSPORT') authlib-1.3.2/tests/flask/test_oauth1/test_authorize.py000066400000000000000000000077651466226534200233230ustar00rootroot00000000000000from tests.util import decode_response from .oauth1_server import db, User, Client from .oauth1_server import ( TestCase, create_authorization_server, ) class AuthorizationWithCacheTest(TestCase): USE_CACHE = True def prepare_data(self): create_authorization_server(self.app, self.USE_CACHE, self.USE_CACHE) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='client', client_secret='secret', default_redirect_uri='https://a.b', ) db.session.add(client) db.session.commit() def test_invalid_authorization(self): self.prepare_data() url = '/oauth/authorize' # case 1 rv = self.client.post(url, data={'user_id': '1'}) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_token', data['error_description']) # case 2 rv = self.client.post(url, data={'user_id': '1', 'oauth_token': 'a'}) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_token') def test_authorize_denied(self): self.prepare_data() initiate_url = '/oauth/initiate' authorize_url = '/oauth/authorize' rv = self.client.post(initiate_url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) data = decode_response(rv.data) self.assertIn('oauth_token', data) rv = self.client.post(authorize_url, data={ 'oauth_token': data['oauth_token'] }) self.assertEqual(rv.status_code, 302) self.assertIn('access_denied', rv.headers['Location']) self.assertIn('https://a.b', rv.headers['Location']) rv = self.client.post(initiate_url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'https://i.test', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) data = decode_response(rv.data) self.assertIn('oauth_token', data) rv = self.client.post(authorize_url, data={ 'oauth_token': data['oauth_token'] }) self.assertEqual(rv.status_code, 302) self.assertIn('access_denied', rv.headers['Location']) self.assertIn('https://i.test', rv.headers['Location']) def test_authorize_granted(self): self.prepare_data() initiate_url = '/oauth/initiate' authorize_url = '/oauth/authorize' rv = self.client.post(initiate_url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) data = decode_response(rv.data) self.assertIn('oauth_token', data) rv = self.client.post(authorize_url, data={ 'user_id': '1', 'oauth_token': data['oauth_token'] }) self.assertEqual(rv.status_code, 302) self.assertIn('oauth_verifier', rv.headers['Location']) self.assertIn('https://a.b', rv.headers['Location']) rv = self.client.post(initiate_url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'https://i.test', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) data = decode_response(rv.data) self.assertIn('oauth_token', data) rv = self.client.post(authorize_url, data={ 'user_id': '1', 'oauth_token': data['oauth_token'] }) self.assertEqual(rv.status_code, 302) self.assertIn('oauth_verifier', rv.headers['Location']) self.assertIn('https://i.test', rv.headers['Location']) class AuthorizationNoCacheTest(AuthorizationWithCacheTest): USE_CACHE = False authlib-1.3.2/tests/flask/test_oauth1/test_resource_protector.py000066400000000000000000000132731466226534200252300ustar00rootroot00000000000000import time from flask import json from authlib.oauth1.rfc5849 import signature from authlib.common.urls import add_params_to_uri from tests.util import read_file_path from .oauth1_server import db, User, Client, TokenCredential from .oauth1_server import ( TestCase, create_resource_server, ) class ResourceCacheTest(TestCase): USE_CACHE = True def prepare_data(self): create_resource_server(self.app, self.USE_CACHE, self.USE_CACHE) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='client', client_secret='secret', default_redirect_uri='https://a.b', ) db.session.add(client) db.session.commit() tok = TokenCredential( user_id=user.id, client_id=client.client_id, oauth_token='valid-token', oauth_token_secret='valid-token-secret' ) db.session.add(tok) db.session.commit() def test_invalid_request_parameters(self): self.prepare_data() url = '/user' # case 1 rv = self.client.get(url) data = json.loads(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_consumer_key', data['error_description']) # case 2 rv = self.client.get( add_params_to_uri(url, {'oauth_consumer_key': 'a'})) data = json.loads(rv.data) self.assertEqual(data['error'], 'invalid_client') # case 3 rv = self.client.get( add_params_to_uri(url, {'oauth_consumer_key': 'client'})) data = json.loads(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_token', data['error_description']) # case 4 rv = self.client.get( add_params_to_uri(url, { 'oauth_consumer_key': 'client', 'oauth_token': 'a' }) ) data = json.loads(rv.data) self.assertEqual(data['error'], 'invalid_token') # case 5 rv = self.client.get( add_params_to_uri(url, { 'oauth_consumer_key': 'client', 'oauth_token': 'valid-token' }) ) data = json.loads(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_timestamp', data['error_description']) def test_plaintext_signature(self): self.prepare_data() url = '/user' # case 1: success auth_header = ( 'OAuth oauth_consumer_key="client",' 'oauth_signature_method="PLAINTEXT",' 'oauth_token="valid-token",' 'oauth_signature="secret&valid-token-secret"' ) headers = {'Authorization': auth_header} rv = self.client.get(url, headers=headers) data = json.loads(rv.data) self.assertIn('username', data) # case 2: invalid signature auth_header = auth_header.replace('valid-token-secret', 'invalid') headers = {'Authorization': auth_header} rv = self.client.get(url, headers=headers) data = json.loads(rv.data) self.assertEqual(data['error'], 'invalid_signature') def test_hmac_sha1_signature(self): self.prepare_data() url = '/user' params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'valid-token'), ('oauth_signature_method', 'HMAC-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'hmac-sha1-nonce'), ] base_string = signature.construct_base_string( 'GET', 'http://localhost/user', params ) sig = signature.hmac_sha1_signature( base_string, 'secret', 'valid-token-secret') params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param headers = {'Authorization': auth_header} # case 1: success rv = self.client.get(url, headers=headers) data = json.loads(rv.data) self.assertIn('username', data) # case 2: exists nonce rv = self.client.get(url, headers=headers) data = json.loads(rv.data) self.assertEqual(data['error'], 'invalid_nonce') def test_rsa_sha1_signature(self): self.prepare_data() url = '/user' params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'valid-token'), ('oauth_signature_method', 'RSA-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'rsa-sha1-nonce'), ] base_string = signature.construct_base_string( 'GET', 'http://localhost/user', params ) sig = signature.rsa_sha1_signature( base_string, read_file_path('rsa_private.pem')) params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param headers = {'Authorization': auth_header} rv = self.client.get(url, headers=headers) data = json.loads(rv.data) self.assertIn('username', data) # case: invalid signature auth_param = auth_param.replace('rsa-sha1-nonce', 'alt-sha1-nonce') auth_header = 'OAuth ' + auth_param headers = {'Authorization': auth_header} rv = self.client.get(url, headers=headers) data = json.loads(rv.data) self.assertEqual(data['error'], 'invalid_signature') class ResourceDBTest(ResourceCacheTest): USE_CACHE = False authlib-1.3.2/tests/flask/test_oauth1/test_temporary_credentials.py000066400000000000000000000240261466226534200256750ustar00rootroot00000000000000import time from authlib.oauth1.rfc5849 import signature from tests.util import read_file_path, decode_response from .oauth1_server import db, User, Client from .oauth1_server import ( TestCase, create_authorization_server, ) class TemporaryCredentialsWithCacheTest(TestCase): USE_CACHE = True def prepare_data(self): self.server = create_authorization_server(self.app, self.USE_CACHE) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='client', client_secret='secret', default_redirect_uri='https://a.b', ) db.session.add(client) db.session.commit() def test_temporary_credential_parameters_errors(self): self.prepare_data() url = '/oauth/initiate' rv = self.client.get(url) data = decode_response(rv.data) self.assertEqual(data['error'], 'method_not_allowed') # case 1 rv = self.client.post(url) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_consumer_key', data['error_description']) # case 2 rv = self.client.post(url, data={'oauth_consumer_key': 'client'}) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_callback', data['error_description']) # case 3 rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'invalid_url' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_request') self.assertIn('oauth_callback', data['error_description']) # case 4 rv = self.client.post(url, data={ 'oauth_consumer_key': 'invalid-client', 'oauth_callback': 'oob' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_client') def test_validate_timestamp_and_nonce(self): self.prepare_data() url = '/oauth/initiate' # case 5 rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_timestamp', data['error_description']) # case 6 rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_timestamp': str(int(time.time())) }) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_nonce', data['error_description']) # case 7 rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_timestamp': '123' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_request') self.assertIn('oauth_timestamp', data['error_description']) # case 8 rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_timestamp': 'sss' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_request') self.assertIn('oauth_timestamp', data['error_description']) # case 9 rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_timestamp': '-1', 'oauth_signature_method': 'PLAINTEXT' }) self.assertEqual(data['error'], 'invalid_request') self.assertIn('oauth_timestamp', data['error_description']) def test_temporary_credential_signatures_errors(self): self.prepare_data() url = '/oauth/initiate' rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'PLAINTEXT' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_signature', data['error_description']) rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_timestamp': str(int(time.time())), 'oauth_nonce': 'a' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_signature_method', data['error_description']) rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_signature_method': 'INVALID', 'oauth_callback': 'oob', 'oauth_timestamp': str(int(time.time())), 'oauth_nonce': 'b', 'oauth_signature': 'c' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'unsupported_signature_method') def test_plaintext_signature(self): self.prepare_data() url = '/oauth/initiate' # case 1: use payload rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) data = decode_response(rv.data) self.assertIn('oauth_token', data) # case 2: use header auth_header = ( 'OAuth oauth_consumer_key="client",' 'oauth_signature_method="PLAINTEXT",' 'oauth_callback="oob",' 'oauth_signature="secret&"' ) headers = {'Authorization': auth_header} rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertIn('oauth_token', data) # case 3: invalid signature rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'invalid-signature' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_signature') def test_hmac_sha1_signature(self): self.prepare_data() url = '/oauth/initiate' params = [ ('oauth_consumer_key', 'client'), ('oauth_callback', 'oob'), ('oauth_signature_method', 'HMAC-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'hmac-sha1-nonce'), ] base_string = signature.construct_base_string( 'POST', 'http://localhost/oauth/initiate', params ) sig = signature.hmac_sha1_signature(base_string, 'secret', None) params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param headers = {'Authorization': auth_header} # case 1: success rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertIn('oauth_token', data) # case 2: exists nonce rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_nonce') def test_rsa_sha1_signature(self): self.prepare_data() url = '/oauth/initiate' params = [ ('oauth_consumer_key', 'client'), ('oauth_callback', 'oob'), ('oauth_signature_method', 'RSA-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'rsa-sha1-nonce'), ] base_string = signature.construct_base_string( 'POST', 'http://localhost/oauth/initiate', params ) sig = signature.rsa_sha1_signature( base_string, read_file_path('rsa_private.pem')) params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param headers = {'Authorization': auth_header} rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertIn('oauth_token', data) # case: invalid signature auth_param = auth_param.replace('rsa-sha1-nonce', 'alt-sha1-nonce') auth_header = 'OAuth ' + auth_param headers = {'Authorization': auth_header} rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_signature') def test_invalid_signature(self): self.app.config.update({ 'OAUTH1_SUPPORTED_SIGNATURE_METHODS': ['INVALID'] }) self.prepare_data() url = '/oauth/initiate' rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'PLAINTEXT', 'oauth_signature': 'secret&' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'unsupported_signature_method') rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_callback': 'oob', 'oauth_signature_method': 'INVALID', 'oauth_timestamp': str(int(time.time())), 'oauth_nonce': 'invalid-nonce', 'oauth_signature': 'secret&' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'unsupported_signature_method') def test_register_signature_method(self): self.prepare_data() def foo(): pass self.server.register_signature_method('foo', foo) self.assertEqual(self.server.SIGNATURE_METHODS['foo'], foo) class TemporaryCredentialsNoCacheTest(TemporaryCredentialsWithCacheTest): USE_CACHE = False authlib-1.3.2/tests/flask/test_oauth1/test_token_credentials.py000066400000000000000000000163361466226534200250000ustar00rootroot00000000000000import time from authlib.oauth1.rfc5849 import signature from tests.util import read_file_path, decode_response from .oauth1_server import db, User, Client from .oauth1_server import ( TestCase, create_authorization_server, ) class TokenCredentialsTest(TestCase): USE_CACHE = True def prepare_data(self): self.server = create_authorization_server(self.app, self.USE_CACHE) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='client', client_secret='secret', default_redirect_uri='https://a.b', ) db.session.add(client) db.session.commit() def prepare_temporary_credential(self): credential = { 'oauth_token': 'abc', 'oauth_token_secret': 'abc-secret', 'oauth_verifier': 'abc-verifier', 'user': 1 } func = self.server._hooks['create_temporary_credential'] func(credential, 'client', 'oob') def test_invalid_token_request_parameters(self): self.prepare_data() url = '/oauth/token' # case 1 rv = self.client.post(url) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_consumer_key', data['error_description']) # case 2 rv = self.client.post(url, data={'oauth_consumer_key': 'a'}) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_client') # case 3 rv = self.client.post(url, data={'oauth_consumer_key': 'client'}) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_token', data['error_description']) # case 4 rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_token': 'a' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_token') def test_invalid_token_and_verifiers(self): self.prepare_data() url = '/oauth/token' hook = self.server._hooks['create_temporary_credential'] # case 5 hook( {'oauth_token': 'abc', 'oauth_token_secret': 'abc-secret'}, 'client', 'oob' ) rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_token': 'abc' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'missing_required_parameter') self.assertIn('oauth_verifier', data['error_description']) # case 6 hook( {'oauth_token': 'abc', 'oauth_token_secret': 'abc-secret'}, 'client', 'oob' ) rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_token': 'abc', 'oauth_verifier': 'abc' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_request') self.assertIn('oauth_verifier', data['error_description']) def test_duplicated_oauth_parameters(self): self.prepare_data() url = '/oauth/token?oauth_consumer_key=client' rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_token': 'abc', 'oauth_verifier': 'abc' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'duplicated_oauth_protocol_parameter') def test_plaintext_signature(self): self.prepare_data() url = '/oauth/token' # case 1: success self.prepare_temporary_credential() auth_header = ( 'OAuth oauth_consumer_key="client",' 'oauth_signature_method="PLAINTEXT",' 'oauth_token="abc",' 'oauth_verifier="abc-verifier",' 'oauth_signature="secret&abc-secret"' ) headers = {'Authorization': auth_header} rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertIn('oauth_token', data) # case 2: invalid signature self.prepare_temporary_credential() rv = self.client.post(url, data={ 'oauth_consumer_key': 'client', 'oauth_signature_method': 'PLAINTEXT', 'oauth_token': 'abc', 'oauth_verifier': 'abc-verifier', 'oauth_signature': 'invalid-signature' }) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_signature') def test_hmac_sha1_signature(self): self.prepare_data() url = '/oauth/token' params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'abc'), ('oauth_verifier', 'abc-verifier'), ('oauth_signature_method', 'HMAC-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'hmac-sha1-nonce'), ] base_string = signature.construct_base_string( 'POST', 'http://localhost/oauth/token', params ) sig = signature.hmac_sha1_signature( base_string, 'secret', 'abc-secret') params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param headers = {'Authorization': auth_header} # case 1: success self.prepare_temporary_credential() rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertIn('oauth_token', data) # case 2: exists nonce self.prepare_temporary_credential() rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_nonce') def test_rsa_sha1_signature(self): self.prepare_data() url = '/oauth/token' self.prepare_temporary_credential() params = [ ('oauth_consumer_key', 'client'), ('oauth_token', 'abc'), ('oauth_verifier', 'abc-verifier'), ('oauth_signature_method', 'RSA-SHA1'), ('oauth_timestamp', str(int(time.time()))), ('oauth_nonce', 'rsa-sha1-nonce'), ] base_string = signature.construct_base_string( 'POST', 'http://localhost/oauth/token', params ) sig = signature.rsa_sha1_signature( base_string, read_file_path('rsa_private.pem')) params.append(('oauth_signature', sig)) auth_param = ','.join([f'{k}="{v}"' for k, v in params]) auth_header = 'OAuth ' + auth_param headers = {'Authorization': auth_header} rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertIn('oauth_token', data) # case: invalid signature self.prepare_temporary_credential() auth_param = auth_param.replace('rsa-sha1-nonce', 'alt-sha1-nonce') auth_header = 'OAuth ' + auth_param headers = {'Authorization': auth_header} rv = self.client.post(url, headers=headers) data = decode_response(rv.data) self.assertEqual(data['error'], 'invalid_signature') authlib-1.3.2/tests/flask/test_oauth2/000077500000000000000000000000001466226534200176625ustar00rootroot00000000000000authlib-1.3.2/tests/flask/test_oauth2/__init__.py000066400000000000000000000000001466226534200217610ustar00rootroot00000000000000authlib-1.3.2/tests/flask/test_oauth2/models.py000066400000000000000000000051071466226534200215220ustar00rootroot00000000000000import time from flask_sqlalchemy import SQLAlchemy from authlib.integrations.sqla_oauth2 import ( OAuth2ClientMixin, OAuth2TokenMixin, OAuth2AuthorizationCodeMixin, ) from authlib.oidc.core import UserInfo db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(40), unique=True, nullable=False) def get_user_id(self): return self.id def check_password(self, password): return password != 'wrong' def generate_user_info(self, scopes): profile = {'sub': str(self.id), 'name': self.username} return UserInfo(profile) class Client(db.Model, OAuth2ClientMixin): id = db.Column(db.Integer, primary_key=True) user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') class AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, nullable=False) @property def user(self): return db.session.get(User, self.user_id) class Token(db.Model, OAuth2TokenMixin): id = db.Column(db.Integer, primary_key=True) user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship('User') def is_refresh_token_active(self): return not self.refresh_token_revoked_at class CodeGrantMixin: def query_authorization_code(self, code, client): item = AuthorizationCode.query.filter_by( code=code, client_id=client.client_id).first() if item and not item.is_expired(): return item def delete_authorization_code(self, authorization_code): db.session.delete(authorization_code) db.session.commit() def authenticate_user(self, authorization_code): return db.session.get(User, authorization_code.user_id) def save_authorization_code(code, request): client = request.client auth_code = AuthorizationCode( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, nonce=request.data.get('nonce'), user_id=request.user.id, code_challenge=request.data.get('code_challenge'), code_challenge_method = request.data.get('code_challenge_method'), ) db.session.add(auth_code) db.session.commit() return auth_code def exists_nonce(nonce, req): exists = AuthorizationCode.query.filter_by( client_id=req.client_id, nonce=nonce ).first() return bool(exists) authlib-1.3.2/tests/flask/test_oauth2/oauth2_server.py000066400000000000000000000056771466226534200230430ustar00rootroot00000000000000import os import base64 import unittest from flask import Flask, request from authlib.common.security import generate_token from authlib.common.encoding import to_bytes, to_unicode from authlib.common.urls import url_encode from authlib.integrations.sqla_oauth2 import ( create_query_client_func, create_save_token_func, ) from authlib.integrations.flask_oauth2 import AuthorizationServer from authlib.oauth2 import OAuth2Error from .models import db, User, Client, Token def token_generator(client, grant_type, user=None, scope=None): token = f'{client.client_id[0]}-{grant_type}' if user: token = f'{token}.{user.get_user_id()}' return f'{token}.{generate_token(32)}' def create_authorization_server(app, lazy=False): query_client = create_query_client_func(db.session, Client) save_token = create_save_token_func(db.session, Token) if lazy: server = AuthorizationServer() server.init_app(app, query_client, save_token) else: server = AuthorizationServer(app, query_client, save_token) @app.route('/oauth/authorize', methods=['GET', 'POST']) def authorize(): if request.method == 'GET': user_id = request.args.get('user_id') if user_id: end_user = db.session.get(User, int(user_id)) else: end_user = None try: grant = server.get_consent_grant(end_user=end_user) return grant.prompt or 'ok' except OAuth2Error as error: return url_encode(error.get_body()) user_id = request.form.get('user_id') if user_id: grant_user = db.session.get(User, int(user_id)) else: grant_user = None return server.create_authorization_response(grant_user=grant_user) @app.route('/oauth/token', methods=['GET', 'POST']) def issue_token(): return server.create_token_response() return server def create_flask_app(): app = Flask(__name__) app.debug = True app.testing = True app.secret_key = 'testing' app.config.update({ 'SQLALCHEMY_TRACK_MODIFICATIONS': False, 'SQLALCHEMY_DATABASE_URI': 'sqlite://', 'OAUTH2_ERROR_URIS': [ ('invalid_client', 'https://a.b/e#invalid_client') ] }) return app class TestCase(unittest.TestCase): def setUp(self): os.environ['AUTHLIB_INSECURE_TRANSPORT'] = 'true' app = create_flask_app() self._ctx = app.app_context() self._ctx.push() db.init_app(app) db.create_all() self.app = app self.client = app.test_client() def tearDown(self): db.drop_all() self._ctx.pop() os.environ.pop('AUTHLIB_INSECURE_TRANSPORT') def create_basic_header(self, username, password): text = f'{username}:{password}' auth = to_unicode(base64.b64encode(to_bytes(text))) return {'Authorization': 'Basic ' + auth} authlib-1.3.2/tests/flask/test_oauth2/test_authorization_code_grant.py000066400000000000000000000232451466226534200263660ustar00rootroot00000000000000from flask import json from authlib.common.urls import urlparse, url_decode from authlib.oauth2.rfc6749.grants import ( AuthorizationCodeGrant as _AuthorizationCodeGrant, ) from .models import db, User, Client, AuthorizationCode from .models import CodeGrantMixin, save_authorization_code from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class AuthorizationCodeGrant(CodeGrantMixin, _AuthorizationCodeGrant): TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none'] def save_authorization_code(self, code, request): return save_authorization_code(code, request) class AuthorizationCodeTest(TestCase): LAZY_INIT = False def register_grant(self, server): server.register_grant(AuthorizationCodeGrant) def prepare_data( self, is_confidential=True, response_type='code', grant_type='authorization_code', token_endpoint_auth_method='client_secret_basic'): server = create_authorization_server(self.app, self.LAZY_INIT) self.register_grant(server) self.server = server user = User(username='foo') db.session.add(user) db.session.commit() if is_confidential: client_secret = 'code-secret' else: client_secret = '' client = Client( user_id=user.id, client_id='code-client', client_secret=client_secret, ) client.set_client_metadata({ 'redirect_uris': ['https://a.b'], 'scope': 'profile address', 'token_endpoint_auth_method': token_endpoint_auth_method, 'response_types': [response_type], 'grant_types': grant_type.splitlines(), }) self.authorize_url = ( '/oauth/authorize?response_type=code' '&client_id=code-client' ) db.session.add(client) db.session.commit() def test_get_authorize(self): self.prepare_data() rv = self.client.get(self.authorize_url) self.assertEqual(rv.data, b'ok') def test_invalid_client_id(self): self.prepare_data() url = '/oauth/authorize?response_type=code' rv = self.client.get(url) self.assertIn(b'invalid_client', rv.data) url = '/oauth/authorize?response_type=code&client_id=invalid' rv = self.client.get(url) self.assertIn(b'invalid_client', rv.data) def test_invalid_authorize(self): self.prepare_data() rv = self.client.post(self.authorize_url) self.assertIn('error=access_denied', rv.location) self.server.scopes_supported = ['profile'] rv = self.client.post(self.authorize_url + '&scope=invalid&state=foo') self.assertIn('error=invalid_scope', rv.location) self.assertIn('state=foo', rv.location) def test_unauthorized_client(self): self.prepare_data(True, 'token') rv = self.client.get(self.authorize_url) self.assertIn(b'unauthorized_client', rv.data) def test_invalid_client(self): self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': 'invalid', 'client_id': 'invalid-id', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = self.create_basic_header('code-client', 'invalid-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': 'invalid', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') self.assertEqual(resp['error_uri'], 'https://a.b/e#invalid_client') def test_invalid_code(self): self.prepare_data() headers = self.create_basic_header('code-client', 'code-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': 'invalid', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_grant') code = AuthorizationCode( code='no-user', client_id='code-client', user_id=0 ) db.session.add(code) db.session.commit() rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': 'no-user', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_grant') def test_invalid_redirect_uri(self): self.prepare_data() uri = self.authorize_url + '&redirect_uri=https%3A%2F%2Fa.c' rv = self.client.post(uri, data={'user_id': '1'}) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') uri = self.authorize_url + '&redirect_uri=https%3A%2F%2Fa.b' rv = self.client.post(uri, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] headers = self.create_basic_header('code-client', 'code-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_grant') def test_invalid_grant_type(self): self.prepare_data( False, token_endpoint_auth_method='none', grant_type='invalid' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'client_id': 'code-client', 'code': 'a', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unauthorized_client') def test_authorize_token_no_refresh_token(self): self.app.config.update({'OAUTH2_REFRESH_TOKEN_GENERATOR': True}) self.prepare_data(False, token_endpoint_auth_method='none') rv = self.client.post(self.authorize_url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'code-client', }) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertNotIn('refresh_token', resp) def test_authorize_token_has_refresh_token(self): # generate refresh token self.app.config.update({'OAUTH2_REFRESH_TOKEN_GENERATOR': True}) self.prepare_data(grant_type='authorization_code\nrefresh_token') url = self.authorize_url + '&state=bar' rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) self.assertEqual(params['state'], 'bar') code = params['code'] headers = self.create_basic_header('code-client', 'code-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('refresh_token', resp) def test_invalid_multiple_request_parameters(self): self.prepare_data() url = self.authorize_url + '&scope=profile&state=bar&redirect_uri=https%3A%2F%2Fa.b&response_type=code' rv = self.client.get(url) self.assertIn(b'invalid_request', rv.data) self.assertIn(b'Multiple+%22response_type%22+in+request.', rv.data) def test_client_secret_post(self): self.app.config.update({'OAUTH2_REFRESH_TOKEN_GENERATOR': True}) self.prepare_data( grant_type='authorization_code\nrefresh_token', token_endpoint_auth_method='client_secret_post', ) url = self.authorize_url + '&state=bar' rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) self.assertEqual(params['state'], 'bar') code = params['code'] rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'client_id': 'code-client', 'client_secret': 'code-secret', 'code': code, }) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('refresh_token', resp) def test_token_generator(self): m = 'tests.flask.test_oauth2.oauth2_server:token_generator' self.app.config.update({'OAUTH2_ACCESS_TOKEN_GENERATOR': m}) self.prepare_data(False, token_endpoint_auth_method='none') rv = self.client.post(self.authorize_url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'code-client', }) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('c-authorization_code.1.', resp['access_token']) authlib-1.3.2/tests/flask/test_oauth2/test_client_configuration_endpoint.py000066400000000000000000000520211466226534200274000ustar00rootroot00000000000000from flask import json from authlib.common.security import generate_token from authlib.jose import jwt from authlib.oauth2.rfc7591.claims import ClientMetadataClaims from authlib.oauth2.rfc7592 import ( ClientConfigurationEndpoint as _ClientConfigurationEndpoint, ) from tests.util import read_file_path from .models import db, User, Client, Token from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class ClientConfigurationEndpoint(_ClientConfigurationEndpoint): software_statement_alg_values_supported = ['RS256'] def authenticate_token(self, request): auth_header = request.headers.get('Authorization') if auth_header: access_token = auth_header.split()[1] return Token.query.filter_by(access_token=access_token).first() def update_client(self, client, client_metadata, request): client.set_client_metadata(client_metadata) db.session.add(client) db.session.commit() return client def authenticate_client(self, request): client_id = request.uri.split('/')[-1] return Client.query.filter_by(client_id=client_id).first() def revoke_access_token(self, request, token): token.revoked = True db.session.add(token) db.session.commit() def check_permission(self, client, request): client_id = request.uri.split('/')[-1] return client_id != 'unauthorized_client_id' def delete_client(self, client, request): db.session.delete(client) db.session.commit() def generate_client_registration_info(self, client, request): return { 'registration_client_uri': request.uri, 'registration_access_token': request.headers['Authorization'].split(' ')[1], } class ClientConfigurationTestMixin(TestCase): def prepare_data(self, endpoint_cls=None, metadata=None): app = self.app server = create_authorization_server(app) if endpoint_cls: server.register_endpoint(endpoint_cls) else: class MyClientConfiguration(ClientConfigurationEndpoint): def get_server_metadata(self): return metadata server.register_endpoint(MyClientConfiguration) @app.route('/configure_client/', methods=['PUT', 'GET', 'DELETE']) def configure_client(client_id): return server.create_endpoint_response( ClientConfigurationEndpoint.ENDPOINT_NAME ) user = User(username='foo') db.session.add(user) client = Client( client_id='client_id', client_secret='client_secret', ) client.set_client_metadata( { 'client_name': 'Authlib', 'scope': 'openid profile', } ) db.session.add(client) token = Token( user_id=user.id, client_id=client.id, token_type='bearer', access_token='a1', refresh_token='r1', scope='openid profile', expires_in=3600, ) db.session.add(token) db.session.commit() return user, client, token class ClientConfigurationReadTest(ClientConfigurationTestMixin): def test_read_client(self): user, client, token = self.prepare_data() self.assertEqual(client.client_name, 'Authlib') headers = {'Authorization': f'bearer {token.access_token}'} rv = self.client.get('/configure_client/client_id', headers=headers) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 200) self.assertEqual(resp['client_id'], client.client_id) self.assertEqual(resp['client_name'], 'Authlib') self.assertEqual( resp['registration_client_uri'], 'http://localhost/configure_client/client_id', ) self.assertEqual(resp['registration_access_token'], token.access_token) def test_access_denied(self): user, client, token = self.prepare_data() rv = self.client.get('/configure_client/client_id') resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'access_denied') headers = {'Authorization': f'bearer invalid_token'} rv = self.client.get('/configure_client/client_id', headers=headers) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'access_denied') headers = {'Authorization': f'bearer unauthorized_token'} rv = self.client.get( '/configure_client/client_id', json={'client_id': 'client_id', 'client_name': 'new client_name'}, headers=headers, ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'access_denied') def test_invalid_client(self): # If the client does not exist on this server, the server MUST respond # with HTTP 401 Unauthorized, and the registration access token used to # make this request SHOULD be immediately revoked. user, client, token = self.prepare_data() headers = {'Authorization': f'bearer {token.access_token}'} rv = self.client.get('/configure_client/invalid_client_id', headers=headers) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 401) self.assertEqual(resp['error'], 'invalid_client') def test_unauthorized_client(self): # If the client does not have permission to read its record, the server # MUST return an HTTP 403 Forbidden. client = Client( client_id='unauthorized_client_id', client_secret='unauthorized_client_secret', ) db.session.add(client) user, client, token = self.prepare_data() headers = {'Authorization': f'bearer {token.access_token}'} rv = self.client.get( '/configure_client/unauthorized_client_id', headers=headers ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 403) self.assertEqual(resp['error'], 'unauthorized_client') class ClientConfigurationUpdateTest(ClientConfigurationTestMixin): def test_update_client(self): # Valid values of client metadata fields in this request MUST replace, # not augment, the values previously associated with this client. # Omitted fields MUST be treated as null or empty values by the server, # indicating the client's request to delete them from the client's # registration. The authorization server MAY ignore any null or empty # value in the request just as any other value. user, client, token = self.prepare_data() self.assertEqual(client.client_name, 'Authlib') headers = {'Authorization': f'bearer {token.access_token}'} body = { 'client_id': client.client_id, 'client_name': 'NewAuthlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 200) self.assertEqual(resp['client_id'], client.client_id) self.assertEqual(resp['client_name'], 'NewAuthlib') self.assertEqual(client.client_name, 'NewAuthlib') self.assertEqual(client.scope, '') def test_access_denied(self): user, client, token = self.prepare_data() rv = self.client.put('/configure_client/client_id', json={}) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'access_denied') headers = {'Authorization': f'bearer invalid_token'} rv = self.client.put('/configure_client/client_id', json={}, headers=headers) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'access_denied') headers = {'Authorization': f'bearer unauthorized_token'} rv = self.client.put( '/configure_client/client_id', json={'client_id': 'client_id', 'client_name': 'new client_name'}, headers=headers, ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'access_denied') def test_invalid_request(self): user, client, token = self.prepare_data() headers = {'Authorization': f'bearer {token.access_token}'} # The client MUST include its 'client_id' field in the request... rv = self.client.put('/configure_client/client_id', json={}, headers=headers) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'invalid_request') # ... and it MUST be the same as its currently issued client identifier. rv = self.client.put( '/configure_client/client_id', json={'client_id': 'invalid_client_id'}, headers=headers, ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'invalid_request') # The updated client metadata fields request MUST NOT include the # 'registration_access_token', 'registration_client_uri', # 'client_secret_expires_at', or 'client_id_issued_at' fields rv = self.client.put( '/configure_client/client_id', json={ 'client_id': 'client_id', 'registration_client_uri': 'https://foobar.com', }, headers=headers, ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'invalid_request') # If the client includes the 'client_secret' field in the request, # the value of this field MUST match the currently issued client # secret for that client. rv = self.client.put( '/configure_client/client_id', json={'client_id': 'client_id', 'client_secret': 'invalid_secret'}, headers=headers, ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'invalid_request') def test_invalid_client(self): # If the client does not exist on this server, the server MUST respond # with HTTP 401 Unauthorized, and the registration access token used to # make this request SHOULD be immediately revoked. user, client, token = self.prepare_data() headers = {'Authorization': f'bearer {token.access_token}'} rv = self.client.put( '/configure_client/invalid_client_id', json={'client_id': 'invalid_client_id', 'client_name': 'new client_name'}, headers=headers, ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 401) self.assertEqual(resp['error'], 'invalid_client') def test_unauthorized_client(self): # If the client does not have permission to read its record, the server # MUST return an HTTP 403 Forbidden. client = Client( client_id='unauthorized_client_id', client_secret='unauthorized_client_secret', ) db.session.add(client) user, client, token = self.prepare_data() headers = {'Authorization': f'bearer {token.access_token}'} rv = self.client.put( '/configure_client/unauthorized_client_id', json={ 'client_id': 'unauthorized_client_id', 'client_name': 'new client_name', }, headers=headers, ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 403) self.assertEqual(resp['error'], 'unauthorized_client') def test_invalid_metadata(self): metadata = {'token_endpoint_auth_methods_supported': ['client_secret_basic']} user, client, token = self.prepare_data(metadata=metadata) headers = {'Authorization': f'bearer {token.access_token}'} # For all metadata fields, the authorization server MAY replace any # invalid values with suitable default values, and it MUST return any # such fields to the client in the response. # If the client attempts to set an invalid metadata field and the # authorization server does not set a default value, the authorization # server responds with an error as described in [RFC7591]. body = { 'client_id': client.client_id, 'client_name': 'NewAuthlib', 'token_endpoint_auth_method': 'invalid_auth_method', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'invalid_client_metadata') def test_scopes_supported(self): metadata = {'scopes_supported': ['profile', 'email']} user, client, token = self.prepare_data(metadata=metadata) headers = {'Authorization': f'bearer {token.access_token}'} body = { 'client_id': 'client_id', 'scope': 'profile email', 'client_name': 'Authlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['client_id'], 'client_id') self.assertEqual(resp['client_name'], 'Authlib') self.assertEqual(resp['scope'], 'profile email') headers = {'Authorization': f'bearer {token.access_token}'} body = { 'client_id': 'client_id', 'scope': '', 'client_name': 'Authlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['client_id'], 'client_id') self.assertEqual(resp['client_name'], 'Authlib') body = { 'client_id': 'client_id', 'scope': 'profile email address', 'client_name': 'Authlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn(resp['error'], 'invalid_client_metadata') def test_response_types_supported(self): metadata = {'response_types_supported': ['code']} user, client, token = self.prepare_data(metadata=metadata) headers = {'Authorization': f'bearer {token.access_token}'} body = { 'client_id': 'client_id', 'response_types': ['code'], 'client_name': 'Authlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['client_id'], 'client_id') self.assertEqual(resp['client_name'], 'Authlib') self.assertEqual(resp['response_types'], ['code']) # https://datatracker.ietf.org/doc/html/rfc7592#section-2.2 # If omitted, the default is that the client will use only the "code" # response type. body = {'client_id': 'client_id', 'client_name': 'Authlib'} rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') self.assertNotIn('response_types', resp) body = { 'client_id': 'client_id', 'response_types': ['code', 'token'], 'client_name': 'Authlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn(resp['error'], 'invalid_client_metadata') def test_grant_types_supported(self): metadata = {'grant_types_supported': ['authorization_code', 'password']} user, client, token = self.prepare_data(metadata=metadata) headers = {'Authorization': f'bearer {token.access_token}'} body = { 'client_id': 'client_id', 'grant_types': ['password'], 'client_name': 'Authlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['client_id'], 'client_id') self.assertEqual(resp['client_name'], 'Authlib') self.assertEqual(resp['grant_types'], ['password']) # https://datatracker.ietf.org/doc/html/rfc7592#section-2.2 # If omitted, the default behavior is that the client will use only # the "authorization_code" Grant Type. body = {'client_id': 'client_id', 'client_name': 'Authlib'} rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') self.assertNotIn('grant_types', resp) body = { 'client_id': 'client_id', 'grant_types': ['client_credentials'], 'client_name': 'Authlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn(resp['error'], 'invalid_client_metadata') def test_token_endpoint_auth_methods_supported(self): metadata = {'token_endpoint_auth_methods_supported': ['client_secret_basic']} user, client, token = self.prepare_data(metadata=metadata) headers = {'Authorization': f'bearer {token.access_token}'} body = { 'client_id': 'client_id', 'token_endpoint_auth_method': 'client_secret_basic', 'client_name': 'Authlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['client_id'], 'client_id') self.assertEqual(resp['client_name'], 'Authlib') self.assertEqual(resp['token_endpoint_auth_method'], 'client_secret_basic') body = { 'client_id': 'client_id', 'token_endpoint_auth_method': 'none', 'client_name': 'Authlib', } rv = self.client.put('/configure_client/client_id', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn(resp['error'], 'invalid_client_metadata') class ClientConfigurationDeleteTest(ClientConfigurationTestMixin): def test_delete_client(self): user, client, token = self.prepare_data() self.assertEqual(client.client_name, 'Authlib') headers = {'Authorization': f'bearer {token.access_token}'} rv = self.client.delete('/configure_client/client_id', headers=headers) self.assertEqual(rv.status_code, 204) self.assertFalse(rv.data) def test_access_denied(self): user, client, token = self.prepare_data() rv = self.client.delete('/configure_client/client_id') resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'access_denied') headers = {'Authorization': f'bearer invalid_token'} rv = self.client.delete('/configure_client/client_id', headers=headers) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'access_denied') headers = {'Authorization': f'bearer unauthorized_token'} rv = self.client.delete( '/configure_client/client_id', json={'client_id': 'client_id', 'client_name': 'new client_name'}, headers=headers, ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 400) self.assertEqual(resp['error'], 'access_denied') def test_invalid_client(self): # If the client does not exist on this server, the server MUST respond # with HTTP 401 Unauthorized, and the registration access token used to # make this request SHOULD be immediately revoked. user, client, token = self.prepare_data() headers = {'Authorization': f'bearer {token.access_token}'} rv = self.client.delete('/configure_client/invalid_client_id', headers=headers) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 401) self.assertEqual(resp['error'], 'invalid_client') def test_unauthorized_client(self): # If the client does not have permission to read its record, the server # MUST return an HTTP 403 Forbidden. client = Client( client_id='unauthorized_client_id', client_secret='unauthorized_client_secret', ) db.session.add(client) user, client, token = self.prepare_data() headers = {'Authorization': f'bearer {token.access_token}'} rv = self.client.delete( '/configure_client/unauthorized_client_id', headers=headers ) resp = json.loads(rv.data) self.assertEqual(rv.status_code, 403) self.assertEqual(resp['error'], 'unauthorized_client') authlib-1.3.2/tests/flask/test_oauth2/test_client_credentials_grant.py000066400000000000000000000065141466226534200263270ustar00rootroot00000000000000from flask import json from authlib.oauth2.rfc6749.grants import ClientCredentialsGrant from .models import db, User, Client from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class ClientCredentialsTest(TestCase): def prepare_data(self, grant_type='client_credentials'): server = create_authorization_server(self.app) server.register_grant(ClientCredentialsGrant) self.server = server user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='credential-client', client_secret='credential-secret', ) client.set_client_metadata({ 'scope': 'profile', 'redirect_uris': ['http://localhost/authorized'], 'grant_types': [grant_type] }) db.session.add(client) db.session.commit() def test_invalid_client(self): self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = self.create_basic_header( 'credential-client', 'invalid-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_invalid_grant_type(self): self.prepare_data(grant_type='invalid') headers = self.create_basic_header( 'credential-client', 'credential-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unauthorized_client') def test_invalid_scope(self): self.prepare_data() self.server.scopes_supported = ['profile'] headers = self.create_basic_header( 'credential-client', 'credential-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', 'scope': 'invalid', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_scope') def test_authorize_token(self): self.prepare_data() headers = self.create_basic_header( 'credential-client', 'credential-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_token_generator(self): m = 'tests.flask.test_oauth2.oauth2_server:token_generator' self.app.config.update({'OAUTH2_ACCESS_TOKEN_GENERATOR': m}) self.prepare_data() headers = self.create_basic_header( 'credential-client', 'credential-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('c-client_credentials.', resp['access_token']) authlib-1.3.2/tests/flask/test_oauth2/test_client_registration_endpoint.py000066400000000000000000000172471466226534200272560ustar00rootroot00000000000000from flask import json from authlib.jose import jwt from authlib.oauth2.rfc7591 import ClientRegistrationEndpoint as _ClientRegistrationEndpoint from tests.util import read_file_path from .models import db, User, Client from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class ClientRegistrationEndpoint(_ClientRegistrationEndpoint): software_statement_alg_values_supported = ['RS256'] def authenticate_token(self, request): auth_header = request.headers.get('Authorization') if auth_header: request.user_id = 1 return auth_header def resolve_public_key(self, request): return read_file_path('rsa_public.pem') def save_client(self, client_info, client_metadata, request): client = Client( user_id=request.user_id, **client_info ) client.set_client_metadata(client_metadata) db.session.add(client) db.session.commit() return client class ClientRegistrationTest(TestCase): def prepare_data(self, endpoint_cls=None, metadata=None): app = self.app server = create_authorization_server(app) if endpoint_cls: server.register_endpoint(endpoint_cls) else: class MyClientRegistration(ClientRegistrationEndpoint): def get_server_metadata(self): return metadata server.register_endpoint(MyClientRegistration) @app.route('/create_client', methods=['POST']) def create_client(): return server.create_endpoint_response('client_registration') user = User(username='foo') db.session.add(user) db.session.commit() def test_access_denied(self): self.prepare_data() rv = self.client.post('/create_client', json={}) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'access_denied') def test_invalid_request(self): self.prepare_data() headers = {'Authorization': 'bearer abc'} rv = self.client.post('/create_client', json={}, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') def test_create_client(self): self.prepare_data() headers = {'Authorization': 'bearer abc'} body = { 'client_name': 'Authlib' } rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') def test_software_statement(self): payload = {'software_id': 'uuid-123', 'client_name': 'Authlib'} s = jwt.encode({'alg': 'RS256'}, payload, read_file_path('rsa_private.pem')) body = { 'software_statement': s.decode('utf-8'), } self.prepare_data() headers = {'Authorization': 'bearer abc'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') def test_no_public_key(self): class ClientRegistrationEndpoint2(ClientRegistrationEndpoint): def get_server_metadata(self): return None def resolve_public_key(self, request): return None payload = {'software_id': 'uuid-123', 'client_name': 'Authlib'} s = jwt.encode({'alg': 'RS256'}, payload, read_file_path('rsa_private.pem')) body = { 'software_statement': s.decode('utf-8'), } self.prepare_data(ClientRegistrationEndpoint2) headers = {'Authorization': 'bearer abc'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn(resp['error'], 'unapproved_software_statement') def test_scopes_supported(self): metadata = {'scopes_supported': ['profile', 'email']} self.prepare_data(metadata=metadata) headers = {'Authorization': 'bearer abc'} body = {'scope': 'profile email', 'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') body = {'scope': 'profile email address', 'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn(resp['error'], 'invalid_client_metadata') def test_response_types_supported(self): metadata = {'response_types_supported': ['code']} self.prepare_data(metadata=metadata) headers = {'Authorization': 'bearer abc'} body = {'response_types': ['code'], 'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') # https://www.rfc-editor.org/rfc/rfc7591.html#section-2 # If omitted, the default is that the client will use only the "code" # response type. body = {'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') body = {'response_types': ['code', 'token'], 'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn(resp['error'], 'invalid_client_metadata') def test_grant_types_supported(self): metadata = {'grant_types_supported': ['authorization_code', 'password']} self.prepare_data(metadata=metadata) headers = {'Authorization': 'bearer abc'} body = {'grant_types': ['password'], 'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') # https://www.rfc-editor.org/rfc/rfc7591.html#section-2 # If omitted, the default behavior is that the client will use only # the "authorization_code" Grant Type. body = {'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') body = {'grant_types': ['client_credentials'], 'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn(resp['error'], 'invalid_client_metadata') def test_token_endpoint_auth_methods_supported(self): metadata = {'token_endpoint_auth_methods_supported': ['client_secret_basic']} self.prepare_data(metadata=metadata) headers = {'Authorization': 'bearer abc'} body = {'token_endpoint_auth_method': 'client_secret_basic', 'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn('client_id', resp) self.assertEqual(resp['client_name'], 'Authlib') body = {'token_endpoint_auth_method': 'none', 'client_name': 'Authlib'} rv = self.client.post('/create_client', json=body, headers=headers) resp = json.loads(rv.data) self.assertIn(resp['error'], 'invalid_client_metadata') authlib-1.3.2/tests/flask/test_oauth2/test_code_challenge.py000066400000000000000000000213251466226534200242120ustar00rootroot00000000000000from flask import json from authlib.common.security import generate_token from authlib.common.urls import urlparse, url_decode from authlib.oauth2.rfc6749 import grants from authlib.oauth2.rfc7636 import ( CodeChallenge as _CodeChallenge, create_s256_code_challenge, ) from .models import db, User, Client from .models import CodeGrantMixin, save_authorization_code from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class AuthorizationCodeGrant(CodeGrantMixin, grants.AuthorizationCodeGrant): TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none'] def save_authorization_code(self, code, request): return save_authorization_code(code, request) class CodeChallenge(_CodeChallenge): SUPPORTED_CODE_CHALLENGE_METHOD = ['plain', 'S256', 'S128'] class CodeChallengeTest(TestCase): def prepare_data(self, token_endpoint_auth_method='none'): server = create_authorization_server(self.app) server.register_grant( AuthorizationCodeGrant, [CodeChallenge(required=True)] ) user = User(username='foo') db.session.add(user) db.session.commit() client_secret = '' if token_endpoint_auth_method != 'none': client_secret = 'code-secret' client = Client( user_id=user.id, client_id='code-client', client_secret=client_secret, ) client.set_client_metadata({ 'redirect_uris': ['https://a.b'], 'scope': 'profile address', 'token_endpoint_auth_method': token_endpoint_auth_method, 'response_types': ['code'], 'grant_types': ['authorization_code'], }) self.authorize_url = ( '/oauth/authorize?response_type=code' '&client_id=code-client' ) db.session.add(client) db.session.commit() def test_missing_code_challenge(self): self.prepare_data() rv = self.client.get(self.authorize_url + '&code_challenge_method=plain') self.assertIn(b'Missing', rv.data) def test_has_code_challenge(self): self.prepare_data() rv = self.client.get(self.authorize_url + '&code_challenge=Zhs2POMonIVVHZteWfoU7cSXQSm0YjghikFGJSDI2_s') self.assertEqual(rv.data, b'ok') def test_invalid_code_challenge(self): self.prepare_data() rv = self.client.get(self.authorize_url + '&code_challenge=abc&code_challenge_method=plain') self.assertIn(b'Invalid', rv.data) def test_invalid_code_challenge_method(self): self.prepare_data() suffix = '&code_challenge=Zhs2POMonIVVHZteWfoU7cSXQSm0YjghikFGJSDI2_s&code_challenge_method=invalid' rv = self.client.get(self.authorize_url + suffix) self.assertIn(b'Unsupported', rv.data) def test_supported_code_challenge_method(self): self.prepare_data() suffix = '&code_challenge=Zhs2POMonIVVHZteWfoU7cSXQSm0YjghikFGJSDI2_s&code_challenge_method=plain' rv = self.client.get(self.authorize_url + suffix) self.assertEqual(rv.data, b'ok') def test_trusted_client_without_code_challenge(self): self.prepare_data('client_secret_basic') rv = self.client.get(self.authorize_url) self.assertEqual(rv.data, b'ok') rv = self.client.post(self.authorize_url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] headers = self.create_basic_header('code-client', 'code-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_missing_code_verifier(self): self.prepare_data() url = self.authorize_url + '&code_challenge=Zhs2POMonIVVHZteWfoU7cSXQSm0YjghikFGJSDI2_s' rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'code-client', }) resp = json.loads(rv.data) self.assertIn('Missing', resp['error_description']) def test_trusted_client_missing_code_verifier(self): self.prepare_data('client_secret_basic') url = self.authorize_url + '&code_challenge=Zhs2POMonIVVHZteWfoU7cSXQSm0YjghikFGJSDI2_s' rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] headers = self.create_basic_header('code-client', 'code-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertIn('Missing', resp['error_description']) def test_plain_code_challenge_invalid(self): self.prepare_data() url = self.authorize_url + '&code_challenge=Zhs2POMonIVVHZteWfoU7cSXQSm0YjghikFGJSDI2_s' rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'code_verifier': 'bar', 'client_id': 'code-client', }) resp = json.loads(rv.data) self.assertIn('Invalid', resp['error_description']) def test_plain_code_challenge_failed(self): self.prepare_data() url = self.authorize_url + '&code_challenge=Zhs2POMonIVVHZteWfoU7cSXQSm0YjghikFGJSDI2_s' rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'code_verifier': generate_token(48), 'client_id': 'code-client', }) resp = json.loads(rv.data) self.assertIn('failed', resp['error_description']) def test_plain_code_challenge_success(self): self.prepare_data() code_verifier = generate_token(48) url = self.authorize_url + '&code_challenge=' + code_verifier rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'code_verifier': code_verifier, 'client_id': 'code-client', }) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_s256_code_challenge_success(self): self.prepare_data() code_verifier = generate_token(48) code_challenge = create_s256_code_challenge(code_verifier) url = self.authorize_url + '&code_challenge=' + code_challenge url += '&code_challenge_method=S256' rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'code_verifier': code_verifier, 'client_id': 'code-client', }) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_not_implemented_code_challenge_method(self): self.prepare_data() url = self.authorize_url + '&code_challenge=Zhs2POMonIVVHZteWfoU7cSXQSm0YjghikFGJSDI2_s' url += '&code_challenge_method=S128' rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) code = params['code'] self.assertRaises( RuntimeError, self.client.post, '/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'code_verifier': generate_token(48), 'client_id': 'code-client', } ) authlib-1.3.2/tests/flask/test_oauth2/test_device_code_grant.py000066400000000000000000000167601466226534200247310ustar00rootroot00000000000000import time from flask import json from authlib.oauth2.rfc8628 import ( DeviceAuthorizationEndpoint as _DeviceAuthorizationEndpoint, DeviceCodeGrant as _DeviceCodeGrant, DeviceCredentialDict, ) from .models import db, User, Client from .oauth2_server import TestCase from .oauth2_server import create_authorization_server device_credentials = { 'valid-device': { 'client_id': 'client', 'expires_in': 1800, 'user_code': 'code', }, 'expired-token': { 'client_id': 'client', 'expires_in': -100, 'user_code': 'none', }, 'invalid-client': { 'client_id': 'invalid', 'expires_in': 1800, 'user_code': 'none', }, 'denied-code': { 'client_id': 'client', 'expires_in': 1800, 'user_code': 'denied', }, 'grant-code': { 'client_id': 'client', 'expires_in': 1800, 'user_code': 'code', }, 'pending-code': { 'client_id': 'client', 'expires_in': 1800, 'user_code': 'none', } } class DeviceCodeGrant(_DeviceCodeGrant): def query_device_credential(self, device_code): data = device_credentials.get(device_code) if not data: return None now = int(time.time()) data['expires_at'] = now + data['expires_in'] data['device_code'] = device_code data['scope'] = 'profile' data['interval'] = 5 data['verification_uri'] = 'https://example.com/activate' return DeviceCredentialDict(data) def query_user_grant(self, user_code): if user_code == 'code': return db.session.get(User, 1), True if user_code == 'denied': return db.session.get(User, 1), False return None def should_slow_down(self, credential): return False class DeviceCodeGrantTest(TestCase): def create_server(self): server = create_authorization_server(self.app) server.register_grant(DeviceCodeGrant) self.server = server return server def prepare_data(self, grant_type=DeviceCodeGrant.GRANT_TYPE): user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='client', client_secret='secret', ) client.set_client_metadata({ 'redirect_uris': ['http://localhost/authorized'], 'scope': 'profile', 'grant_types': [grant_type], 'token_endpoint_auth_method': 'none', }) db.session.add(client) db.session.commit() def test_invalid_request(self): self.create_server() self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': DeviceCodeGrant.GRANT_TYPE, 'client_id': 'test', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') rv = self.client.post('/oauth/token', data={ 'grant_type': DeviceCodeGrant.GRANT_TYPE, 'device_code': 'missing', 'client_id': 'client', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') def test_unauthorized_client(self): self.create_server() rv = self.client.post('/oauth/token', data={ 'grant_type': DeviceCodeGrant.GRANT_TYPE, 'device_code': 'valid-device', 'client_id': 'invalid', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') self.prepare_data(grant_type='password') rv = self.client.post('/oauth/token', data={ 'grant_type': DeviceCodeGrant.GRANT_TYPE, 'device_code': 'valid-device', 'client_id': 'client', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unauthorized_client') def test_invalid_client(self): self.create_server() self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': DeviceCodeGrant.GRANT_TYPE, 'device_code': 'invalid-client', 'client_id': 'invalid', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_expired_token(self): self.create_server() self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': DeviceCodeGrant.GRANT_TYPE, 'device_code': 'expired-token', 'client_id': 'client', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'expired_token') def test_denied_by_user(self): self.create_server() self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': DeviceCodeGrant.GRANT_TYPE, 'device_code': 'denied-code', 'client_id': 'client', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'access_denied') def test_authorization_pending(self): self.create_server() self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': DeviceCodeGrant.GRANT_TYPE, 'device_code': 'pending-code', 'client_id': 'client', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'authorization_pending') def test_get_access_token(self): self.create_server() self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': DeviceCodeGrant.GRANT_TYPE, 'device_code': 'grant-code', 'client_id': 'client', }) resp = json.loads(rv.data) self.assertIn('access_token', resp) class DeviceAuthorizationEndpoint(_DeviceAuthorizationEndpoint): def get_verification_uri(self): return 'https://example.com/activate' def save_device_credential(self, client_id, scope, data): pass class DeviceAuthorizationEndpointTest(TestCase): def create_server(self): server = create_authorization_server(self.app) server.register_endpoint(DeviceAuthorizationEndpoint) self.server = server @self.app.route('/device_authorize', methods=['POST']) def device_authorize(): name = DeviceAuthorizationEndpoint.ENDPOINT_NAME return server.create_endpoint_response(name) return server def test_missing_client_id(self): self.create_server() rv = self.client.post('/device_authorize', data={ 'scope': 'profile' }) self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_create_authorization_response(self): self.create_server() client = Client( user_id=1, client_id='client', client_secret='secret', ) db.session.add(client) db.session.commit() rv = self.client.post('/device_authorize', data={ 'client_id': 'client', }) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertIn('device_code', resp) self.assertIn('user_code', resp) self.assertEqual(resp['verification_uri'], 'https://example.com/activate') self.assertEqual( resp['verification_uri_complete'], 'https://example.com/activate?user_code=' + resp['user_code'] ) authlib-1.3.2/tests/flask/test_oauth2/test_implicit_grant.py000066400000000000000000000057621466226534200243120ustar00rootroot00000000000000from authlib.oauth2.rfc6749.grants import ImplicitGrant from .models import db, User, Client from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class ImplicitTest(TestCase): def prepare_data(self, is_confidential=False, response_type='token'): server = create_authorization_server(self.app) server.register_grant(ImplicitGrant) self.server = server user = User(username='foo') db.session.add(user) db.session.commit() if is_confidential: client_secret = 'implicit-secret' token_endpoint_auth_method = 'client_secret_basic' else: client_secret = '' token_endpoint_auth_method = 'none' client = Client( user_id=user.id, client_id='implicit-client', client_secret=client_secret, ) client.set_client_metadata({ 'redirect_uris': ['http://localhost/authorized'], 'scope': 'profile', 'response_types': [response_type], 'grant_types': ['implicit'], 'token_endpoint_auth_method': token_endpoint_auth_method, }) self.authorize_url = ( '/oauth/authorize?response_type=token' '&client_id=implicit-client' ) db.session.add(client) db.session.commit() def test_get_authorize(self): self.prepare_data() rv = self.client.get(self.authorize_url) self.assertEqual(rv.data, b'ok') def test_confidential_client(self): self.prepare_data(True) rv = self.client.get(self.authorize_url) self.assertIn(b'invalid_client', rv.data) def test_unsupported_client(self): self.prepare_data(response_type='code') rv = self.client.get(self.authorize_url) self.assertIn(b'unauthorized_client', rv.data) def test_invalid_authorize(self): self.prepare_data() rv = self.client.post(self.authorize_url) self.assertIn('#error=access_denied', rv.location) self.server.scopes_supported = ['profile'] rv = self.client.post(self.authorize_url + '&scope=invalid') self.assertIn('#error=invalid_scope', rv.location) def test_authorize_token(self): self.prepare_data() rv = self.client.post(self.authorize_url, data={'user_id': '1'}) self.assertIn('access_token=', rv.location) url = self.authorize_url + '&state=bar&scope=profile' rv = self.client.post(url, data={'user_id': '1'}) self.assertIn('access_token=', rv.location) self.assertIn('state=bar', rv.location) self.assertIn('scope=profile', rv.location) def test_token_generator(self): m = 'tests.flask.test_oauth2.oauth2_server:token_generator' self.app.config.update({'OAUTH2_ACCESS_TOKEN_GENERATOR': m}) self.prepare_data() rv = self.client.post(self.authorize_url, data={'user_id': '1'}) self.assertIn('access_token=i-implicit.1.', rv.location) authlib-1.3.2/tests/flask/test_oauth2/test_introspection_endpoint.py000066400000000000000000000124611466226534200260770ustar00rootroot00000000000000from flask import json from authlib.integrations.sqla_oauth2 import create_query_token_func from authlib.oauth2.rfc7662 import IntrospectionEndpoint from .models import db, User, Client, Token from .oauth2_server import TestCase from .oauth2_server import create_authorization_server query_token = create_query_token_func(db.session, Token) class MyIntrospectionEndpoint(IntrospectionEndpoint): def check_permission(self, token, client, request): return True def query_token(self, token, token_type_hint): return query_token(token, token_type_hint) def introspect_token(self, token): user = db.session.get(User, token.user_id) return { "active": True, "client_id": token.client_id, "username": user.username, "scope": token.scope, "sub": user.get_user_id(), "aud": token.client_id, "iss": "https://server.example.com/", "exp": token.issued_at + token.expires_in, "iat": token.issued_at, } class IntrospectTokenTest(TestCase): def prepare_data(self): app = self.app server = create_authorization_server(app) server.register_endpoint(MyIntrospectionEndpoint) @app.route('/oauth/introspect', methods=['POST']) def introspect_token(): return server.create_endpoint_response('introspection') user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='introspect-client', client_secret='introspect-secret', ) client.set_client_metadata({ 'scope': 'profile', 'redirect_uris': ['http://a.b/c'], }) db.session.add(client) db.session.commit() def create_token(self): token = Token( user_id=1, client_id='introspect-client', token_type='bearer', access_token='a1', refresh_token='r1', scope='profile', expires_in=3600, ) db.session.add(token) db.session.commit() def test_invalid_client(self): self.prepare_data() rv = self.client.post('/oauth/introspect') resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = {'Authorization': 'invalid token_string'} rv = self.client.post('/oauth/introspect', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = self.create_basic_header( 'invalid-client', 'introspect-secret' ) rv = self.client.post('/oauth/introspect', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = self.create_basic_header( 'introspect-client', 'invalid-secret' ) rv = self.client.post('/oauth/introspect', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_invalid_token(self): self.prepare_data() headers = self.create_basic_header( 'introspect-client', 'introspect-secret' ) rv = self.client.post('/oauth/introspect', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') rv = self.client.post('/oauth/introspect', data={ 'token_type_hint': 'refresh_token', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') rv = self.client.post('/oauth/introspect', data={ 'token': 'a1', 'token_type_hint': 'unsupported_token_type', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unsupported_token_type') rv = self.client.post('/oauth/introspect', data={ 'token': 'invalid-token', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['active'], False) rv = self.client.post('/oauth/introspect', data={ 'token': 'a1', 'token_type_hint': 'refresh_token', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['active'], False) def test_introspect_token_with_hint(self): self.prepare_data() self.create_token() headers = self.create_basic_header( 'introspect-client', 'introspect-secret' ) rv = self.client.post('/oauth/introspect', data={ 'token': 'a1', 'token_type_hint': 'access_token', }, headers=headers) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertEqual(resp['client_id'], 'introspect-client') def test_introspect_token_without_hint(self): self.prepare_data() self.create_token() headers = self.create_basic_header( 'introspect-client', 'introspect-secret' ) rv = self.client.post('/oauth/introspect', data={ 'token': 'a1', }, headers=headers) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertEqual(resp['client_id'], 'introspect-client') authlib-1.3.2/tests/flask/test_oauth2/test_jwt_access_token.py000066400000000000000000000766151466226534200246370ustar00rootroot00000000000000import time import pytest from flask import json from flask import jsonify from .models import Client from .models import CodeGrantMixin from .models import db from .models import save_authorization_code from .models import Token from .models import User from .oauth2_server import create_authorization_server from .oauth2_server import TestCase from authlib.common.security import generate_token from authlib.common.urls import url_decode from authlib.common.urls import urlparse from authlib.integrations.flask_oauth2 import current_token from authlib.integrations.flask_oauth2 import ResourceProtector from authlib.jose import jwt from authlib.oauth2.rfc6749.grants import ( AuthorizationCodeGrant as _AuthorizationCodeGrant, ) from authlib.oauth2.rfc7009 import RevocationEndpoint from authlib.oauth2.rfc7662 import IntrospectionEndpoint from authlib.oauth2.rfc9068 import JWTBearerTokenGenerator from authlib.oauth2.rfc9068 import JWTBearerTokenValidator from authlib.oauth2.rfc9068 import JWTIntrospectionEndpoint from authlib.oauth2.rfc9068 import JWTRevocationEndpoint from tests.util import read_file_path def create_token_validator(issuer, resource_server, jwks): class MyJWTBearerTokenValidator(JWTBearerTokenValidator): def get_jwks(self): return jwks validator = MyJWTBearerTokenValidator( issuer=issuer, resource_server=resource_server ) return validator def create_resource_protector(app, validator): require_oauth = ResourceProtector() require_oauth.register_token_validator(validator) @app.route('/protected') @require_oauth() def protected(): user = db.session.get(User, current_token['sub']) return jsonify(id=user.id, username=user.username, token=current_token) @app.route('/protected-by-scope') @require_oauth('profile') def protected_by_scope(): user = db.session.get(User, current_token['sub']) return jsonify(id=user.id, username=user.username, token=current_token) @app.route('/protected-by-groups') @require_oauth(groups=['admins']) def protected_by_groups(): user = db.session.get(User, current_token['sub']) return jsonify(id=user.id, username=user.username, token=current_token) @app.route('/protected-by-roles') @require_oauth(roles=['student']) def protected_by_roles(): user = db.session.get(User, current_token['sub']) return jsonify(id=user.id, username=user.username, token=current_token) @app.route('/protected-by-entitlements') @require_oauth(entitlements=['captain']) def protected_by_entitlements(): user = db.session.get(User, current_token['sub']) return jsonify(id=user.id, username=user.username, token=current_token) return require_oauth def create_token_generator(authorization_server, issuer, jwks): class MyJWTBearerTokenGenerator(JWTBearerTokenGenerator): def get_jwks(self): return jwks token_generator = MyJWTBearerTokenGenerator(issuer=issuer) authorization_server.register_token_generator('default', token_generator) return token_generator def create_introspection_endpoint(app, authorization_server, issuer, jwks): class MyJWTIntrospectionEndpoint(JWTIntrospectionEndpoint): def get_jwks(self): return jwks def check_permission(self, token, client, request): return client.client_id == 'client-id' endpoint = MyJWTIntrospectionEndpoint(issuer=issuer) authorization_server.register_endpoint(endpoint) @app.route('/oauth/introspect', methods=['POST']) def introspect_token(): return authorization_server.create_endpoint_response( MyJWTIntrospectionEndpoint.ENDPOINT_NAME ) return endpoint def create_revocation_endpoint(app, authorization_server, issuer, jwks): class MyJWTRevocationEndpoint(JWTRevocationEndpoint): def get_jwks(self): return jwks endpoint = MyJWTRevocationEndpoint(issuer=issuer) authorization_server.register_endpoint(endpoint) @app.route('/oauth/revoke', methods=['POST']) def revoke_token(): return authorization_server.create_endpoint_response( MyJWTRevocationEndpoint.ENDPOINT_NAME ) return endpoint def create_user(): user = User(username='foo') db.session.add(user) db.session.commit() return user def create_oauth_client(client_id, user): oauth_client = Client( user_id=user.id, client_id=client_id, client_secret=client_id, ) oauth_client.set_client_metadata( { 'scope': 'profile', 'redirect_uris': ['http://localhost/authorized'], 'response_types': ['code'], 'token_endpoint_auth_method': 'client_secret_post', 'grant_types': ['authorization_code'], } ) db.session.add(oauth_client) db.session.commit() return oauth_client def create_access_token_claims(client, user, issuer, **kwargs): now = int(time.time()) expires_in = now + 3600 auth_time = now - 60 return { 'iss': kwargs.get('issuer', issuer), 'exp': kwargs.get('exp', expires_in), 'aud': kwargs.get('aud', client.client_id), 'sub': kwargs.get('sub', user.get_user_id()), 'client_id': kwargs.get('client_id', client.client_id), 'iat': kwargs.get('iat', now), 'jti': kwargs.get('jti', generate_token(16)), 'auth_time': kwargs.get('auth_time', auth_time), 'scope': kwargs.get('scope', client.scope), 'groups': kwargs.get('groups', ['admins']), 'roles': kwargs.get('groups', ['student']), 'entitlements': kwargs.get('groups', ['captain']), } def create_access_token(claims, jwks, alg='RS256', typ='at+jwt'): header = {'alg': alg, 'typ': typ} access_token = jwt.encode( header, claims, key=jwks, check=False, ) return access_token.decode() def create_token(access_token): token = Token( user_id=1, client_id='resource-server', token_type='bearer', access_token=access_token, scope='profile', expires_in=3600, ) db.session.add(token) db.session.commit() return token class AuthorizationCodeGrant(CodeGrantMixin, _AuthorizationCodeGrant): TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none'] def save_authorization_code(self, code, request): return save_authorization_code(code, request) class JWTAccessTokenGenerationTest(TestCase): def setUp(self): super().setUp() self.issuer = 'https://authlib.org/' self.jwks = read_file_path('jwks_private.json') self.authorization_server = create_authorization_server(self.app) self.authorization_server.register_grant(AuthorizationCodeGrant) self.token_generator = create_token_generator( self.authorization_server, self.issuer, self.jwks ) self.user = create_user() self.oauth_client = create_oauth_client('client-id', self.user) def test_generate_jwt_access_token(self): res = self.client.post( '/oauth/authorize', data={ 'response_type': self.oauth_client.response_types[0], 'client_id': self.oauth_client.client_id, 'redirect_uri': self.oauth_client.redirect_uris[0], 'scope': self.oauth_client.scope, 'user_id': self.user.id, }, ) params = dict(url_decode(urlparse.urlparse(res.location).query)) code = params['code'] res = self.client.post( '/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': self.oauth_client.client_id, 'client_secret': self.oauth_client.client_secret, 'scope': ' '.join(self.oauth_client.scope), 'redirect_uri': self.oauth_client.redirect_uris[0], }, ) access_token = res.json['access_token'] claims = jwt.decode(access_token, self.jwks) assert claims['iss'] == self.issuer assert claims['sub'] == self.user.id assert claims['scope'] == self.oauth_client.scope assert claims['client_id'] == self.oauth_client.client_id # This specification registers the 'application/at+jwt' media type, which can # be used to indicate that the content is a JWT access token. JWT access tokens # MUST include this media type in the 'typ' header parameter to explicitly # declare that the JWT represents an access token complying with this profile. # Per the definition of 'typ' in Section 4.1.9 of [RFC7515], it is RECOMMENDED # that the 'application/' prefix be omitted. Therefore, the 'typ' value used # SHOULD be 'at+jwt'. assert claims.header['typ'] == 'at+jwt' def test_generate_jwt_access_token_extra_claims(self): ''' Authorization servers MAY return arbitrary attributes not defined in any existing specification, as long as the corresponding claim names are collision resistant or the access tokens are meant to be used only within a private subsystem. Please refer to Sections 4.2 and 4.3 of [RFC7519] for details. ''' def get_extra_claims(client, grant_type, user, scope): return {'username': user.username} self.token_generator.get_extra_claims = get_extra_claims res = self.client.post( '/oauth/authorize', data={ 'response_type': self.oauth_client.response_types[0], 'client_id': self.oauth_client.client_id, 'redirect_uri': self.oauth_client.redirect_uris[0], 'scope': self.oauth_client.scope, 'user_id': self.user.id, }, ) params = dict(url_decode(urlparse.urlparse(res.location).query)) code = params['code'] res = self.client.post( '/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': self.oauth_client.client_id, 'client_secret': self.oauth_client.client_secret, 'scope': ' '.join(self.oauth_client.scope), 'redirect_uri': self.oauth_client.redirect_uris[0], }, ) access_token = res.json['access_token'] claims = jwt.decode(access_token, self.jwks) assert claims['username'] == self.user.username @pytest.mark.skip def test_generate_jwt_access_token_no_user(self): res = self.client.post( '/oauth/authorize', data={ 'response_type': self.oauth_client.response_types[0], 'client_id': self.oauth_client.client_id, 'redirect_uri': self.oauth_client.redirect_uris[0], 'scope': self.oauth_client.scope, #'user_id': self.user.id, }, ) params = dict(url_decode(urlparse.urlparse(res.location).query)) code = params['code'] res = self.client.post( '/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': self.oauth_client.client_id, 'client_secret': self.oauth_client.client_secret, 'scope': ' '.join(self.oauth_client.scope), 'redirect_uri': self.oauth_client.redirect_uris[0], }, ) access_token = res.json['access_token'] claims = jwt.decode(access_token, self.jwks) assert claims['sub'] == self.oauth_client.client_id def test_optional_fields(self): self.token_generator.get_auth_time = lambda *args: 1234 self.token_generator.get_amr = lambda *args: 'amr' self.token_generator.get_acr = lambda *args: 'acr' res = self.client.post( '/oauth/authorize', data={ 'response_type': self.oauth_client.response_types[0], 'client_id': self.oauth_client.client_id, 'redirect_uri': self.oauth_client.redirect_uris[0], 'scope': self.oauth_client.scope, 'user_id': self.user.id, }, ) params = dict(url_decode(urlparse.urlparse(res.location).query)) code = params['code'] res = self.client.post( '/oauth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': self.oauth_client.client_id, 'client_secret': self.oauth_client.client_secret, 'scope': ' '.join(self.oauth_client.scope), 'redirect_uri': self.oauth_client.redirect_uris[0], }, ) access_token = res.json['access_token'] claims = jwt.decode(access_token, self.jwks) assert claims['auth_time'] == 1234 assert claims['amr'] == 'amr' assert claims['acr'] == 'acr' class JWTAccessTokenResourceServerTest(TestCase): def setUp(self): super().setUp() self.issuer = 'https://authorization-server.example.org/' self.resource_server = 'resource-server-id' self.jwks = read_file_path('jwks_private.json') self.token_validator = create_token_validator( self.issuer, self.resource_server, self.jwks ) self.resource_protector = create_resource_protector( self.app, self.token_validator ) self.user = create_user() self.oauth_client = create_oauth_client(self.resource_server, self.user) self.claims = create_access_token_claims( self.oauth_client, self.user, self.issuer ) self.access_token = create_access_token(self.claims, self.jwks) self.token = create_token(self.access_token) def test_access_resource(self): headers = {'Authorization': f'Bearer {self.access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['username'], 'foo') def test_missing_authorization(self): rv = self.client.get('/protected') self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'missing_authorization') def test_unsupported_token_type(self): headers = {'Authorization': 'invalid token'} rv = self.client.get('/protected', headers=headers) self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unsupported_token_type') def test_invalid_token(self): headers = {'Authorization': 'Bearer invalid'} rv = self.client.get('/protected', headers=headers) self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') def test_typ(self): ''' The resource server MUST verify that the 'typ' header value is 'at+jwt' or 'application/at+jwt' and reject tokens carrying any other value. ''' access_token = create_access_token(self.claims, self.jwks, typ='at+jwt') headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['username'], 'foo') access_token = create_access_token( self.claims, self.jwks, typ='application/at+jwt' ) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['username'], 'foo') access_token = create_access_token(self.claims, self.jwks, typ='invalid') headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') def test_missing_required_claims(self): required_claims = ['iss', 'exp', 'aud', 'sub', 'client_id', 'iat', 'jti'] for claim in required_claims: claims = create_access_token_claims( self.oauth_client, self.user, self.issuer ) del claims[claim] access_token = create_access_token(claims, self.jwks) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') def test_invalid_iss(self): ''' The issuer identifier for the authorization server (which is typically obtained during discovery) MUST exactly match the value of the 'iss' claim. ''' self.claims['iss'] = 'invalid-issuer' access_token = create_access_token(self.claims, self.jwks) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') def test_invalid_aud(self): ''' The resource server MUST validate that the 'aud' claim contains a resource indicator value corresponding to an identifier the resource server expects for itself. The JWT access token MUST be rejected if 'aud' does not contain a resource indicator of the current resource server as a valid audience. ''' self.claims['aud'] = 'invalid-resource-indicator' access_token = create_access_token(self.claims, self.jwks) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') def test_invalid_exp(self): ''' The current time MUST be before the time represented by the 'exp' claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. ''' self.claims['exp'] = time.time() - 1 access_token = create_access_token(self.claims, self.jwks) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') def test_scope_restriction(self): ''' If an authorization request includes a scope parameter, the corresponding issued JWT access token SHOULD include a 'scope' claim as defined in Section 4.2 of [RFC8693]. All the individual scope strings in the 'scope' claim MUST have meaning for the resources indicated in the 'aud' claim. See Section 5 for more considerations about the relationship between scope strings and resources indicated by the 'aud' claim. ''' self.claims['scope'] = ['invalid-scope'] access_token = create_access_token(self.claims, self.jwks) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['username'], 'foo') rv = self.client.get('/protected-by-scope', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'insufficient_scope') def test_entitlements_restriction(self): ''' Many authorization servers embed authorization attributes that go beyond the delegated scenarios described by [RFC7519] in the access tokens they issue. Typical examples include resource owner memberships in roles and groups that are relevant to the resource being accessed, entitlements assigned to the resource owner for the targeted resource that the authorization server knows about, and so on. An authorization server wanting to include such attributes in a JWT access token SHOULD use the 'groups', 'roles', and 'entitlements' attributes of the 'User' resource schema defined by Section 4.1.2 of [RFC7643]) as claim types. ''' for claim in ['groups', 'roles', 'entitlements']: claims = create_access_token_claims( self.oauth_client, self.user, self.issuer ) claims[claim] = ['invalid'] access_token = create_access_token(claims, self.jwks) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['username'], 'foo') rv = self.client.get(f'/protected-by-{claim}', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') def test_extra_attributes(self): ''' Authorization servers MAY return arbitrary attributes not defined in any existing specification, as long as the corresponding claim names are collision resistant or the access tokens are meant to be used only within a private subsystem. Please refer to Sections 4.2 and 4.3 of [RFC7519] for details. ''' self.claims['email'] = 'user@example.org' access_token = create_access_token(self.claims, self.jwks) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['token']['email'], 'user@example.org') def test_invalid_auth_time(self): self.claims['auth_time'] = 'invalid-auth-time' access_token = create_access_token(self.claims, self.jwks) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') def test_invalid_amr(self): self.claims['amr'] = 'invalid-amr' access_token = create_access_token(self.claims, self.jwks) headers = {'Authorization': f'Bearer {access_token}'} rv = self.client.get('/protected', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') class JWTAccessTokenIntrospectionTest(TestCase): def setUp(self): super().setUp() self.issuer = 'https://authlib.org/' self.resource_server = 'resource-server-id' self.jwks = read_file_path('jwks_private.json') self.authorization_server = create_authorization_server(self.app) self.authorization_server.register_grant(AuthorizationCodeGrant) self.introspection_endpoint = create_introspection_endpoint( self.app, self.authorization_server, self.issuer, self.jwks ) self.user = create_user() self.oauth_client = create_oauth_client('client-id', self.user) self.claims = create_access_token_claims( self.oauth_client, self.user, self.issuer, aud=[self.resource_server], ) self.access_token = create_access_token(self.claims, self.jwks) def test_introspection(self): headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/introspect', data={'token': self.access_token}, headers=headers ) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertTrue(resp['active']) self.assertEqual(resp['client_id'], self.oauth_client.client_id) self.assertEqual(resp['token_type'], 'Bearer') self.assertEqual(resp['scope'], self.oauth_client.scope) self.assertEqual(resp['sub'], self.user.id) self.assertEqual(resp['aud'], [self.resource_server]) self.assertEqual(resp['iss'], self.issuer) def test_introspection_username(self): self.introspection_endpoint.get_username = lambda user_id: db.session.get( User, user_id ).username headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/introspect', data={'token': self.access_token}, headers=headers ) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertTrue(resp['active']) self.assertEqual(resp['username'], self.user.username) def test_non_access_token_skipped(self): class MyIntrospectionEndpoint(IntrospectionEndpoint): def query_token(self, token, token_type_hint): return None self.authorization_server.register_endpoint(MyIntrospectionEndpoint) headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/introspect', data={ 'token': 'refresh-token', 'token_type_hint': 'refresh_token', }, headers=headers, ) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertFalse(resp['active']) def test_access_token_non_jwt_skipped(self): class MyIntrospectionEndpoint(IntrospectionEndpoint): def query_token(self, token, token_type_hint): return None self.authorization_server.register_endpoint(MyIntrospectionEndpoint) headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/introspect', data={ 'token': 'non-jwt-access-token', }, headers=headers, ) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertFalse(resp['active']) def test_permission_denied(self): self.introspection_endpoint.check_permission = lambda *args: False headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/introspect', data={'token': self.access_token}, headers=headers ) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertFalse(resp['active']) def test_token_expired(self): self.claims['exp'] = time.time() - 3600 access_token = create_access_token(self.claims, self.jwks) headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/introspect', data={'token': access_token}, headers=headers ) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertFalse(resp['active']) def test_introspection_different_issuer(self): class MyIntrospectionEndpoint(IntrospectionEndpoint): def query_token(self, token, token_type_hint): return None self.authorization_server.register_endpoint(MyIntrospectionEndpoint) self.claims['iss'] = 'different-issuer' access_token = create_access_token(self.claims, self.jwks) headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/introspect', data={'token': access_token}, headers=headers ) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertFalse(resp['active']) def test_introspection_invalid_claim(self): self.claims['exp'] = "invalid" access_token = create_access_token(self.claims, self.jwks) headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/introspect', data={'token': access_token}, headers=headers ) self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') class JWTAccessTokenRevocationTest(TestCase): def setUp(self): super().setUp() self.issuer = 'https://authlib.org/' self.resource_server = 'resource-server-id' self.jwks = read_file_path('jwks_private.json') self.authorization_server = create_authorization_server(self.app) self.authorization_server.register_grant(AuthorizationCodeGrant) self.revocation_endpoint = create_revocation_endpoint( self.app, self.authorization_server, self.issuer, self.jwks ) self.user = create_user() self.oauth_client = create_oauth_client('client-id', self.user) self.claims = create_access_token_claims( self.oauth_client, self.user, self.issuer, aud=[self.resource_server], ) self.access_token = create_access_token(self.claims, self.jwks) def test_revocation(self): headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/revoke', data={'token': self.access_token}, headers=headers ) self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unsupported_token_type') def test_non_access_token_skipped(self): class MyRevocationEndpoint(RevocationEndpoint): def query_token(self, token, token_type_hint): return None self.authorization_server.register_endpoint(MyRevocationEndpoint) headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/revoke', data={ 'token': 'refresh-token', 'token_type_hint': 'refresh_token', }, headers=headers, ) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertEqual(resp, {}) def test_access_token_non_jwt_skipped(self): class MyRevocationEndpoint(RevocationEndpoint): def query_token(self, token, token_type_hint): return None self.authorization_server.register_endpoint(MyRevocationEndpoint) headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/revoke', data={ 'token': 'non-jwt-access-token', }, headers=headers, ) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertEqual(resp, {}) def test_revocation_different_issuer(self): self.claims['iss'] = 'different-issuer' access_token = create_access_token(self.claims, self.jwks) headers = self.create_basic_header( self.oauth_client.client_id, self.oauth_client.client_secret ) rv = self.client.post( '/oauth/revoke', data={'token': access_token}, headers=headers ) self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unsupported_token_type') authlib-1.3.2/tests/flask/test_oauth2/test_jwt_bearer_client_auth.py000066400000000000000000000134601466226534200260020ustar00rootroot00000000000000from flask import json from authlib.oauth2.rfc6749.grants import ClientCredentialsGrant from authlib.oauth2.rfc7523 import ( JWTBearerClientAssertion, client_secret_jwt_sign, private_key_jwt_sign, ) from tests.util import read_file_path from .models import db, User, Client from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class JWTClientCredentialsGrant(ClientCredentialsGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ JWTBearerClientAssertion.CLIENT_AUTH_METHOD, ] class JWTClientAuth(JWTBearerClientAssertion): def validate_jti(self, claims, jti): return True def resolve_client_public_key(self, client, headers): if headers['alg'] == 'RS256': return read_file_path('jwk_public.json') return client.client_secret class ClientCredentialsTest(TestCase): def prepare_data(self, auth_method, validate_jti=True): server = create_authorization_server(self.app) server.register_grant(JWTClientCredentialsGrant) server.register_client_auth_method( JWTClientAuth.CLIENT_AUTH_METHOD, JWTClientAuth('https://localhost/oauth/token', validate_jti) ) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='credential-client', client_secret='credential-secret', ) client.set_client_metadata({ 'scope': 'profile', 'redirect_uris': ['http://localhost/authorized'], 'grant_types': ['client_credentials'], 'token_endpoint_auth_method': auth_method, }) db.session.add(client) db.session.commit() def test_invalid_client(self): self.prepare_data(JWTBearerClientAssertion.CLIENT_AUTH_METHOD) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', 'client_assertion_type': JWTBearerClientAssertion.CLIENT_ASSERTION_TYPE }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_invalid_jwt(self): self.prepare_data(JWTBearerClientAssertion.CLIENT_AUTH_METHOD) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', 'client_assertion_type': JWTBearerClientAssertion.CLIENT_ASSERTION_TYPE, 'client_assertion': client_secret_jwt_sign( client_secret='invalid-secret', client_id='credential-client', token_endpoint='https://localhost/oauth/token', ) }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_not_found_client(self): self.prepare_data(JWTBearerClientAssertion.CLIENT_AUTH_METHOD) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', 'client_assertion_type': JWTBearerClientAssertion.CLIENT_ASSERTION_TYPE, 'client_assertion': client_secret_jwt_sign( client_secret='credential-secret', client_id='invalid-client', token_endpoint='https://localhost/oauth/token', ) }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_not_supported_auth_method(self): self.prepare_data('invalid') rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', 'client_assertion_type': JWTBearerClientAssertion.CLIENT_ASSERTION_TYPE, 'client_assertion': client_secret_jwt_sign( client_secret='credential-secret', client_id='credential-client', token_endpoint='https://localhost/oauth/token', ) }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_client_secret_jwt(self): self.prepare_data(JWTBearerClientAssertion.CLIENT_AUTH_METHOD) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', 'client_assertion_type': JWTBearerClientAssertion.CLIENT_ASSERTION_TYPE, 'client_assertion': client_secret_jwt_sign( client_secret='credential-secret', client_id='credential-client', token_endpoint='https://localhost/oauth/token', claims={'jti': 'nonce'}, ) }) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_private_key_jwt(self): self.prepare_data(JWTBearerClientAssertion.CLIENT_AUTH_METHOD) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', 'client_assertion_type': JWTBearerClientAssertion.CLIENT_ASSERTION_TYPE, 'client_assertion': private_key_jwt_sign( private_key=read_file_path('jwk_private.json'), client_id='credential-client', token_endpoint='https://localhost/oauth/token', ) }) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_not_validate_jti(self): self.prepare_data(JWTBearerClientAssertion.CLIENT_AUTH_METHOD, False) rv = self.client.post('/oauth/token', data={ 'grant_type': 'client_credentials', 'client_assertion_type': JWTBearerClientAssertion.CLIENT_ASSERTION_TYPE, 'client_assertion': client_secret_jwt_sign( client_secret='credential-secret', client_id='credential-client', token_endpoint='https://localhost/oauth/token', ) }) resp = json.loads(rv.data) self.assertIn('access_token', resp) authlib-1.3.2/tests/flask/test_oauth2/test_jwt_bearer_grant.py000066400000000000000000000112471466226534200246170ustar00rootroot00000000000000from flask import json from authlib.oauth2.rfc7523 import JWTBearerGrant as _JWTBearerGrant from authlib.oauth2.rfc7523 import JWTBearerTokenGenerator from tests.util import read_file_path from .models import db, User, Client from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class JWTBearerGrant(_JWTBearerGrant): def resolve_issuer_client(self, issuer): return Client.query.filter_by(client_id=issuer).first() def resolve_client_key(self, client, headers, payload): keys = {'1': 'foo', '2': 'bar'} return keys[headers['kid']] def authenticate_user(self, subject): return None def has_granted_permission(self, client, user): return True class JWTBearerGrantTest(TestCase): def prepare_data(self, grant_type=None, token_generator=None): server = create_authorization_server(self.app) server.register_grant(JWTBearerGrant) if token_generator: server.register_token_generator(JWTBearerGrant.GRANT_TYPE, token_generator) if grant_type is None: grant_type = JWTBearerGrant.GRANT_TYPE user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='jwt-client', client_secret='jwt-secret', ) client.set_client_metadata({ 'scope': 'profile', 'redirect_uris': ['http://localhost/authorized'], 'grant_types': [grant_type], }) db.session.add(client) db.session.commit() def test_missing_assertion(self): self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': JWTBearerGrant.GRANT_TYPE }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') self.assertIn('assertion', resp['error_description']) def test_invalid_assertion(self): self.prepare_data() assertion = JWTBearerGrant.sign( 'foo', issuer='jwt-client', audience='https://i.b/token', subject='none', header={'alg': 'HS256', 'kid': '1'} ) rv = self.client.post('/oauth/token', data={ 'grant_type': JWTBearerGrant.GRANT_TYPE, 'assertion': assertion }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_grant') def test_authorize_token(self): self.prepare_data() assertion = JWTBearerGrant.sign( 'foo', issuer='jwt-client', audience='https://i.b/token', subject=None, header={'alg': 'HS256', 'kid': '1'} ) rv = self.client.post('/oauth/token', data={ 'grant_type': JWTBearerGrant.GRANT_TYPE, 'assertion': assertion }) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_unauthorized_client(self): self.prepare_data('password') assertion = JWTBearerGrant.sign( 'bar', issuer='jwt-client', audience='https://i.b/token', subject=None, header={'alg': 'HS256', 'kid': '2'} ) rv = self.client.post('/oauth/token', data={ 'grant_type': JWTBearerGrant.GRANT_TYPE, 'assertion': assertion }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unauthorized_client') def test_token_generator(self): m = 'tests.flask.test_oauth2.oauth2_server:token_generator' self.app.config.update({'OAUTH2_ACCESS_TOKEN_GENERATOR': m}) self.prepare_data() assertion = JWTBearerGrant.sign( 'foo', issuer='jwt-client', audience='https://i.b/token', subject=None, header={'alg': 'HS256', 'kid': '1'} ) rv = self.client.post('/oauth/token', data={ 'grant_type': JWTBearerGrant.GRANT_TYPE, 'assertion': assertion }) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('j-', resp['access_token']) def test_jwt_bearer_token_generator(self): private_key = read_file_path('jwks_private.json') self.prepare_data(token_generator=JWTBearerTokenGenerator(private_key)) assertion = JWTBearerGrant.sign( 'foo', issuer='jwt-client', audience='https://i.b/token', subject=None, header={'alg': 'HS256', 'kid': '1'} ) rv = self.client.post('/oauth/token', data={ 'grant_type': JWTBearerGrant.GRANT_TYPE, 'assertion': assertion }) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertEqual(resp['access_token'].count('.'), 2) authlib-1.3.2/tests/flask/test_oauth2/test_oauth2_server.py000066400000000000000000000145401466226534200240670ustar00rootroot00000000000000from flask import json, jsonify from authlib.integrations.flask_oauth2 import ResourceProtector, current_token from authlib.integrations.sqla_oauth2 import create_bearer_token_validator from .models import db, User, Client, Token from .oauth2_server import TestCase from .oauth2_server import create_authorization_server require_oauth = ResourceProtector() BearerTokenValidator = create_bearer_token_validator(db.session, Token) require_oauth.register_token_validator(BearerTokenValidator()) def create_resource_server(app): @app.route('/user') @require_oauth('profile') def user_profile(): user = current_token.user return jsonify(id=user.id, username=user.username) @app.route('/user/email') @require_oauth('email') def user_email(): user = current_token.user return jsonify(email=user.username + '@example.com') @app.route('/info') @require_oauth() def public_info(): return jsonify(status='ok') @app.route('/operator-and') @require_oauth(['profile email']) def operator_and(): return jsonify(status='ok') @app.route('/operator-or') @require_oauth(['profile', 'email']) def operator_or(): return jsonify(status='ok') @app.route('/acquire') def test_acquire(): with require_oauth.acquire('profile') as token: user = token.user return jsonify(id=user.id, username=user.username) @app.route('/optional') @require_oauth('profile', optional=True) def test_optional_token(): if current_token: user = current_token.user return jsonify(id=user.id, username=user.username) else: return jsonify(id=0, username='anonymous') class AuthorizationTest(TestCase): def test_none_grant(self): create_authorization_server(self.app) authorize_url = ( '/oauth/authorize?response_type=token' '&client_id=implicit-client' ) rv = self.client.get(authorize_url) self.assertIn(b'unsupported_response_type', rv.data) rv = self.client.post(authorize_url, data={'user_id': '1'}) self.assertNotEqual(rv.status, 200) rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'code': 'x', }) data = json.loads(rv.data) self.assertEqual(data['error'], 'unsupported_grant_type') class ResourceTest(TestCase): def prepare_data(self): create_resource_server(self.app) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='resource-client', client_secret='resource-secret', ) client.set_client_metadata({ 'scope': 'profile', 'redirect_uris': ['http://localhost/authorized'], }) db.session.add(client) db.session.commit() def create_token(self, expires_in=3600): token = Token( user_id=1, client_id='resource-client', token_type='bearer', access_token='a1', scope='profile', expires_in=expires_in, ) db.session.add(token) db.session.commit() def create_bearer_header(self, token): return {'Authorization': 'Bearer ' + token} def test_invalid_token(self): self.prepare_data() rv = self.client.get('/user') self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'missing_authorization') headers = {'Authorization': 'invalid token'} rv = self.client.get('/user', headers=headers) self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unsupported_token_type') headers = self.create_bearer_header('invalid') rv = self.client.get('/user', headers=headers) self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') def test_expired_token(self): self.prepare_data() self.create_token(-10) headers = self.create_bearer_header('a1') rv = self.client.get('/user', headers=headers) self.assertEqual(rv.status_code, 401) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_token') rv = self.client.get('/acquire', headers=headers) self.assertEqual(rv.status_code, 401) def test_insufficient_token(self): self.prepare_data() self.create_token() headers = self.create_bearer_header('a1') rv = self.client.get('/user/email', headers=headers) self.assertEqual(rv.status_code, 403) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'insufficient_scope') def test_access_resource(self): self.prepare_data() self.create_token() headers = self.create_bearer_header('a1') rv = self.client.get('/user', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['username'], 'foo') rv = self.client.get('/acquire', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['username'], 'foo') rv = self.client.get('/info', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['status'], 'ok') def test_scope_operator(self): self.prepare_data() self.create_token() headers = self.create_bearer_header('a1') rv = self.client.get('/operator-and', headers=headers) self.assertEqual(rv.status_code, 403) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'insufficient_scope') rv = self.client.get('/operator-or', headers=headers) self.assertEqual(rv.status_code, 200) def test_optional_token(self): self.prepare_data() rv = self.client.get('/optional') self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertEqual(resp['username'], 'anonymous') self.create_token() headers = self.create_bearer_header('a1') rv = self.client.get('/optional', headers=headers) self.assertEqual(rv.status_code, 200) resp = json.loads(rv.data) self.assertEqual(resp['username'], 'foo') authlib-1.3.2/tests/flask/test_oauth2/test_openid_code_grant.py000066400000000000000000000177101466226534200247440ustar00rootroot00000000000000from flask import json, current_app from authlib.common.urls import urlparse, url_decode, url_encode from authlib.jose import jwt from authlib.oidc.core import CodeIDToken from authlib.oidc.core.grants import OpenIDCode as _OpenIDCode from authlib.oauth2.rfc6749.grants import ( AuthorizationCodeGrant as _AuthorizationCodeGrant, ) from tests.util import read_file_path from .models import db, User, Client, exists_nonce from .models import CodeGrantMixin, save_authorization_code from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class AuthorizationCodeGrant(CodeGrantMixin, _AuthorizationCodeGrant): def save_authorization_code(self, code, request): return save_authorization_code(code, request) class OpenIDCode(_OpenIDCode): def get_jwt_config(self, grant): key = current_app.config['OAUTH2_JWT_KEY'] alg = current_app.config['OAUTH2_JWT_ALG'] iss = current_app.config['OAUTH2_JWT_ISS'] return dict(key=key, alg=alg, iss=iss, exp=3600) def exists_nonce(self, nonce, request): return exists_nonce(nonce, request) def generate_user_info(self, user, scopes): return user.generate_user_info(scopes) class BaseTestCase(TestCase): def config_app(self): self.app.config.update({ 'OAUTH2_JWT_ISS': 'Authlib', 'OAUTH2_JWT_KEY': 'secret', 'OAUTH2_JWT_ALG': 'HS256', }) def prepare_data(self): self.config_app() server = create_authorization_server(self.app) server.register_grant(AuthorizationCodeGrant, [OpenIDCode()]) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='code-client', client_secret='code-secret', ) client.set_client_metadata({ 'redirect_uris': ['https://a.b'], 'scope': 'openid profile address', 'response_types': ['code'], 'grant_types': ['authorization_code'], }) db.session.add(client) db.session.commit() class OpenIDCodeTest(BaseTestCase): def test_authorize_token(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'code', 'client_id': 'code-client', 'state': 'bar', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', 'user_id': '1' }) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) self.assertEqual(params['state'], 'bar') code = params['code'] headers = self.create_basic_header('code-client', 'code-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'redirect_uri': 'https://a.b', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('id_token', resp) claims = jwt.decode( resp['id_token'], 'secret', claims_cls=CodeIDToken, claims_options={'iss': {'value': 'Authlib'}} ) claims.validate() def test_pure_code_flow(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'code', 'client_id': 'code-client', 'state': 'bar', 'scope': 'profile', 'redirect_uri': 'https://a.b', 'user_id': '1' }) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) self.assertEqual(params['state'], 'bar') code = params['code'] headers = self.create_basic_header('code-client', 'code-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'redirect_uri': 'https://a.b', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertNotIn('id_token', resp) def test_nonce_replay(self): self.prepare_data() data = { 'response_type': 'code', 'client_id': 'code-client', 'user_id': '1', 'state': 'bar', 'nonce': 'abc', 'scope': 'openid profile', 'redirect_uri': 'https://a.b' } rv = self.client.post('/oauth/authorize', data=data) self.assertIn('code=', rv.location) rv = self.client.post('/oauth/authorize', data=data) self.assertIn('error=', rv.location) def test_prompt(self): self.prepare_data() params = [ ('response_type', 'code'), ('client_id', 'code-client'), ('state', 'bar'), ('nonce', 'abc'), ('scope', 'openid profile'), ('redirect_uri', 'https://a.b') ] query = url_encode(params) rv = self.client.get('/oauth/authorize?' + query) self.assertEqual(rv.data, b'login') query = url_encode(params + [('user_id', '1')]) rv = self.client.get('/oauth/authorize?' + query) self.assertEqual(rv.data, b'ok') query = url_encode(params + [('prompt', 'login')]) rv = self.client.get('/oauth/authorize?' + query) self.assertEqual(rv.data, b'login') class RSAOpenIDCodeTest(BaseTestCase): def config_app(self): self.app.config.update({ 'OAUTH2_JWT_ISS': 'Authlib', 'OAUTH2_JWT_KEY': read_file_path('jwk_private.json'), 'OAUTH2_JWT_ALG': 'RS256', }) def get_validate_key(self): return read_file_path('jwk_public.json') def test_authorize_token(self): # generate refresh token self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'code', 'client_id': 'code-client', 'state': 'bar', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', 'user_id': '1' }) self.assertIn('code=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) self.assertEqual(params['state'], 'bar') code = params['code'] headers = self.create_basic_header('code-client', 'code-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'redirect_uri': 'https://a.b', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('id_token', resp) claims = jwt.decode( resp['id_token'], self.get_validate_key(), claims_cls=CodeIDToken, claims_options={'iss': {'value': 'Authlib'}} ) claims.validate() class JWKSOpenIDCodeTest(RSAOpenIDCodeTest): def config_app(self): self.app.config.update({ 'OAUTH2_JWT_ISS': 'Authlib', 'OAUTH2_JWT_KEY': read_file_path('jwks_private.json'), 'OAUTH2_JWT_ALG': 'PS256', }) def get_validate_key(self): return read_file_path('jwks_public.json') class ECOpenIDCodeTest(RSAOpenIDCodeTest): def config_app(self): self.app.config.update({ 'OAUTH2_JWT_ISS': 'Authlib', 'OAUTH2_JWT_KEY': read_file_path('secp521r1-private.json'), 'OAUTH2_JWT_ALG': 'ES512', }) def get_validate_key(self): return read_file_path('secp521r1-public.json') class PEMOpenIDCodeTest(RSAOpenIDCodeTest): def config_app(self): self.app.config.update({ 'OAUTH2_JWT_ISS': 'Authlib', 'OAUTH2_JWT_KEY': read_file_path('rsa_private.pem'), 'OAUTH2_JWT_ALG': 'RS256', }) def get_validate_key(self): return read_file_path('rsa_public.pem') authlib-1.3.2/tests/flask/test_oauth2/test_openid_hybrid_grant.py000066400000000000000000000233461466226534200253150ustar00rootroot00000000000000from flask import json from authlib.common.urls import urlparse, url_decode from authlib.jose import jwt from authlib.oidc.core import HybridIDToken from authlib.oidc.core.grants import ( OpenIDCode as _OpenIDCode, OpenIDHybridGrant as _OpenIDHybridGrant, ) from authlib.oauth2.rfc6749.grants import ( AuthorizationCodeGrant as _AuthorizationCodeGrant, ) from .models import db, User, Client, exists_nonce from .models import CodeGrantMixin, save_authorization_code from .oauth2_server import TestCase from .oauth2_server import create_authorization_server JWT_CONFIG = {'iss': 'Authlib', 'key': 'secret', 'alg': 'HS256', 'exp': 3600} class AuthorizationCodeGrant(CodeGrantMixin, _AuthorizationCodeGrant): def save_authorization_code(self, code, request): return save_authorization_code(code, request) class OpenIDCode(_OpenIDCode): def get_jwt_config(self, grant): return dict(JWT_CONFIG) def exists_nonce(self, nonce, request): return exists_nonce(nonce, request) def generate_user_info(self, user, scopes): return user.generate_user_info(scopes) class OpenIDHybridGrant(_OpenIDHybridGrant): def save_authorization_code(self, code, request): return save_authorization_code(code, request) def get_jwt_config(self): return dict(JWT_CONFIG) def exists_nonce(self, nonce, request): return exists_nonce(nonce, request) def generate_user_info(self, user, scopes): return user.generate_user_info(scopes) class OpenIDCodeTest(TestCase): def prepare_data(self): server = create_authorization_server(self.app) server.register_grant(OpenIDHybridGrant) server.register_grant(AuthorizationCodeGrant, [OpenIDCode()]) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='hybrid-client', client_secret='hybrid-secret', ) client.set_client_metadata({ 'redirect_uris': ['https://a.b'], 'scope': 'openid profile address', 'response_types': ['code id_token', 'code token', 'code id_token token'], 'grant_types': ['authorization_code'], }) db.session.add(client) db.session.commit() def validate_claims(self, id_token, params): claims = jwt.decode( id_token, 'secret', claims_cls=HybridIDToken, claims_params=params ) claims.validate() def test_invalid_client_id(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'code token', 'state': 'bar', 'nonce': 'abc', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', 'user_id': '1', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') rv = self.client.post('/oauth/authorize', data={ 'client_id': 'invalid-client', 'response_type': 'code token', 'state': 'bar', 'nonce': 'abc', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', 'user_id': '1', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_require_nonce(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'client_id': 'hybrid-client', 'response_type': 'code token', 'scope': 'openid profile', 'state': 'bar', 'redirect_uri': 'https://a.b', 'user_id': '1' }) self.assertIn('error=invalid_request', rv.location) self.assertIn('nonce', rv.location) def test_invalid_response_type(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'client_id': 'hybrid-client', 'response_type': 'code id_token invalid', 'state': 'bar', 'nonce': 'abc', 'scope': 'profile', 'redirect_uri': 'https://a.b', 'user_id': '1', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unsupported_response_type') def test_invalid_scope(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'client_id': 'hybrid-client', 'response_type': 'code id_token', 'state': 'bar', 'nonce': 'abc', 'scope': 'profile', 'redirect_uri': 'https://a.b', 'user_id': '1', }) self.assertIn('error=invalid_scope', rv.location) def test_access_denied(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'client_id': 'hybrid-client', 'response_type': 'code token', 'state': 'bar', 'nonce': 'abc', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', }) self.assertIn('error=access_denied', rv.location) def test_code_access_token(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'client_id': 'hybrid-client', 'response_type': 'code token', 'state': 'bar', 'nonce': 'abc', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', 'user_id': '1', }) self.assertIn('code=', rv.location) self.assertIn('access_token=', rv.location) self.assertNotIn('id_token=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).fragment)) self.assertEqual(params['state'], 'bar') code = params['code'] headers = self.create_basic_header('hybrid-client', 'hybrid-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'redirect_uri': 'https://a.b', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('id_token', resp) def test_code_id_token(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'client_id': 'hybrid-client', 'response_type': 'code id_token', 'state': 'bar', 'nonce': 'abc', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', 'user_id': '1', }) self.assertIn('code=', rv.location) self.assertIn('id_token=', rv.location) self.assertNotIn('access_token=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).fragment)) self.assertEqual(params['state'], 'bar') params['nonce'] = 'abc' params['client_id'] = 'hybrid-client' self.validate_claims(params['id_token'], params) code = params['code'] headers = self.create_basic_header('hybrid-client', 'hybrid-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'redirect_uri': 'https://a.b', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('id_token', resp) def test_code_id_token_access_token(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'client_id': 'hybrid-client', 'response_type': 'code id_token token', 'state': 'bar', 'nonce': 'abc', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', 'user_id': '1', }) self.assertIn('code=', rv.location) self.assertIn('id_token=', rv.location) self.assertIn('access_token=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).fragment)) self.assertEqual(params['state'], 'bar') self.validate_claims(params['id_token'], params) code = params['code'] headers = self.create_basic_header('hybrid-client', 'hybrid-secret') rv = self.client.post('/oauth/token', data={ 'grant_type': 'authorization_code', 'redirect_uri': 'https://a.b', 'code': code, }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('id_token', resp) def test_response_mode_query(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'client_id': 'hybrid-client', 'response_type': 'code id_token token', 'response_mode': 'query', 'state': 'bar', 'nonce': 'abc', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', 'user_id': '1', }) self.assertIn('code=', rv.location) self.assertIn('id_token=', rv.location) self.assertIn('access_token=', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) self.assertEqual(params['state'], 'bar') def test_response_mode_form_post(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'client_id': 'hybrid-client', 'response_type': 'code id_token token', 'response_mode': 'form_post', 'state': 'bar', 'nonce': 'abc', 'scope': 'openid profile', 'redirect_uri': 'https://a.b', 'user_id': '1', }) self.assertIn(b'name="code"', rv.data) self.assertIn(b'name="id_token"', rv.data) self.assertIn(b'name="access_token"', rv.data) authlib-1.3.2/tests/flask/test_oauth2/test_openid_implict_grant.py000066400000000000000000000136761466226534200255020ustar00rootroot00000000000000from authlib.jose import JsonWebToken from authlib.oidc.core import ImplicitIDToken from authlib.oidc.core.grants import ( OpenIDImplicitGrant as _OpenIDImplicitGrant ) from authlib.common.urls import urlparse, url_decode, add_params_to_uri from .models import db, User, Client, exists_nonce from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class OpenIDImplicitGrant(_OpenIDImplicitGrant): def get_jwt_config(self): return dict(key='secret', alg='HS256', iss='Authlib', exp=3600) def generate_user_info(self, user, scopes): return user.generate_user_info(scopes) def exists_nonce(self, nonce, request): return exists_nonce(nonce, request) class ImplicitTest(TestCase): def prepare_data(self): server = create_authorization_server(self.app) server.register_grant(OpenIDImplicitGrant) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='implicit-client', client_secret='', ) client.set_client_metadata({ 'redirect_uris': ['https://a.b/c'], 'scope': 'openid profile', 'token_endpoint_auth_method': 'none', 'response_types': ['id_token', 'id_token token'], }) self.authorize_url = ( '/oauth/authorize?response_type=token' '&client_id=implicit-client' ) db.session.add(client) db.session.commit() def validate_claims(self, id_token, params): jwt = JsonWebToken(['HS256']) claims = jwt.decode( id_token, 'secret', claims_cls=ImplicitIDToken, claims_params=params ) claims.validate() def test_consent_view(self): self.prepare_data() rv = self.client.get(add_params_to_uri('/oauth/authorize', { 'response_type': 'id_token', 'client_id': 'implicit-client', 'scope': 'openid profile', 'state': 'foo', 'redirect_uri': 'https://a.b/c', 'user_id': '1' })) self.assertIn(b'error=invalid_request', rv.data) self.assertIn(b'nonce', rv.data) def test_require_nonce(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'id_token', 'client_id': 'implicit-client', 'scope': 'openid profile', 'state': 'bar', 'redirect_uri': 'https://a.b/c', 'user_id': '1' }) self.assertIn('error=invalid_request', rv.location) self.assertIn('nonce', rv.location) def test_missing_openid_in_scope(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'id_token token', 'client_id': 'implicit-client', 'scope': 'profile', 'state': 'bar', 'nonce': 'abc', 'redirect_uri': 'https://a.b/c', 'user_id': '1' }) self.assertIn('error=invalid_scope', rv.location) def test_denied(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'id_token', 'client_id': 'implicit-client', 'scope': 'openid profile', 'state': 'bar', 'nonce': 'abc', 'redirect_uri': 'https://a.b/c', }) self.assertIn('error=access_denied', rv.location) def test_authorize_access_token(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'id_token token', 'client_id': 'implicit-client', 'scope': 'openid profile', 'state': 'bar', 'nonce': 'abc', 'redirect_uri': 'https://a.b/c', 'user_id': '1' }) self.assertIn('access_token=', rv.location) self.assertIn('id_token=', rv.location) self.assertIn('state=bar', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).fragment)) self.validate_claims(params['id_token'], params) def test_authorize_id_token(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'id_token', 'client_id': 'implicit-client', 'scope': 'openid profile', 'state': 'bar', 'nonce': 'abc', 'redirect_uri': 'https://a.b/c', 'user_id': '1' }) self.assertIn('id_token=', rv.location) self.assertIn('state=bar', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).fragment)) self.validate_claims(params['id_token'], params) def test_response_mode_query(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'id_token', 'response_mode': 'query', 'client_id': 'implicit-client', 'scope': 'openid profile', 'state': 'bar', 'nonce': 'abc', 'redirect_uri': 'https://a.b/c', 'user_id': '1' }) self.assertIn('id_token=', rv.location) self.assertIn('state=bar', rv.location) params = dict(url_decode(urlparse.urlparse(rv.location).query)) self.validate_claims(params['id_token'], params) def test_response_mode_form_post(self): self.prepare_data() rv = self.client.post('/oauth/authorize', data={ 'response_type': 'id_token', 'response_mode': 'form_post', 'client_id': 'implicit-client', 'scope': 'openid profile', 'state': 'bar', 'nonce': 'abc', 'redirect_uri': 'https://a.b/c', 'user_id': '1' }) self.assertIn(b'name="id_token"', rv.data) self.assertIn(b'name="state"', rv.data) authlib-1.3.2/tests/flask/test_oauth2/test_password_grant.py000066400000000000000000000146541466226534200243420ustar00rootroot00000000000000from flask import json from authlib.common.urls import add_params_to_uri from authlib.oauth2.rfc6749.grants import ( ResourceOwnerPasswordCredentialsGrant as _PasswordGrant, ) from authlib.oidc.core import OpenIDToken from .models import db, User, Client from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class IDToken(OpenIDToken): def get_jwt_config(self, grant): return { 'iss': 'Authlib', 'key': 'secret', 'alg': 'HS256', } def generate_user_info(self, user, scopes): return user.generate_user_info(scopes) class PasswordGrant(_PasswordGrant): def authenticate_user(self, username, password): user = User.query.filter_by(username=username).first() if user.check_password(password): return user class PasswordTest(TestCase): def prepare_data(self, grant_type='password', extensions=None): server = create_authorization_server(self.app) server.register_grant(PasswordGrant, extensions) self.server = server user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='password-client', client_secret='password-secret', ) client.set_client_metadata({ 'scope': 'openid profile', 'grant_types': [grant_type], 'redirect_uris': ['http://localhost/authorized'], }) db.session.add(client) db.session.commit() def test_invalid_client(self): self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = self.create_basic_header( 'password-client', 'invalid-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_invalid_scope(self): self.prepare_data() self.server.scopes_supported = ['profile'] headers = self.create_basic_header( 'password-client', 'password-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', 'scope': 'invalid', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_scope') def test_invalid_request(self): self.prepare_data() headers = self.create_basic_header( 'password-client', 'password-secret' ) rv = self.client.get(add_params_to_uri('/oauth/token', { 'grant_type': 'password', }), headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unsupported_grant_type') rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'wrong', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') def test_invalid_grant_type(self): self.prepare_data(grant_type='invalid') headers = self.create_basic_header( 'password-client', 'password-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unauthorized_client') def test_authorize_token(self): self.prepare_data() headers = self.create_basic_header( 'password-client', 'password-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_token_generator(self): m = 'tests.flask.test_oauth2.oauth2_server:token_generator' self.app.config.update({'OAUTH2_ACCESS_TOKEN_GENERATOR': m}) self.prepare_data() headers = self.create_basic_header( 'password-client', 'password-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('p-password.1.', resp['access_token']) def test_custom_expires_in(self): self.app.config.update({ 'OAUTH2_TOKEN_EXPIRES_IN': {'password': 1800} }) self.prepare_data() headers = self.create_basic_header( 'password-client', 'password-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertEqual(resp['expires_in'], 1800) def test_id_token_extension(self): self.prepare_data(extensions=[IDToken()]) headers = self.create_basic_header( 'password-client', 'password-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'password', 'username': 'foo', 'password': 'ok', 'scope': 'openid profile', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('id_token', resp) authlib-1.3.2/tests/flask/test_oauth2/test_refresh_token.py000066400000000000000000000173631466226534200241430ustar00rootroot00000000000000import time from flask import json from authlib.oauth2.rfc6749.grants import ( RefreshTokenGrant as _RefreshTokenGrant, ) from .models import db, User, Client, Token from .oauth2_server import TestCase from .oauth2_server import create_authorization_server class RefreshTokenGrant(_RefreshTokenGrant): def authenticate_refresh_token(self, refresh_token): item = Token.query.filter_by(refresh_token=refresh_token).first() if item and item.is_refresh_token_active(): return item def authenticate_user(self, credential): return db.session.get(User, credential.user_id) def revoke_old_credential(self, credential): now = int(time.time()) credential.access_token_revoked_at = now credential.refresh_token_revoked_at = now db.session.add(credential) db.session.commit() class RefreshTokenTest(TestCase): def prepare_data(self, grant_type='refresh_token'): server = create_authorization_server(self.app) server.register_grant(RefreshTokenGrant) user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='refresh-client', client_secret='refresh-secret', ) client.set_client_metadata({ 'scope': 'profile', 'grant_types': [grant_type], 'redirect_uris': ['http://localhost/authorized'], }) db.session.add(client) db.session.commit() def create_token(self, scope='profile', user_id=1): token = Token( user_id=user_id, client_id='refresh-client', token_type='bearer', access_token='a1', refresh_token='r1', scope=scope, expires_in=3600, ) db.session.add(token) db.session.commit() def test_invalid_client(self): self.prepare_data() rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'foo', }) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = self.create_basic_header( 'invalid-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'foo', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = self.create_basic_header( 'refresh-client', 'invalid-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'foo', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_invalid_refresh_token(self): self.prepare_data() headers = self.create_basic_header( 'refresh-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') self.assertIn('Missing', resp['error_description']) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'foo', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_grant') def test_invalid_scope(self): self.prepare_data() self.create_token() headers = self.create_basic_header( 'refresh-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'invalid', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_scope') def test_invalid_scope_none(self): self.prepare_data() self.create_token(scope=None) headers = self.create_basic_header( 'refresh-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'invalid', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_scope') def test_invalid_user(self): self.prepare_data() self.create_token(user_id=5) headers = self.create_basic_header( 'refresh-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'profile', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') def test_invalid_grant_type(self): self.prepare_data(grant_type='invalid') self.create_token() headers = self.create_basic_header( 'refresh-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'profile', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unauthorized_client') def test_authorize_token_no_scope(self): self.prepare_data() self.create_token() headers = self.create_basic_header( 'refresh-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_authorize_token_scope(self): self.prepare_data() self.create_token() headers = self.create_basic_header( 'refresh-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'profile', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) def test_revoke_old_credential(self): self.prepare_data() self.create_token() headers = self.create_basic_header( 'refresh-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'profile', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', 'scope': 'profile', }, headers=headers) self.assertEqual(rv.status_code, 400) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_grant') def test_token_generator(self): m = 'tests.flask.test_oauth2.oauth2_server:token_generator' self.app.config.update({'OAUTH2_ACCESS_TOKEN_GENERATOR': m}) self.prepare_data() self.create_token() headers = self.create_basic_header( 'refresh-client', 'refresh-secret' ) rv = self.client.post('/oauth/token', data={ 'grant_type': 'refresh_token', 'refresh_token': 'r1', }, headers=headers) resp = json.loads(rv.data) self.assertIn('access_token', resp) self.assertIn('r-refresh_token.1.', resp['access_token']) authlib-1.3.2/tests/flask/test_oauth2/test_revocation_endpoint.py000066400000000000000000000114121466226534200253430ustar00rootroot00000000000000from flask import json from authlib.integrations.sqla_oauth2 import create_revocation_endpoint from .models import db, User, Client, Token from .oauth2_server import TestCase from .oauth2_server import create_authorization_server RevocationEndpoint = create_revocation_endpoint(db.session, Token) class RevokeTokenTest(TestCase): def prepare_data(self): app = self.app server = create_authorization_server(app) server.register_endpoint(RevocationEndpoint) @app.route('/oauth/revoke', methods=['POST']) def revoke_token(): return server.create_endpoint_response('revocation') user = User(username='foo') db.session.add(user) db.session.commit() client = Client( user_id=user.id, client_id='revoke-client', client_secret='revoke-secret', ) client.set_client_metadata({ 'scope': 'profile', 'redirect_uris': ['http://localhost/authorized'], }) db.session.add(client) db.session.commit() def create_token(self): token = Token( user_id=1, client_id='revoke-client', token_type='bearer', access_token='a1', refresh_token='r1', scope='profile', expires_in=3600, ) db.session.add(token) db.session.commit() def test_invalid_client(self): self.prepare_data() rv = self.client.post('/oauth/revoke') resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = {'Authorization': 'invalid token_string'} rv = self.client.post('/oauth/revoke', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = self.create_basic_header( 'invalid-client', 'revoke-secret' ) rv = self.client.post('/oauth/revoke', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') headers = self.create_basic_header( 'revoke-client', 'invalid-secret' ) rv = self.client.post('/oauth/revoke', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_client') def test_invalid_token(self): self.prepare_data() headers = self.create_basic_header( 'revoke-client', 'revoke-secret' ) rv = self.client.post('/oauth/revoke', headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_request') rv = self.client.post('/oauth/revoke', data={ 'token': 'invalid-token', }, headers=headers) self.assertEqual(rv.status_code, 200) rv = self.client.post('/oauth/revoke', data={ 'token': 'a1', 'token_type_hint': 'unsupported_token_type', }, headers=headers) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'unsupported_token_type') rv = self.client.post('/oauth/revoke', data={ 'token': 'a1', 'token_type_hint': 'refresh_token', }, headers=headers) self.assertEqual(rv.status_code, 200) def test_revoke_token_with_hint(self): self.prepare_data() self.create_token() headers = self.create_basic_header( 'revoke-client', 'revoke-secret' ) rv = self.client.post('/oauth/revoke', data={ 'token': 'a1', 'token_type_hint': 'access_token', }, headers=headers) self.assertEqual(rv.status_code, 200) def test_revoke_token_without_hint(self): self.prepare_data() self.create_token() headers = self.create_basic_header( 'revoke-client', 'revoke-secret' ) rv = self.client.post('/oauth/revoke', data={ 'token': 'a1', }, headers=headers) self.assertEqual(rv.status_code, 200) def test_revoke_token_bound_to_client(self): self.prepare_data() self.create_token() client2 = Client( user_id=1, client_id='revoke-client-2', client_secret='revoke-secret-2', ) client2.set_client_metadata({ 'scope': 'profile', 'redirect_uris': ['http://localhost/authorized'], }) db.session.add(client2) db.session.commit() headers = self.create_basic_header( 'revoke-client-2', 'revoke-secret-2' ) rv = self.client.post('/oauth/revoke', data={ 'token': 'a1', }, headers=headers) self.assertEqual(rv.status_code, 400) resp = json.loads(rv.data) self.assertEqual(resp['error'], 'invalid_grant') authlib-1.3.2/tests/jose/000077500000000000000000000000001466226534200152615ustar00rootroot00000000000000authlib-1.3.2/tests/jose/__init__.py000066400000000000000000000000001466226534200173600ustar00rootroot00000000000000authlib-1.3.2/tests/jose/test_chacha20.py000066400000000000000000000055111466226534200202450ustar00rootroot00000000000000import unittest from authlib.jose import JsonWebEncryption from authlib.jose import OctKey from authlib.jose.drafts import register_jwe_draft register_jwe_draft(JsonWebEncryption) class ChaCha20Test(unittest.TestCase): def test_dir_alg_c20p(self): jwe = JsonWebEncryption() key = OctKey.generate_key(256, is_private=True) protected = {'alg': 'dir', 'enc': 'C20P'} data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') key2 = OctKey.generate_key(128, is_private=True) self.assertRaises(ValueError, jwe.deserialize_compact, data, key2) self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', key2 ) def test_dir_alg_xc20p(self): jwe = JsonWebEncryption() key = OctKey.generate_key(256, is_private=True) protected = {'alg': 'dir', 'enc': 'XC20P'} data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') key2 = OctKey.generate_key(128, is_private=True) self.assertRaises(ValueError, jwe.deserialize_compact, data, key2) self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', key2 ) def test_xc20p_content_encryption_decryption(self): # https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03#appendix-A.3.1 enc = JsonWebEncryption.ENC_REGISTRY['XC20P'] plaintext = bytes.fromhex( '4c616469657320616e642047656e746c656d656e206f662074686520636c6173' + '73206f66202739393a204966204920636f756c64206f6666657220796f75206f' + '6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73' + '637265656e20776f756c642062652069742e' ) aad = bytes.fromhex('50515253c0c1c2c3c4c5c6c7') key = bytes.fromhex('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f') iv = bytes.fromhex('404142434445464748494a4b4c4d4e4f5051525354555657') ciphertext, tag = enc.encrypt(plaintext, aad, iv, key) self.assertEqual( ciphertext, bytes.fromhex( 'bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb' + '731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452' + '2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9' + '21f9664c97637da9768812f615c68b13b52e' ) ) self.assertEqual(tag, bytes.fromhex('c0875924c1c7987947deafd8780acf49')) decrypted_plaintext = enc.decrypt(ciphertext, aad, iv, tag, key) self.assertEqual(decrypted_plaintext, plaintext) authlib-1.3.2/tests/jose/test_ecdh_1pu.py000066400000000000000000001624741466226534200204000ustar00rootroot00000000000000import unittest from collections import OrderedDict from cryptography.hazmat.primitives.keywrap import InvalidUnwrap from authlib.common.encoding import urlsafe_b64encode, json_b64encode, to_bytes, urlsafe_b64decode, json_loads from authlib.jose import JsonWebEncryption from authlib.jose import OKPKey from authlib.jose import ECKey from authlib.jose.drafts import register_jwe_draft from authlib.jose.errors import InvalidEncryptionAlgorithmForECDH1PUWithKeyWrappingError, \ InvalidAlgorithmForMultipleRecipientsMode from authlib.jose.rfc7516.models import JWEHeader register_jwe_draft(JsonWebEncryption) class ECDH1PUTest(unittest.TestCase): def test_ecdh_1pu_key_agreement_computation_appx_a(self): # https://datatracker.ietf.org/doc/html/draft-madden-jose-ecdh-1pu-04#appendix-A alice_static_key = { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", "d": "Hndv7ZZjs_ke8o9zXYo3iq-Yr8SewI5vrqd0pAvEPqg" } bob_static_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } alice_ephemeral_key = { "kty": "EC", "crv": "P-256", "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", "d": "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo" } headers = { "alg": "ECDH-1PU", "enc": "A256GCM", "apu": "QWxpY2U", "apv": "Qm9i", "epk": { "kty": "EC", "crv": "P-256", "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps" } } alg = JsonWebEncryption.ALG_REGISTRY['ECDH-1PU'] enc = JsonWebEncryption.ENC_REGISTRY['A256GCM'] alice_static_key = alg.prepare_key(alice_static_key) bob_static_key = alg.prepare_key(bob_static_key) alice_ephemeral_key = alg.prepare_key(alice_ephemeral_key) alice_static_pubkey = alice_static_key.get_op_key('wrapKey') bob_static_pubkey = bob_static_key.get_op_key('wrapKey') alice_ephemeral_pubkey = alice_ephemeral_key.get_op_key('wrapKey') # Derived key computation at Alice # Step-by-step methods verification _shared_key_e_at_alice = alice_ephemeral_key.exchange_shared_key(bob_static_pubkey) self.assertEqual( _shared_key_e_at_alice, b'\x9e\x56\xd9\x1d\x81\x71\x35\xd3\x72\x83\x42\x83\xbf\x84\x26\x9c' + b'\xfb\x31\x6e\xa3\xda\x80\x6a\x48\xf6\xda\xa7\x79\x8c\xfe\x90\xc4' ) _shared_key_s_at_alice = alice_static_key.exchange_shared_key(bob_static_pubkey) self.assertEqual( _shared_key_s_at_alice, b'\xe3\xca\x34\x74\x38\x4c\x9f\x62\xb3\x0b\xfd\x4c\x68\x8b\x3e\x7d' + b'\x41\x10\xa1\xb4\xba\xdc\x3c\xc5\x4e\xf7\xb8\x12\x41\xef\xd5\x0d' ) _shared_key_at_alice = alg.compute_shared_key(_shared_key_e_at_alice, _shared_key_s_at_alice) self.assertEqual( _shared_key_at_alice, b'\x9e\x56\xd9\x1d\x81\x71\x35\xd3\x72\x83\x42\x83\xbf\x84\x26\x9c' + b'\xfb\x31\x6e\xa3\xda\x80\x6a\x48\xf6\xda\xa7\x79\x8c\xfe\x90\xc4' + b'\xe3\xca\x34\x74\x38\x4c\x9f\x62\xb3\x0b\xfd\x4c\x68\x8b\x3e\x7d' + b'\x41\x10\xa1\xb4\xba\xdc\x3c\xc5\x4e\xf7\xb8\x12\x41\xef\xd5\x0d' ) _fixed_info_at_alice = alg.compute_fixed_info(headers, enc.key_size, None) self.assertEqual( _fixed_info_at_alice, b'\x00\x00\x00\x07\x41\x32\x35\x36\x47\x43\x4d\x00\x00\x00\x05\x41' + b'\x6c\x69\x63\x65\x00\x00\x00\x03\x42\x6f\x62\x00\x00\x01\x00' ) _dk_at_alice = alg.compute_derived_key(_shared_key_at_alice, _fixed_info_at_alice, enc.key_size) self.assertEqual( _dk_at_alice, b'\x6c\xaf\x13\x72\x3d\x14\x85\x0a\xd4\xb4\x2c\xd6\xdd\xe9\x35\xbf' + b'\xfd\x2f\xff\x00\xa9\xba\x70\xde\x05\xc2\x03\xa5\xe1\x72\x2c\xa7' ) self.assertEqual(urlsafe_b64encode(_dk_at_alice), b'bK8Tcj0UhQrUtCzW3ek1v_0v_wCpunDeBcIDpeFyLKc') # All-in-one method verification dk_at_alice = alg.deliver_at_sender( alice_static_key, alice_ephemeral_key, bob_static_pubkey, headers, enc.key_size, None) self.assertEqual(urlsafe_b64encode(dk_at_alice), b'bK8Tcj0UhQrUtCzW3ek1v_0v_wCpunDeBcIDpeFyLKc') # Derived key computation at Bob # Step-by-step methods verification _shared_key_e_at_bob = bob_static_key.exchange_shared_key(alice_ephemeral_pubkey) self.assertEqual(_shared_key_e_at_bob, _shared_key_e_at_alice) _shared_key_s_at_bob = bob_static_key.exchange_shared_key(alice_static_pubkey) self.assertEqual(_shared_key_s_at_bob, _shared_key_s_at_alice) _shared_key_at_bob = alg.compute_shared_key(_shared_key_e_at_bob, _shared_key_s_at_bob) self.assertEqual(_shared_key_at_bob, _shared_key_at_alice) _fixed_info_at_bob = alg.compute_fixed_info(headers, enc.key_size, None) self.assertEqual(_fixed_info_at_bob, _fixed_info_at_alice) _dk_at_bob = alg.compute_derived_key(_shared_key_at_bob, _fixed_info_at_bob, enc.key_size) self.assertEqual(_dk_at_bob, _dk_at_alice) # All-in-one method verification dk_at_bob = alg.deliver_at_recipient( bob_static_key, alice_static_pubkey, alice_ephemeral_pubkey, headers, enc.key_size, None) self.assertEqual(dk_at_bob, dk_at_alice) def test_ecdh_1pu_key_agreement_computation_appx_b(self): # https://datatracker.ietf.org/doc/html/draft-madden-jose-ecdh-1pu-04#appendix-B alice_static_key = { "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" } bob_static_key = { "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" } charlie_static_key = { "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" } alice_ephemeral_key = { "kty": "OKP", "crv": "X25519", "x": "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc", "d": "x8EVZH4Fwk673_mUujnliJoSrLz0zYzzCWp5GUX2fc8" } protected = OrderedDict({ "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll", "epk": OrderedDict({ "kty": "OKP", "crv": "X25519", "x": "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc" }) }) cek = b'\xff\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf0' \ b'\xef\xee\xed\xec\xeb\xea\xe9\xe8\xe7\xe6\xe5\xe4\xe3\xe2\xe1\xe0' \ b'\xdf\xde\xdd\xdc\xdb\xda\xd9\xd8\xd7\xd6\xd5\xd4\xd3\xd2\xd1\xd0' \ b'\xcf\xce\xcd\xcc\xcb\xca\xc9\xc8\xc7\xc6\xc5\xc4\xc3\xc2\xc1\xc0' iv = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' payload = b'Three is a magic number.' alg = JsonWebEncryption.ALG_REGISTRY['ECDH-1PU+A128KW'] enc = JsonWebEncryption.ENC_REGISTRY['A256CBC-HS512'] alice_static_key = OKPKey.import_key(alice_static_key) bob_static_key = OKPKey.import_key(bob_static_key) charlie_static_key = OKPKey.import_key(charlie_static_key) alice_ephemeral_key = OKPKey.import_key(alice_ephemeral_key) alice_static_pubkey = alice_static_key.get_op_key('wrapKey') bob_static_pubkey = bob_static_key.get_op_key('wrapKey') charlie_static_pubkey = charlie_static_key.get_op_key('wrapKey') alice_ephemeral_pubkey = alice_ephemeral_key.get_op_key('wrapKey') protected_segment = json_b64encode(protected) aad = to_bytes(protected_segment, 'ascii') ciphertext, tag = enc.encrypt(payload, aad, iv, cek) self.assertEqual(urlsafe_b64encode(ciphertext), b'Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw') self.assertEqual(urlsafe_b64encode(tag), b'HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ') # Derived key computation at Alice for Bob # Step-by-step methods verification _shared_key_e_at_alice_for_bob = alice_ephemeral_key.exchange_shared_key(bob_static_pubkey) self.assertEqual( _shared_key_e_at_alice_for_bob, b'\x32\x81\x08\x96\xe0\xfe\x4d\x57\x0e\xd1\xac\xfc\xed\xf6\x71\x17' + b'\xdc\x19\x4e\xd5\xda\xac\x21\xd8\xff\x7a\xf3\x24\x46\x94\x89\x7f' ) _shared_key_s_at_alice_for_bob = alice_static_key.exchange_shared_key(bob_static_pubkey) self.assertEqual( _shared_key_s_at_alice_for_bob, b'\x21\x57\x61\x2c\x90\x48\xed\xfa\xe7\x7c\xb2\xe4\x23\x71\x40\x60' + b'\x59\x67\xc0\x5c\x7f\x77\xa4\x8e\xea\xf2\xcf\x29\xa5\x73\x7c\x4a' ) _shared_key_at_alice_for_bob = alg.compute_shared_key(_shared_key_e_at_alice_for_bob, _shared_key_s_at_alice_for_bob) self.assertEqual( _shared_key_at_alice_for_bob, b'\x32\x81\x08\x96\xe0\xfe\x4d\x57\x0e\xd1\xac\xfc\xed\xf6\x71\x17' + b'\xdc\x19\x4e\xd5\xda\xac\x21\xd8\xff\x7a\xf3\x24\x46\x94\x89\x7f' + b'\x21\x57\x61\x2c\x90\x48\xed\xfa\xe7\x7c\xb2\xe4\x23\x71\x40\x60' + b'\x59\x67\xc0\x5c\x7f\x77\xa4\x8e\xea\xf2\xcf\x29\xa5\x73\x7c\x4a' ) _fixed_info_at_alice_for_bob = alg.compute_fixed_info(protected, alg.key_size, tag) self.assertEqual( _fixed_info_at_alice_for_bob, b'\x00\x00\x00\x0f\x45\x43\x44\x48\x2d\x31\x50\x55\x2b\x41\x31\x32' + b'\x38\x4b\x57\x00\x00\x00\x05\x41\x6c\x69\x63\x65\x00\x00\x00\x0f' + b'\x42\x6f\x62\x20\x61\x6e\x64\x20\x43\x68\x61\x72\x6c\x69\x65\x00' + b'\x00\x00\x80\x00\x00\x00\x20\x1c\xb6\xf8\x7d\x39\x66\xf2\xca\x46' + b'\x9a\x28\xf7\x47\x23\xac\xda\x02\x78\x0e\x91\xcc\xe2\x18\x55\x47' + b'\x07\x45\xfe\x11\x9b\xdd\x64' ) _dk_at_alice_for_bob = alg.compute_derived_key(_shared_key_at_alice_for_bob, _fixed_info_at_alice_for_bob, alg.key_size) self.assertEqual(_dk_at_alice_for_bob, b'\xdf\x4c\x37\xa0\x66\x83\x06\xa1\x1e\x3d\x6b\x00\x74\xb5\xd8\xdf') # All-in-one method verification dk_at_alice_for_bob = alg.deliver_at_sender( alice_static_key, alice_ephemeral_key, bob_static_pubkey, protected, alg.key_size, tag) self.assertEqual(dk_at_alice_for_bob, b'\xdf\x4c\x37\xa0\x66\x83\x06\xa1\x1e\x3d\x6b\x00\x74\xb5\xd8\xdf') kek_at_alice_for_bob = alg.aeskw.prepare_key(dk_at_alice_for_bob) wrapped_for_bob = alg.aeskw.wrap_cek(cek, kek_at_alice_for_bob) ek_for_bob = wrapped_for_bob['ek'] self.assertEqual( urlsafe_b64encode(ek_for_bob), b'pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQeU1cSl55cQ0hGezJu2N9IY0QN') # Derived key computation at Alice for Charlie # Step-by-step methods verification _shared_key_e_at_alice_for_charlie = alice_ephemeral_key.exchange_shared_key(charlie_static_pubkey) self.assertEqual( _shared_key_e_at_alice_for_charlie, b'\x89\xdc\xfe\x4c\x37\xc1\xdc\x02\x71\xf3\x46\xb5\xb3\xb1\x9c\x3b' + b'\x70\x5c\xa2\xa7\x2f\x9a\x23\x77\x85\xc3\x44\x06\xfc\xb7\x5f\x10' ) _shared_key_s_at_alice_for_charlie = alice_static_key.exchange_shared_key(charlie_static_pubkey) self.assertEqual( _shared_key_s_at_alice_for_charlie, b'\x78\xfe\x63\xfc\x66\x1c\xf8\xd1\x8f\x92\xa8\x42\x2a\x64\x18\xe4' + b'\xed\x5e\x20\xa9\x16\x81\x85\xfd\xee\xdc\xa1\xc3\xd8\xe6\xa6\x1c' ) _shared_key_at_alice_for_charlie = alg.compute_shared_key(_shared_key_e_at_alice_for_charlie, _shared_key_s_at_alice_for_charlie) self.assertEqual( _shared_key_at_alice_for_charlie, b'\x89\xdc\xfe\x4c\x37\xc1\xdc\x02\x71\xf3\x46\xb5\xb3\xb1\x9c\x3b' + b'\x70\x5c\xa2\xa7\x2f\x9a\x23\x77\x85\xc3\x44\x06\xfc\xb7\x5f\x10' + b'\x78\xfe\x63\xfc\x66\x1c\xf8\xd1\x8f\x92\xa8\x42\x2a\x64\x18\xe4' + b'\xed\x5e\x20\xa9\x16\x81\x85\xfd\xee\xdc\xa1\xc3\xd8\xe6\xa6\x1c' ) _fixed_info_at_alice_for_charlie = alg.compute_fixed_info(protected, alg.key_size, tag) self.assertEqual(_fixed_info_at_alice_for_charlie, _fixed_info_at_alice_for_bob) _dk_at_alice_for_charlie = alg.compute_derived_key(_shared_key_at_alice_for_charlie, _fixed_info_at_alice_for_charlie, alg.key_size) self.assertEqual(_dk_at_alice_for_charlie, b'\x57\xd8\x12\x6f\x1b\x7e\xc4\xcc\xb0\x58\x4d\xac\x03\xcb\x27\xcc') # All-in-one method verification dk_at_alice_for_charlie = alg.deliver_at_sender( alice_static_key, alice_ephemeral_key, charlie_static_pubkey, protected, alg.key_size, tag) self.assertEqual(dk_at_alice_for_charlie, b'\x57\xd8\x12\x6f\x1b\x7e\xc4\xcc\xb0\x58\x4d\xac\x03\xcb\x27\xcc') kek_at_alice_for_charlie = alg.aeskw.prepare_key(dk_at_alice_for_charlie) wrapped_for_charlie = alg.aeskw.wrap_cek(cek, kek_at_alice_for_charlie) ek_for_charlie = wrapped_for_charlie['ek'] self.assertEqual( urlsafe_b64encode(ek_for_charlie), b'56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8fe4z3PQ2YH2afvjQ28aiCTWFE') # Derived key computation at Bob for Alice # Step-by-step methods verification _shared_key_e_at_bob_for_alice = bob_static_key.exchange_shared_key(alice_ephemeral_pubkey) self.assertEqual(_shared_key_e_at_bob_for_alice, _shared_key_e_at_alice_for_bob) _shared_key_s_at_bob_for_alice = bob_static_key.exchange_shared_key(alice_static_pubkey) self.assertEqual(_shared_key_s_at_bob_for_alice, _shared_key_s_at_alice_for_bob) _shared_key_at_bob_for_alice = alg.compute_shared_key(_shared_key_e_at_bob_for_alice, _shared_key_s_at_bob_for_alice) self.assertEqual(_shared_key_at_bob_for_alice, _shared_key_at_alice_for_bob) _fixed_info_at_bob_for_alice = alg.compute_fixed_info(protected, alg.key_size, tag) self.assertEqual(_fixed_info_at_bob_for_alice, _fixed_info_at_alice_for_bob) _dk_at_bob_for_alice = alg.compute_derived_key(_shared_key_at_bob_for_alice, _fixed_info_at_bob_for_alice, alg.key_size) self.assertEqual(_dk_at_bob_for_alice, _dk_at_alice_for_bob) # All-in-one method verification dk_at_bob_for_alice = alg.deliver_at_recipient( bob_static_key, alice_static_pubkey, alice_ephemeral_pubkey, protected, alg.key_size, tag) self.assertEqual(dk_at_bob_for_alice, dk_at_alice_for_bob) kek_at_bob_for_alice = alg.aeskw.prepare_key(dk_at_bob_for_alice) cek_unwrapped_by_bob = alg.aeskw.unwrap(enc, ek_for_bob, protected, kek_at_bob_for_alice) self.assertEqual(cek_unwrapped_by_bob, cek) payload_decrypted_by_bob = enc.decrypt(ciphertext, aad, iv, tag, cek_unwrapped_by_bob) self.assertEqual(payload_decrypted_by_bob, payload) # Derived key computation at Charlie for Alice # Step-by-step methods verification _shared_key_e_at_charlie_for_alice = charlie_static_key.exchange_shared_key(alice_ephemeral_pubkey) self.assertEqual(_shared_key_e_at_charlie_for_alice, _shared_key_e_at_alice_for_charlie) _shared_key_s_at_charlie_for_alice = charlie_static_key.exchange_shared_key(alice_static_pubkey) self.assertEqual(_shared_key_s_at_charlie_for_alice, _shared_key_s_at_alice_for_charlie) _shared_key_at_charlie_for_alice = alg.compute_shared_key(_shared_key_e_at_charlie_for_alice, _shared_key_s_at_charlie_for_alice) self.assertEqual(_shared_key_at_charlie_for_alice, _shared_key_at_alice_for_charlie) _fixed_info_at_charlie_for_alice = alg.compute_fixed_info(protected, alg.key_size, tag) self.assertEqual(_fixed_info_at_charlie_for_alice, _fixed_info_at_alice_for_charlie) _dk_at_charlie_for_alice = alg.compute_derived_key(_shared_key_at_charlie_for_alice, _fixed_info_at_charlie_for_alice, alg.key_size) self.assertEqual(_dk_at_charlie_for_alice, _dk_at_alice_for_charlie) # All-in-one method verification dk_at_charlie_for_alice = alg.deliver_at_recipient( charlie_static_key, alice_static_pubkey, alice_ephemeral_pubkey, protected, alg.key_size, tag) self.assertEqual(dk_at_charlie_for_alice, dk_at_alice_for_charlie) kek_at_charlie_for_alice = alg.aeskw.prepare_key(dk_at_charlie_for_alice) cek_unwrapped_by_charlie = alg.aeskw.unwrap(enc, ek_for_charlie, protected, kek_at_charlie_for_alice) self.assertEqual(cek_unwrapped_by_charlie, cek) payload_decrypted_by_charlie = enc.decrypt(ciphertext, aad, iv, tag, cek_unwrapped_by_charlie) self.assertEqual(payload_decrypted_by_charlie, payload) def test_ecdh_1pu_jwe_in_direct_key_agreement_mode(self): jwe = JsonWebEncryption() alice_key = { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", "d": "Hndv7ZZjs_ke8o9zXYo3iq-Yr8SewI5vrqd0pAvEPqg" } bob_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } for enc in [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM', ]: protected = {'alg': 'ECDH-1PU', 'enc': enc} data = jwe.serialize_compact(protected, b'hello', bob_key, sender_key=alice_key) rv = jwe.deserialize_compact(data, bob_key, sender_key=alice_key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_1pu_jwe_json_serialization_single_recipient_in_direct_key_agreement_mode(self): jwe = JsonWebEncryption() alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = OKPKey.generate_key('X25519', is_private=True) protected = {'alg': 'ECDH-1PU', 'enc': 'A128GCM'} header_obj = {'protected': protected} data = jwe.serialize_json(header_obj, b'hello', bob_key, sender_key=alice_key) rv = jwe.deserialize_json(data, bob_key, sender_key=alice_key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_1pu_jwe_in_key_agreement_with_key_wrapping_mode(self): jwe = JsonWebEncryption() alice_key = { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", "d": "Hndv7ZZjs_ke8o9zXYo3iq-Yr8SewI5vrqd0pAvEPqg" } bob_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } for alg in [ 'ECDH-1PU+A128KW', 'ECDH-1PU+A192KW', 'ECDH-1PU+A256KW', ]: for enc in [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', ]: protected = {'alg': alg, 'enc': enc} data = jwe.serialize_compact(protected, b'hello', bob_key, sender_key=alice_key) rv = jwe.deserialize_compact(data, bob_key, sender_key=alice_key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_1pu_jwe_with_compact_serialization_ignores_kid_provided_separately_on_decryption(self): jwe = JsonWebEncryption() alice_kid = "Alice's key" alice_key = { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", "d": "Hndv7ZZjs_ke8o9zXYo3iq-Yr8SewI5vrqd0pAvEPqg" } bob_kid = "Bob's key" bob_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } for alg in [ 'ECDH-1PU+A128KW', 'ECDH-1PU+A192KW', 'ECDH-1PU+A256KW', ]: for enc in [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', ]: protected = {'alg': alg, 'enc': enc} data = jwe.serialize_compact(protected, b'hello', bob_key, sender_key=alice_key) rv = jwe.deserialize_compact(data, (bob_kid, bob_key), sender_key=alice_key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_1pu_jwe_with_okp_keys_in_direct_key_agreement_mode(self): jwe = JsonWebEncryption() alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = OKPKey.generate_key('X25519', is_private=True) for enc in [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM', ]: protected = {'alg': 'ECDH-1PU', 'enc': enc} data = jwe.serialize_compact(protected, b'hello', bob_key, sender_key=alice_key) rv = jwe.deserialize_compact(data, bob_key, sender_key=alice_key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_1pu_jwe_with_okp_keys_in_key_agreement_with_key_wrapping_mode(self): jwe = JsonWebEncryption() alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = OKPKey.generate_key('X25519', is_private=True) for alg in [ 'ECDH-1PU+A128KW', 'ECDH-1PU+A192KW', 'ECDH-1PU+A256KW', ]: for enc in [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', ]: protected = {'alg': alg, 'enc': enc} data = jwe.serialize_compact(protected, b'hello', bob_key, sender_key=alice_key) rv = jwe.deserialize_compact(data, bob_key, sender_key=alice_key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_1pu_encryption_with_json_serialization(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) protected = { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, [bob_key, charlie_key], sender_key=alice_key) self.assertEqual( data.keys(), { 'protected', 'unprotected', 'recipients', 'aad', 'iv', 'ciphertext', 'tag' } ) decoded_protected = json_loads(urlsafe_b64decode(to_bytes(data['protected'])).decode('utf-8')) self.assertEqual(decoded_protected.keys(), protected.keys() | {'epk'}) self.assertEqual({k: decoded_protected[k] for k in decoded_protected.keys() - {'epk'}}, protected) self.assertEqual(data['unprotected'], unprotected) self.assertEqual(len(data['recipients']), len(recipients)) for i in range(len(data['recipients'])): self.assertEqual(data['recipients'][i].keys(), {'header', 'encrypted_key'}) self.assertEqual(data['recipients'][i]['header'], recipients[i]['header']) self.assertEqual(urlsafe_b64decode(to_bytes(data['aad'])), jwe_aad) iv = urlsafe_b64decode(to_bytes(data['iv'])) ciphertext = urlsafe_b64decode(to_bytes(data['ciphertext'])) tag = urlsafe_b64decode(to_bytes(data['tag'])) alg = JsonWebEncryption.ALG_REGISTRY[protected['alg']] enc = JsonWebEncryption.ENC_REGISTRY[protected['enc']] aad = to_bytes(data['protected']) + b'.' + to_bytes(data['aad']) aad = to_bytes(aad, 'ascii') ek_for_bob = urlsafe_b64decode(to_bytes(data['recipients'][0]['encrypted_key'])) header_for_bob = JWEHeader(decoded_protected, data['unprotected'], data['recipients'][0]['header']) cek_at_bob = alg.unwrap(enc, ek_for_bob, header_for_bob, bob_key, sender_key=alice_key, tag=tag) payload_at_bob = enc.decrypt(ciphertext, aad, iv, tag, cek_at_bob) self.assertEqual(payload_at_bob, payload) ek_for_charlie = urlsafe_b64decode(to_bytes(data['recipients'][1]['encrypted_key'])) header_for_charlie = JWEHeader(decoded_protected, data['unprotected'], data['recipients'][1]['header']) cek_at_charlie = alg.unwrap(enc, ek_for_charlie, header_for_charlie, charlie_key, sender_key=alice_key, tag=tag) payload_at_charlie = enc.decrypt(ciphertext, aad, iv, tag, cek_at_charlie) self.assertEqual(cek_at_charlie, cek_at_bob) self.assertEqual(payload_at_charlie, payload) def test_ecdh_1pu_decryption_with_json_serialization(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) data = { "protected": "eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1Ijoi" + "UVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9L" + "UCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFB" + "RnFVQUZhMzlkeUJjIn19", "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "bob-key-2" }, "encrypted_key": "pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQ" + "eU1cSl55cQ0hGezJu2N9IY0QN" }, { "header": { "kid": "2021-05-06" }, "encrypted_key": "56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8" + "fe4z3PQ2YH2afvjQ28aiCTWFE" } ], "iv": "AAECAwQFBgcICQoLDA0ODw", "ciphertext": "Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw", "tag": "HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" } rv_at_bob = jwe.deserialize_json(data, bob_key, sender_key=alice_key) self.assertEqual(rv_at_bob.keys(), {'header', 'payload'}) self.assertEqual(rv_at_bob['header'].keys(), {'protected', 'unprotected', 'recipients'}) self.assertEqual( rv_at_bob['header']['protected'], { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll", "epk": { "kty": "OKP", "crv": "X25519", "x": "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc" } } ) self.assertEqual( rv_at_bob['header']['unprotected'], { "jku": "https://alice.example.com/keys.jwks" } ) self.assertEqual( rv_at_bob['header']['recipients'], [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ] ) self.assertEqual(rv_at_bob['payload'], b'Three is a magic number.') rv_at_charlie = jwe.deserialize_json(data, charlie_key, sender_key=alice_key) self.assertEqual(rv_at_charlie.keys(), {'header', 'payload'}) self.assertEqual(rv_at_charlie['header'].keys(), {'protected', 'unprotected', 'recipients'}) self.assertEqual( rv_at_charlie['header']['protected'], { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll", "epk": { "kty": "OKP", "crv": "X25519", "x": "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc" } } ) self.assertEqual( rv_at_charlie['header']['unprotected'], { "jku": "https://alice.example.com/keys.jwks" } ) self.assertEqual( rv_at_charlie['header']['recipients'], [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ] ) self.assertEqual(rv_at_charlie['payload'], b'Three is a magic number.') def test_ecdh_1pu_jwe_with_json_serialization_when_kid_is_not_specified(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) protected = { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, [bob_key, charlie_key], sender_key=alice_key) rv_at_bob = jwe.deserialize_json(data, bob_key, sender_key=alice_key) self.assertEqual(rv_at_bob['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_bob['header']['protected'][k] for k in rv_at_bob['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_bob['header']['unprotected'], unprotected) self.assertEqual(rv_at_bob['header']['recipients'], recipients) self.assertEqual(rv_at_bob['header']['aad'], jwe_aad) self.assertEqual(rv_at_bob['payload'], payload) rv_at_charlie = jwe.deserialize_json(data, charlie_key, sender_key=alice_key) self.assertEqual(rv_at_charlie['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_charlie['header']['protected'][k] for k in rv_at_charlie['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_charlie['header']['unprotected'], unprotected) self.assertEqual(rv_at_charlie['header']['recipients'], recipients) self.assertEqual(rv_at_charlie['header']['aad'], jwe_aad) self.assertEqual(rv_at_charlie['payload'], payload) def test_ecdh_1pu_jwe_with_json_serialization_when_kid_is_specified(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "kid": "alice-key", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "kid": "bob-key-2", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "kid": "2021-05-06", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) protected = { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, [bob_key, charlie_key], sender_key=alice_key) rv_at_bob = jwe.deserialize_json(data, bob_key, sender_key=alice_key) self.assertEqual(rv_at_bob['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_bob['header']['protected'][k] for k in rv_at_bob['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_bob['header']['unprotected'], unprotected) self.assertEqual(rv_at_bob['header']['recipients'], recipients) self.assertEqual(rv_at_bob['header']['aad'], jwe_aad) self.assertEqual(rv_at_bob['payload'], payload) rv_at_charlie = jwe.deserialize_json(data, charlie_key, sender_key=alice_key) self.assertEqual(rv_at_charlie['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_charlie['header']['protected'][k] for k in rv_at_charlie['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_charlie['header']['unprotected'], unprotected) self.assertEqual(rv_at_charlie['header']['recipients'], recipients) self.assertEqual(rv_at_charlie['header']['aad'], jwe_aad) self.assertEqual(rv_at_charlie['payload'], payload) def test_ecdh_1pu_jwe_with_json_serialization_when_kid_is_provided_separately_on_decryption(self): jwe = JsonWebEncryption() alice_kid = "did:example:123#WjKgJV7VRw3hmgU6--4v15c0Aewbcvat1BsRFTIqa5Q" alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "kid": "WjKgJV7VRw3hmgU6--4v15c0Aewbcvat1BsRFTIqa5Q", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_kid = "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A" bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "kid": "_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_kid = "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw" charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "kid": "_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) protected = { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A" } }, { "header": { "kid": "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, [bob_key, charlie_key], sender_key=alice_key) rv_at_bob = jwe.deserialize_json(data, (bob_kid, bob_key), sender_key=alice_key) self.assertEqual(rv_at_bob['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_bob['header']['protected'][k] for k in rv_at_bob['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_bob['header']['unprotected'], unprotected) self.assertEqual(rv_at_bob['header']['recipients'], recipients) self.assertEqual(rv_at_bob['header']['aad'], jwe_aad) self.assertEqual(rv_at_bob['payload'], payload) rv_at_charlie = jwe.deserialize_json(data, (charlie_kid, charlie_key), sender_key=alice_key) self.assertEqual(rv_at_charlie['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_charlie['header']['protected'][k] for k in rv_at_charlie['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_charlie['header']['unprotected'], unprotected) self.assertEqual(rv_at_charlie['header']['recipients'], recipients) self.assertEqual(rv_at_charlie['header']['aad'], jwe_aad) self.assertEqual(rv_at_charlie['payload'], payload) def test_ecdh_1pu_jwe_with_json_serialization_for_single_recipient(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) protected = { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9i" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "bob-key-2" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, bob_key, sender_key=alice_key) rv = jwe.deserialize_json(data, bob_key, sender_key=alice_key) self.assertEqual(rv['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv['header']['protected'][k] for k in rv['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv['header']['unprotected'], unprotected) self.assertEqual(rv['header']['recipients'], recipients) self.assertEqual(rv['header']['aad'], jwe_aad) self.assertEqual(rv['payload'], payload) def test_ecdh_1pu_encryption_fails_json_serialization_multiple_recipients_in_direct_key_agreement_mode(self): jwe = JsonWebEncryption() alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = OKPKey.generate_key('X25519', is_private=True) charlie_key = OKPKey.generate_key('X25519', is_private=True) protected = {'alg': 'ECDH-1PU', 'enc': 'A128GCM'} header_obj = {'protected': protected} self.assertRaises( InvalidAlgorithmForMultipleRecipientsMode, jwe.serialize_json, header_obj, b'hello', [bob_key, charlie_key], sender_key=alice_key ) def test_ecdh_1pu_encryption_fails_if_not_aes_cbc_hmac_sha2_enc_is_used_with_kw(self): jwe = JsonWebEncryption() alice_key = { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", "d": "Hndv7ZZjs_ke8o9zXYo3iq-Yr8SewI5vrqd0pAvEPqg" } bob_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck" } for alg in [ 'ECDH-1PU+A128KW', 'ECDH-1PU+A192KW', 'ECDH-1PU+A256KW', ]: for enc in [ 'A128GCM', 'A192GCM', 'A256GCM', ]: protected = {'alg': alg, 'enc': enc} self.assertRaises( InvalidEncryptionAlgorithmForECDH1PUWithKeyWrappingError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) def test_ecdh_1pu_encryption_with_public_sender_key_fails(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU', 'enc': 'A256GCM'} alice_key = { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE" } bob_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) def test_ecdh_1pu_decryption_with_public_recipient_key_fails(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU', 'enc': 'A256GCM'} alice_key = { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", "d": "Hndv7ZZjs_ke8o9zXYo3iq-Yr8SewI5vrqd0pAvEPqg" } bob_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck" } data = jwe.serialize_compact(protected, b'hello', bob_key, sender_key=alice_key) self.assertRaises( ValueError, jwe.deserialize_compact, data, bob_key, sender_key=alice_key ) def test_ecdh_1pu_encryption_fails_if_key_types_are_different(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU', 'enc': 'A256GCM'} alice_key = ECKey.generate_key('P-256', is_private=True) bob_key = OKPKey.generate_key('X25519', is_private=False) self.assertRaises( Exception, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = ECKey.generate_key('P-256', is_private=False) self.assertRaises( Exception, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) def test_ecdh_1pu_encryption_fails_if_keys_curves_are_different(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU', 'enc': 'A256GCM'} alice_key = ECKey.generate_key('P-256', is_private=True) bob_key = ECKey.generate_key('secp256k1', is_private=False) self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) alice_key = ECKey.generate_key('P-384', is_private=True) bob_key = ECKey.generate_key('P-521', is_private=False) self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = OKPKey.generate_key('X448', is_private=False) self.assertRaises( TypeError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) def test_ecdh_1pu_encryption_fails_if_key_points_are_not_actually_on_same_curve(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU', 'enc': 'A256GCM'} alice_key = { "kty": "EC", "crv": "P-256", "x": "aDHtGkIYyhR5geqfMaFL0T9cG4JEMI8nyMFJA7gRUDs", "y": "AjGN5_f-aCt4vYg74my6n1ALIq746nlc_httIgcBSYY", "d": "Sim3EIzXsWaWu9QW8yKVHwxBM5CTlnrVU_Eq-y_KRQA" } # the point is indeed on P-256 curve bob_key = { "kty": "EC", "crv": "P-256", "x": "5ZFnZbs_BtLBIZxwt5hS7SBDtI2a-dJ871dJ8ZnxZ6c", "y": "K0srqSkbo1Yeckr0YoQA8r_rOz0ZUStiv3mc1qn46pg" } # the point is not on P-256 curve but is actually on secp256k1 curve self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) alice_key = { "kty": "EC", "crv": "P-521", "x": "1JDMOjnMgASo01PVHRcyCDtE6CLgKuwXLXLbdLGxpdubLuHYBa0KAepyimnxCWsX", "y": "w7BSC8Xb3XgMMfE7IFCJpoOmx1Sf3T3_3OZ4CrF6_iCFAw4VOdFYR42OnbKMFG--", "d": "lCkpFBaVwHzfHtkJEV3PzxefObOPnMgUjNZSLryqC5AkERgXT3-DZLEi6eBzq5gk" } # the point is not on P-521 curve but is actually on P-384 curve bob_key = { "kty": "EC", "crv": "P-521", "x": "Cd6rinJdgS4WJj6iaNyXiVhpMbhZLmPykmrnFhIad04B3ulf5pURb5v9mx21c_Cv8Q1RBOptwleLg5Qjq2J1qa4", "y": "hXo9p1EjW6W4opAQdmfNgyxztkNxYwn9L4FVTLX51KNEsW0aqueLm96adRmf0HoGIbNhIdcIlXOKlRUHqgunDkM" } # the point is indeed on P-521 curve self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "TAB1oIsjPob3guKwTEeQsAsupSRPdXdxHhnV8JrVJTA", "d": "kO2LzPr4vLg_Hn-7_MDq66hJZgvTIkzDG4p6nCsgNHk" }) # the point is indeed on X25519 curve bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "lVHcPx4R9bExaoxXZY9tAq7SNW9pJKCoVQxURLtkAs3Dg5ZRxcjhf0JUyg2lod5OGDptJ7wowwY" }) # the point is not on X25519 curve but is actually on X448 curve self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X448", "x": "TAB1oIsjPob3guKwTEeQsAsupSRPdXdxHhnV8JrVJTA", "d": "kO2LzPr4vLg_Hn-7_MDq66hJZgvTIkzDG4p6nCsgNHk" }) # the point is not on X448 curve but is actually on X25519 curve bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X448", "x": "lVHcPx4R9bExaoxXZY9tAq7SNW9pJKCoVQxURLtkAs3Dg5ZRxcjhf0JUyg2lod5OGDptJ7wowwY" }) # the point is indeed on X448 curve self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) def test_ecdh_1pu_encryption_fails_if_keys_curve_is_inappropriate(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU', 'enc': 'A256GCM'} alice_key = OKPKey.generate_key('Ed25519', is_private=True) # use Ed25519 instead of X25519 bob_key = OKPKey.generate_key('Ed25519', is_private=False) # use Ed25519 instead of X25519 self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) def test_ecdh_1pu_encryption_for_multiple_recipients_fails_if_key_types_are_different(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU+A128KW', 'enc': 'A128CBC-HS256'} header_obj = {'protected': protected} alice_key = ECKey.generate_key('P-256', is_private=True) bob_key = ECKey.generate_key('P-256', is_private=False) charlie_key = OKPKey.generate_key('X25519', is_private=False) self.assertRaises( Exception, jwe.serialize_json, header_obj, b'hello', [bob_key, charlie_key], sender_key=alice_key ) def test_ecdh_1pu_encryption_for_multiple_recipients_fails_if_keys_curves_are_different(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU+A128KW', 'enc': 'A128CBC-HS256'} header_obj = {'protected': protected} alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = OKPKey.generate_key('X448', is_private=False) charlie_key = OKPKey.generate_key('X25519', is_private=False) self.assertRaises( TypeError, jwe.serialize_json, header_obj, b'hello', [bob_key, charlie_key], sender_key=alice_key ) def test_ecdh_1pu_encryption_for_multiple_recipients_fails_if_key_points_are_not_actually_on_same_curve(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU+A128KW', 'enc': 'A128CBC-HS256'} header_obj = {'protected': protected} alice_key = { "kty": "EC", "crv": "P-256", "x": "aDHtGkIYyhR5geqfMaFL0T9cG4JEMI8nyMFJA7gRUDs", "y": "AjGN5_f-aCt4vYg74my6n1ALIq746nlc_httIgcBSYY", "d": "Sim3EIzXsWaWu9QW8yKVHwxBM5CTlnrVU_Eq-y_KRQA" } # the point is indeed on P-256 curve bob_key = { "kty": "EC", "crv": "P-256", "x": "HgF88mm6yw4gjG7yG6Sqz66pHnpZcyx7c842BQghYuc", "y": "KZ1ywvTOYnpNb4Gepa5eSgfEOb5gj5hCaCFIrTFuI2o" } # the point is indeed on P-256 curve charlie_key = { "kty": "EC", "crv": "P-256", "x": "5ZFnZbs_BtLBIZxwt5hS7SBDtI2a-dJ871dJ8ZnxZ6c", "y": "K0srqSkbo1Yeckr0YoQA8r_rOz0ZUStiv3mc1qn46pg" } # the point is not on P-256 curve but is actually on secp256k1 curve self.assertRaises( ValueError, jwe.serialize_json, header_obj, b'hello', [bob_key, charlie_key], sender_key=alice_key ) def test_ecdh_1pu_encryption_for_multiple_recipients_fails_if_keys_curve_is_inappropriate(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-1PU+A128KW', 'enc': 'A128CBC-HS256'} header_obj = {'protected': protected} alice_key = OKPKey.generate_key('Ed25519', is_private=True) # use Ed25519 instead of X25519 bob_key = OKPKey.generate_key('Ed25519', is_private=False) # use Ed25519 instead of X25519 charlie_key = OKPKey.generate_key('Ed25519', is_private=False) # use Ed25519 instead of X25519 self.assertRaises( ValueError, jwe.serialize_json, header_obj, b'hello', [bob_key, charlie_key], sender_key=alice_key ) def test_ecdh_1pu_decryption_fails_if_key_matches_to_no_recipient(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) protected = { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9i" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "bob-key-2" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, bob_key, sender_key=alice_key) self.assertRaises( InvalidUnwrap, jwe.deserialize_json, data, charlie_key, sender_key=alice_key ) authlib-1.3.2/tests/jose/test_jwe.py000066400000000000000000001605371466226534200174730ustar00rootroot00000000000000import json import os import unittest from cryptography.hazmat.primitives.keywrap import InvalidUnwrap from authlib.common.encoding import urlsafe_b64encode, json_b64encode, to_bytes, to_unicode from authlib.jose import JsonWebEncryption from authlib.jose import OctKey, OKPKey from authlib.jose import errors from authlib.jose.drafts import register_jwe_draft from authlib.jose.errors import InvalidAlgorithmForMultipleRecipientsMode, DecodeError, InvalidHeaderParameterNameError from authlib.jose.util import extract_header from tests.util import read_file_path register_jwe_draft(JsonWebEncryption) class JWETest(unittest.TestCase): def test_not_enough_segments(self): s = 'a.b.c' jwe = JsonWebEncryption() self.assertRaises( errors.DecodeError, jwe.deserialize_compact, s, None ) def test_invalid_header(self): jwe = JsonWebEncryption() public_key = read_file_path('rsa_public.pem') self.assertRaises( errors.MissingAlgorithmError, jwe.serialize_compact, {}, 'a', public_key ) self.assertRaises( errors.UnsupportedAlgorithmError, jwe.serialize_compact, {'alg': 'invalid'}, 'a', public_key ) self.assertRaises( errors.MissingEncryptionAlgorithmError, jwe.serialize_compact, {'alg': 'RSA-OAEP'}, 'a', public_key ) self.assertRaises( errors.UnsupportedEncryptionAlgorithmError, jwe.serialize_compact, {'alg': 'RSA-OAEP', 'enc': 'invalid'}, 'a', public_key ) self.assertRaises( errors.UnsupportedCompressionAlgorithmError, jwe.serialize_compact, {'alg': 'RSA-OAEP', 'enc': 'A256GCM', 'zip': 'invalid'}, 'a', public_key ) def test_not_supported_alg(self): public_key = read_file_path('rsa_public.pem') private_key = read_file_path('rsa_private.pem') jwe = JsonWebEncryption() s = jwe.serialize_compact( {'alg': 'RSA-OAEP', 'enc': 'A256GCM'}, 'hello', public_key ) jwe = JsonWebEncryption(algorithms=['RSA1_5', 'A256GCM']) self.assertRaises( errors.UnsupportedAlgorithmError, jwe.serialize_compact, {'alg': 'RSA-OAEP', 'enc': 'A256GCM'}, 'hello', public_key ) self.assertRaises( errors.UnsupportedCompressionAlgorithmError, jwe.serialize_compact, {'alg': 'RSA1_5', 'enc': 'A256GCM', 'zip': 'DEF'}, 'hello', public_key ) self.assertRaises( errors.UnsupportedAlgorithmError, jwe.deserialize_compact, s, private_key, ) jwe = JsonWebEncryption(algorithms=['RSA-OAEP', 'A192GCM']) self.assertRaises( errors.UnsupportedEncryptionAlgorithmError, jwe.serialize_compact, {'alg': 'RSA-OAEP', 'enc': 'A256GCM'}, 'hello', public_key ) self.assertRaises( errors.UnsupportedCompressionAlgorithmError, jwe.serialize_compact, {'alg': 'RSA-OAEP', 'enc': 'A192GCM', 'zip': 'DEF'}, 'hello', public_key ) self.assertRaises( errors.UnsupportedEncryptionAlgorithmError, jwe.deserialize_compact, s, private_key, ) def test_inappropriate_sender_key_for_serialize_compact(self): jwe = JsonWebEncryption() alice_key = { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", "d": "Hndv7ZZjs_ke8o9zXYo3iq-Yr8SewI5vrqd0pAvEPqg" } bob_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } protected = {'alg': 'ECDH-1PU', 'enc': 'A256GCM'} self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key ) protected = {'alg': 'ECDH-ES', 'enc': 'A256GCM'} self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', bob_key, sender_key=alice_key ) def test_inappropriate_sender_key_for_deserialize_compact(self): jwe = JsonWebEncryption() alice_key = { "kty": "EC", "crv": "P-256", "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", "d": "Hndv7ZZjs_ke8o9zXYo3iq-Yr8SewI5vrqd0pAvEPqg" } bob_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } protected = {'alg': 'ECDH-1PU', 'enc': 'A256GCM'} data = jwe.serialize_compact(protected, b'hello', bob_key, sender_key=alice_key) self.assertRaises( ValueError, jwe.deserialize_compact, data, bob_key ) protected = {'alg': 'ECDH-ES', 'enc': 'A256GCM'} data = jwe.serialize_compact(protected, b'hello', bob_key) self.assertRaises( ValueError, jwe.deserialize_compact, data, bob_key, sender_key=alice_key ) def test_compact_rsa(self): jwe = JsonWebEncryption() s = jwe.serialize_compact( {'alg': 'RSA-OAEP', 'enc': 'A256GCM'}, 'hello', read_file_path('rsa_public.pem') ) data = jwe.deserialize_compact(s, read_file_path('rsa_private.pem')) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'RSA-OAEP') def test_with_zip_header(self): jwe = JsonWebEncryption() s = jwe.serialize_compact( {'alg': 'RSA-OAEP', 'enc': 'A128CBC-HS256', 'zip': 'DEF'}, 'hello', read_file_path('rsa_public.pem') ) data = jwe.deserialize_compact(s, read_file_path('rsa_private.pem')) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'RSA-OAEP') def test_aes_jwe(self): jwe = JsonWebEncryption() sizes = [128, 192, 256] _enc_choices = [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM' ] for s in sizes: alg = f'A{s}KW' key = os.urandom(s // 8) for enc in _enc_choices: protected = {'alg': alg, 'enc': enc} data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_aes_jwe_invalid_key(self): jwe = JsonWebEncryption() protected = {'alg': 'A128KW', 'enc': 'A128GCM'} self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', b'invalid-key' ) def test_aes_gcm_jwe(self): jwe = JsonWebEncryption() sizes = [128, 192, 256] _enc_choices = [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM' ] for s in sizes: alg = f'A{s}GCMKW' key = os.urandom(s // 8) for enc in _enc_choices: protected = {'alg': alg, 'enc': enc} data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_aes_gcm_jwe_invalid_key(self): jwe = JsonWebEncryption() protected = {'alg': 'A128GCMKW', 'enc': 'A128GCM'} self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', b'invalid-key' ) def test_serialize_compact_fails_if_header_contains_unknown_field_while_private_fields_restricted(self): jwe = JsonWebEncryption(private_headers=set()) key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM", "foo": "bar" } self.assertRaises( InvalidHeaderParameterNameError, jwe.serialize_compact, protected, b'hello', key ) def test_serialize_compact_allows_unknown_fields_in_header_while_private_fields_not_restricted(self): jwe = JsonWebEncryption() key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM", "foo": "bar" } data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_serialize_json_fails_if_protected_header_contains_unknown_field_while_private_fields_restricted(self): jwe = JsonWebEncryption(private_headers=set()) key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM", "foo": "bar" } header_obj = { "protected": protected } self.assertRaises( InvalidHeaderParameterNameError, jwe.serialize_json, header_obj, b'hello', key ) def test_serialize_json_fails_if_unprotected_header_contains_unknown_field_while_private_fields_restricted(self): jwe = JsonWebEncryption(private_headers=set()) key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM" } unprotected = { "foo": "bar" } header_obj = { "protected": protected, "unprotected": unprotected } self.assertRaises( InvalidHeaderParameterNameError, jwe.serialize_json, header_obj, b'hello', key ) def test_serialize_json_fails_if_recipient_header_contains_unknown_field_while_private_fields_restricted(self): jwe = JsonWebEncryption(private_headers=set()) key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM" } recipients = [ { "header": { "foo": "bar" } } ] header_obj = { "protected": protected, "recipients": recipients } self.assertRaises( InvalidHeaderParameterNameError, jwe.serialize_json, header_obj, b'hello', key ) def test_serialize_json_allows_unknown_fields_in_headers_while_private_fields_not_restricted(self): jwe = JsonWebEncryption() key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM", "foo1": "bar1" } unprotected = { "foo2": "bar2" } recipients = [ { "header": { "foo3": "bar3" } } ] header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients } data = jwe.serialize_json(header_obj, b'hello', key) rv = jwe.deserialize_json(data, key) self.assertEqual(rv['payload'], b'hello') def test_serialize_json_ignores_additional_members_in_recipients_elements(self): jwe = JsonWebEncryption() key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM" } recipients = [ { "foo": "bar" } ] header_obj = { "protected": protected, "recipients": recipients } data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_deserialize_json_fails_if_protected_header_contains_unknown_field_while_private_fields_restricted(self): jwe = JsonWebEncryption(private_headers=set()) key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM" } header_obj = { "protected": protected } data = jwe.serialize_json(header_obj, b'hello', key) decoded_protected = extract_header(to_bytes(data["protected"]), DecodeError) decoded_protected["foo"] = "bar" data["protected"] = to_unicode(json_b64encode(decoded_protected)) self.assertRaises( InvalidHeaderParameterNameError, jwe.deserialize_json, data, key ) def test_deserialize_json_fails_if_unprotected_header_contains_unknown_field_while_private_fields_restricted(self): jwe = JsonWebEncryption(private_headers=set()) key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM" } header_obj = { "protected": protected } data = jwe.serialize_json(header_obj, b'hello', key) data["unprotected"] = { "foo": "bar" } self.assertRaises( InvalidHeaderParameterNameError, jwe.deserialize_json, data, key ) def test_deserialize_json_fails_if_recipient_header_contains_unknown_field_while_private_fields_restricted(self): jwe = JsonWebEncryption(private_headers=set()) key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM" } header_obj = { "protected": protected } data = jwe.serialize_json(header_obj, b'hello', key) data["recipients"][0]["header"] = { "foo": "bar" } self.assertRaises( InvalidHeaderParameterNameError, jwe.deserialize_json, data, key ) def test_deserialize_json_allows_unknown_fields_in_headers_while_private_fields_not_restricted(self): jwe = JsonWebEncryption() key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM" } header_obj = { "protected": protected } data = jwe.serialize_json(header_obj, b'hello', key) data["unprotected"] = { "foo1": "bar1" } data["recipients"][0]["header"] = { "foo2": "bar2" } rv = jwe.deserialize_json(data, key) self.assertEqual(rv['payload'], b'hello') def test_deserialize_json_ignores_additional_members_in_recipients_elements(self): jwe = JsonWebEncryption() key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM" } header_obj = { "protected": protected } data = jwe.serialize_json(header_obj, b'hello', key) data["recipients"][0]["foo"] = "bar" data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_deserialize_json_ignores_additional_members_in_jwe_message(self): jwe = JsonWebEncryption() key = OKPKey.generate_key('X25519', is_private=True) protected = { "alg": "ECDH-ES+A128KW", "enc": "A128GCM" } header_obj = { "protected": protected } data = jwe.serialize_json(header_obj, b'hello', key) data["foo"] = "bar" data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_es_key_agreement_computation(self): # https://tools.ietf.org/html/rfc7518#appendix-C alice_ephemeral_key = { "kty": "EC", "crv": "P-256", "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps", "d": "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo" } bob_static_key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } headers = { "alg": "ECDH-ES", "enc": "A128GCM", "apu": "QWxpY2U", "apv": "Qm9i", "epk": { "kty": "EC", "crv": "P-256", "x": "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0", "y": "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps" } } alg = JsonWebEncryption.ALG_REGISTRY['ECDH-ES'] enc = JsonWebEncryption.ENC_REGISTRY['A128GCM'] alice_ephemeral_key = alg.prepare_key(alice_ephemeral_key) bob_static_key = alg.prepare_key(bob_static_key) alice_ephemeral_pubkey = alice_ephemeral_key.get_op_key('wrapKey') bob_static_pubkey = bob_static_key.get_op_key('wrapKey') # Derived key computation at Alice # Step-by-step methods verification _shared_key_at_alice = alice_ephemeral_key.exchange_shared_key(bob_static_pubkey) self.assertEqual( _shared_key_at_alice, bytes([158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132, 38, 156, 251, 49, 110, 163, 218, 128, 106, 72, 246, 218, 167, 121, 140, 254, 144, 196]) ) _fixed_info_at_alice = alg.compute_fixed_info(headers, enc.key_size) self.assertEqual( _fixed_info_at_alice, bytes([0, 0, 0, 7, 65, 49, 50, 56, 71, 67, 77, 0, 0, 0, 5, 65, 108, 105, 99, 101, 0, 0, 0, 3, 66, 111, 98, 0, 0, 0, 128]) ) _dk_at_alice = alg.compute_derived_key(_shared_key_at_alice, _fixed_info_at_alice, enc.key_size) self.assertEqual(_dk_at_alice, bytes([86, 170, 141, 234, 248, 35, 109, 32, 92, 34, 40, 205, 113, 167, 16, 26])) self.assertEqual(urlsafe_b64encode(_dk_at_alice), b'VqqN6vgjbSBcIijNcacQGg') # All-in-one method verification dk_at_alice = alg.deliver(alice_ephemeral_key, bob_static_pubkey, headers, enc.key_size) self.assertEqual(dk_at_alice, bytes([86, 170, 141, 234, 248, 35, 109, 32, 92, 34, 40, 205, 113, 167, 16, 26])) self.assertEqual(urlsafe_b64encode(dk_at_alice), b'VqqN6vgjbSBcIijNcacQGg') # Derived key computation at Bob # Step-by-step methods verification _shared_key_at_bob = bob_static_key.exchange_shared_key(alice_ephemeral_pubkey) self.assertEqual(_shared_key_at_bob, _shared_key_at_alice) _fixed_info_at_bob = alg.compute_fixed_info(headers, enc.key_size) self.assertEqual(_fixed_info_at_bob, _fixed_info_at_alice) _dk_at_bob = alg.compute_derived_key(_shared_key_at_bob, _fixed_info_at_bob, enc.key_size) self.assertEqual(_dk_at_bob, _dk_at_alice) # All-in-one method verification dk_at_bob = alg.deliver(bob_static_key, alice_ephemeral_pubkey, headers, enc.key_size) self.assertEqual(dk_at_bob, dk_at_alice) def test_ecdh_es_jwe_in_direct_key_agreement_mode(self): jwe = JsonWebEncryption() key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } for enc in [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM', ]: protected = {'alg': 'ECDH-ES', 'enc': enc} data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_es_jwe_json_serialization_single_recipient_in_direct_key_agreement_mode(self): jwe = JsonWebEncryption() key = OKPKey.generate_key('X25519', is_private=True) protected = {'alg': 'ECDH-ES', 'enc': 'A128GCM'} header_obj = {'protected': protected} data = jwe.serialize_json(header_obj, b'hello', key) rv = jwe.deserialize_json(data, key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_es_jwe_in_key_agreement_with_key_wrapping_mode(self): jwe = JsonWebEncryption() key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck", "d": "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw" } for alg in [ 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW', ]: for enc in [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM', ]: protected = {'alg': alg, 'enc': enc} data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_es_jwe_with_okp_key_in_direct_key_agreement_mode(self): jwe = JsonWebEncryption() key = OKPKey.generate_key('X25519', is_private=True) for enc in [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM', ]: protected = {'alg': 'ECDH-ES', 'enc': enc} data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_es_jwe_with_okp_key_in_key_agreement_with_key_wrapping_mode(self): jwe = JsonWebEncryption() key = OKPKey.generate_key('X25519', is_private=True) for alg in [ 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW', ]: for enc in [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM', ]: protected = {'alg': alg, 'enc': enc} data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') def test_ecdh_es_jwe_with_json_serialization_when_kid_is_not_specified(self): jwe = JsonWebEncryption() bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) protected = { "alg": "ECDH-ES+A256KW", "enc": "A256GCM", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, [bob_key, charlie_key]) rv_at_bob = jwe.deserialize_json(data, bob_key) self.assertEqual(rv_at_bob['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_bob['header']['protected'][k] for k in rv_at_bob['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_bob['header']['unprotected'], unprotected) self.assertEqual(rv_at_bob['header']['recipients'], recipients) self.assertEqual(rv_at_bob['header']['aad'], jwe_aad) self.assertEqual(rv_at_bob['payload'], payload) rv_at_charlie = jwe.deserialize_json(data, charlie_key) self.assertEqual(rv_at_charlie['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_charlie['header']['protected'][k] for k in rv_at_charlie['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_charlie['header']['unprotected'], unprotected) self.assertEqual(rv_at_charlie['header']['recipients'], recipients) self.assertEqual(rv_at_charlie['header']['aad'], jwe_aad) self.assertEqual(rv_at_charlie['payload'], payload) def test_ecdh_es_jwe_with_json_serialization_when_kid_is_specified(self): jwe = JsonWebEncryption() bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "kid": "bob-key-2", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "kid": "2021-05-06", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) protected = { "alg": "ECDH-ES+A256KW", "enc": "A256GCM", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, [bob_key, charlie_key]) rv_at_bob = jwe.deserialize_json(data, bob_key) self.assertEqual(rv_at_bob['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_bob['header']['protected'][k] for k in rv_at_bob['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_bob['header']['unprotected'], unprotected) self.assertEqual(rv_at_bob['header']['recipients'], recipients) self.assertEqual(rv_at_bob['header']['aad'], jwe_aad) self.assertEqual(rv_at_bob['payload'], payload) rv_at_charlie = jwe.deserialize_json(data, charlie_key) self.assertEqual(rv_at_charlie['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv_at_charlie['header']['protected'][k] for k in rv_at_charlie['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv_at_charlie['header']['unprotected'], unprotected) self.assertEqual(rv_at_charlie['header']['recipients'], recipients) self.assertEqual(rv_at_charlie['header']['aad'], jwe_aad) self.assertEqual(rv_at_charlie['payload'], payload) def test_ecdh_es_jwe_with_json_serialization_for_single_recipient(self): jwe = JsonWebEncryption() key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) protected = { "alg": "ECDH-ES+A256KW", "enc": "A256GCM", "apu": "QWxpY2U", "apv": "Qm9i" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "bob-key-2" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, key) rv = jwe.deserialize_json(data, key) self.assertEqual(rv['header']['protected'].keys(), protected.keys() | {'epk'}) self.assertEqual( {k: rv['header']['protected'][k] for k in rv['header']['protected'].keys() - {'epk'}}, protected ) self.assertEqual(rv['header']['unprotected'], unprotected) self.assertEqual(rv['header']['recipients'], recipients) self.assertEqual(rv['header']['aad'], jwe_aad) self.assertEqual(rv['payload'], payload) def test_ecdh_es_encryption_fails_json_serialization_multiple_recipients_in_direct_key_agreement_mode(self): jwe = JsonWebEncryption() bob_key = OKPKey.generate_key('X25519', is_private=True) charlie_key = OKPKey.generate_key('X25519', is_private=True) protected = {'alg': 'ECDH-ES', 'enc': 'A128GCM'} header_obj = {'protected': protected} self.assertRaises( InvalidAlgorithmForMultipleRecipientsMode, jwe.serialize_json, header_obj, b'hello', [bob_key, charlie_key] ) def test_ecdh_es_decryption_with_public_key_fails(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-ES', 'enc': 'A128GCM'} key = { "kty": "EC", "crv": "P-256", "x": "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ", "y": "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck" } data = jwe.serialize_compact(protected, b'hello', key) self.assertRaises( ValueError, jwe.deserialize_compact, data, key ) def test_ecdh_es_encryption_fails_if_key_curve_is_inappropriate(self): jwe = JsonWebEncryption() protected = {'alg': 'ECDH-ES', 'enc': 'A128GCM'} key = OKPKey.generate_key('Ed25519', is_private=False) self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', key ) def test_ecdh_es_decryption_fails_if_key_matches_to_no_recipient(self): jwe = JsonWebEncryption() bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) protected = { "alg": "ECDH-ES+A256KW", "enc": "A256GCM", "apu": "QWxpY2U", "apv": "Qm9i" } unprotected = { "jku": "https://alice.example.com/keys.jwks" } recipients = [ { "header": { "kid": "bob-key-2" } } ] jwe_aad = b'Authenticate me too.' header_obj = { "protected": protected, "unprotected": unprotected, "recipients": recipients, "aad": jwe_aad } payload = b'Three is a magic number.' data = jwe.serialize_json(header_obj, payload, bob_key) self.assertRaises( InvalidUnwrap, jwe.deserialize_json, data, charlie_key ) def test_decryption_with_json_serialization_succeeds_while_encrypted_key_for_another_recipient_is_invalid(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kid": "Alice's key", "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kid": "Bob's key", "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kid": "Charlie's key", "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) data = { "protected": "eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1Ijoi" + "UVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9L" + "UCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFB" + "RnFVQUZhMzlkeUJjIn19", "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "Bob's key" }, "encrypted_key": "pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQ" + "eU1cSl55cQ0hGezJu2N9IY0QM" # Invalid encrypted key }, { "header": { "kid": "Charlie's key" }, "encrypted_key": "56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8" + "fe4z3PQ2YH2afvjQ28aiCTWFE" # Valid encrypted key } ], "iv": "AAECAwQFBgcICQoLDA0ODw", "ciphertext": "Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw", "tag": "HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" } rv_at_charlie = jwe.deserialize_json(data, charlie_key, sender_key=alice_key) self.assertEqual(rv_at_charlie.keys(), {'header', 'payload'}) self.assertEqual(rv_at_charlie['header'].keys(), {'protected', 'unprotected', 'recipients'}) self.assertEqual( rv_at_charlie['header']['protected'], { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll", "epk": { "kty": "OKP", "crv": "X25519", "x": "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc" } } ) self.assertEqual( rv_at_charlie['header']['unprotected'], { "jku": "https://alice.example.com/keys.jwks" } ) self.assertEqual( rv_at_charlie['header']['recipients'], [ { "header": { "kid": "Bob's key" } }, { "header": { "kid": "Charlie's key" } } ] ) self.assertEqual(rv_at_charlie['payload'], b'Three is a magic number.') def test_decryption_with_json_serialization_fails_if_encrypted_key_for_this_recipient_is_invalid(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kid": "Alice's key", "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kid": "Bob's key", "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kid": "Charlie's key", "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) data = { "protected": "eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1Ijoi" + "UVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9L" + "UCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFB" + "RnFVQUZhMzlkeUJjIn19", "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "Bob's key" }, "encrypted_key": "pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQ" + "eU1cSl55cQ0hGezJu2N9IY0QM" # Invalid encrypted key }, { "header": { "kid": "Charlie's key" }, "encrypted_key": "56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8" + "fe4z3PQ2YH2afvjQ28aiCTWFE" # Valid encrypted key } ], "iv": "AAECAwQFBgcICQoLDA0ODw", "ciphertext": "Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw", "tag": "HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" } self.assertRaises( InvalidUnwrap, jwe.deserialize_json, data, bob_key, sender_key=alice_key ) def test_dir_alg(self): jwe = JsonWebEncryption() key = OctKey.generate_key(128, is_private=True) protected = {'alg': 'dir', 'enc': 'A128GCM'} data = jwe.serialize_compact(protected, b'hello', key) rv = jwe.deserialize_compact(data, key) self.assertEqual(rv['payload'], b'hello') key2 = OctKey.generate_key(256, is_private=True) self.assertRaises(ValueError, jwe.deserialize_compact, data, key2) self.assertRaises( ValueError, jwe.serialize_compact, protected, b'hello', key2 ) def test_decryption_of_message_to_multiple_recipients_by_matching_key(self): jwe = JsonWebEncryption() alice_public_key_id = "did:example:123#WjKgJV7VRw3hmgU6--4v15c0Aewbcvat1BsRFTIqa5Q" alice_public_key = OKPKey.import_key({ "kid": "WjKgJV7VRw3hmgU6--4v15c0Aewbcvat1BsRFTIqa5Q", "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4" }) key_store = {} charlie_X448_key_id = "did:example:123#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw" charlie_X448_key = OKPKey.import_key({ "kid": "_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", "kty": "OKP", "crv": "X448", "x": "M-OMugy74ksznVQ-Bp6MC_-GEPSrT8yiAtminJvw0j_UxJtpNHl_hcWMSf_Pfm_ws0vVWvAfwwA", "d": "VGZPkclj_7WbRaRMzBqxpzXIpc2xz1d3N1ay36UxdVLfKaP33hABBMpddTRv1f-hRsQUNvmlGOg" }) key_store[charlie_X448_key_id] = charlie_X448_key charlie_X25519_key_id = "did:example:123#ZC2jXTO6t4R501bfCXv3RxarZyUbdP2w_psLwMuY6ec" charlie_X25519_key = OKPKey.import_key({ "kid": "ZC2jXTO6t4R501bfCXv3RxarZyUbdP2w_psLwMuY6ec", "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) key_store[charlie_X25519_key_id] = charlie_X25519_key data = """ { "protected": "eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjoiUVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9LUCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFBRnFVQUZhMzlkeUJjIn19", "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A" }, "encrypted_key": "pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQeU1cSl55cQ0hGezJu2N9IY0QN" }, { "header": { "kid": "did:example:123#ZC2jXTO6t4R501bfCXv3RxarZyUbdP2w_psLwMuY6ec" }, "encrypted_key": "56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8fe4z3PQ2YH2afvjQ28aiCTWFE" } ], "iv": "AAECAwQFBgcICQoLDA0ODw", "ciphertext": "Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw", "tag": "HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" }""" parsed_data = jwe.parse_json(data) available_key_id = next(recipient['header']['kid'] for recipient in parsed_data['recipients'] if recipient['header']['kid'] in key_store.keys()) available_key = key_store[available_key_id] rv = jwe.deserialize_json(parsed_data, (available_key_id, available_key), sender_key=alice_public_key) self.assertEqual(rv.keys(), {'header', 'payload'}) self.assertEqual(rv['header'].keys(), {'protected', 'unprotected', 'recipients'}) self.assertEqual( rv['header']['protected'], { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll", "epk": { "kty": "OKP", "crv": "X25519", "x": "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc" } } ) self.assertEqual( rv['header']['unprotected'], { "jku": "https://alice.example.com/keys.jwks" } ) self.assertEqual( rv['header']['recipients'], [ { "header": { "kid": "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A" } }, { "header": { "kid": "did:example:123#ZC2jXTO6t4R501bfCXv3RxarZyUbdP2w_psLwMuY6ec" } } ] ) self.assertEqual(rv['payload'], b'Three is a magic number.') def test_decryption_of_json_string(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) charlie_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" }) data = """ { "protected": "eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjoiUVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9LUCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFBRnFVQUZhMzlkeUJjIn19", "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "bob-key-2" }, "encrypted_key": "pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQeU1cSl55cQ0hGezJu2N9IY0QN" }, { "header": { "kid": "2021-05-06" }, "encrypted_key": "56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8fe4z3PQ2YH2afvjQ28aiCTWFE" } ], "iv": "AAECAwQFBgcICQoLDA0ODw", "ciphertext": "Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw", "tag": "HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" }""" rv_at_bob = jwe.deserialize_json(data, bob_key, sender_key=alice_key) self.assertEqual(rv_at_bob.keys(), {'header', 'payload'}) self.assertEqual(rv_at_bob['header'].keys(), {'protected', 'unprotected', 'recipients'}) self.assertEqual( rv_at_bob['header']['protected'], { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll", "epk": { "kty": "OKP", "crv": "X25519", "x": "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc" } } ) self.assertEqual( rv_at_bob['header']['unprotected'], { "jku": "https://alice.example.com/keys.jwks" } ) self.assertEqual( rv_at_bob['header']['recipients'], [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ] ) self.assertEqual(rv_at_bob['payload'], b'Three is a magic number.') rv_at_charlie = jwe.deserialize_json(data, charlie_key, sender_key=alice_key) self.assertEqual(rv_at_charlie.keys(), {'header', 'payload'}) self.assertEqual(rv_at_charlie['header'].keys(), {'protected', 'unprotected', 'recipients'}) self.assertEqual( rv_at_charlie['header']['protected'], { "alg": "ECDH-1PU+A128KW", "enc": "A256CBC-HS512", "apu": "QWxpY2U", "apv": "Qm9iIGFuZCBDaGFybGll", "epk": { "kty": "OKP", "crv": "X25519", "x": "k9of_cpAajy0poW5gaixXGs9nHkwg1AFqUAFa39dyBc" } } ) self.assertEqual( rv_at_charlie['header']['unprotected'], { "jku": "https://alice.example.com/keys.jwks" } ) self.assertEqual( rv_at_charlie['header']['recipients'], [ { "header": { "kid": "bob-key-2" } }, { "header": { "kid": "2021-05-06" } } ] ) self.assertEqual(rv_at_charlie['payload'], b'Three is a magic number.') def test_parse_json(self): json_msg = """ { "protected": "eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjoiUVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9LUCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFBRnFVQUZhMzlkeUJjIn19", "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "bob-key-2" }, "encrypted_key": "pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQeU1cSl55cQ0hGezJu2N9IY0QN" }, { "header": { "kid": "2021-05-06" }, "encrypted_key": "56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8fe4z3PQ2YH2afvjQ28aiCTWFE" } ], "iv": "AAECAwQFBgcICQoLDA0ODw", "ciphertext": "Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw", "tag": "HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" }""" parsed_msg = JsonWebEncryption.parse_json(json_msg) self.assertEqual( parsed_msg, { "protected": "eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjoiUVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9LUCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFBRnFVQUZhMzlkeUJjIn19", "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "bob-key-2" }, "encrypted_key": "pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQeU1cSl55cQ0hGezJu2N9IY0QN" }, { "header": { "kid": "2021-05-06" }, "encrypted_key": "56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8fe4z3PQ2YH2afvjQ28aiCTWFE" } ], "iv": "AAECAwQFBgcICQoLDA0ODw", "ciphertext": "Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw", "tag": "HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" } ) def test_parse_json_fails_if_json_msg_is_invalid(self): json_msg = """ { "protected": "eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjoiUVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9LUCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFBRnFVQUZhMzlkeUJjIn19", "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "bob-key-2" , "encrypted_key": "pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQeU1cSl55cQ0hGezJu2N9IY0QN" }, { "header": { "kid": "2021-05-06" }, "encrypted_key": "56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8fe4z3PQ2YH2afvjQ28aiCTWFE" } ], "iv": "AAECAwQFBgcICQoLDA0ODw", "ciphertext": "Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw", "tag": "HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" }""" self.assertRaises( DecodeError, JsonWebEncryption.parse_json, json_msg ) def test_decryption_fails_if_ciphertext_is_invalid(self): jwe = JsonWebEncryption() alice_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" }) bob_key = OKPKey.import_key({ "kty": "OKP", "crv": "X25519", "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" }) data = { "protected": "eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1Ijoi" + "UVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9L" + "UCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFB" + "RnFVQUZhMzlkeUJjIn19", "unprotected": { "jku": "https://alice.example.com/keys.jwks" }, "recipients": [ { "header": { "kid": "bob-key-2" }, "encrypted_key": "pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQ" + "eU1cSl55cQ0hGezJu2N9IY0QN" } ], "iv": "AAECAwQFBgcICQoLDA0ODw", "ciphertext": "Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFY", # invalid ciphertext "tag": "HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" } self.assertRaises( Exception, jwe.deserialize_json, data, bob_key, sender_key=alice_key ) def test_generic_serialize_deserialize_for_compact_serialization(self): jwe = JsonWebEncryption() alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = OKPKey.generate_key('X25519', is_private=True) header_obj = {'alg': 'ECDH-1PU+A128KW', 'enc': 'A128CBC-HS256'} data = jwe.serialize(header_obj, b'hello', bob_key, sender_key=alice_key) self.assertIsInstance(data, bytes) rv = jwe.deserialize(data, bob_key, sender_key=alice_key) self.assertEqual(rv['payload'], b'hello') def test_generic_serialize_deserialize_for_json_serialization(self): jwe = JsonWebEncryption() alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = OKPKey.generate_key('X25519', is_private=True) protected = {'alg': 'ECDH-1PU+A128KW', 'enc': 'A128CBC-HS256'} header_obj = {'protected': protected} data = jwe.serialize(header_obj, b'hello', bob_key, sender_key=alice_key) self.assertIsInstance(data, dict) rv = jwe.deserialize(data, bob_key, sender_key=alice_key) self.assertEqual(rv['payload'], b'hello') def test_generic_deserialize_for_json_serialization_string(self): jwe = JsonWebEncryption() alice_key = OKPKey.generate_key('X25519', is_private=True) bob_key = OKPKey.generate_key('X25519', is_private=True) protected = {'alg': 'ECDH-1PU+A128KW', 'enc': 'A128CBC-HS256'} header_obj = {'protected': protected} data = jwe.serialize(header_obj, b'hello', bob_key, sender_key=alice_key) self.assertIsInstance(data, dict) data_as_string = json.dumps(data) rv = jwe.deserialize(data_as_string, bob_key, sender_key=alice_key) self.assertEqual(rv['payload'], b'hello') authlib-1.3.2/tests/jose/test_jwk.py000066400000000000000000000234421466226534200174720ustar00rootroot00000000000000import unittest from authlib.jose import JsonWebKey, KeySet from authlib.jose import OctKey, RSAKey, ECKey, OKPKey from authlib.common.encoding import base64_to_int, json_dumps from tests.util import read_file_path class BaseTest(unittest.TestCase): def assertBase64IntEqual(self, x, y): self.assertEqual(base64_to_int(x), base64_to_int(y)) class OctKeyTest(BaseTest): def test_import_oct_key(self): # https://tools.ietf.org/html/rfc7520#section-3.5 obj = { "kty": "oct", "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037", "use": "sig", "alg": "HS256", "k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg" } key = OctKey.import_key(obj) new_obj = key.as_dict() self.assertEqual(obj['k'], new_obj['k']) self.assertIn('use', new_obj) def test_invalid_oct_key(self): self.assertRaises(ValueError, OctKey.import_key, {}) def test_generate_oct_key(self): self.assertRaises(ValueError, OctKey.generate_key, 251) with self.assertRaises(ValueError) as cm: OctKey.generate_key(is_private=False) self.assertEqual(str(cm.exception), 'oct key can not be generated as public') key = OctKey.generate_key() self.assertIn('kid', key.as_dict()) self.assertNotIn('use', key.as_dict()) key2 = OctKey.import_key(key, {'use': 'sig'}) self.assertIn('use', key2.as_dict()) class RSAKeyTest(BaseTest): def test_import_ssh_pem(self): raw = read_file_path('ssh_public.pem') key = RSAKey.import_key(raw) obj = key.as_dict() self.assertEqual(obj['kty'], 'RSA') def test_rsa_public_key(self): # https://tools.ietf.org/html/rfc7520#section-3.3 obj = read_file_path('jwk_public.json') key = RSAKey.import_key(obj) new_obj = key.as_dict() self.assertBase64IntEqual(new_obj['n'], obj['n']) self.assertBase64IntEqual(new_obj['e'], obj['e']) def test_rsa_private_key(self): # https://tools.ietf.org/html/rfc7520#section-3.4 obj = read_file_path('jwk_private.json') key = RSAKey.import_key(obj) new_obj = key.as_dict(is_private=True) self.assertBase64IntEqual(new_obj['n'], obj['n']) self.assertBase64IntEqual(new_obj['e'], obj['e']) self.assertBase64IntEqual(new_obj['d'], obj['d']) self.assertBase64IntEqual(new_obj['p'], obj['p']) self.assertBase64IntEqual(new_obj['q'], obj['q']) self.assertBase64IntEqual(new_obj['dp'], obj['dp']) self.assertBase64IntEqual(new_obj['dq'], obj['dq']) self.assertBase64IntEqual(new_obj['qi'], obj['qi']) def test_rsa_private_key2(self): rsa_obj = read_file_path('jwk_private.json') obj = { "kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": rsa_obj['n'], 'd': rsa_obj['d'], "e": "AQAB" } key = RSAKey.import_key(obj) new_obj = key.as_dict(is_private=True) self.assertBase64IntEqual(new_obj['n'], obj['n']) self.assertBase64IntEqual(new_obj['e'], obj['e']) self.assertBase64IntEqual(new_obj['d'], obj['d']) self.assertBase64IntEqual(new_obj['p'], rsa_obj['p']) self.assertBase64IntEqual(new_obj['q'], rsa_obj['q']) self.assertBase64IntEqual(new_obj['dp'], rsa_obj['dp']) self.assertBase64IntEqual(new_obj['dq'], rsa_obj['dq']) self.assertBase64IntEqual(new_obj['qi'], rsa_obj['qi']) def test_invalid_rsa(self): self.assertRaises(ValueError, RSAKey.import_key, {'kty': 'RSA'}) rsa_obj = read_file_path('jwk_private.json') obj = { "kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": rsa_obj['n'], 'd': rsa_obj['d'], 'p': rsa_obj['p'], "e": "AQAB" } self.assertRaises(ValueError, RSAKey.import_key, obj) def test_rsa_key_generate(self): self.assertRaises(ValueError, RSAKey.generate_key, 256) self.assertRaises(ValueError, RSAKey.generate_key, 2001) key1 = RSAKey.generate_key(is_private=True) self.assertIn(b'PRIVATE', key1.as_pem(is_private=True)) self.assertIn(b'PUBLIC', key1.as_pem(is_private=False)) key2 = RSAKey.generate_key(is_private=False) self.assertRaises(ValueError, key2.as_pem, True) self.assertIn(b'PUBLIC', key2.as_pem(is_private=False)) class ECKeyTest(BaseTest): def test_ec_public_key(self): # https://tools.ietf.org/html/rfc7520#section-3.1 obj = read_file_path('secp521r1-public.json') key = ECKey.import_key(obj) new_obj = key.as_dict() self.assertEqual(new_obj['crv'], obj['crv']) self.assertBase64IntEqual(new_obj['x'], obj['x']) self.assertBase64IntEqual(new_obj['y'], obj['y']) self.assertEqual(key.as_json()[0], '{') def test_ec_private_key(self): # https://tools.ietf.org/html/rfc7520#section-3.2 obj = read_file_path('secp521r1-private.json') key = ECKey.import_key(obj) new_obj = key.as_dict(is_private=True) self.assertEqual(new_obj['crv'], obj['crv']) self.assertBase64IntEqual(new_obj['x'], obj['x']) self.assertBase64IntEqual(new_obj['y'], obj['y']) self.assertBase64IntEqual(new_obj['d'], obj['d']) def test_invalid_ec(self): self.assertRaises(ValueError, ECKey.import_key, {'kty': 'EC'}) def test_ec_key_generate(self): self.assertRaises(ValueError, ECKey.generate_key, 'Invalid') key1 = ECKey.generate_key('P-384', is_private=True) self.assertIn(b'PRIVATE', key1.as_pem(is_private=True)) self.assertIn(b'PUBLIC', key1.as_pem(is_private=False)) key2 = ECKey.generate_key('P-256', is_private=False) self.assertRaises(ValueError, key2.as_pem, True) self.assertIn(b'PUBLIC', key2.as_pem(is_private=False)) class OKPKeyTest(BaseTest): def test_import_okp_ssh_key(self): raw = read_file_path('ed25519-ssh.pub') key = OKPKey.import_key(raw) obj = key.as_dict() self.assertEqual(obj['kty'], 'OKP') self.assertEqual(obj['crv'], 'Ed25519') def test_import_okp_public_key(self): obj = { "x": "AD9E0JYnpV-OxZbd8aN1t4z71Vtf6JcJC7TYHT0HDbg", "crv": "Ed25519", "kty": "OKP" } key = OKPKey.import_key(obj) new_obj = key.as_dict() self.assertEqual(obj['x'], new_obj['x']) def test_import_okp_private_pem(self): raw = read_file_path('ed25519-pkcs8.pem') key = OKPKey.import_key(raw) obj = key.as_dict(is_private=True) self.assertEqual(obj['kty'], 'OKP') self.assertEqual(obj['crv'], 'Ed25519') self.assertIn('d', obj) def test_import_okp_private_dict(self): obj = { 'x': '11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo', 'd': 'nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A', 'crv': 'Ed25519', 'kty': 'OKP' } key = OKPKey.import_key(obj) new_obj = key.as_dict(is_private=True) self.assertEqual(obj['d'], new_obj['d']) def test_okp_key_generate_pem(self): self.assertRaises(ValueError, OKPKey.generate_key, 'invalid') key1 = OKPKey.generate_key('Ed25519', is_private=True) self.assertIn(b'PRIVATE', key1.as_pem(is_private=True)) self.assertIn(b'PUBLIC', key1.as_pem(is_private=False)) key2 = OKPKey.generate_key('X25519', is_private=False) self.assertRaises(ValueError, key2.as_pem, True) self.assertIn(b'PUBLIC', key2.as_pem(is_private=False)) class JWKTest(BaseTest): def test_generate_keys(self): key = JsonWebKey.generate_key(kty='oct', crv_or_size=256, is_private=True) self.assertEqual(key['kty'], 'oct') key = JsonWebKey.generate_key(kty='EC', crv_or_size='P-256') self.assertEqual(key['kty'], 'EC') key = JsonWebKey.generate_key(kty='RSA', crv_or_size=2048) self.assertEqual(key['kty'], 'RSA') key = JsonWebKey.generate_key(kty='OKP', crv_or_size='Ed25519') self.assertEqual(key['kty'], 'OKP') def test_import_keys(self): rsa_pub_pem = read_file_path('rsa_public.pem') self.assertRaises(ValueError, JsonWebKey.import_key, rsa_pub_pem, {'kty': 'EC'}) key = JsonWebKey.import_key(raw=rsa_pub_pem, options={'kty': 'RSA'}) self.assertIn('e', dict(key)) self.assertIn('n', dict(key)) key = JsonWebKey.import_key(raw=rsa_pub_pem) self.assertIn('e', dict(key)) self.assertIn('n', dict(key)) def test_import_key_set(self): jwks_public = read_file_path('jwks_public.json') key_set1 = JsonWebKey.import_key_set(jwks_public) key1 = key_set1.find_by_kid('abc') self.assertEqual(key1['e'], 'AQAB') key_set2 = JsonWebKey.import_key_set(jwks_public['keys']) key2 = key_set2.find_by_kid('abc') self.assertEqual(key2['e'], 'AQAB') key_set3 = JsonWebKey.import_key_set(json_dumps(jwks_public)) key3 = key_set3.find_by_kid('abc') self.assertEqual(key3['e'], 'AQAB') self.assertRaises(ValueError, JsonWebKey.import_key_set, 'invalid') def test_thumbprint(self): # https://tools.ietf.org/html/rfc7638#section-3.1 data = read_file_path('thumbprint_example.json') key = JsonWebKey.import_key(data) expected = 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs' self.assertEqual(key.thumbprint(), expected) def test_key_set(self): key = RSAKey.generate_key(is_private=True) key_set = KeySet([key]) obj = key_set.as_dict()['keys'][0] self.assertIn('kid', obj) self.assertEqual(key_set.as_json()[0], '{') authlib-1.3.2/tests/jose/test_jws.py000066400000000000000000000205331466226534200175000ustar00rootroot00000000000000import unittest import json from authlib.jose import JsonWebSignature from authlib.jose import errors from tests.util import read_file_path class JWSTest(unittest.TestCase): def test_invalid_input(self): jws = JsonWebSignature() self.assertRaises(errors.DecodeError, jws.deserialize, 'a', 'k') self.assertRaises(errors.DecodeError, jws.deserialize, 'a.b.c', 'k') self.assertRaises( errors.DecodeError, jws.deserialize, 'YQ.YQ.YQ', 'k') # a self.assertRaises( errors.DecodeError, jws.deserialize, 'W10.a.YQ', 'k') # [] self.assertRaises( errors.DecodeError, jws.deserialize, 'e30.a.YQ', 'k') # {} self.assertRaises( errors.DecodeError, jws.deserialize, 'eyJhbGciOiJzIn0.a.YQ', 'k') self.assertRaises( errors.DecodeError, jws.deserialize, 'eyJhbGciOiJzIn0.YQ.a', 'k') def test_invalid_alg(self): jws = JsonWebSignature() self.assertRaises( errors.UnsupportedAlgorithmError, jws.deserialize, 'eyJhbGciOiJzIn0.YQ.YQ', 'k') self.assertRaises( errors.MissingAlgorithmError, jws.serialize, {}, '', 'k' ) self.assertRaises( errors.UnsupportedAlgorithmError, jws.serialize, {'alg': 's'}, '', 'k' ) def test_bad_signature(self): jws = JsonWebSignature() s = 'eyJhbGciOiJIUzI1NiJ9.YQ.YQ' self.assertRaises(errors.BadSignatureError, jws.deserialize, s, 'k') def test_not_supported_alg(self): jws = JsonWebSignature(algorithms=['HS256']) s = jws.serialize({'alg': 'HS256'}, 'hello', 'secret') jws = JsonWebSignature(algorithms=['RS256']) self.assertRaises( errors.UnsupportedAlgorithmError, lambda: jws.serialize({'alg': 'HS256'}, 'hello', 'secret') ) self.assertRaises( errors.UnsupportedAlgorithmError, jws.deserialize, s, 'secret' ) def test_compact_jws(self): jws = JsonWebSignature(algorithms=['HS256']) s = jws.serialize({'alg': 'HS256'}, 'hello', 'secret') data = jws.deserialize(s, 'secret') header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'HS256') self.assertNotIn('signature', data) def test_compact_rsa(self): jws = JsonWebSignature() private_key = read_file_path('rsa_private.pem') public_key = read_file_path('rsa_public.pem') s = jws.serialize({'alg': 'RS256'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'RS256') # can deserialize with private key data2 = jws.deserialize(s, private_key) self.assertEqual(data, data2) ssh_pub_key = read_file_path('ssh_public.pem') self.assertRaises(errors.BadSignatureError, jws.deserialize, s, ssh_pub_key) def test_compact_rsa_pss(self): jws = JsonWebSignature() private_key = read_file_path('rsa_private.pem') public_key = read_file_path('rsa_public.pem') s = jws.serialize({'alg': 'PS256'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'PS256') ssh_pub_key = read_file_path('ssh_public.pem') self.assertRaises(errors.BadSignatureError, jws.deserialize, s, ssh_pub_key) def test_compact_none(self): jws = JsonWebSignature() s = jws.serialize({'alg': 'none'}, 'hello', '') self.assertRaises(errors.BadSignatureError, jws.deserialize, s, '') def test_flattened_json_jws(self): jws = JsonWebSignature() protected = {'alg': 'HS256'} header = {'protected': protected, 'header': {'kid': 'a'}} s = jws.serialize(header, 'hello', 'secret') self.assertIsInstance(s, dict) data = jws.deserialize(s, 'secret') header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'HS256') self.assertNotIn('protected', data) def test_nested_json_jws(self): jws = JsonWebSignature() protected = {'alg': 'HS256'} header = {'protected': protected, 'header': {'kid': 'a'}} s = jws.serialize([header], 'hello', 'secret') self.assertIsInstance(s, dict) self.assertIn('signatures', s) data = jws.deserialize(s, 'secret') header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header[0]['alg'], 'HS256') self.assertNotIn('signatures', data) # test bad signature self.assertRaises(errors.BadSignatureError, jws.deserialize, s, 'f') def test_function_key(self): protected = {'alg': 'HS256'} header = [ {'protected': protected, 'header': {'kid': 'a'}}, {'protected': protected, 'header': {'kid': 'b'}}, ] def load_key(header, payload): self.assertEqual(payload, b'hello') kid = header.get('kid') if kid == 'a': return 'secret-a' return 'secret-b' jws = JsonWebSignature() s = jws.serialize(header, b'hello', load_key) self.assertIsInstance(s, dict) self.assertIn('signatures', s) data = jws.deserialize(json.dumps(s), load_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header[0]['alg'], 'HS256') self.assertNotIn('signature', data) def test_serialize_json_empty_payload(self): jws = JsonWebSignature() protected = {'alg': 'HS256'} header = {'protected': protected, 'header': {'kid': 'a'}} s = jws.serialize_json(header, b'', 'secret') data = jws.deserialize_json(s, 'secret') self.assertEqual(data['payload'], b'') def test_fail_deserialize_json(self): jws = JsonWebSignature() self.assertRaises(errors.DecodeError, jws.deserialize_json, None, '') self.assertRaises(errors.DecodeError, jws.deserialize_json, '[]', '') self.assertRaises(errors.DecodeError, jws.deserialize_json, '{}', '') # missing protected s = json.dumps({'payload': 'YQ'}) self.assertRaises(errors.DecodeError, jws.deserialize_json, s, '') # missing signature s = json.dumps({'payload': 'YQ', 'protected': 'YQ'}) self.assertRaises(errors.DecodeError, jws.deserialize_json, s, '') def test_validate_header(self): jws = JsonWebSignature(private_headers=[]) protected = {'alg': 'HS256', 'invalid': 'k'} header = {'protected': protected, 'header': {'kid': 'a'}} self.assertRaises( errors.InvalidHeaderParameterNameError, jws.serialize, header, b'hello', 'secret' ) jws = JsonWebSignature(private_headers=['invalid']) s = jws.serialize(header, b'hello', 'secret') self.assertIsInstance(s, dict) jws = JsonWebSignature() s = jws.serialize(header, b'hello', 'secret') self.assertIsInstance(s, dict) def test_ES512_alg(self): jws = JsonWebSignature() private_key = read_file_path('secp521r1-private.json') public_key = read_file_path('secp521r1-public.json') self.assertRaises(ValueError, jws.serialize, {'alg': 'ES256'}, 'hello', private_key) s = jws.serialize({'alg': 'ES512'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'ES512') def test_ES256K_alg(self): jws = JsonWebSignature(algorithms=['ES256K']) private_key = read_file_path('secp256k1-private.pem') public_key = read_file_path('secp256k1-pub.pem') s = jws.serialize({'alg': 'ES256K'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'ES256K') authlib-1.3.2/tests/jose/test_jwt.py000066400000000000000000000227571466226534200175130ustar00rootroot00000000000000import unittest import datetime from authlib.jose import errors from authlib.jose import JsonWebToken, JWTClaims, jwt from authlib.jose.errors import UnsupportedAlgorithmError from tests.util import read_file_path class JWTTest(unittest.TestCase): def test_init_algorithms(self): _jwt = JsonWebToken(['RS256']) self.assertRaises( UnsupportedAlgorithmError, _jwt.encode, {'alg': 'HS256'}, {}, 'k' ) _jwt = JsonWebToken('RS256') self.assertRaises( UnsupportedAlgorithmError, _jwt.encode, {'alg': 'HS256'}, {}, 'k' ) def test_encode_sensitive_data(self): # check=False won't raise error jwt.encode({'alg': 'HS256'}, {'password': ''}, 'k', check=False) self.assertRaises( errors.InsecureClaimError, jwt.encode, {'alg': 'HS256'}, {'password': ''}, 'k' ) self.assertRaises( errors.InsecureClaimError, jwt.encode, {'alg': 'HS256'}, {'text': '4242424242424242'}, 'k' ) def test_encode_datetime(self): now = datetime.datetime.utcnow() id_token = jwt.encode({'alg': 'HS256'}, {'exp': now}, 'k') claims = jwt.decode(id_token, 'k') self.assertIsInstance(claims.exp, int) def test_validate_essential_claims(self): id_token = jwt.encode({'alg': 'HS256'}, {'iss': 'foo'}, 'k') claims_options = { 'iss': { 'essential': True, 'values': ['foo'] } } claims = jwt.decode(id_token, 'k', claims_options=claims_options) claims.validate() claims.options = {'sub': {'essential': True}} self.assertRaises( errors.MissingClaimError, claims.validate ) def test_attribute_error(self): claims = JWTClaims({'iss': 'foo'}, {'alg': 'HS256'}) self.assertRaises(AttributeError, lambda: claims.invalid) def test_invalid_values(self): id_token = jwt.encode({'alg': 'HS256'}, {'iss': 'foo'}, 'k') claims_options = {'iss': {'values': ['bar']}} claims = jwt.decode(id_token, 'k', claims_options=claims_options) self.assertRaises( errors.InvalidClaimError, claims.validate, ) claims.options = {'iss': {'value': 'bar'}} self.assertRaises( errors.InvalidClaimError, claims.validate, ) def test_validate_expected_issuer_received_None(self): id_token = jwt.encode({'alg': 'HS256'}, {'iss': None, 'sub': None}, 'k') claims_options = { 'iss': { 'essential': True, 'values': ['foo'] } } claims = jwt.decode(id_token, 'k', claims_options=claims_options) self.assertRaises( errors.InvalidClaimError, claims.validate ) def test_validate_aud(self): id_token = jwt.encode({'alg': 'HS256'}, {'aud': 'foo'}, 'k') claims_options = { 'aud': { 'essential': True, 'value': 'foo' } } claims = jwt.decode(id_token, 'k', claims_options=claims_options) claims.validate() claims.options = { 'aud': {'values': ['bar']} } self.assertRaises( errors.InvalidClaimError, claims.validate ) id_token = jwt.encode({'alg': 'HS256'}, {'aud': ['foo', 'bar']}, 'k') claims = jwt.decode(id_token, 'k', claims_options=claims_options) claims.validate() # no validate claims.options = {'aud': {'values': []}} claims.validate() def test_validate_exp(self): id_token = jwt.encode({'alg': 'HS256'}, {'exp': 'invalid'}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises( errors.InvalidClaimError, claims.validate ) id_token = jwt.encode({'alg': 'HS256'}, {'exp': 1234}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises( errors.ExpiredTokenError, claims.validate ) def test_validate_nbf(self): id_token = jwt.encode({'alg': 'HS256'}, {'nbf': 'invalid'}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises( errors.InvalidClaimError, claims.validate ) id_token = jwt.encode({'alg': 'HS256'}, {'nbf': 1234}, 'k') claims = jwt.decode(id_token, 'k') claims.validate() id_token = jwt.encode({'alg': 'HS256'}, {'nbf': 1234}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises( errors.InvalidTokenError, claims.validate, 123 ) def test_validate_iat_issued_in_future(self): in_future = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) id_token = jwt.encode({'alg': 'HS256'}, {'iat': in_future}, 'k') claims = jwt.decode(id_token, 'k') with self.assertRaises(errors.InvalidTokenError) as error_ctx: claims.validate() self.assertEqual( str(error_ctx.exception), 'invalid_token: The token is not valid as it was issued in the future' ) def test_validate_iat_issued_in_future_with_insufficient_leeway(self): in_future = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) id_token = jwt.encode({'alg': 'HS256'}, {'iat': in_future}, 'k') claims = jwt.decode(id_token, 'k') with self.assertRaises(errors.InvalidTokenError) as error_ctx: claims.validate(leeway=5) self.assertEqual( str(error_ctx.exception), 'invalid_token: The token is not valid as it was issued in the future' ) def test_validate_iat_issued_in_future_with_sufficient_leeway(self): in_future = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) id_token = jwt.encode({'alg': 'HS256'}, {'iat': in_future}, 'k') claims = jwt.decode(id_token, 'k') claims.validate(leeway=20) def test_validate_iat_issued_in_past(self): in_future = datetime.datetime.utcnow() - datetime.timedelta(seconds=10) id_token = jwt.encode({'alg': 'HS256'}, {'iat': in_future}, 'k') claims = jwt.decode(id_token, 'k') claims.validate() def test_validate_iat(self): id_token = jwt.encode({'alg': 'HS256'}, {'iat': 'invalid'}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises( errors.InvalidClaimError, claims.validate ) def test_validate_jti(self): id_token = jwt.encode({'alg': 'HS256'}, {'jti': 'bar'}, 'k') claims_options = { 'jti': { 'validate': lambda c, o: o == 'foo' } } claims = jwt.decode(id_token, 'k', claims_options=claims_options) self.assertRaises( errors.InvalidClaimError, claims.validate ) def test_validate_custom(self): id_token = jwt.encode({'alg': 'HS256'}, {'custom': 'foo'}, 'k') claims_options = { 'custom': { 'validate': lambda c, o: o == 'bar' } } claims = jwt.decode(id_token, 'k', claims_options=claims_options) self.assertRaises( errors.InvalidClaimError, claims.validate ) def test_use_jws(self): payload = {'name': 'hi'} private_key = read_file_path('rsa_private.pem') pub_key = read_file_path('rsa_public.pem') data = jwt.encode({'alg': 'RS256'}, payload, private_key) self.assertEqual(data.count(b'.'), 2) claims = jwt.decode(data, pub_key) self.assertEqual(claims['name'], 'hi') def test_use_jwe(self): payload = {'name': 'hi'} private_key = read_file_path('rsa_private.pem') pub_key = read_file_path('rsa_public.pem') _jwt = JsonWebToken(['RSA-OAEP', 'A256GCM']) data = _jwt.encode( {'alg': 'RSA-OAEP', 'enc': 'A256GCM'}, payload, pub_key ) self.assertEqual(data.count(b'.'), 4) claims = _jwt.decode(data, private_key) self.assertEqual(claims['name'], 'hi') def test_use_jwks(self): header = {'alg': 'RS256', 'kid': 'abc'} payload = {'name': 'hi'} private_key = read_file_path('jwks_private.json') pub_key = read_file_path('jwks_public.json') data = jwt.encode(header, payload, private_key) self.assertEqual(data.count(b'.'), 2) claims = jwt.decode(data, pub_key) self.assertEqual(claims['name'], 'hi') def test_use_jwks_single_kid(self): """Test that jwks can be decoded if a kid for decoding is given and encoded data has no kid and only one key is set.""" header = {'alg': 'RS256'} payload = {'name': 'hi'} private_key = read_file_path('jwks_single_private.json') pub_key = read_file_path('jwks_single_public.json') data = jwt.encode(header, payload, private_key) self.assertEqual(data.count(b'.'), 2) claims = jwt.decode(data, pub_key) self.assertEqual(claims['name'], 'hi') def test_with_ec(self): payload = {'name': 'hi'} private_key = read_file_path('secp521r1-private.json') pub_key = read_file_path('secp521r1-public.json') data = jwt.encode({'alg': 'ES512'}, payload, private_key) self.assertEqual(data.count(b'.'), 2) claims = jwt.decode(data, pub_key) self.assertEqual(claims['name'], 'hi') authlib-1.3.2/tests/jose/test_rfc8037.py000066400000000000000000000011211466226534200177610ustar00rootroot00000000000000import unittest from authlib.jose import JsonWebSignature from tests.util import read_file_path class EdDSATest(unittest.TestCase): def test_EdDSA_alg(self): jws = JsonWebSignature(algorithms=['EdDSA']) private_key = read_file_path('ed25519-pkcs8.pem') public_key = read_file_path('ed25519-pub.pem') s = jws.serialize({'alg': 'EdDSA'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'EdDSA') authlib-1.3.2/tests/requirements-base.txt000066400000000000000000000000541466226534200205140ustar00rootroot00000000000000cryptography pytest coverage pytest-asyncio authlib-1.3.2/tests/requirements-clients.txt000066400000000000000000000004071466226534200212450ustar00rootroot00000000000000requests anyio httpx starlette cachelib werkzeug flask django # there is an incompatibility with asgiref, pypy and coverage, # see https://github.com/django/asgiref/issues/393 for details asgiref==3.6.0 ; implementation_name == 'pypy' and python_version < '3.10' authlib-1.3.2/tests/requirements-django.txt000066400000000000000000000003361466226534200210470ustar00rootroot00000000000000Django pytest-django # there is an incompatibility with asgiref, pypy and coverage, # see https://github.com/django/asgiref/issues/393 for details asgiref==3.6.0 ; implementation_name == 'pypy' and python_version < '3.10' authlib-1.3.2/tests/requirements-flask.txt000066400000000000000000000000271466226534200207020ustar00rootroot00000000000000Flask Flask-SQLAlchemy authlib-1.3.2/tests/util.py000066400000000000000000000007261466226534200156550ustar00rootroot00000000000000import os import json from authlib.common.encoding import to_unicode from authlib.common.urls import url_decode ROOT = os.path.abspath(os.path.dirname(__file__)) def get_file_path(name): return os.path.join(ROOT, 'files', name) def read_file_path(name): with open(get_file_path(name)) as f: if name.endswith('.json'): return json.load(f) return f.read() def decode_response(data): return dict(url_decode(to_unicode(data))) authlib-1.3.2/tox.ini000066400000000000000000000021151466226534200144710ustar00rootroot00000000000000[tox] isolated_build = True envlist = py{38,39,310,311,312,py38,py39,py310} py{38,39,310,311,312,py38,py39,py310}-{clients,flask,django,jose} coverage [testenv] deps = -r tests/requirements-base.txt jose: pycryptodomex>=3.10,<4 clients: -r tests/requirements-clients.txt flask: -r tests/requirements-flask.txt django: -r tests/requirements-django.txt setenv = TESTPATH=tests/core jose: TESTPATH=tests/jose clients: TESTPATH=tests/clients clients: DJANGO_SETTINGS_MODULE=tests.clients.test_django.settings flask: TESTPATH=tests/flask django: TESTPATH=tests/django django: DJANGO_SETTINGS_MODULE=tests.django.settings commands = coverage run --source=authlib -p -m pytest {posargs: {env:TESTPATH}} [pytest] asyncio_mode = auto [testenv:coverage] skip_install = true commands = coverage combine coverage report coverage html [coverage:run] branch = True [coverage:report] exclude_lines = pragma: no cover except ImportError def __repr__ raise NotImplementedError raise DeprecationWarning deprecate