pax_global_header 0000666 0000000 0000000 00000000064 14133261713 0014513 g ustar 00root root 0000000 0000000 52 comment=d8e428c9350c792fc3d25dbaaffa3bfefaabd8e3
authlib-0.15.5/ 0000775 0000000 0000000 00000000000 14133261713 0013233 5 ustar 00root root 0000000 0000000 authlib-0.15.5/.codeclimate.yml 0000664 0000000 0000000 00000000670 14133261713 0016310 0 ustar 00root root 0000000 0000000 version: "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-0.15.5/.codecov.yml 0000664 0000000 0000000 00000000171 14133261713 0015455 0 ustar 00root root 0000000 0000000 coverage:
status:
patch: false
changes: false
project:
default:
target: '80'
comment: false
authlib-0.15.5/.github/ 0000775 0000000 0000000 00000000000 14133261713 0014573 5 ustar 00root root 0000000 0000000 authlib-0.15.5/.github/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000002617 14133261713 0017400 0 ustar 00root root 0000000 0000000 # 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-0.15.5/.github/FUNDING.yml 0000664 0000000 0000000 00000000374 14133261713 0016414 0 ustar 00root root 0000000 0000000 # 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-0.15.5/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14133261713 0016756 5 ustar 00root root 0000000 0000000 authlib-0.15.5/.github/ISSUE_TEMPLATE/ask-for-help.md 0000664 0000000 0000000 00000000350 14133261713 0021566 0 ustar 00root root 0000000 0000000 ---
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-0.15.5/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001011 14133261713 0021441 0 ustar 00root root 0000000 0000000 ---
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-0.15.5/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000001127 14133261713 0022504 0 ustar 00root root 0000000 0000000 ---
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-0.15.5/.github/PULL_REQUEST_TEMPLATE.md 0000664 0000000 0000000 00000001116 14133261713 0020373 0 ustar 00root root 0000000 0000000 > 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-0.15.5/.github/workflows/ 0000775 0000000 0000000 00000000000 14133261713 0016630 5 ustar 00root root 0000000 0000000 authlib-0.15.5/.github/workflows/python.yml 0000664 0000000 0000000 00000002526 14133261713 0020701 0 ustar 00root root 0000000 0000000 name: tests
on:
push:
branches-ignore:
- 'wip-*'
paths-ignore:
- 'docs/**'
pull_request:
branches-ignore:
- 'wip-*'
paths-ignore:
- 'docs/**'
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 3
matrix:
python:
- version: 2.7
toxenv: py27,py27-flask
- version: 3.6
toxenv: py36,flask,django,py3
- version: 3.7
toxenv: py37,flask,django,py3
- version: 3.8
toxenv: py38,flask,django,py3
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
pip install -r requirements-test.txt
- name: Test with tox ${{ matrix.python.toxenv }}
env:
TOXENV: ${{ matrix.python.toxenv }}
run: tox
- name: Report coverage
run: |
coverage combine
coverage report
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: unittests
name: GitHub
authlib-0.15.5/.gitignore 0000664 0000000 0000000 00000000254 14133261713 0015224 0 ustar 00root root 0000000 0000000 *.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-0.15.5/.py27conf 0000664 0000000 0000000 00000000524 14133261713 0014704 0 ustar 00root root 0000000 0000000 [coverage:run]
branch = True
omit =
authlib/integrations/base_client/async_app.py
authlib/integrations/httpx_client/*
authlib/integrations/starlette_client/*
[coverage:report]
exclude_lines =
pragma: no cover
except ImportError
def __repr__
raise NotImplementedError
raise DeprecationWarning
deprecate
authlib-0.15.5/BACKERS.md 0000664 0000000 0000000 00000003027 14133261713 0014631 0 ustar 00root root 0000000 0000000 # 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 authencation, feel free to check Authing's Python SDK.
## Awesome Backers
Evilham
Aveline
Callam
authlib-0.15.5/COMMERCIAL-LICENSE 0000664 0000000 0000000 00000021633 14133261713 0015656 0 ustar 00root root 0000000 0000000 ==============================
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-0.15.5/LICENSE 0000664 0000000 0000000 00000002752 14133261713 0014246 0 ustar 00root root 0000000 0000000 BSD 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-0.15.5/MANIFEST.in 0000664 0000000 0000000 00000000060 14133261713 0014765 0 ustar 00root root 0000000 0000000 include README.rst
include LICENSE
prune tests*
authlib-0.15.5/Makefile 0000664 0000000 0000000 00000000755 14133261713 0014702 0 ustar 00root root 0000000 0000000 .PHONY: tests clean clean-pyc clean-build docs
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/
docs:
@$(MAKE) -C docs html
authlib-0.15.5/README.md 0000664 0000000 0000000 00000017577 14133261713 0014533 0 ustar 00root root 0000000 0000000
# Authlib
The ultimate Python library in building OAuth and OpenID Connect servers.
JWS, JWK, JWA, JWT are included.
Authlib is compatible with Python2.7+ and Python3.6+.
## 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)
- [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
- [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)
- [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)
- [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)
## 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/developers.
For quickly implementing token-based authentication, feel free to check Authing's Python SDK.
[**Support Me via GitHub Sponsors**](https://github.com/users/lepture/sponsorship).
## 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-0.15.5/README.rst 0000664 0000000 0000000 00000004200 14133261713 0014716 0 ustar 00root root 0000000 0000000 Authlib
=======
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
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-0.15.5/authlib/ 0000775 0000000 0000000 00000000000 14133261713 0014663 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/__init__.py 0000664 0000000 0000000 00000000734 14133261713 0017000 0 ustar 00root root 0000000 0000000 """
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-0.15.5/authlib/common/ 0000775 0000000 0000000 00000000000 14133261713 0016153 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/common/__init__.py 0000664 0000000 0000000 00000000000 14133261713 0020252 0 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/common/encoding.py 0000664 0000000 0000000 00000004074 14133261713 0020320 0 ustar 00root root 0000000 0000000 import sys
import json
import base64
import struct
is_py2 = sys.version_info[0] == 2
if is_py2:
unicode_type = unicode # noqa: F821
byte_type = str
text_types = (unicode, str) # noqa: F821
else:
unicode_type = str
byte_type = bytes
text_types = (str, )
def to_bytes(x, charset='utf-8', errors='strict'):
if x is None:
return None
if isinstance(x, byte_type):
return x
if isinstance(x, unicode_type):
return x.encode(charset, errors)
if isinstance(x, (int, float)):
return str(x).encode(charset, errors)
return byte_type(x)
def to_unicode(x, charset='utf-8', errors='strict'):
if x is None or isinstance(x, unicode_type):
return x
if isinstance(x, byte_type):
return x.decode(charset, errors)
return unicode_type(x)
def to_native(x, encoding='ascii'):
if isinstance(x, str):
return x
if is_py2:
return x.encode(encoding)
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')
if hasattr(int, 'to_bytes'):
s = num.to_bytes((num.bit_length() + 7) // 8, 'big', signed=False)
else:
buf = []
while num:
num, remainder = divmod(num, 256)
buf.append(remainder)
buf.reverse()
s = struct.pack('%sB' % len(buf), *buf)
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-0.15.5/authlib/common/errors.py 0000664 0000000 0000000 00000004242 14133261713 0020043 0 ustar 00root root 0000000 0000000 #: coding: utf-8
from 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 = '{}: {}'.format(self.error, self.description)
super(AuthlibBaseError, self).__init__(message)
def __repr__(self):
return '<{} "{}">'.format(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(AuthlibHTTPError, self).__init__(error, description, uri)
if status_code is not None:
self.status_code = status_code
self._translations = None
self._error_uris = None
def gettext(self, s):
if self._translations:
return self._translations.gettext(s)
return s
def get_error_description(self):
return self.description
def get_error_uri(self):
if self.uri:
return self.uri
if self._error_uris:
return self._error_uris.get(self.error)
def get_body(self):
error = [('error', self.error)]
description = self.get_error_description()
if description:
error.append(('error_description', description))
uri = self.get_error_uri()
if uri:
error.append(('error_uri', uri))
return error
def get_headers(self):
return default_json_headers[:]
def __call__(self, translations=None, error_uris=None):
self._translations = translations
self._error_uris = error_uris
body = dict(self.get_body())
headers = self.get_headers()
return self.status_code, body, headers
authlib-0.15.5/authlib/common/security.py 0000664 0000000 0000000 00000000755 14133261713 0020403 0 ustar 00root root 0000000 0000000 import 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-0.15.5/authlib/common/urls.py 0000664 0000000 0000000 00000011555 14133261713 0017521 0 ustar 00root root 0000000 0000000 """
authlib.util.urls
~~~~~~~~~~~~~~~~~
Wrapper functions for URL encoding and decoding.
"""
import re
try:
from urllib import quote as _quote
from urllib import unquote as _unquote
from urllib import urlencode as _urlencode
except ImportError:
from urllib.parse import quote as _quote
from urllib.parse import unquote as _unquote
from urllib.parse import urlencode as _urlencode
try:
from urllib2 import parse_keqv_list # noqa: F401
from urllib2 import parse_http_list # noqa: F401
except ImportError:
from urllib.request import parse_keqv_list # noqa: F401
from urllib.request import parse_http_list # noqa: F401
try:
import urlparse
except ImportError:
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-0.15.5/authlib/consts.py 0000664 0000000 0000000 00000000471 14133261713 0016550 0 ustar 00root root 0000000 0000000 name = 'Authlib'
version = '0.15.5'
author = 'Hsiaoming Yang '
homepage = 'https://authlib.org/'
default_user_agent = '{}/{} (+{})'.format(name, version, homepage)
default_json_headers = [
('Content-Type', 'application/json'),
('Cache-Control', 'no-store'),
('Pragma', 'no-cache'),
]
authlib-0.15.5/authlib/deprecate.py 0000664 0000000 0000000 00000000763 14133261713 0017177 0 ustar 00root root 0000000 0000000 import warnings
class AuthlibDeprecationWarning(DeprecationWarning):
pass
warnings.simplefilter('always', AuthlibDeprecationWarning)
def deprecate(message, version=None, link_uid=None, link_file=None):
if version:
message += '\nIt will be compatible before version {}.'.format(version)
if link_uid and link_file:
message += '\nRead more '.format(link_uid, link_file)
warnings.warn(AuthlibDeprecationWarning(message), stacklevel=2)
authlib-0.15.5/authlib/integrations/ 0000775 0000000 0000000 00000000000 14133261713 0017371 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/__init__.py 0000664 0000000 0000000 00000000000 14133261713 0021470 0 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/base_client/ 0000775 0000000 0000000 00000000000 14133261713 0021641 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/base_client/__init__.py 0000664 0000000 0000000 00000001112 14133261713 0023745 0 ustar 00root root 0000000 0000000 from .base_oauth import BaseOAuth
from .base_app import BaseApp
from .remote_app import RemoteApp
from .framework_integration import FrameworkIntegration
from .errors import (
OAuthError, MissingRequestTokenError, MissingTokenError,
TokenExpiredError, InvalidTokenError, UnsupportedTokenTypeError,
MismatchingStateError,
)
__all__ = [
'BaseOAuth', 'BaseApp', 'RemoteApp', 'FrameworkIntegration',
'OAuthError', 'MissingRequestTokenError', 'MissingTokenError',
'TokenExpiredError', 'InvalidTokenError', 'UnsupportedTokenTypeError',
'MismatchingStateError',
]
authlib-0.15.5/authlib/integrations/base_client/async_app.py 0000664 0000000 0000000 00000017410 14133261713 0024173 0 ustar 00root root 0000000 0000000 import time
import logging
from authlib.common.urls import urlparse
from authlib.jose import JsonWebToken, JsonWebKey
from authlib.oidc.core import UserInfo, CodeIDToken, ImplicitIDToken
from .base_app import BaseApp
from .errors import (
MissingRequestTokenError,
MissingTokenError,
)
__all__ = ['AsyncRemoteApp']
log = logging.getLogger(__name__)
class AsyncRemoteApp(BaseApp):
async def load_server_metadata(self):
if self._server_metadata_url and '_loaded_at' not in self.server_metadata:
metadata = await self._fetch_server_metadata(self._server_metadata_url)
metadata['_loaded_at'] = time.time()
self.server_metadata.update(metadata)
return self.server_metadata
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 _create_oauth1_authorization_url(self, client, authorization_endpoint, **kwargs):
params = {}
if self.request_token_params:
params.update(self.request_token_params)
token = await client.fetch_request_token(
self.request_token_url, **params
)
log.debug('Fetch request token: {!r}'.format(token))
url = client.create_authorization_url(authorization_endpoint, **kwargs)
return {'url': url, 'request_token': token}
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
if not authorization_endpoint and not self.request_token_url:
authorization_endpoint = 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
if self.request_token_url:
return await self._create_oauth1_authorization_url(
client, authorization_endpoint, **kwargs)
else:
return self._create_oauth2_authorization_url(
client, authorization_endpoint, **kwargs)
async def fetch_access_token(self, redirect_uri=None, request_token=None, **params):
"""Fetch access token in one step.
:param redirect_uri: Callback or Redirect URI that is used in
previous :meth:`authorize_redirect`.
:param request_token: A previous request token for OAuth 1.
:param params: Extra parameters to fetch access token.
:return: A token dict.
"""
metadata = await self.load_server_metadata()
token_endpoint = self.access_token_url
if not token_endpoint and not self.request_token_url:
token_endpoint = metadata.get('token_endpoint')
async with self._get_oauth_client(**metadata) as client:
if self.request_token_url:
if redirect_uri is not None:
client.redirect_uri = redirect_uri
if request_token is None:
raise MissingRequestTokenError()
# merge request token with verifier
token = {}
token.update(request_token)
token.update(params)
client.token = token
kwargs = self.access_token_params or {}
token = await client.fetch_access_token(token_endpoint, **kwargs)
client.redirect_uri = None
else:
if redirect_uri is not None:
client.redirect_uri = redirect_uri
kwargs = {}
if self.access_token_params:
kwargs.update(self.access_token_params)
kwargs.update(params)
token = await client.fetch_token(token_endpoint, **kwargs)
return token
async def request(self, method, url, token=None, **kwargs):
if self.api_base_url and not url.startswith(('https://', 'http://')):
url = urlparse.urljoin(self.api_base_url, url)
withhold_token = kwargs.get('withhold_token')
if not withhold_token:
metadata = await self.load_server_metadata()
else:
metadata = {}
async with self._get_oauth_client(**metadata) as client:
request = kwargs.pop('request', None)
if withhold_token:
return await client.request(method, url, **kwargs)
if token is None and request:
token = await self._fetch_token(request)
if token is None:
raise MissingTokenError()
client.token = token
return await client.request(method, url, **kwargs)
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)
data = resp.json()
compliance_fix = metadata.get('userinfo_compliance_fix')
if compliance_fix:
data = await compliance_fix(self, data)
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,
)
claims.validate(leeway=120)
return UserInfo(claims)
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')
jwk_set = await self._fetch_server_metadata(uri)
self.server_metadata['jwks'] = jwk_set
return jwk_set
async def _fetch_server_metadata(self, url):
async with self._get_oauth_client() as client:
resp = await client.request('GET', url, withhold_token=True)
return resp.json()
authlib-0.15.5/authlib/integrations/base_client/base_app.py 0000664 0000000 0000000 00000022517 14133261713 0023774 0 ustar 00root root 0000000 0000000 import logging
from authlib.common.security import generate_token
from authlib.consts import default_user_agent
from .errors import (
MismatchingStateError,
)
__all__ = ['BaseApp']
log = logging.getLogger(__name__)
class BaseApp(object):
"""The remote application for OAuth 1 and OAuth 2. It is used together
with OAuth registry.
:param name: The name of the OAuth client, like: github, twitter
:param fetch_token: A function to fetch access token from database
:param update_token: A function to update access token to database
:param client_id: Client key of OAuth 1, or Client ID of OAuth 2
:param client_secret: Client secret of OAuth 2, or Client Secret of OAuth 2
:param request_token_url: Request Token endpoint for OAuth 1
:param request_token_params: Extra parameters for Request Token endpoint
:param access_token_url: Access Token endpoint for OAuth 1 and OAuth 2
:param access_token_params: Extra parameters for Access Token endpoint
:param authorize_url: Endpoint for user authorization of OAuth 1 or OAuth 2
:param authorize_params: Extra parameters for Authorization Endpoint
:param api_base_url: The base API endpoint to make requests simple
:param client_kwargs: Extra keyword arguments for session
:param server_metadata_url: Discover server metadata from this URL
:param user_agent: Define a custom user agent to be used in HTTP request
:param kwargs: Extra server metadata
Create an instance of ``RemoteApp``. If ``request_token_url`` is configured,
it would be an OAuth 1 instance, otherwise it is OAuth 2 instance::
oauth1_client = RemoteApp(
client_id='Twitter Consumer Key',
client_secret='Twitter Consumer Secret',
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/',
)
oauth2_client = RemoteApp(
client_id='GitHub Client ID',
client_secret='GitHub Client Secret',
api_base_url='https://api.github.com/',
access_token_url='https://github.com/login/oauth/access_token',
authorize_url='https://github.com/login/oauth/authorize',
client_kwargs={'scope': 'user:email'},
)
"""
OAUTH_APP_CONFIG = None
def __init__(
self, framework, name=None, fetch_token=None, update_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, 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.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.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, **kwargs):
client_kwargs = {}
client_kwargs.update(self.client_kwargs)
client_kwargs.update(kwargs)
if self.request_token_url:
session = self.framework.oauth1_client_cls(
self.client_id, self.client_secret,
**client_kwargs
)
else:
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.framework.oauth2_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)
# only OAuth2 has compliance_fix currently
if self.compliance_fix:
self.compliance_fix(session)
session.headers['User-Agent'] = self._user_agent
return session
def _retrieve_oauth2_access_token_params(self, request, params):
request_state = params.pop('state', None)
state = self.framework.get_session_data(request, 'state')
if state != request_state:
raise MismatchingStateError()
code_verifier = self.framework.get_session_data(request, 'code_verifier')
if code_verifier:
params['code_verifier'] = code_verifier
return params
def retrieve_access_token_params(self, request, request_token=None):
"""Retrieve parameters for fetching access token, those parameters come
from request and previously saved temporary data in session.
"""
params = self.framework.generate_access_token_params(self.request_token_url, request)
if self.request_token_url:
if request_token is None:
request_token = self.framework.get_session_data(request, 'request_token')
params['request_token'] = request_token
else:
params = self._retrieve_oauth2_access_token_params(request, params)
redirect_uri = self.framework.get_session_data(request, 'redirect_uri')
if redirect_uri:
params['redirect_uri'] = redirect_uri
log.debug('Retrieve temporary data: {!r}'.format(params))
return params
def save_authorize_data(self, request, **kwargs):
"""Save temporary data into session for the authorization step. These
data can be retrieved later when fetching access token.
"""
log.debug('Saving authorize data: {!r}'.format(kwargs))
keys = [
'redirect_uri', 'request_token',
'state', 'code_verifier', 'nonce'
]
for k in keys:
if k in kwargs:
self.framework.set_session_data(request, k, kwargs[k])
@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('Using code_verifier: {!r}'.format(code_verifier))
scope = kwargs.get('scope', client.scope)
if scope and scope.startswith('openid'):
# 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
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)
def _fetch_server_metadata(self, url):
with self._get_oauth_client() as session:
resp = session.request('GET', url, withhold_token=True)
return resp.json()
authlib-0.15.5/authlib/integrations/base_client/base_oauth.py 0000664 0000000 0000000 00000007554 14133261713 0024340 0 ustar 00root root 0000000 0000000 import 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(object):
"""Registry for oauth clients.
Create an instance for registry::
oauth = OAuth()
"""
framework_client_cls = None
framework_integration_cls = FrameworkIntegration
def __init__(self, fetch_token=None, update_token=None):
self._registry = {}
self._clients = {}
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', self.framework_client_cls)
if 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)
client = client_cls(self.framework_integration_cls(name), 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-0.15.5/authlib/integrations/base_client/errors.py 0000664 0000000 0000000 00000001170 14133261713 0023526 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/integrations/base_client/framework_integration.py 0000664 0000000 0000000 00000001416 14133261713 0026615 0 ustar 00root root 0000000 0000000
class FrameworkIntegration(object):
oauth1_client_cls = None
oauth2_client_cls = None
def __init__(self, name):
self.name = name
def set_session_data(self, request, key, value):
sess_key = '_{}_authlib_{}_'.format(self.name, key)
request.session[sess_key] = value
def get_session_data(self, request, key):
sess_key = '_{}_authlib_{}_'.format(self.name, key)
return request.session.pop(sess_key, None)
def update_token(self, token, refresh_token=None, access_token=None):
raise NotImplementedError()
def generate_access_token_params(self, request_token_url, request):
raise NotImplementedError()
@staticmethod
def load_config(oauth, name, params):
raise NotImplementedError()
authlib-0.15.5/authlib/integrations/base_client/remote_app.py 0000664 0000000 0000000 00000017405 14133261713 0024355 0 ustar 00root root 0000000 0000000 import time
import logging
from authlib.common.urls import urlparse
from authlib.jose import JsonWebToken, JsonWebKey
from authlib.oidc.core import UserInfo, CodeIDToken, ImplicitIDToken
from .base_app import BaseApp
from .errors import (
MissingRequestTokenError,
MissingTokenError,
)
__all__ = ['RemoteApp']
log = logging.getLogger(__name__)
class RemoteApp(BaseApp):
def load_server_metadata(self):
if self._server_metadata_url and '_loaded_at' not in self.server_metadata:
metadata = self._fetch_server_metadata(self._server_metadata_url)
metadata['_loaded_at'] = time.time()
self.server_metadata.update(metadata)
return self.server_metadata
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 _create_oauth1_authorization_url(self, client, authorization_endpoint, **kwargs):
params = {}
if self.request_token_params:
params.update(self.request_token_params)
token = client.fetch_request_token(
self.request_token_url, **params
)
log.debug('Fetch request token: {!r}'.format(token))
url = client.create_authorization_url(authorization_endpoint, **kwargs)
return {'url': url, 'request_token': token}
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
if not authorization_endpoint and not self.request_token_url:
authorization_endpoint = 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:
client.redirect_uri = redirect_uri
if self.request_token_url:
return self._create_oauth1_authorization_url(
client, authorization_endpoint, **kwargs)
else:
return self._create_oauth2_authorization_url(
client, authorization_endpoint, **kwargs)
def fetch_access_token(self, redirect_uri=None, request_token=None, **params):
"""Fetch access token in one step.
:param redirect_uri: Callback or Redirect URI that is used in
previous :meth:`authorize_redirect`.
:param request_token: A previous request token for OAuth 1.
:param params: Extra parameters to fetch access token.
:return: A token dict.
"""
metadata = self.load_server_metadata()
token_endpoint = self.access_token_url
if not token_endpoint and not self.request_token_url:
token_endpoint = metadata.get('token_endpoint')
with self._get_oauth_client(**metadata) as client:
if self.request_token_url:
if redirect_uri is not None:
client.redirect_uri = redirect_uri
if request_token is None:
raise MissingRequestTokenError()
# merge request token with verifier
token = {}
token.update(request_token)
token.update(params)
client.token = token
kwargs = self.access_token_params or {}
token = client.fetch_access_token(token_endpoint, **kwargs)
client.redirect_uri = None
else:
if redirect_uri is not None:
client.redirect_uri = redirect_uri
kwargs = {}
if self.access_token_params:
kwargs.update(self.access_token_params)
kwargs.update(params)
token = client.fetch_token(token_endpoint, **kwargs)
return token
def request(self, method, url, token=None, **kwargs):
if self.api_base_url and not url.startswith(('https://', 'http://')):
url = urlparse.urljoin(self.api_base_url, url)
withhold_token = kwargs.get('withhold_token')
if not withhold_token:
metadata = self.load_server_metadata()
else:
metadata = {}
with self._get_oauth_client(**metadata) as session:
request = kwargs.pop('request', None)
if withhold_token:
return session.request(method, url, **kwargs)
if token is None and self._fetch_token and request:
token = self._fetch_token(request)
if token is None:
raise MissingTokenError()
session.token = token
return session.request(method, url, **kwargs)
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')
jwk_set = self._fetch_server_metadata(uri)
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)
data = resp.json()
compliance_fix = metadata.get('userinfo_compliance_fix')
if compliance_fix:
data = compliance_fix(self, data)
return UserInfo(data)
def _parse_id_token(self, request, token, claims_options=None, leeway=120):
"""Return an instance of UserInfo from token's ``id_token``."""
if 'id_token' not in token:
return None
def load_key(header, payload):
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'))
nonce = self.framework.get_session_data(request, 'nonce')
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 not alg_values:
alg_values = ['RS256']
jwt = JsonWebToken(alg_values)
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)
authlib-0.15.5/authlib/integrations/django_client/ 0000775 0000000 0000000 00000000000 14133261713 0022171 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/django_client/__init__.py 0000664 0000000 0000000 00000000550 14133261713 0024302 0 ustar 00root root 0000000 0000000 # flake8: noqa
from .integration import DjangoIntegration, DjangoRemoteApp, token_update
from ..base_client import BaseOAuth, OAuthError
class OAuth(BaseOAuth):
framework_integration_cls = DjangoIntegration
framework_client_cls = DjangoRemoteApp
__all__ = [
'OAuth', 'DjangoRemoteApp', 'DjangoIntegration',
'token_update', 'OAuthError',
]
authlib-0.15.5/authlib/integrations/django_client/integration.py 0000664 0000000 0000000 00000005143 14133261713 0025071 0 ustar 00root root 0000000 0000000 from django.conf import settings
from django.dispatch import Signal
from django.http import HttpResponseRedirect
from ..base_client import FrameworkIntegration, RemoteApp, OAuthError
from ..requests_client import OAuth1Session, OAuth2Session
token_update = Signal()
class DjangoIntegration(FrameworkIntegration):
oauth1_client_cls = OAuth1Session
oauth2_client_cls = OAuth2Session
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,
)
def generate_access_token_params(self, request_token_url, request):
if request_token_url:
return request.GET.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'),
}
return params
@staticmethod
def load_config(oauth, name, params):
config = getattr(settings, 'AUTHLIB_OAUTH_CLIENTS', None)
if config:
return config.get(name)
class DjangoRemoteApp(RemoteApp):
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'])
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 = self.retrieve_access_token_params(request)
params.update(kwargs)
return self.fetch_access_token(**params)
def parse_id_token(self, request, token, claims_options=None, leeway=120):
return self._parse_id_token(request, token, claims_options, leeway)
authlib-0.15.5/authlib/integrations/django_helpers.py 0000664 0000000 0000000 00000003515 14133261713 0022733 0 ustar 00root root 0000000 0000000 try:
from collections.abc import MutableMapping as DictMixin
except ImportError:
from collections import MutableMapping as DictMixin
from authlib.common.encoding import to_unicode, json_loads
def create_oauth_request(request, request_cls, use_json=False):
if isinstance(request, request_cls):
return request
if request.method == 'POST':
if use_json:
body = json_loads(request.body)
else:
body = request.POST.dict()
else:
body = None
headers = parse_request_headers(request)
url = request.get_raw_uri()
return request_cls(request.method, url, body, headers)
def parse_request_headers(request):
return WSGIHeaderDict(request.META)
class WSGIHeaderDict(DictMixin):
CGI_KEYS = ('CONTENT_TYPE', 'CONTENT_LENGTH')
def __init__(self, environ):
self.environ = environ
def keys(self):
return [x for x in self]
def _ekey(self, key):
key = key.replace('-', '_').upper()
if key in self.CGI_KEYS:
return key
return 'HTTP_' + key
def __getitem__(self, key):
return _unicode_value(self.environ[self._ekey(key)])
def __delitem__(self, key): # pragma: no cover
raise ValueError('Can not delete item')
def __setitem__(self, key, value): # pragma: no cover
raise ValueError('Can not set item')
def __iter__(self):
for key in self.environ:
if key[:5] == 'HTTP_':
yield _unify_key(key[5:])
elif key in self.CGI_KEYS:
yield _unify_key(key)
def __len__(self):
return len(self.keys())
def __contains__(self, key):
return self._ekey(key) in self.environ
def _unicode_value(value):
return to_unicode(value, 'latin-1')
def _unify_key(key):
return key.replace('_', '-').title()
authlib-0.15.5/authlib/integrations/django_oauth1/ 0000775 0000000 0000000 00000000000 14133261713 0022114 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/django_oauth1/__init__.py 0000664 0000000 0000000 00000000335 14133261713 0024226 0 ustar 00root root 0000000 0000000 # flake8: noqa
from .authorization_server import (
BaseServer, CacheAuthorizationServer
)
from .resource_protector import ResourceProtector
__all__ = ['BaseServer', 'CacheAuthorizationServer', 'ResourceProtector']
authlib-0.15.5/authlib/integrations/django_oauth1/authorization_server.py 0000664 0000000 0000000 00000010536 14133261713 0026761 0 ustar 00root root 0000000 0000000 import 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
from ..django_helpers import create_oauth_request
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):
return create_oauth_request(request, OAuth1Request)
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(CacheAuthorizationServer, self).__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-0.15.5/authlib/integrations/django_oauth1/nonce.py 0000664 0000000 0000000 00000000644 14133261713 0023574 0 ustar 00root root 0000000 0000000 from 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 = '{}{}-{}-{}'.format(key_prefix, nonce, timestamp, client_id)
if token:
key = '{}-{}'.format(key, token)
rv = bool(cache.get(key))
cache.set(key, 1, timeout=timeout)
return rv
authlib-0.15.5/authlib/integrations/django_oauth1/resource_protector.py 0000664 0000000 0000000 00000004574 14133261713 0026430 0 ustar 00root root 0000000 0000000 import 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
from ..django_helpers import parse_request_headers
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
headers = parse_request_headers(request)
url = request.get_raw_uri()
req = self.validate_request(request.method, url, body, 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-0.15.5/authlib/integrations/django_oauth2/ 0000775 0000000 0000000 00000000000 14133261713 0022115 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/django_oauth2/__init__.py 0000664 0000000 0000000 00000000426 14133261713 0024230 0 ustar 00root root 0000000 0000000 # 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-0.15.5/authlib/integrations/django_oauth2/authorization_server.py 0000664 0000000 0000000 00000012521 14133261713 0026756 0 ustar 00root root 0000000 0000000 import json
from django.http import HttpResponse
from django.utils.module_loading import import_string
from django.conf import settings
from authlib.oauth2 import (
OAuth2Request,
HttpRequest,
AuthorizationServer as _AuthorizationServer,
)
from authlib.oauth2.rfc6750 import BearerToken
from authlib.oauth2.rfc8414 import AuthorizationServerMetadata
from authlib.common.security import generate_token as _generate_token
from authlib.common.encoding import json_dumps
from .signals import client_authenticated, token_revoked
from ..django_helpers import create_oauth_request
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)
"""
metadata_class = AuthorizationServerMetadata
def __init__(self, client_model, token_model, generate_token=None, metadata=None):
self.config = getattr(settings, 'AUTHLIB_OAUTH2_PROVIDER', {})
self.client_model = client_model
self.token_model = token_model
if generate_token is None:
generate_token = self.create_bearer_token_generator()
if metadata is None:
metadata_file = self.config.get('metadata_file')
if metadata_file:
with open(metadata_file) as f:
metadata = json.load(f)
if metadata:
metadata = self.metadata_class(metadata)
metadata.validate()
super(AuthorizationServer, self).__init__(
query_client=self.get_client_by_id,
save_token=self.save_oauth2_token,
generate_token=generate_token,
metadata=metadata,
)
def get_client_by_id(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_oauth2_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 create_oauth_request(request, OAuth2Request)
def create_json_request(self, request):
req = create_oauth_request(request, HttpRequest, True)
req.user = request.user
return req
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 BearerToken(
access_token_generator=access_token_generator,
refresh_token_generator=refresh_token_generator,
expires_generator=expires_generator,
)
def get_consent_grant(self, request):
grant = self.get_authorization_grant(request)
grant.validate_consent_request()
if not hasattr(grant, 'prompt'):
grant.prompt = None
return grant
def validate_consent_request(self, request, end_user=None):
req = self.create_oauth2_request(request)
req.user = end_user
return self.get_consent_grant(req)
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(BearerToken.GRANT_TYPES_EXPIRES_IN)
if expires_in_conf:
data.update(expires_in_conf)
def expires_in(client, grant_type):
return data.get(grant_type, BearerToken.DEFAULT_EXPIRES_IN)
return expires_in
authlib-0.15.5/authlib/integrations/django_oauth2/endpoints.py 0000664 0000000 0000000 00000003654 14133261713 0024502 0 ustar 00root root 0000000 0000000 from 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, client):
"""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)
client_id = client.get_client_id()
if rv and rv.client_id == client_id:
return rv
return None
def revoke_token(self, token):
"""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-0.15.5/authlib/integrations/django_oauth2/resource_protector.py 0000664 0000000 0000000 00000005233 14133261713 0026422 0 ustar 00root root 0000000 0000000 import functools
from django.http import JsonResponse
from authlib.oauth2 import (
OAuth2Error,
ResourceProtector as _ResourceProtector,
)
from authlib.oauth2.rfc6749 import (
MissingAuthorizationError,
HttpRequest,
)
from authlib.oauth2.rfc6750 import (
BearerTokenValidator as _BearerTokenValidator
)
from .signals import token_authenticated
from ..django_helpers import parse_request_headers
class ResourceProtector(_ResourceProtector):
def acquire_token(self, request, scope=None, operator='AND'):
"""A method to acquire current valid token with the given scope.
:param request: Django HTTP request instance
:param scope: string or list of scope values
:param operator: value of "AND" or "OR"
:return: token object
"""
headers = parse_request_headers(request)
url = request.get_raw_uri()
req = HttpRequest(request.method, url, request.body, headers)
if not callable(operator):
operator = operator.upper()
token = self.validate_request(scope, req, operator)
token_authenticated.send(sender=self.__class__, token=token)
return token
def __call__(self, scope=None, operator='AND', optional=False):
def wrapper(f):
@functools.wraps(f)
def decorated(request, *args, **kwargs):
try:
token = self.acquire_token(request, scope, operator)
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):
self.token_model = token_model
super(BearerTokenValidator, self).__init__(realm)
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 request_invalid(self, request):
return False
def token_revoked(self, token):
return token.revoked
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-0.15.5/authlib/integrations/django_oauth2/signals.py 0000664 0000000 0000000 00000000354 14133261713 0024131 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/integrations/flask_client/ 0000775 0000000 0000000 00000000000 14133261713 0022027 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/flask_client/__init__.py 0000664 0000000 0000000 00000000432 14133261713 0024137 0 ustar 00root root 0000000 0000000 # flake8: noqa
from .oauth_registry import OAuth
from .remote_app import FlaskRemoteApp
from .integration import token_update, FlaskIntegration
from ..base_client import OAuthError
__all__ = [
'OAuth', 'FlaskRemoteApp', 'FlaskIntegration',
'token_update', 'OAuthError',
]
authlib-0.15.5/authlib/integrations/flask_client/integration.py 0000664 0000000 0000000 00000003667 14133261713 0024740 0 ustar 00root root 0000000 0000000 from flask import current_app, session
from flask.signals import Namespace
from ..base_client import FrameworkIntegration, OAuthError
from ..requests_client import OAuth1Session, OAuth2Session
_signal = Namespace()
#: signal when token is updated
token_update = _signal.signal('token_update')
class FlaskIntegration(FrameworkIntegration):
oauth1_client_cls = OAuth1Session
oauth2_client_cls = OAuth2Session
def set_session_data(self, request, key, value):
sess_key = '_{}_authlib_{}_'.format(self.name, key)
session[sess_key] = value
def get_session_data(self, request, key):
sess_key = '_{}_authlib_{}_'.format(self.name, key)
return session.pop(sess_key, None)
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,
)
def generate_access_token_params(self, request_token_url, request):
if request_token_url:
return request.args.to_dict(flat=True)
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['code'],
'state': request.args.get('state'),
}
else:
params = {
'code': request.form['code'],
'state': request.form.get('state'),
}
return params
@staticmethod
def load_config(oauth, name, params):
rv = {}
for k in params:
conf_key = '{}_{}'.format(name, k).upper()
v = oauth.app.config.get(conf_key, None)
if v is not None:
rv[k] = v
return rv
authlib-0.15.5/authlib/integrations/flask_client/oauth_registry.py 0000664 0000000 0000000 00000007237 14133261713 0025462 0 ustar 00root root 0000000 0000000 import uuid
from flask import session
from werkzeug.local import LocalProxy
from .integration import FlaskIntegration
from .remote_app import FlaskRemoteApp
from ..base_client import BaseOAuth
__all__ = ['OAuth']
_req_token_tpl = '_{}_authlib_req_token_'
class OAuth(BaseOAuth):
"""A Flask OAuth registry for oauth clients.
Create an instance with Flask::
oauth = OAuth(app, cache=cache)
You can also pass the instance of Flask later::
oauth = OAuth()
oauth.init_app(app, cache=cache)
:param app: Flask application instance
:param cache: A cache instance that has .get .set and .delete methods
:param fetch_token: a shared function to get current user's token
:param update_token: a share function to update current user's token
"""
framework_client_cls = FlaskRemoteApp
framework_integration_cls = FlaskIntegration
def __init__(self, app=None, cache=None, fetch_token=None, update_token=None):
super(OAuth, self).__init__(fetch_token, update_token)
self.app = app
self.cache = cache
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(OAuth, self).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))
def generate_client_kwargs(self, name, overwrite, **kwargs):
kwargs = super(OAuth, self).generate_client_kwargs(name, overwrite, **kwargs)
if kwargs.get('request_token_url'):
if self.cache:
_add_cache_request_token(self.cache, name, kwargs)
else:
_add_session_request_token(name, kwargs)
return kwargs
def _add_cache_request_token(cache, name, kwargs):
if not kwargs.get('fetch_request_token'):
def fetch_request_token():
key = _req_token_tpl.format(name)
sid = session.pop(key, None)
if not sid:
return None
token = cache.get(sid)
cache.delete(sid)
return token
kwargs['fetch_request_token'] = fetch_request_token
if not kwargs.get('save_request_token'):
def save_request_token(token):
key = _req_token_tpl.format(name)
sid = uuid.uuid4().hex
session[key] = sid
cache.set(sid, token, 600)
kwargs['save_request_token'] = save_request_token
return kwargs
def _add_session_request_token(name, kwargs):
if not kwargs.get('fetch_request_token'):
def fetch_request_token():
key = _req_token_tpl.format(name)
return session.pop(key, None)
kwargs['fetch_request_token'] = fetch_request_token
if not kwargs.get('save_request_token'):
def save_request_token(token):
key = _req_token_tpl.format(name)
session[key] = token
kwargs['save_request_token'] = save_request_token
return kwargs
authlib-0.15.5/authlib/integrations/flask_client/remote_app.py 0000664 0000000 0000000 00000005605 14133261713 0024542 0 ustar 00root root 0000000 0000000 from flask import redirect
from flask import request as flask_req
from flask import _app_ctx_stack
from ..base_client import RemoteApp
class FlaskRemoteApp(RemoteApp):
"""Flask integrated RemoteApp of :class:`~authlib.client.OAuthClient`.
It has built-in hooks for OAuthClient. The only required configuration
is token model.
"""
def __init__(self, framework, name=None, fetch_token=None, **kwargs):
fetch_request_token = kwargs.pop('fetch_request_token', None)
save_request_token = kwargs.pop('save_request_token', None)
super(FlaskRemoteApp, self).__init__(framework, name, fetch_token, **kwargs)
self._fetch_request_token = fetch_request_token
self._save_request_token = save_request_token
def _on_update_token(self, token, refresh_token=None, access_token=None):
self.token = token
super(FlaskRemoteApp, self)._on_update_token(
token, refresh_token, access_token
)
@property
def token(self):
ctx = _app_ctx_stack.top
attr = 'authlib_oauth_token_{}'.format(self.name)
token = getattr(ctx, attr, None)
if token:
return token
if self._fetch_token:
token = self._fetch_token()
self.token = token
return token
@token.setter
def token(self, token):
ctx = _app_ctx_stack.top
attr = 'authlib_oauth_token_{}'.format(self.name)
setattr(ctx, attr, token)
def request(self, method, url, token=None, **kwargs):
if token is None and not kwargs.get('withhold_token'):
token = self.token
return super(FlaskRemoteApp, self).request(
method, url, token=token, **kwargs)
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)
if self.request_token_url:
request_token = rv.pop('request_token', None)
self._save_request_token(request_token)
self.save_authorize_data(flask_req, redirect_uri=redirect_uri, **rv)
return redirect(rv['url'])
def authorize_access_token(self, **kwargs):
"""Authorize access token."""
if self.request_token_url:
request_token = self._fetch_request_token()
else:
request_token = None
params = self.retrieve_access_token_params(flask_req, request_token)
params.update(kwargs)
token = self.fetch_access_token(**params)
self.token = token
return token
def parse_id_token(self, token, claims_options=None, leeway=120):
return self._parse_id_token(flask_req, token, claims_options, leeway)
authlib-0.15.5/authlib/integrations/flask_helpers.py 0000664 0000000 0000000 00000001355 14133261713 0022571 0 ustar 00root root 0000000 0000000 from flask import request as flask_req
from authlib.common.encoding import to_unicode
def create_oauth_request(request, request_cls, use_json=False):
if isinstance(request, request_cls):
return request
if not request:
request = flask_req
if request.method == 'POST':
if use_json:
body = request.get_json()
else:
body = request.form.to_dict(flat=True)
else:
body = None
# query string in werkzeug Request.url is very weird
# scope=profile%20email will be scope=profile email
url = request.base_url
if request.query_string:
url = url + '?' + to_unicode(request.query_string)
return request_cls(request.method, url, body, request.headers)
authlib-0.15.5/authlib/integrations/flask_oauth1/ 0000775 0000000 0000000 00000000000 14133261713 0021752 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/flask_oauth1/__init__.py 0000664 0000000 0000000 00000000404 14133261713 0024061 0 ustar 00root root 0000000 0000000 # 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-0.15.5/authlib/integrations/flask_oauth1/authorization_server.py 0000664 0000000 0000000 00000014260 14133261713 0026615 0 ustar 00root root 0000000 0000000 import logging
from werkzeug.utils import import_string
from flask import Response
from authlib.oauth1 import (
OAuth1Request,
AuthorizationServer as _AuthorizationServer,
)
from authlib.common.security import generate_token
from authlib.common.urls import url_encode
from ..flask_helpers import create_oauth_request
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 create_temporary_credentials_response(self, request=None):
return super(AuthorizationServer, self)\
.create_temporary_credentials_response(request)
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(AuthorizationServer, self)\
.create_authorization_response(request, grant_user)
def create_token_response(self, request=None):
return super(AuthorizationServer, self).create_token_response(request)
def create_oauth1_request(self, request):
return create_oauth_request(request, OAuth1Request)
def handle_response(self, status_code, payload, headers):
return Response(
url_encode(payload),
status=status_code,
headers=headers
)
authlib-0.15.5/authlib/integrations/flask_oauth1/cache.py 0000664 0000000 0000000 00000005714 14133261713 0023376 0 ustar 00root root 0000000 0000000 from 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 = '{}{}-{}-{}'.format(key_prefix, nonce, timestamp, client_id)
if oauth_token:
key = '{}-{}'.format(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-0.15.5/authlib/integrations/flask_oauth1/resource_protector.py 0000664 0000000 0000000 00000007677 14133261713 0026275 0 ustar 00root root 0000000 0000000 import functools
from flask import json, Response
from flask import request as _req
from flask import _app_ctx_stack
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 query_token method::
from authlib.integrations.flask_oauth1 import ResourceProtector, current_credential
from authlib.integrations.flask_oauth1 import create_exists_nonce_func
from authlib.integrations.sqla_oauth1 import (
create_query_client_func,
create_query_token_func,
)
from your_project.models import Token, User, cache
# you need to define a ``cache`` instance yourself
require_oauth= ResourceProtector(
app,
query_client=create_query_client_func(db.session, OAuth1Client),
query_token=create_query_token_func(db.session, OAuth1Token),
exists_nonce=create_exists_nonce_func(cache)
)
# or initialize it lazily
require_oauth = ResourceProtector()
require_oauth.init_app(
app,
query_client=create_query_client_func(db.session, OAuth1Client),
query_token=create_query_token_func(db.session, OAuth1Token),
exists_nonce=create_exists_nonce_func(cache)
)
"""
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
)
ctx = _app_ctx_stack.top
ctx.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():
ctx = _app_ctx_stack.top
return getattr(ctx, 'authlib_server_oauth1_credential', None)
current_credential = LocalProxy(_get_current_credential)
authlib-0.15.5/authlib/integrations/flask_oauth2/ 0000775 0000000 0000000 00000000000 14133261713 0021753 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/flask_oauth2/__init__.py 0000664 0000000 0000000 00000000363 14133261713 0024066 0 ustar 00root root 0000000 0000000 # flake8: noqa
from .authorization_server import AuthorizationServer
from .resource_protector import (
ResourceProtector,
current_token,
)
from .signals import (
client_authenticated,
token_authenticated,
token_revoked,
)
authlib-0.15.5/authlib/integrations/flask_oauth2/authorization_server.py 0000664 0000000 0000000 00000017513 14133261713 0026622 0 ustar 00root root 0000000 0000000 from werkzeug.utils import import_string
from flask import Response, json
from authlib.deprecate import deprecate
from authlib.oauth2 import (
OAuth2Request,
HttpRequest,
AuthorizationServer as _AuthorizationServer,
)
from authlib.oauth2.rfc6750 import BearerToken
from authlib.oauth2.rfc8414 import AuthorizationServerMetadata
from authlib.common.security import generate_token
from authlib.common.encoding import to_unicode
from .signals import client_authenticated, token_revoked
from ..flask_helpers import create_oauth_request
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.get_user_id()
else:
user_id = None
client = request.client
tok = Token(
client_id=client.client_id,
user_id=user.get_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)
"""
metadata_class = AuthorizationServerMetadata
def __init__(self, app=None, query_client=None, save_token=None):
super(AuthorizationServer, self).__init__(
query_client=query_client,
save_token=save_token,
)
self.config = {}
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.generate_token = self.create_bearer_token_generator(app.config)
metadata_file = app.config.get('OAUTH2_METADATA_FILE')
if metadata_file:
with open(metadata_file) as f:
metadata = self.metadata_class(json.load(f))
metadata.validate()
self.metadata = metadata
self.config.setdefault('error_uris', app.config.get('OAUTH2_ERROR_URIS'))
if app.config.get('OAUTH2_JWT_ENABLED'):
deprecate('Define "get_jwt_config" in OpenID Connect grants', '1.0')
self.init_jwt_config(app.config)
def init_jwt_config(self, config):
"""Initialize JWT related configuration."""
jwt_iss = config.get('OAUTH2_JWT_ISS')
if not jwt_iss:
raise RuntimeError('Missing "OAUTH2_JWT_ISS" configuration.')
jwt_key_path = config.get('OAUTH2_JWT_KEY_PATH')
if jwt_key_path:
with open(jwt_key_path, 'r') as f:
if jwt_key_path.endswith('.json'):
jwt_key = json.load(f)
else:
jwt_key = to_unicode(f.read())
else:
jwt_key = config.get('OAUTH2_JWT_KEY')
if not jwt_key:
raise RuntimeError('Missing "OAUTH2_JWT_KEY" configuration.')
jwt_alg = config.get('OAUTH2_JWT_ALG')
if not jwt_alg:
raise RuntimeError('Missing "OAUTH2_JWT_ALG" configuration.')
jwt_exp = config.get('OAUTH2_JWT_EXP', 3600)
self.config.setdefault('jwt_iss', jwt_iss)
self.config.setdefault('jwt_key', jwt_key)
self.config.setdefault('jwt_alg', jwt_alg)
self.config.setdefault('jwt_exp', jwt_exp)
def get_error_uris(self, request):
error_uris = self.config.get('error_uris')
if error_uris:
return dict(error_uris)
def create_oauth2_request(self, request):
return create_oauth_request(request, OAuth2Request)
def create_json_request(self, request):
return create_oauth_request(request, HttpRequest, True)
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_token_expires_in_generator(self, config):
"""Create a generator function for generating ``expires_in`` value.
Developers can re-implement this method with a subclass if other means
required. The default expires_in value is defined by ``grant_type``,
different ``grant_type`` has different value. It can be configured
with::
OAUTH2_TOKEN_EXPIRES_IN = {
'authorization_code': 864000,
'urn:ietf:params:oauth:grant-type:jwt-bearer': 3600,
}
"""
expires_conf = config.get('OAUTH2_TOKEN_EXPIRES_IN')
return create_token_expires_in_generator(expires_conf)
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`. By default, it will not
generate ``refresh_token``, which can be turn on by configuration
``OAUTH2_REFRESH_TOKEN_GENERATOR=True``.
"""
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_generator = self.create_token_expires_in_generator(config)
return BearerToken(
access_token_generator,
refresh_token_generator,
expires_generator
)
def validate_consent_request(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::
@app.route('/authorize', methods=['GET'])
def authorize():
try:
grant = server.validate_consent_request(end_user=current_user)
return render_template(
'authorize.html',
grant=grant,
user=current_user
)
except OAuth2Error as error:
return render_template(
'error.html',
error=error
)
"""
req = self.create_oauth2_request(request)
req.user = end_user
grant = self.get_authorization_grant(req)
grant.validate_consent_request()
if not hasattr(grant, 'prompt'):
grant.prompt = None
return grant
def create_token_expires_in_generator(expires_in_conf=None):
data = {}
data.update(BearerToken.GRANT_TYPES_EXPIRES_IN)
if expires_in_conf:
data.update(expires_in_conf)
def expires_in(client, grant_type):
return data.get(grant_type, BearerToken.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-0.15.5/authlib/integrations/flask_oauth2/errors.py 0000664 0000000 0000000 00000002106 14133261713 0023640 0 ustar 00root root 0000000 0000000 import werkzeug
from werkzeug.exceptions import HTTPException
_version = werkzeug.__version__.split('.')[0]
if _version in ('0', '1'):
class _HTTPException(HTTPException):
def __init__(self, code, body, headers, response=None):
super(_HTTPException, self).__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(_HTTPException, self).__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-0.15.5/authlib/integrations/flask_oauth2/resource_protector.py 0000664 0000000 0000000 00000007757 14133261713 0026275 0 ustar 00root root 0000000 0000000 import functools
from contextlib import contextmanager
from flask import json
from flask import request as _req
from flask import _app_ctx_stack
from werkzeug.local import LocalProxy
from authlib.oauth2 import (
OAuth2Error,
ResourceProtector as _ResourceProtector
)
from authlib.oauth2.rfc6749 import (
MissingAuthorizationError,
HttpRequest,
)
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()
def request_invalid(self, request):
return False
def token_revoked(self, token):
return False
require_oauth.register_token_validator(MyBearerTokenValidator())
# protect resource with require_oauth
@app.route('/user')
@require_oauth('profile')
def user_profile():
user = User.query.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, scope=None, operator='AND'):
"""A method to acquire current valid token with the given scope.
:param scope: string or list of scope values
:param operator: value of "AND" or "OR"
:return: token object
"""
request = HttpRequest(
_req.method,
_req.full_path,
_req.data,
_req.headers
)
if not callable(operator):
operator = operator.upper()
token = self.validate_request(scope, request, operator)
token_authenticated.send(self, token=token)
ctx = _app_ctx_stack.top
ctx.authlib_server_oauth2_token = token
return token
@contextmanager
def acquire(self, scope=None, operator='AND'):
"""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.query.get(token.user_id)
return jsonify(user.to_dict())
"""
try:
yield self.acquire_token(scope, operator)
except OAuth2Error as error:
self.raise_error_response(error)
def __call__(self, scope=None, operator='AND', optional=False):
def wrapper(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
try:
self.acquire_token(scope, operator)
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():
ctx = _app_ctx_stack.top
return getattr(ctx, 'authlib_server_oauth2_token', None)
current_token = LocalProxy(_get_current_token)
authlib-0.15.5/authlib/integrations/flask_oauth2/signals.py 0000664 0000000 0000000 00000000525 14133261713 0023767 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/integrations/httpx_client/ 0000775 0000000 0000000 00000000000 14133261713 0022076 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/httpx_client/__init__.py 0000664 0000000 0000000 00000001444 14133261713 0024212 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/integrations/httpx_client/assertion_client.py 0000664 0000000 0000000 00000006502 14133261713 0026020 0 ustar 00root root 0000000 0000000 from httpx import AsyncClient, Client, USE_CLIENT_DEFAULT
try:
from httpx._config import UNSET
except ImportError:
UNSET = None
from authlib.oauth2.rfc7521 import AssertionClient as _AssertionClient
from authlib.oauth2.rfc7523 import JWTBearerGrant
from authlib.oauth2 import OAuth2Error
from .utils import extract_client_kwargs
from .oauth2_client import OAuth2Auth
__all__ = ['AsyncAssertionClient', 'AssertionClient']
class AsyncAssertionClient(_AssertionClient, AsyncClient):
token_auth_class = OAuth2Auth
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)
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=None, **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():
await self.refresh_token()
auth = self.token_auth
return await super(AsyncAssertionClient, self).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)
token = resp.json()
if 'error' in token:
raise OAuth2Error(
error=token['error'],
description=token.get('error_description')
)
self.token = token
return self.token
class AssertionClient(_AssertionClient, Client):
token_auth_class = OAuth2Auth
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)
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=None, **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(AssertionClient, self).request(
method, url, auth=auth, **kwargs)
authlib-0.15.5/authlib/integrations/httpx_client/oauth1_client.py 0000664 0000000 0000000 00000007715 14133261713 0025221 0 ustar 00root root 0000000 0000000 import typing
from httpx import AsyncClient, Auth, Client, 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 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 Request(method=request.method, url=url, headers=headers, data=body)
class AsyncOAuth1Client(_OAuth1Client, 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)
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, 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)
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-0.15.5/authlib/integrations/httpx_client/oauth2_client.py 0000664 0000000 0000000 00000020313 14133261713 0025207 0 ustar 00root root 0000000 0000000 import asyncio
import typing
from httpx import AsyncClient, Auth, Client, Request, Response, USE_CLIENT_DEFAULT
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
from ..base_client import (
OAuthError,
InvalidTokenError,
MissingTokenError,
UnsupportedTokenTypeError,
)
__all__ = [
'OAuth2Auth', 'OAuth2ClientAuth',
'AsyncOAuth2Client',
]
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 Request(method=request.method, url=url, headers=headers, data=body)
except KeyError as error:
description = 'Unsupported token_type: {}'.format(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 Request(method=request.method, url=url, headers=headers, data=body)
class AsyncOAuth2Client(_OAuth2Client, AsyncClient):
SESSION_REQUEST_PARAMS = HTTPX_CLIENT_KWARGS
client_auth_class = OAuth2ClientAuth
token_auth_class = OAuth2Auth
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)
AsyncClient.__init__(self, **client_kwargs)
# We use a "reverse" Event to synchronize coroutines to prevent
# multiple concurrent attempts to refresh the same token
self._token_refresh_event = asyncio.Event()
self._token_refresh_event.set()
_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, **kwargs
)
@staticmethod
def handle_error(error_type, error_description):
raise OAuthError(error_type, error_description)
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()
if self.token.is_expired():
await self.ensure_active_token()
auth = self.token_auth
return await super(AsyncOAuth2Client, self).request(
method, url, auth=auth, **kwargs)
async def ensure_active_token(self):
if self._token_refresh_event.is_set():
# Unset the event so other coroutines don't try to update the token
self._token_refresh_event.clear()
refresh_token = self.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 = self.token['access_token']
token = await self.fetch_token(url, grant_type='client_credentials')
if self.update_token:
await self.update_token(token, access_token=access_token)
else:
raise InvalidTokenError()
# Notify coroutines that token is refreshed
self._token_refresh_event.set()
return
await self._token_refresh_event.wait() # wait until the token is ready
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.json())
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.json())
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, Client):
SESSION_REQUEST_PARAMS = HTTPX_CLIENT_KWARGS
client_auth_class = OAuth2ClientAuth
token_auth_class = OAuth2Auth
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)
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 self.token.is_expired():
self.ensure_active_token()
auth = self.token_auth
return super(OAuth2Client, self).request(
method, url, auth=auth, **kwargs)
def ensure_active_token(self):
refresh_token = self.token.get('refresh_token')
url = self.metadata.get('token_endpoint')
if refresh_token and url:
self.refresh_token(url, refresh_token=refresh_token)
elif self.metadata.get('grant_type') == 'client_credentials':
access_token = self.token['access_token']
token = self.fetch_token(url, grant_type='client_credentials')
if self.update_token:
self.update_token(token, access_token=access_token)
else:
raise InvalidTokenError()
authlib-0.15.5/authlib/integrations/httpx_client/utils.py 0000664 0000000 0000000 00000000624 14133261713 0023612 0 ustar 00root root 0000000 0000000 HTTPX_CLIENT_KWARGS = [
'headers', 'cookies', 'verify', 'cert', 'http_versions',
'proxies', 'timeout', 'pool_limits', 'max_redirects',
'base_url', 'dispatch', 'app', 'backend', 'trust_env',
'json',
]
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
authlib-0.15.5/authlib/integrations/requests_client/ 0000775 0000000 0000000 00000000000 14133261713 0022602 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/requests_client/__init__.py 0000664 0000000 0000000 00000001214 14133261713 0024711 0 ustar 00root root 0000000 0000000 from .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-0.15.5/authlib/integrations/requests_client/assertion_session.py 0000664 0000000 0000000 00000003443 14133261713 0026732 0 ustar 00root root 0000000 0000000 from requests import Session
from authlib.deprecate import deprecate
from authlib.oauth2.rfc7521 import AssertionClient
from authlib.oauth2.rfc7523 import JWTBearerGrant
from .oauth2_session import OAuth2Auth
class AssertionAuth(OAuth2Auth):
def ensure_active_token(self):
if not self.token or self.token.is_expired() and self.client:
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, **kwargs):
Session.__init__(self)
token_url = kwargs.pop('token_url', None)
if token_url:
deprecate('Use "token_endpoint" instead of "token_url"', '1.0')
token_endpoint = token_url
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=None, **kwargs):
"""Send request with auto refresh token feature."""
if not withhold_token and auth is None:
auth = self.token_auth
return super(AssertionSession, self).request(
method, url, auth=auth, **kwargs)
authlib-0.15.5/authlib/integrations/requests_client/oauth1_session.py 0000664 0000000 0000000 00000004136 14133261713 0026124 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from 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
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)
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-0.15.5/authlib/integrations/requests_client/oauth2_session.py 0000664 0000000 0000000 00000011256 14133261713 0026126 0 ustar 00root root 0000000 0000000 from 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,
)
__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 self.token.is_expired():
refresh_token = self.token.get('refresh_token')
client = self.client
url = client.metadata.get('token_endpoint')
if refresh_token and url:
client.refresh_token(url, refresh_token=refresh_token)
elif client.metadata.get('grant_type') == 'client_credentials':
access_token = self.token['access_token']
token = client.fetch_token(url, grant_type='client_credentials')
if client.update_token:
client.update_token(token, access_token=access_token)
else:
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 = 'Unsupported token_type: {}'.format(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 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.
"""
client_auth_class = OAuth2ClientAuth
token_auth_class = OAuth2Auth
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, redirect_uri=None,
token=None, token_placement='header',
update_token=None, **kwargs):
Session.__init__(self)
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
)
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 not withhold_token and auth is None:
if not self.token:
raise MissingTokenError()
auth = self.token_auth
return super(OAuth2Session, self).request(
method, url, auth=auth, **kwargs)
@staticmethod
def handle_error(error_type, error_description):
raise OAuthError(error_type, error_description)
authlib-0.15.5/authlib/integrations/sqla_oauth1/ 0000775 0000000 0000000 00000000000 14133261713 0021612 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/sqla_oauth1/__init__.py 0000664 0000000 0000000 00000000640 14133261713 0023723 0 ustar 00root root 0000000 0000000 # flake8: noqa
from .mixins import (
OAuth1ClientMixin,
OAuth1TemporaryCredentialMixin,
OAuth1TimestampNonceMixin,
OAuth1TokenCredentialMixin,
)
from .functions import (
create_query_client_func,
create_query_token_func,
register_temporary_credential_hooks,
create_exists_nonce_func,
register_nonce_hooks,
register_token_credential_hooks,
register_authorization_hooks,
)
authlib-0.15.5/authlib/integrations/sqla_oauth1/functions.py 0000664 0000000 0000000 00000012206 14133261713 0024175 0 ustar 00root root 0000000 0000000 def create_query_client_func(session, model_class):
"""Create an ``query_client`` function that can be used in authorization
server and resource protector.
:param session: SQLAlchemy session
:param model_class: Client class
"""
def query_client(client_id):
q = session.query(model_class)
return q.filter_by(client_id=client_id).first()
return query_client
def create_query_token_func(session, model_class):
"""Create an ``query_token`` function that can be used in
resource protector.
:param session: SQLAlchemy session
:param model_class: TokenCredential class
"""
def query_token(client_id, oauth_token):
q = session.query(model_class)
return q.filter_by(
client_id=client_id, oauth_token=oauth_token).first()
return query_token
def register_temporary_credential_hooks(
authorization_server, session, model_class):
"""Register temporary credential related hooks to authorization server.
:param authorization_server: AuthorizationServer instance
:param session: SQLAlchemy session
:param model_class: TemporaryCredential class
"""
def create_temporary_credential(token, client_id, redirect_uri):
item = model_class(
client_id=client_id,
oauth_token=token['oauth_token'],
oauth_token_secret=token['oauth_token_secret'],
oauth_callback=redirect_uri,
)
session.add(item)
session.commit()
return item
def get_temporary_credential(oauth_token):
q = session.query(model_class).filter_by(oauth_token=oauth_token)
return q.first()
def delete_temporary_credential(oauth_token):
q = session.query(model_class).filter_by(oauth_token=oauth_token)
q.delete(synchronize_session=False)
session.commit()
def create_authorization_verifier(credential, grant_user, verifier):
credential.set_user_id(grant_user.get_user_id())
credential.oauth_verifier = verifier
session.add(credential)
session.commit()
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(session, model_class):
"""Create an ``exists_nonce`` function that can be used in hooks and
resource protector.
:param session: SQLAlchemy session
:param model_class: TimestampNonce class
"""
def exists_nonce(nonce, timestamp, client_id, oauth_token):
q = session.query(model_class.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
item = model_class(
nonce=nonce,
timestamp=timestamp,
client_id=client_id,
oauth_token=oauth_token,
)
session.add(item)
session.commit()
return False
return exists_nonce
def register_nonce_hooks(authorization_server, session, model_class):
"""Register nonce related hooks to authorization server.
:param authorization_server: AuthorizationServer instance
:param session: SQLAlchemy session
:param model_class: TimestampNonce class
"""
exists_nonce = create_exists_nonce_func(session, model_class)
authorization_server.register_hook('exists_nonce', exists_nonce)
def register_token_credential_hooks(
authorization_server, session, model_class):
"""Register token credential related hooks to authorization server.
:param authorization_server: AuthorizationServer instance
:param session: SQLAlchemy session
:param model_class: TokenCredential class
"""
def create_token_credential(token, temporary_credential):
item = model_class(
oauth_token=token['oauth_token'],
oauth_token_secret=token['oauth_token_secret'],
client_id=temporary_credential.get_client_id()
)
item.set_user_id(temporary_credential.get_user_id())
session.add(item)
session.commit()
return item
authorization_server.register_hook(
'create_token_credential', create_token_credential)
def register_authorization_hooks(
authorization_server, session,
token_credential_model,
temporary_credential_model=None,
timestamp_nonce_model=None):
register_token_credential_hooks(
authorization_server, session, token_credential_model)
if temporary_credential_model is not None:
register_temporary_credential_hooks(
authorization_server, session, temporary_credential_model)
if timestamp_nonce_model is not None:
register_nonce_hooks(
authorization_server, session, timestamp_nonce_model)
authlib-0.15.5/authlib/integrations/sqla_oauth1/mixins.py 0000664 0000000 0000000 00000005353 14133261713 0023501 0 ustar 00root root 0000000 0000000 from sqlalchemy import Column, UniqueConstraint
from sqlalchemy import String, Integer, Text
from authlib.oauth1 import (
ClientMixin,
TemporaryCredentialMixin,
TokenCredentialMixin,
)
class OAuth1ClientMixin(ClientMixin):
client_id = Column(String(48), index=True)
client_secret = Column(String(120), nullable=False)
default_redirect_uri = Column(Text, nullable=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
class OAuth1TemporaryCredentialMixin(TemporaryCredentialMixin):
client_id = Column(String(48), index=True)
oauth_token = Column(String(84), unique=True, index=True)
oauth_token_secret = Column(String(84))
oauth_verifier = Column(String(84))
oauth_callback = Column(Text, default='')
def get_user_id(self):
"""A method to get the grant user information of this temporary
credential. For instance, grant user is stored in database on
``user_id`` column::
def get_user_id(self):
return self.user_id
:return: User ID
"""
if hasattr(self, 'user_id'):
return self.user_id
else:
raise NotImplementedError()
def set_user_id(self, user_id):
if hasattr(self, 'user_id'):
setattr(self, 'user_id', user_id)
else:
raise NotImplementedError()
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 OAuth1TimestampNonceMixin(object):
__table_args__ = (
UniqueConstraint(
'client_id', 'timestamp', 'nonce', 'oauth_token',
name='unique_nonce'
),
)
client_id = Column(String(48), nullable=False)
timestamp = Column(Integer, nullable=False)
nonce = Column(String(48), nullable=False)
oauth_token = Column(String(84))
class OAuth1TokenCredentialMixin(TokenCredentialMixin):
client_id = Column(String(48), index=True)
oauth_token = Column(String(84), unique=True, index=True)
oauth_token_secret = Column(String(84))
def set_user_id(self, user_id):
if hasattr(self, 'user_id'):
setattr(self, 'user_id', user_id)
else:
raise NotImplementedError()
def get_oauth_token(self):
return self.oauth_token
def get_oauth_token_secret(self):
return self.oauth_token_secret
authlib-0.15.5/authlib/integrations/sqla_oauth2/ 0000775 0000000 0000000 00000000000 14133261713 0021613 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/sqla_oauth2/__init__.py 0000664 0000000 0000000 00000001044 14133261713 0023723 0 ustar 00root root 0000000 0000000 from .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-0.15.5/authlib/integrations/sqla_oauth2/client_mixin.py 0000664 0000000 0000000 00000007622 14133261713 0024656 0 ustar 00root root 0000000 0000000 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.util 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)
@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 has_client_secret(self):
return bool(self.client_secret)
def check_client_secret(self, client_secret):
return self.client_secret == client_secret
def check_token_endpoint_auth_method(self, method):
return self.token_endpoint_auth_method == method
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-0.15.5/authlib/integrations/sqla_oauth2/functions.py 0000664 0000000 0000000 00000006212 14133261713 0024176 0 ustar 00root root 0000000 0000000 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, client):
q = session.query(token_model)
q = q.filter_by(client_id=client.client_id, revoked=False)
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, client):
return query_token(token, token_type_hint, client)
def revoke_token(self, token):
token.revoked = True
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()
def request_invalid(self, request):
return False
def token_revoked(self, token):
return token.revoked
return _BearerTokenValidator
authlib-0.15.5/authlib/integrations/sqla_oauth2/tokens_mixins.py 0000664 0000000 0000000 00000003216 14133261713 0025061 0 ustar 00root root 0000000 0000000 import time
from sqlalchemy import Column, String, Boolean, 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='')
revoked = Column(Boolean, default=False)
issued_at = Column(
Integer, nullable=False, default=lambda: int(time.time())
)
expires_in = Column(Integer, nullable=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
authlib-0.15.5/authlib/integrations/starlette_client/ 0000775 0000000 0000000 00000000000 14133261713 0022736 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/integrations/starlette_client/__init__.py 0000664 0000000 0000000 00000001055 14133261713 0025050 0 ustar 00root root 0000000 0000000 # flake8: noqa
from ..base_client import BaseOAuth, OAuthError
from .integration import StartletteIntegration, StarletteRemoteApp
class OAuth(BaseOAuth):
framework_client_cls = StarletteRemoteApp
framework_integration_cls = StartletteIntegration
def __init__(self, config=None, cache=None, fetch_token=None, update_token=None):
super(OAuth, self).__init__(fetch_token, update_token)
self.cache = cache
self.config = config
__all__ = [
'OAuth', 'StartletteIntegration', 'StarletteRemoteApp',
'OAuthError',
]
authlib-0.15.5/authlib/integrations/starlette_client/integration.py 0000664 0000000 0000000 00000005060 14133261713 0025634 0 ustar 00root root 0000000 0000000 from starlette.responses import RedirectResponse
from ..httpx_client import AsyncOAuth1Client, AsyncOAuth2Client
from ..base_client import FrameworkIntegration, OAuthError
from ..base_client.async_app import AsyncRemoteApp
class StartletteIntegration(FrameworkIntegration):
oauth1_client_cls = AsyncOAuth1Client
oauth2_client_cls = AsyncOAuth2Client
def update_token(self, token, refresh_token=None, access_token=None):
pass
def generate_access_token_params(self, request_token_url, request):
if request_token_url:
return dict(request.query_params)
error = request.query_params.get('error')
if error:
description = request.query_params.get('error_description')
raise OAuthError(error=error, description=description)
return {
'code': request.query_params.get('code'),
'state': request.query_params.get('state'),
}
@staticmethod
def load_config(oauth, name, params):
if not oauth.config:
return {}
rv = {}
for k in params:
conf_key = '{}_{}'.format(name, k).upper()
v = oauth.config.get(conf_key, default=None)
if v is not None:
rv[k] = v
return rv
class StarletteRemoteApp(AsyncRemoteApp):
async def authorize_redirect(self, request, redirect_uri=None, **kwargs):
"""Create a HTTP Redirect for Authorization Endpoint.
:param request: Starlette Request instance.
:param redirect_uri: Callback or redirect URI for authorization.
:param kwargs: Extra parameters to include.
:return: Starlette ``RedirectResponse`` instance.
"""
rv = await self.create_authorization_url(redirect_uri, **kwargs)
self.save_authorize_data(request, redirect_uri=redirect_uri, **rv)
return RedirectResponse(rv['url'], status_code=302)
async def authorize_access_token(self, request, **kwargs):
"""Fetch an access token.
:param request: Starlette Request instance.
:return: A token dict.
"""
params = self.retrieve_access_token_params(request)
params.update(kwargs)
return await self.fetch_access_token(**params)
async def parse_id_token(self, request, token, claims_options=None):
"""Return an instance of UserInfo from token's ``id_token``."""
if 'id_token' not in token:
return None
nonce = self.framework.get_session_data(request, 'nonce')
return await self._parse_id_token(token, nonce, claims_options)
authlib-0.15.5/authlib/jose/ 0000775 0000000 0000000 00000000000 14133261713 0015623 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/jose/__init__.py 0000664 0000000 0000000 00000003475 14133261713 0017745 0 ustar 00root root 0000000 0000000 """
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,
ECDHAlgorithm,
OctKey,
RSAKey,
ECKey,
)
from .rfc7519 import JsonWebToken, BaseClaims, JWTClaims
from .rfc8037 import OKPKey, register_jws_rfc8037
from .drafts import register_jwe_draft
from .errors import JoseError
# register algorithms
register_jws_rfc7518()
register_jwe_rfc7518()
register_jws_rfc8037()
register_jwe_draft()
# attach algorithms
ECDHAlgorithm.ALLOWED_KEY_CLS = (ECKey, OKPKey)
# register supported keys
JsonWebKey.JWK_KEY_CLS = {
OctKey.kty: OctKey,
RSAKey.kty: RSAKey,
ECKey.kty: ECKey,
OKPKey.kty: OKPKey,
}
# compatible constants
JWS_ALGORITHMS = list(JsonWebSignature.ALGORITHMS_REGISTRY.keys())
JWE_ALG_ALGORITHMS = list(JsonWebEncryption.ALG_REGISTRY.keys())
JWE_ENC_ALGORITHMS = list(JsonWebEncryption.ENC_REGISTRY.keys())
JWE_ZIP_ALGORITHMS = list(JsonWebEncryption.ZIP_REGISTRY.keys())
JWE_ALGORITHMS = JWE_ALG_ALGORITHMS + JWE_ENC_ALGORITHMS + JWE_ZIP_ALGORITHMS
# compatible imports
JWS = JsonWebSignature
JWE = JsonWebEncryption
JWK = JsonWebKey
JWT = JsonWebToken
jwt = JsonWebToken()
__all__ = [
'JoseError',
'JWS', 'JsonWebSignature', 'JWSAlgorithm', 'JWSHeader', 'JWSObject',
'JWE', 'JsonWebEncryption', 'JWEAlgorithm', 'JWEEncAlgorithm', 'JWEZipAlgorithm',
'JWK', 'JsonWebKey', 'Key', 'KeySet',
'OctKey', 'RSAKey', 'ECKey', 'OKPKey',
'JWT', 'JsonWebToken', 'BaseClaims', 'JWTClaims',
'jwt',
]
authlib-0.15.5/authlib/jose/drafts/ 0000775 0000000 0000000 00000000000 14133261713 0017106 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/jose/drafts/__init__.py 0000664 0000000 0000000 00000000130 14133261713 0021211 0 ustar 00root root 0000000 0000000 from ._jwe_enc_cryptography import register_jwe_draft
__all__ = ['register_jwe_draft']
authlib-0.15.5/authlib/jose/drafts/_jwe_enc_cryptography.py 0000664 0000000 0000000 00000003416 14133261713 0024050 0 ustar 00root root 0000000 0000000 """
authlib.jose.draft
~~~~~~~~~~~~~~~~~~~~
Content Encryption per `Section 4`_.
.. _`Section 4`: https://tools.ietf.org/html/draft-amringer-jose-chacha-02#section-4
"""
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
from authlib.jose.rfc7516 import JWEEncAlgorithm, JsonWebEncryption
class C20PEncAlgorithm(JWEEncAlgorithm):
# Use of an IV of size 96 bits is REQUIRED with this algorithm.
# https://tools.ietf.org/html/draft-amringer-jose-chacha-02#section-2.2.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):
"""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, 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):
"""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)
chacha = ChaCha20Poly1305(key)
return chacha.decrypt(iv, ciphertext + tag, aad)
def register_jwe_draft():
JsonWebEncryption.register_algorithm(C20PEncAlgorithm(256)) # C20P
authlib-0.15.5/authlib/jose/errors.py 0000664 0000000 0000000 00000004326 14133261713 0017516 0 ustar 00root root 0000000 0000000 from 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(BadSignatureError, self).__init__()
self.result = result
class InvalidHeaderParameterName(JoseError):
error = 'invalid_header_parameter_name'
def __init__(self, name):
description = 'Invalid Header Parameter Names: {}'.format(name)
super(InvalidHeaderParameterName, self).__init__(
description=description)
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):
description = 'Invalid claim "{}"'.format(claim)
super(InvalidClaimError, self).__init__(description=description)
class MissingClaimError(JoseError):
error = 'missing_claim'
def __init__(self, claim):
description = 'Missing "{}" claim'.format(claim)
super(MissingClaimError, self).__init__(description=description)
class InsecureClaimError(JoseError):
error = 'insecure_claim'
def __init__(self, claim):
description = 'Insecure claim "{}"'.format(claim)
super(InsecureClaimError, self).__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-0.15.5/authlib/jose/jwk.py 0000664 0000000 0000000 00000000630 14133261713 0016767 0 ustar 00root root 0000000 0000000 from .rfc7517 import JsonWebKey
def loads(obj, kid=None):
# TODO: deprecate
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):
# TODO: deprecate
if kty:
params['kty'] = kty
key = JsonWebKey.import_key(key, params)
data = key.as_dict()
return data
authlib-0.15.5/authlib/jose/rfc7515/ 0000775 0000000 0000000 00000000000 14133261713 0016717 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/jose/rfc7515/__init__.py 0000664 0000000 0000000 00000000550 14133261713 0021030 0 ustar 00root root 0000000 0000000 """
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-0.15.5/authlib/jose/rfc7515/jws.py 0000664 0000000 0000000 00000026404 14133261713 0020102 0 ustar 00root root 0000000 0000000 from authlib.common.encoding import (
to_bytes,
to_unicode,
urlsafe_b64encode,
json_b64encode,
json_loads,
)
from authlib.jose.util import (
extract_header,
extract_segment,
)
from authlib.jose.errors import (
DecodeError,
MissingAlgorithmError,
UnsupportedAlgorithmError,
BadSignatureError,
InvalidHeaderParameterName,
)
from .models import JWSHeader, JWSObject
class JsonWebSignature(object):
#: 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(
'Invalid algorithm for JWS, {!r}'.format(algorithm))
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)
payload_segment = obj.get('payload')
if not payload_segment:
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 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 '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 InvalidHeaderParameterName(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')
def _ensure_dict(s):
if not isinstance(s, dict):
try:
s = json_loads(to_unicode(s))
except (ValueError, TypeError):
raise DecodeError('Invalid JWS')
if not isinstance(s, dict):
raise DecodeError('Invalid JWS')
return s
authlib-0.15.5/authlib/jose/rfc7515/models.py 0000664 0000000 0000000 00000004663 14133261713 0020565 0 ustar 00root root 0000000 0000000 class JWSAlgorithm(object):
"""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(JWSHeader, self).__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(JWSObject, self).__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-0.15.5/authlib/jose/rfc7516/ 0000775 0000000 0000000 00000000000 14133261713 0016720 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/jose/rfc7516/__init__.py 0000664 0000000 0000000 00000000603 14133261713 0021030 0 ustar 00root root 0000000 0000000 """
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, JWEEncAlgorithm, JWEZipAlgorithm
__all__ = [
'JsonWebEncryption',
'JWEAlgorithm', 'JWEEncAlgorithm', 'JWEZipAlgorithm'
]
authlib-0.15.5/authlib/jose/rfc7516/jwe.py 0000664 0000000 0000000 00000016261 14133261713 0020065 0 ustar 00root root 0000000 0000000 from authlib.common.encoding import (
to_bytes, urlsafe_b64encode, json_b64encode
)
from authlib.jose.util import (
extract_header,
extract_segment,
)
from authlib.jose.errors import (
DecodeError,
MissingAlgorithmError,
UnsupportedAlgorithmError,
MissingEncryptionAlgorithmError,
UnsupportedEncryptionAlgorithmError,
UnsupportedCompressionAlgorithmError,
InvalidHeaderParameterName,
)
class JsonWebEncryption(object):
#: 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(
'Invalid algorithm for JWE, {!r}'.format(algorithm))
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):
"""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: A string/dict of payload
:param key: Private key used to generate signature
:return: byte
"""
# 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_private_headers(protected, alg)
key = prepare_key(alg, protected, key)
# self._post_validate_header(protected, algorithm)
# step 2: Generate a random Content Encryption Key (CEK)
# use enc_alg.generate_cek() in .wrap method
# step 3: Encrypt the CEK with the recipient's public key
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)
return b'.'.join([
protected_segment,
urlsafe_b64encode(ek),
urlsafe_b64encode(iv),
urlsafe_b64encode(ciphertext),
urlsafe_b64encode(tag)
])
def deserialize_compact(self, s, key, decode=None):
"""Exact JWS Compact Serialization, and validate with the given key.
:param s: text of JWS Compact Serialization
:param key: key used to verify the signature
:param decode: a function to decode plaintext data
:return: dict
"""
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_private_headers(protected, alg)
key = prepare_key(alg, protected, key)
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 get_header_alg(self, header):
if 'alg' not in header:
raise MissingAlgorithmError()
alg = header['alg']
if self._algorithms 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 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 and z not in self._algorithms:
raise UnsupportedCompressionAlgorithmError()
if z not in self.ZIP_REGISTRY:
raise UnsupportedCompressionAlgorithmError()
return self.ZIP_REGISTRY[z]
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 InvalidHeaderParameterName(k)
def prepare_key(alg, header, key):
if callable(key):
key = key(header, None)
elif 'jwk' in header:
key = header['jwk']
return alg.prepare_key(key)
authlib-0.15.5/authlib/jose/rfc7516/models.py 0000664 0000000 0000000 00000003747 14133261713 0020570 0 ustar 00root root 0000000 0000000 import os
class JWEAlgorithm(object):
"""Interface for JWE algorithm. JWA specification (RFC7518) SHOULD
implement the algorithms for JWE with this base implementation.
"""
EXTRA_HEADERS = None
name = None
description = None
algorithm_type = 'JWE'
algorithm_location = 'alg'
def prepare_key(self, raw_data):
raise NotImplementedError
def wrap(self, enc_alg, headers, key):
raise NotImplementedError
def unwrap(self, enc_alg, ek, headers, key):
raise NotImplementedError
class JWEEncAlgorithm(object):
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, iv, 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(object):
name = None
description = None
algorithm_type = 'JWE'
algorithm_location = 'zip'
def compress(self, s):
raise NotImplementedError
def decompress(self, s):
raise NotImplementedError
authlib-0.15.5/authlib/jose/rfc7517/ 0000775 0000000 0000000 00000000000 14133261713 0016721 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/jose/rfc7517/__init__.py 0000664 0000000 0000000 00000000527 14133261713 0021036 0 ustar 00root root 0000000 0000000 """
authlib.jose.rfc7517
~~~~~~~~~~~~~~~~~~~~~
This module represents a direct implementation of
JSON Web Key (JWK).
https://tools.ietf.org/html/rfc7517
"""
from .models import Key, KeySet
from ._cryptography_key import load_pem_key
from .jwk import JsonWebKey
__all__ = ['Key', 'KeySet', 'JsonWebKey', 'load_pem_key']
authlib-0.15.5/authlib/jose/rfc7517/_cryptography_key.py 0000664 0000000 0000000 00000002351 14133261713 0023036 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/jose/rfc7517/jwk.py 0000664 0000000 0000000 00000003750 14133261713 0020073 0 ustar 00root root 0000000 0000000 from authlib.common.encoding import text_types, json_loads
from ._cryptography_key import load_pem_key
from .models import KeySet
class JsonWebKey(object):
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 isinstance(raw_key, key_cls.RAW_KEY_CLS):
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])
def _transform_raw_key(raw):
if isinstance(raw, text_types) and \
raw.startswith('{') and raw.endswith('}'):
return json_loads(raw)
elif isinstance(raw, (tuple, list)):
return {'keys': raw}
return raw
authlib-0.15.5/authlib/jose/rfc7517/models.py 0000664 0000000 0000000 00000010740 14133261713 0020560 0 ustar 00root root 0000000 0000000 import hashlib
from collections import OrderedDict
from authlib.common.encoding import (
json_dumps,
to_bytes,
to_unicode,
urlsafe_b64encode,
)
from ..errors import InvalidUseError
class Key(dict):
"""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 = []
RAW_KEY_CLS = bytes
def __init__(self, payload):
super(Key, self).__init__(payload)
self.key_type = 'secret'
self.raw_key = None
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.key_type == 'private':
return self.raw_key.public_key()
return self.raw_key
def get_private_key(self):
if self.key_type == 'private':
return self.raw_key
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.get('key_ops')
if key_ops is not None and operation not in key_ops:
raise ValueError('Unsupported key_op "{}"'.format(operation))
if operation in self.PRIVATE_KEY_OPS and self.key_type == 'public':
raise ValueError('Invalid key_op "{}" for public key'.format(operation))
use = self.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_key(self):
"""Represent this key as raw key."""
return self.raw_key
def as_dict(self, add_kid=False):
"""Represent this key as a dict of the JSON Web Key."""
obj = dict(self)
obj['kty'] = self.kty
if add_kid and 'kid' not in obj:
obj['kid'] = self.thumbprint()
return obj
def as_json(self):
"""Represent this key as a JSON string."""
obj = self.as_dict()
return json_dumps(obj)
def as_pem(self):
"""Represent this key as string in PEM format."""
raise RuntimeError('Not supported')
def thumbprint(self):
"""Implementation of RFC7638 JSON Web Key (JWK) Thumbprint."""
fields = list(self.REQUIRED_JSON_FIELDS)
fields.append('kty')
fields.sort()
data = OrderedDict()
obj = self.as_dict()
for k in fields:
data[k] = obj[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('Missing required field: "{}"'.format(k))
@classmethod
def generate_key(cls, crv_or_size, options=None, is_private=False):
raise NotImplementedError()
@classmethod
def import_key(cls, raw, options=None):
raise NotImplementedError()
class KeySet(object):
"""This class represents a JSON Web Key Set."""
def __init__(self, keys):
self.keys = keys
def as_dict(self):
"""Represent this key as a dict of the JSON Web Key Set."""
return {'keys': [k.as_dict(True) for k in self.keys]}
def as_json(self):
"""Represent this key set as a JSON string."""
obj = self.as_dict()
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.get('kid') == kid:
return k
raise ValueError('Invalid JSON Web Key Set')
authlib-0.15.5/authlib/jose/rfc7518/ 0000775 0000000 0000000 00000000000 14133261713 0016722 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/jose/rfc7518/__init__.py 0000664 0000000 0000000 00000000657 14133261713 0021043 0 ustar 00root root 0000000 0000000 from .jws_algorithms import register_jws_rfc7518
from .jwe_algorithms import register_jwe_rfc7518
from .oct_key import OctKey
from ._cryptography_backends import (
RSAKey, ECKey, ECDHAlgorithm,
import_key, load_pem_key, export_key,
)
__all__ = [
'register_jws_rfc7518',
'register_jwe_rfc7518',
'ECDHAlgorithm',
'OctKey',
'RSAKey',
'ECKey',
'import_key',
'load_pem_key',
'export_key',
]
authlib-0.15.5/authlib/jose/rfc7518/_cryptography_backends/ 0000775 0000000 0000000 00000000000 14133261713 0023446 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/jose/rfc7518/_cryptography_backends/__init__.py 0000664 0000000 0000000 00000000326 14133261713 0025560 0 ustar 00root root 0000000 0000000 from ._jws import JWS_ALGORITHMS
from ._jwe_alg import JWE_ALG_ALGORITHMS, ECDHAlgorithm
from ._jwe_enc import JWE_ENC_ALGORITHMS
from ._keys import (
RSAKey, ECKey,
load_pem_key, import_key, export_key,
)
authlib-0.15.5/authlib/jose/rfc7518/_cryptography_backends/_jwe_alg.py 0000664 0000000 0000000 00000021325 14133261713 0025572 0 ustar 00root root 0000000 0000000 import 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 ._keys import RSAKey, ECKey
from ..oct_key import OctKey
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 wrap(self, enc_alg, headers, key):
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)
print(cek, enc_alg.key_size)
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 = 'A{}KW'.format(key_size)
self.description = 'AES Key Wrap using {}-bit key'.format(key_size)
self.key_size = key_size
def prepare_key(self, raw_data):
return OctKey.import_key(raw_data)
def _check_key(self, key):
if len(key) * 8 != self.key_size:
raise ValueError(
'A key of size {} bits is required.'.format(self.key_size))
def wrap(self, enc_alg, headers, key):
cek = enc_alg.generate_cek()
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 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 = 'A{}GCMKW'.format(key_size)
self.description = 'Key wrapping with AES GCM using {}-bit key'.format(key_size)
self.key_size = key_size
def prepare_key(self, raw_data):
return OctKey.import_key(raw_data)
def _check_key(self, key):
if len(key) * 8 != self.key_size:
raise ValueError(
'A key of size {} bits is required.'.format(self.key_size))
def wrap(self, enc_alg, headers, key):
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 ECDHAlgorithm(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 = 'ECDH-ES+A{}KW'.format(key_size)
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 deliver(self, key, pubkey, 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)
other_info = alg_id + apu_info + apv_info + pub_info
shared_key = key.exchange_shared_key(pubkey)
ckdf = ConcatKDFHash(
algorithm=hashes.SHA256(),
length=bit_size // 8,
otherinfo=other_info,
backend=default_backend()
)
return ckdf.derive(shared_key)
def wrap(self, enc_alg, headers, key):
if self.key_size is None:
bit_size = enc_alg.key_size
else:
bit_size = self.key_size
epk = key.generate_key(key['crv'], is_private=True)
public_key = key.get_op_key('wrapKey')
dk = self.deliver(epk, public_key, headers, bit_size)
# REQUIRED_JSON_FIELDS contains only public fields
pub_epk = {k: epk[k] for k in epk.REQUIRED_JSON_FIELDS}
pub_epk['kty'] = epk.kty
h = {'epk': pub_epk}
if self.key_size is None:
return {'ek': b'', 'cek': dk, 'header': h}
kek = self.aeskw.prepare_key(dk)
rv = self.aeskw.wrap(enc_alg, headers, kek)
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.key_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 = [
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
ECDHAlgorithm(None), # ECDH-ES
ECDHAlgorithm(128), # ECDH-ES+A128KW
ECDHAlgorithm(192), # ECDH-ES+A192KW
ECDHAlgorithm(256), # ECDH-ES+A256KW
]
# 'PBES2-HS256+A128KW': '',
# 'PBES2-HS384+A192KW': '',
# 'PBES2-HS512+A256KW': '',
authlib-0.15.5/authlib/jose/rfc7518/_cryptography_backends/_jwe_enc.py 0000664 0000000 0000000 00000011745 14133261713 0025601 0 ustar 00root root 0000000 0000000 """
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 authlib.jose.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 = 'A{}CBC-HS{}'.format(key_size, 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, 'sha{}'.format(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 = 'A{}GCM'.format(key_size)
self.description = 'AES GCM using {}-bit key'.format(key_size)
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-0.15.5/authlib/jose/rfc7518/_cryptography_backends/_jws.py 0000664 0000000 0000000 00000011454 14133261713 0024767 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
authlib.jose.rfc7518
~~~~~~~~~~~~~~~~~~~~
"alg" (Algorithm) Header Parameter Values for JWS per `Section 3`_.
.. _`Section 3`: https://tools.ietf.org/html/rfc7518#section-3
"""
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 authlib.jose.rfc7515 import JWSAlgorithm
from ._keys import RSAKey, ECKey
from ..util import encode_int, decode_int
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 = 'RS{}'.format(sha_type)
self.description = 'RSASSA-PKCS1-v1_5 using SHA-{}'.format(sha_type)
self.hash_alg = getattr(self, 'SHA{}'.format(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, sha_type):
self.name = 'ES{}'.format(sha_type)
self.description = 'ECDSA using P-{} and SHA-{}'.format(sha_type, sha_type)
self.hash_alg = getattr(self, 'SHA{}'.format(sha_type))
def prepare_key(self, raw_data):
return ECKey.import_key(raw_data)
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 = 'PS{}'.format(sha_type)
tpl = 'RSASSA-PSS using SHA-{} and MGF1 with SHA-{}'
self.description = tpl.format(sha_type, sha_type)
self.hash_alg = getattr(self, 'SHA{}'.format(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 = [
RSAAlgorithm(256), # RS256
RSAAlgorithm(384), # RS384
RSAAlgorithm(512), # RS512
ECAlgorithm(256), # ES256
ECAlgorithm(384), # ES384
ECAlgorithm(512), # ES512
RSAPSSAlgorithm(256), # PS256
RSAPSSAlgorithm(384), # PS384
RSAPSSAlgorithm(512), # PS512
]
authlib-0.15.5/authlib/jose/rfc7518/_cryptography_backends/_keys.py 0000664 0000000 0000000 00000023775 14133261713 0025150 0 ustar 00root root 0000000 0000000 from cryptography.hazmat.primitives.serialization import (
Encoding, PrivateFormat, PublicFormat,
BestAvailableEncryption, NoEncryption,
)
from 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.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import (
EllipticCurvePublicKey, EllipticCurvePrivateKeyWithSerialization,
EllipticCurvePrivateNumbers, EllipticCurvePublicNumbers,
SECP256R1, SECP384R1, SECP521R1,
)
from cryptography.hazmat.backends import default_backend
from authlib.jose.rfc7517 import Key, load_pem_key
from authlib.common.encoding import to_bytes
from authlib.common.encoding import base64_to_int, int_to_base64
class RSAKey(Key):
"""Key class of the ``RSA`` key type."""
kty = 'RSA'
RAW_KEY_CLS = (RSAPublicKey, RSAPrivateKeyWithSerialization)
REQUIRED_JSON_FIELDS = ['e', 'n']
def as_pem(self, is_private=False, password=None):
"""Export key into PEM format bytes.
:param is_private: export private key or public key
:param password: encrypt private key with password
:return: bytes
"""
return export_key(self, is_private=is_private, password=password)
@staticmethod
def dumps_private_key(raw_key):
numbers = raw_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)
}
@staticmethod
def dumps_public_key(raw_key):
numbers = raw_key.public_numbers()
return {
'n': int_to_base64(numbers.n),
'e': int_to_base64(numbers.e)
}
@staticmethod
def loads_private_key(obj):
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')
props = ['p', 'q', 'dp', 'dq', 'qi']
props_found = [prop in obj for prop in props]
any_props_found = any(props_found)
if any_props_found and not all(props_found):
raise ValueError(
'RSA key must include all parameters '
'if any are present besides d')
public_numbers = RSAPublicNumbers(
base64_to_int(obj['e']), base64_to_int(obj['n']))
if any_props_found:
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())
@staticmethod
def loads_public_key(obj):
numbers = RSAPublicNumbers(
base64_to_int(obj['e']),
base64_to_int(obj['n'])
)
return numbers.public_key(default_backend())
@classmethod
def import_key(cls, raw, options=None):
"""Import a key from PEM or dict data."""
return import_key(
cls, raw,
RSAPublicKey, RSAPrivateKeyWithSerialization,
b'ssh-rsa', options
)
@classmethod
def generate_key(cls, key_size=2048, options=None, is_private=False):
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)
class ECKey(Key):
"""Key class of the ``EC`` key type."""
kty = 'EC'
DSS_CURVES = {
'P-256': SECP256R1,
'P-384': SECP384R1,
'P-521': SECP521R1,
}
CURVES_DSS = {
SECP256R1.name: 'P-256',
SECP384R1.name: 'P-384',
SECP521R1.name: 'P-521',
}
REQUIRED_JSON_FIELDS = ['crv', 'x', 'y']
RAW_KEY_CLS = (EllipticCurvePublicKey, EllipticCurvePrivateKeyWithSerialization)
def as_pem(self, is_private=False, password=None):
"""Export key into PEM format bytes.
:param is_private: export private key or public key
:param password: encrypt private key with password
:return: bytes
"""
return export_key(self, is_private=is_private, password=password)
def exchange_shared_key(self, pubkey):
# # used in ECDHAlgorithm
if isinstance(self.raw_key, EllipticCurvePrivateKeyWithSerialization):
return self.raw_key.exchange(ec.ECDH(), pubkey)
raise ValueError('Invalid key for exchanging shared key')
@property
def curve_key_size(self):
return self.raw_key.curve.key_size
@classmethod
def loads_private_key(cls, obj):
curve = cls.DSS_CURVES[obj['crv']]()
public_numbers = EllipticCurvePublicNumbers(
base64_to_int(obj['x']),
base64_to_int(obj['y']),
curve,
)
private_numbers = EllipticCurvePrivateNumbers(
base64_to_int(obj['d']),
public_numbers
)
return private_numbers.private_key(default_backend())
@classmethod
def loads_public_key(cls, obj):
curve = cls.DSS_CURVES[obj['crv']]()
public_numbers = EllipticCurvePublicNumbers(
base64_to_int(obj['x']),
base64_to_int(obj['y']),
curve,
)
return public_numbers.public_key(default_backend())
@classmethod
def dumps_private_key(cls, raw_key):
numbers = raw_key.private_numbers()
return {
'crv': cls.CURVES_DSS[raw_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),
}
@classmethod
def dumps_public_key(cls, raw_key):
numbers = raw_key.public_numbers()
return {
'crv': cls.CURVES_DSS[numbers.curve.name],
'x': int_to_base64(numbers.x),
'y': int_to_base64(numbers.y)
}
@classmethod
def import_key(cls, raw, options=None):
"""Import a key from PEM or dict data."""
return import_key(
cls, raw,
EllipticCurvePublicKey, EllipticCurvePrivateKeyWithSerialization,
b'ecdsa-sha2-', options
)
@classmethod
def generate_key(cls, crv='P-256', options=None, is_private=False):
if crv not in cls.DSS_CURVES:
raise ValueError('Invalid crv value: "{}"'.format(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)
def import_key(cls, raw, public_key_cls, private_key_cls, ssh_type=None, options=None):
if isinstance(raw, cls):
if options is not None:
raw.update(options)
return raw
payload = None
if isinstance(raw, (public_key_cls, private_key_cls)):
raw_key = raw
elif isinstance(raw, dict):
cls.check_required_fields(raw)
payload = raw
if 'd' in payload:
raw_key = cls.loads_private_key(payload)
else:
raw_key = cls.loads_public_key(payload)
else:
if options is not None:
password = options.get('password')
else:
password = None
raw_key = load_pem_key(raw, ssh_type, password=password)
if isinstance(raw_key, private_key_cls):
if payload is None:
payload = cls.dumps_private_key(raw_key)
key_type = 'private'
elif isinstance(raw_key, public_key_cls):
if payload is None:
payload = cls.dumps_public_key(raw_key)
key_type = 'public'
else:
raise ValueError('Invalid data for importing key')
obj = cls(payload)
obj.raw_key = raw_key
obj.key_type = key_type
return obj
def export_key(key, encoding=None, is_private=False, password=None):
if encoding is None or encoding == 'PEM':
encoding = Encoding.PEM
elif encoding == 'DER':
encoding = Encoding.DER
else:
raise ValueError('Invalid encoding: {!r}'.format(encoding))
if is_private:
if key.key_type == 'private':
if password is None:
encryption_algorithm = NoEncryption()
else:
encryption_algorithm = BestAvailableEncryption(to_bytes(password))
return key.raw_key.private_bytes(
encoding=encoding,
format=PrivateFormat.PKCS8,
encryption_algorithm=encryption_algorithm,
)
raise ValueError('This is a public key')
if key.key_type == 'private':
raw_key = key.raw_key.public_key()
else:
raw_key = key.raw_key
return raw_key.public_bytes(
encoding=encoding,
format=PublicFormat.SubjectPublicKeyInfo,
)
authlib-0.15.5/authlib/jose/rfc7518/jwe_algorithms.py 0000664 0000000 0000000 00000003024 14133261713 0022311 0 ustar 00root root 0000000 0000000 import zlib
from .oct_key import OctKey
from ._cryptography_backends import JWE_ALG_ALGORITHMS, JWE_ENC_ALGORITHMS
from ..rfc7516 import JWEAlgorithm, JWEZipAlgorithm, JsonWebEncryption
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 wrap(self, enc_alg, headers, key):
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 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(DirectAlgorithm())
JsonWebEncryption.register_algorithm(DeflateZipAlgorithm())
for algorithm in JWE_ALG_ALGORITHMS:
JsonWebEncryption.register_algorithm(algorithm)
for algorithm in JWE_ENC_ALGORITHMS:
JsonWebEncryption.register_algorithm(algorithm)
authlib-0.15.5/authlib/jose/rfc7518/jws_algorithms.py 0000664 0000000 0000000 00000003723 14133261713 0022335 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
authlib.jose.rfc7518.jws_algorithms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"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 .oct_key import OctKey
from ._cryptography_backends import JWS_ALGORITHMS
from ..rfc7515 import JWSAlgorithm, JsonWebSignature
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 = 'HS{}'.format(sha_type)
self.description = 'HMAC using SHA-{}'.format(sha_type)
self.hash_alg = getattr(self, 'SHA{}'.format(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)
def register_jws_rfc7518():
JsonWebSignature.register_algorithm(NoneAlgorithm())
JsonWebSignature.register_algorithm(HMACAlgorithm(256))
JsonWebSignature.register_algorithm(HMACAlgorithm(384))
JsonWebSignature.register_algorithm(HMACAlgorithm(512))
for algorithm in JWS_ALGORITHMS:
JsonWebSignature.register_algorithm(algorithm)
authlib-0.15.5/authlib/jose/rfc7518/oct_key.py 0000664 0000000 0000000 00000002622 14133261713 0020733 0 ustar 00root root 0000000 0000000 from authlib.common.encoding import (
to_bytes, to_unicode,
urlsafe_b64encode, urlsafe_b64decode,
)
from authlib.common.security import generate_token
from authlib.jose.rfc7517 import Key
class OctKey(Key):
"""Key class of the ``oct`` key type."""
kty = 'oct'
REQUIRED_JSON_FIELDS = ['k']
def get_op_key(self, key_op):
self.check_key_op(key_op)
return self.raw_key
@classmethod
def import_key(cls, raw, options=None):
"""Import a key from bytes, string, or dict data."""
if isinstance(raw, dict):
cls.check_required_fields(raw)
payload = raw
raw_key = urlsafe_b64decode(to_bytes(payload['k']))
else:
raw_key = to_bytes(raw)
k = to_unicode(urlsafe_b64encode(raw_key))
payload = {'k': k}
if options is not None:
payload.update(options)
obj = cls(payload)
obj.raw_key = raw_key
obj.key_type = 'secret'
return obj
@classmethod
def generate_key(cls, key_size=256, options=None, is_private=False):
"""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-0.15.5/authlib/jose/rfc7518/util.py 0000664 0000000 0000000 00000000411 14133261713 0020245 0 ustar 00root root 0000000 0000000 import 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-0.15.5/authlib/jose/rfc7519/ 0000775 0000000 0000000 00000000000 14133261713 0016723 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/jose/rfc7519/__init__.py 0000664 0000000 0000000 00000000515 14133261713 0021035 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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-0.15.5/authlib/jose/rfc7519/claims.py 0000664 0000000 0000000 00000017722 14133261713 0020556 0 ustar 00root root 0000000 0000000 import 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(BaseClaims, self).__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') and k not in self:
raise MissingClaimError(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()
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. 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')
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-0.15.5/authlib/jose/rfc7519/jwt.py 0000664 0000000 0000000 00000011356 14133261713 0020107 0 ustar 00root root 0000000 0000000 import re
import datetime
import calendar
from authlib.common.encoding import (
text_types, 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
class JsonWebToken(object):
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=None, 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, text_types) 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['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 = prepare_raw_key(key, header)
if callable(key):
key = key(header, payload)
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 JWS 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
def load_key(header, payload):
key_func = prepare_raw_key(key, header)
if callable(key_func):
return key_func(header, payload)
return key_func
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, headers):
if isinstance(raw, KeySet):
return raw.find_by_kid(headers.get('kid'))
if isinstance(raw, text_types) and \
raw.startswith('{') and raw.endswith('}'):
raw = json_loads(raw)
elif isinstance(raw, (tuple, list)):
raw = {'keys': raw}
if isinstance(raw, dict) and 'keys' in raw:
keys = raw['keys']
kid = headers.get('kid')
for k in keys:
if k.get('kid') == kid:
return k
raise ValueError('Invalid JSON Web Key Set')
return raw
authlib-0.15.5/authlib/jose/rfc8037/ 0000775 0000000 0000000 00000000000 14133261713 0016717 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/jose/rfc8037/__init__.py 0000664 0000000 0000000 00000000177 14133261713 0021035 0 ustar 00root root 0000000 0000000 from .okp_key import OKPKey
from ._jws_cryptography import register_jws_rfc8037
__all__ = ['register_jws_rfc8037', 'OKPKey']
authlib-0.15.5/authlib/jose/rfc8037/_jws_cryptography.py 0000664 0000000 0000000 00000001653 14133261713 0023053 0 ustar 00root root 0000000 0000000 from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PublicKey, Ed25519PrivateKey
)
from authlib.jose.rfc7515 import JWSAlgorithm, JsonWebSignature
from .okp_key import OKPKey
class EdDSAAlgorithm(JWSAlgorithm):
name = 'EdDSA'
description = 'Edwards-curve Digital Signature Algorithm for JWS'
private_key_cls = Ed25519PrivateKey
public_key_cls = Ed25519PublicKey
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():
JsonWebSignature.register_algorithm(EdDSAAlgorithm())
authlib-0.15.5/authlib/jose/rfc8037/okp_key.py 0000664 0000000 0000000 00000010133 14133261713 0020730 0 ustar 00root root 0000000 0000000 from 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 authlib.jose.rfc7517 import Key
from ..rfc7518 import import_key, export_key
PUBLIC_KEYS_MAP = {
'Ed25519': Ed25519PublicKey,
'Ed448': Ed448PublicKey,
'X25519': X25519PublicKey,
'X448': X448PublicKey,
}
PRIVATE_KEYS_MAP = {
'Ed25519': Ed25519PrivateKey,
'Ed448': Ed448PrivateKey,
'X25519': X25519PrivateKey,
'X448': X448PrivateKey,
}
PUBLIC_KEY_TUPLE = tuple(PUBLIC_KEYS_MAP.values())
PRIVATE_KEY_TUPLE = tuple(PRIVATE_KEYS_MAP.values())
class OKPKey(Key):
"""Key class of the ``OKP`` key type."""
kty = 'OKP'
REQUIRED_JSON_FIELDS = ['crv', 'x']
RAW_KEY_CLS = (
Ed25519PublicKey, Ed25519PrivateKey,
Ed448PublicKey, Ed448PrivateKey,
X25519PublicKey, X25519PrivateKey,
X448PublicKey, X448PrivateKey,
)
def as_pem(self, is_private=False, password=None):
"""Export key into PEM format bytes.
:param is_private: export private key or public key
:param password: encrypt private key with password
:return: bytes
"""
return export_key(self, is_private=is_private, password=password)
def exchange_shared_key(self, pubkey):
# used in ECDHAlgorithm
if isinstance(self.raw_key, (X25519PrivateKey, X448PrivateKey)):
return self.raw_key.exchange(pubkey)
raise ValueError('Invalid key for exchanging shared key')
@property
def curve_key_size(self):
raise NotImplementedError()
@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'
@staticmethod
def loads_private_key(obj):
crv_key = PRIVATE_KEYS_MAP[obj['crv']]
d_bytes = urlsafe_b64decode(to_bytes(obj['d']))
return crv_key.from_private_bytes(d_bytes)
@staticmethod
def loads_public_key(obj):
crv_key = PUBLIC_KEYS_MAP[obj['crv']]
x_bytes = urlsafe_b64decode(to_bytes(obj['x']))
return crv_key.from_public_bytes(x_bytes)
@staticmethod
def dumps_private_key(raw_key):
obj = OKPKey.dumps_public_key(raw_key.public_key())
d_bytes = raw_key.private_bytes(
Encoding.Raw,
PrivateFormat.Raw,
NoEncryption()
)
obj['d'] = to_unicode(urlsafe_b64encode(d_bytes))
return obj
@staticmethod
def dumps_public_key(raw_key):
x_bytes = raw_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
return {
'crv': OKPKey.get_key_curve(raw_key),
'x': to_unicode(urlsafe_b64encode(x_bytes)),
}
@classmethod
def import_key(cls, raw, options=None):
"""Import a key from PEM or dict data."""
return import_key(
cls, raw,
PUBLIC_KEY_TUPLE, PRIVATE_KEY_TUPLE,
b'ssh-ed25519', options
)
@classmethod
def generate_key(cls, crv='Ed25519', options=None, is_private=False):
if crv not in PRIVATE_KEYS_MAP:
raise ValueError('Invalid crv value: "{}"'.format(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-0.15.5/authlib/jose/util.py 0000664 0000000 0000000 00000001273 14133261713 0017155 0 ustar 00root root 0000000 0000000 import binascii
from authlib.common.encoding import urlsafe_b64decode, json_loads
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('Invalid header string: {}'.format(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 = 'Invalid {} padding'.format(name)
raise error_cls(msg)
authlib-0.15.5/authlib/oauth1/ 0000775 0000000 0000000 00000000000 14133261713 0016064 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth1/__init__.py 0000664 0000000 0000000 00000001360 14133261713 0020175 0 ustar 00root root 0000000 0000000 # coding: utf-8
from .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-0.15.5/authlib/oauth1/client.py 0000664 0000000 0000000 00000015714 14133261713 0017724 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from 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(object):
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, **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,
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 = 'oauth_token is missing: {!r}'.format(token)
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
self.auth.redirect_uri = None
self.auth.realm = None
return add_params_to_uri(url, kwargs.items())
def fetch_request_token(self, url, realm=None, **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 realm: A string/list/tuple of realm for Authorization header.
:param kwargs: Extra parameters to include for fetching token.
:return: A Request Token dict.
Note, ``realm`` can also be configured when session created::
session = OAuth1Session(client_id, client_secret, ..., realm='')
"""
if realm is None:
realm = self._kwargs.get('realm', None)
if realm:
if isinstance(realm, (tuple, list)):
realm = ' '.join(realm)
self.auth.realm = realm
else:
self.auth.realm = None
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('{}: {}'.format(error_type, error_description))
authlib-0.15.5/authlib/oauth1/errors.py 0000664 0000000 0000000 00000000056 14133261713 0017753 0 ustar 00root root 0000000 0000000 # flake8: noqa
from .rfc5849.errors import *
authlib-0.15.5/authlib/oauth1/rfc5849/ 0000775 0000000 0000000 00000000000 14133261713 0017170 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth1/rfc5849/__init__.py 0000664 0000000 0000000 00000002014 14133261713 0021276 0 ustar 00root root 0000000 0000000 """
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-0.15.5/authlib/oauth1/rfc5849/authorization_server.py 0000664 0000000 0000000 00000033070 14133261713 0024033 0 ustar 00root root 0000000 0000000 from 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.get_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-0.15.5/authlib/oauth1/rfc5849/base_server.py 0000664 0000000 0000000 00000007421 14133261713 0022046 0 ustar 00root root 0000000 0000000 import 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(object):
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-0.15.5/authlib/oauth1/rfc5849/client_auth.py 0000664 0000000 0000000 00000015354 14133261713 0022051 0 ustar 00root root 0000000 0000000 import time
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(object):
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, nonce=None, timestamp=None):
"""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.
:param nonce: A string to represent nonce value. If not configured,
this method will generate one for you.
:param timestamp: Current timestamp. If not configured, this method
will generate one for you.
:return: uri, headers, body
"""
if nonce is None:
nonce = generate_nonce()
if timestamp is None:
timestamp = generate_timestamp()
if body is None:
body = ''
# transform int to str
timestamp = str(timestamp)
if headers is None:
headers = {}
oauth_params = self.get_oauth_params(nonce, timestamp)
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, '')
body = ''
return uri, headers, body
def generate_nonce():
return generate_token()
def generate_timestamp():
return str(int(time.time()))
authlib-0.15.5/authlib/oauth1/rfc5849/errors.py 0000664 0000000 0000000 00000005045 14133261713 0021062 0 ustar 00root root 0000000 0000000 """
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(OAuth1Error, self).__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'
def get_error_description(self):
return self.gettext('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):
super(MissingRequiredParameterError, self).__init__()
self._key = key
def get_error_description(self):
return self.gettext(
'missing "%(key)s" in parameters') % dict(key=self._key)
class DuplicatedOAuthProtocolParameterError(OAuth1Error):
error = 'duplicated_oauth_protocol_parameter'
class InvalidClientError(OAuth1Error):
error = 'invalid_client'
status_code = 401
class InvalidTokenError(OAuth1Error):
error = 'invalid_token'
status_code = 401
def get_error_description(self):
return self.gettext('Invalid or expired "oauth_token" in parameters')
class InvalidSignatureError(OAuth1Error):
error = 'invalid_signature'
status_code = 401
class InvalidNonceError(OAuth1Error):
error = 'invalid_nonce'
status_code = 401
class AccessDeniedError(OAuth1Error):
error = 'access_denied'
def get_error_description(self):
return self.gettext(
'The resource owner or authorization server denied the request')
class MethodNotAllowedError(OAuth1Error):
error = 'method_not_allowed'
status_code = 405
authlib-0.15.5/authlib/oauth1/rfc5849/models.py 0000664 0000000 0000000 00000006553 14133261713 0021036 0 ustar 00root root 0000000 0000000
class ClientMixin(object):
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(object):
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-0.15.5/authlib/oauth1/rfc5849/parameters.py 0000664 0000000 0000000 00000006664 14133261713 0021721 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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([
'{0}="{1}"'.format(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 = 'realm="{}", '.format(realm) + header_parameters
# the auth-scheme name set to "OAuth" (case insensitive).
headers['Authorization'] = 'OAuth {}'.format(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-0.15.5/authlib/oauth1/rfc5849/resource_protector.py 0000664 0000000 0000000 00000002352 14133261713 0023474 0 ustar 00root root 0000000 0000000 from .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-0.15.5/authlib/oauth1/rfc5849/rsa.py 0000664 0000000 0000000 00000001600 14133261713 0020324 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/oauth1/rfc5849/signature.py 0000664 0000000 0000000 00000033517 14133261713 0021554 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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 = ['{0}={1}'.format(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-0.15.5/authlib/oauth1/rfc5849/util.py 0000664 0000000 0000000 00000000210 14133261713 0020510 0 ustar 00root root 0000000 0000000 from authlib.common.urls import quote, unquote
def escape(s):
return quote(s, safe=b'~')
def unescape(s):
return unquote(s)
authlib-0.15.5/authlib/oauth1/rfc5849/wrapper.py 0000664 0000000 0000000 00000007533 14133261713 0021232 0 ustar 00root root 0000000 0000000 from authlib.common.urls import (
urlparse, extract_params, url_decode,
parse_http_list, parse_keqv_list,
)
from .signature import (
SIGNATURE_TYPE_QUERY,
SIGNATURE_TYPE_BODY,
SIGNATURE_TYPE_HEADER
)
from .errors import (
InsecureTransportError,
DuplicatedOAuthProtocolParameterError
)
from .util import unescape
class OAuth1Request(object):
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-0.15.5/authlib/oauth2/ 0000775 0000000 0000000 00000000000 14133261713 0016065 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/__init__.py 0000664 0000000 0000000 00000000647 14133261713 0020205 0 ustar 00root root 0000000 0000000 from .base import OAuth2Error
from .auth import ClientAuth, TokenAuth
from .client import OAuth2Client
from .rfc6749 import (
OAuth2Request,
HttpRequest,
AuthorizationServer,
ClientAuthentication,
ResourceProtector,
)
__all__ = [
'OAuth2Error', 'ClientAuth', 'TokenAuth', 'OAuth2Client',
'OAuth2Request', 'HttpRequest', 'AuthorizationServer',
'ClientAuthentication', 'ResourceProtector',
]
authlib-0.15.5/authlib/oauth2/auth.py 0000664 0000000 0000000 00000006631 14133261713 0017406 0 ustar 00root root 0000000 0000000 import 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 = '{}:{}'.format(client.client_id, client.client_secret)
auth = to_native(base64.urlsafe_b64encode(to_bytes(text, 'latin1')))
headers['Authorization'] = 'Basic {}'.format(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(object):
"""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(object):
"""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-0.15.5/authlib/oauth2/base.py 0000664 0000000 0000000 00000002055 14133261713 0017353 0 ustar 00root root 0000000 0000000 from 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(OAuth2Error, self).__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(OAuth2Error, self).get_body()
if self.state:
error.append(('state', self.state))
return error
def __call__(self, translations=None, error_uris=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(OAuth2Error, self).__call__(translations, error_uris)
authlib-0.15.5/authlib/oauth2/client.py 0000664 0000000 0000000 00000040420 14133261713 0017715 0 ustar 00root root 0000000 0000000 from authlib.common.security import generate_token
from authlib.common.urls import url_decode
from authlib.common.encoding import text_types
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
DEFAULT_HEADERS = {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
class OAuth2Client(object):
"""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 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.
"""
client_auth_class = ClientAuth
token_auth_class = TokenAuth
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, redirect_uri=None, code_challenge_method=None,
token=None, token_placement='header', update_token=None, **metadata):
self.session = session
self.client_id = client_id
self.client_secret = client_secret
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 = {}
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, text_types) 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, **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).
"""
# implicit grant_type
authorization_response = kwargs.pop('authorization_response', None)
if authorization_response and '#' in authorization_response:
return self.token_from_fragment(authorization_response, kwargs.get('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=kwargs.get('state'),
)
kwargs['code'] = params['code']
if grant_type is None:
grant_type = self.metadata.get('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 _fetch_token(self, url, body='', headers=None, auth=None,
method='POST', **kwargs):
if method == 'GET':
if '?' in url:
url = '&'.join([url, body])
else:
url = '?'.join([url, body])
body = ''
if headers is None:
headers = DEFAULT_HEADERS
resp = self.session.request(
method, url, data=body, headers=headers, auth=auth, **kwargs)
for hook in self.compliance_hook['access_token_response']:
resp = hook(resp)
return self.parse_response_token(resp.json())
def token_from_fragment(self, authorization_response, state=None):
token = parse_implicit_response(authorization_response, state)
return self.parse_response_token(token)
def refresh_token(self, url, 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
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 _refresh_token(self, url, refresh_token=None, body='', headers=None,
auth=None, **kwargs):
resp = self.session.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.json())
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 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 _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 _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 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, token):
if 'error' not in token:
self.token = token
return self.token
error = token['error']
description = token.get('error_description', error)
self.handle_error(error, description)
def _prepare_token_endpoint_body(self, body, grant_type, **kwargs):
if grant_type is None:
grant_type = _guess_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
@staticmethod
def handle_error(error_type, error_description):
raise ValueError('{}: {}'.format(error_type, error_description))
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-0.15.5/authlib/oauth2/rfc6749/ 0000775 0000000 0000000 00000000000 14133261713 0017171 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc6749/__init__.py 0000664 0000000 0000000 00000004121 14133261713 0021300 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
authlib.oauth2.rfc6749
~~~~~~~~~~~~~~~~~~~~~~
This module represents a direct implementation of
The OAuth 2.0 Authorization Framework.
https://tools.ietf.org/html/rfc6749
"""
from .wrappers import OAuth2Request, OAuth2Token, HttpRequest
from .errors import (
OAuth2Error,
AccessDeniedError,
MissingAuthorizationError,
InvalidGrantError,
InvalidClientError,
InvalidRequestError,
InvalidScopeError,
InsecureTransportError,
UnauthorizedClientError,
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
from .token_endpoint import TokenEndpoint
from .grants import (
BaseGrant,
AuthorizationEndpointMixin,
TokenEndpointMixin,
AuthorizationCodeGrant,
ImplicitGrant,
ResourceOwnerPasswordCredentialsGrant,
ClientCredentialsGrant,
RefreshTokenGrant,
)
__all__ = [
'OAuth2Request', 'OAuth2Token', 'HttpRequest',
'OAuth2Error',
'AccessDeniedError',
'MissingAuthorizationError',
'InvalidGrantError',
'InvalidClientError',
'InvalidRequestError',
'InvalidScopeError',
'InsecureTransportError',
'UnauthorizedClientError',
'UnsupportedGrantTypeError',
'UnsupportedTokenTypeError',
'MissingCodeException',
'MissingTokenException',
'MissingTokenTypeException',
'MismatchingStateException',
'ClientMixin', 'AuthorizationCodeMixin', 'TokenMixin',
'ClientAuthentication',
'AuthorizationServer',
'ResourceProtector',
'TokenEndpoint',
'BaseGrant',
'AuthorizationEndpointMixin',
'TokenEndpointMixin',
'AuthorizationCodeGrant',
'ImplicitGrant',
'ResourceOwnerPasswordCredentialsGrant',
'ClientCredentialsGrant',
'RefreshTokenGrant',
]
authlib-0.15.5/authlib/oauth2/rfc6749/authenticate_client.py 0000664 0000000 0000000 00000010044 14133261713 0023556 0 ustar 00root root 0000000 0000000 """
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(object):
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):
for method in methods:
func = self._methods[method]
client = func(self.query_client, request)
if client:
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):
return self.authenticate(request, methods)
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_token_endpoint_auth_method('client_secret_basic') \
and client.check_client_secret(client_secret):
log.debug(
'Authenticate %s via "client_secret_basic" '
'success', client_id
)
return client
log.debug(
'Authenticate %s via "client_secret_basic" '
'failed', client_id
)
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_token_endpoint_auth_method('client_secret_post') \
and client.check_client_secret(client_secret):
log.debug(
'Authenticate %s via "client_secret_post" '
'success', client_id
)
return client
log.debug(
'Authenticate %s via "client_secret_post" '
'failed', client_id
)
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 'client_secret' not in request.data:
client = _validate_client(query_client, client_id, request.state)
if client.check_token_endpoint_auth_method('none'):
log.debug(
'Authenticate %s via "none" '
'success', client_id
)
return client
log.debug(
'Authenticate {} via "none" '
'failed'.format(client_id)
)
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-0.15.5/authlib/oauth2/rfc6749/authorization_server.py 0000664 0000000 0000000 00000022104 14133261713 0024030 0 ustar 00root root 0000000 0000000 from .authenticate_client import ClientAuthentication
from .errors import (
OAuth2Error,
InvalidGrantError,
InvalidScopeError,
UnsupportedGrantTypeError,
)
from .util import scope_to_list
class AuthorizationServer(object):
"""Authorization server that handles Authorization Endpoint and Token
Endpoint.
:param query_client: A function to get client by client_id. The client
model class MUST implement the methods described by
:class:`~authlib.oauth2.rfc6749.ClientMixin`.
:param save_token: A method to save tokens.
:param generate_token: A method to generate tokens.
:param metadata: A dict of Authorization Server Metadata
"""
def __init__(self, query_client, save_token, generate_token=None, metadata=None):
self.query_client = query_client
self.save_token = save_token
self.generate_token = generate_token
self.metadata = metadata
self._client_auth = None
self._authorization_grants = []
self._token_grants = []
self._endpoints = {}
def authenticate_client(self, request, methods):
"""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)
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_translations(self, request):
"""Return a translations instance used for i18n error messages.
Framework SHOULD implement this function.
"""
return None
def get_error_uris(self, request):
"""Return a dict of error uris mapping. Framework SHOULD implement
this function.
"""
return None
def send_signal(self, name, *args, **kwargs):
"""Framework integration can re-implement this method to support
signal system.
"""
pass
def create_oauth2_request(self, request):
"""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):
"""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.metadata:
scopes_supported = self.metadata.get('scopes_supported')
scopes = set(scope_to_list(scope))
if scopes_supported and not set(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_cls):
"""Add extra endpoint to authorization server. e.g.
RevocationEndpoint::
authorization_server.register_endpoint(RevocationEndpoint)
:param endpoint_cls: A endpoint class
"""
self._endpoints[endpoint_cls.ENDPOINT_NAME] = endpoint_cls(self)
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 InvalidGrantError(
'Response type {!r} is not supported'.format(request.response_type))
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) and \
request.method in grant_cls.TOKEN_ENDPOINT_HTTP_METHODS:
return _create_grant(grant_cls, extensions, request, self)
raise UnsupportedGrantTypeError(
'Grant type {!r} is not supported'.format(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('There is no "{}" endpoint.'.format(name))
endpoint = self._endpoints[name]
request = endpoint.create_endpoint_request(request)
try:
return self.handle_response(*endpoint(request))
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
"""
request = self.create_oauth2_request(request)
try:
grant = self.get_authorization_grant(request)
except InvalidGrantError 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(
translations=self.get_translations(request),
error_uris=self.get_error_uris(request)
))
def _create_grant(grant_cls, extensions, request, server):
grant = grant_cls(request, server)
if extensions:
for ext in extensions:
ext(grant)
return grant
authlib-0.15.5/authlib/oauth2/rfc6749/errors.py 0000664 0000000 0000000 00000014252 14133261713 0021063 0 ustar 00root root 0000000 0000000 """
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', 'InvalidGrantError',
'UnauthorizedClientError', 'UnsupportedGrantTypeError',
'InvalidScopeError', 'AccessDeniedError',
'MissingAuthorizationError', 'UnsupportedTokenTypeError',
'MissingCodeException', 'MissingTokenException',
'MissingTokenTypeException', 'MismatchingStateException',
]
class InsecureTransportError(OAuth2Error):
error = 'insecure_transport'
def get_error_description(self):
return self.gettext('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(InvalidClientError, self).get_headers()
if self.status_code == 401:
error_description = self.get_error_description()
# safe escape
error_description = error_description.replace('"', '|')
extras = [
'error="{}"'.format(self.error),
'error_description="{}"'.format(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 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'
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'
def get_error_description(self):
return self.gettext(
'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'
def get_error_description(self):
return self.gettext(
'The resource owner or authorization server denied the request')
# -- below are extended errors -- #
class MissingAuthorizationError(OAuth2Error):
error = 'missing_authorization'
status_code = 401
def get_error_description(self):
return self.gettext('Missing "Authorization" in headers.')
class UnsupportedTokenTypeError(OAuth2Error):
error = 'unsupported_token_type'
status_code = 401
# -- 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-0.15.5/authlib/oauth2/rfc6749/grants/ 0000775 0000000 0000000 00000000000 14133261713 0020467 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc6749/grants/__init__.py 0000664 0000000 0000000 00000002442 14133261713 0022602 0 ustar 00root root 0000000 0000000 """
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-0.15.5/authlib/oauth2/rfc6749/grants/authorization_code.py 0000664 0000000 0000000 00000036665 14133261713 0024753 0 ustar 00root root 0000000 0000000 import logging
from authlib.deprecate import deprecate
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,
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, 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
if hasattr(self, 'create_authorization_code'): # pragma: no cover
deprecate('Use "generate_authorization_code" instead', '1.0')
client = self.request.client
code = self.create_authorization_code(client, grant_user, self.request)
else:
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()
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 InvalidRequestError('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 InvalidRequestError('Invalid "redirect_uri" in request.')
# save for create_token_response
self.request.client = client
self.request.credential = 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.credential
user = self.authenticate_user(authorization_code)
if not user:
raise InvalidRequestError('There is no "user" for this code.')
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.request.user = user
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
"""
if hasattr(self, 'parse_authorization_code'):
deprecate('Use "query_authorization_code" instead', '1.0')
return self.parse_authorization_code(code, client)
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.query.get(authorization_code.user_id)
:param authorization_code: AuthorizationCode object
:return: user
"""
raise NotImplementedError()
def validate_code_authorization_request(grant):
client_id = grant.request.client_id
log.debug('Validate authorization request of %r', client_id)
if client_id is None:
raise InvalidClientError(state=grant.request.state)
client = grant.server.query_client(client_id)
if not client:
raise InvalidClientError(state=grant.request.state)
redirect_uri = grant.validate_authorization_redirect_uri(grant.request, client)
response_type = grant.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=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-0.15.5/authlib/oauth2/rfc6749/grants/base.py 0000664 0000000 0000000 00000011617 14133261713 0021761 0 ustar 00root root 0000000 0000000 from authlib.consts import default_json_headers
from ..errors import InvalidRequestError
class BaseGrant(object):
#: 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, server):
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
client = self.request.client
if scope is not None:
scope = client.get_allowed_scope(scope)
return self.server.generate_token(
client=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(object):
#: 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):
return request.grant_type == cls.GRANT_TYPE
def validate_token_request(self):
raise NotImplementedError()
def create_token_response(self):
raise NotImplementedError()
class AuthorizationEndpointMixin(object):
RESPONSE_TYPES = set()
ERROR_RESPONSE_FRAGMENT = False
@classmethod
def check_authorization_endpoint(cls, request):
return request.response_type in cls.RESPONSE_TYPES
@staticmethod
def validate_authorization_redirect_uri(request, client):
if request.redirect_uri:
if not client.check_redirect_uri(request.redirect_uri):
raise InvalidRequestError(
'Redirect URI {!r} is not supported by client.'.format(request.redirect_uri),
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.'
)
return redirect_uri
def validate_consent_request(self):
redirect_uri = self.validate_authorization_request()
self.execute_hook('after_validate_consent_request', redirect_uri)
def validate_authorization_request(self):
raise NotImplementedError()
def create_authorization_response(self, redirect_uri, grant_user):
raise NotImplementedError()
authlib-0.15.5/authlib/oauth2/rfc6749/grants/client_credentials.py 0000664 0000000 0000000 00000007524 14133261713 0024704 0 ustar 00root root 0000000 0000000 import 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)
"""
client = self.request.client
token = self.generate_token(scope=self.request.scope, include_refresh_token=False)
log.debug('Issue token %r to %r', token, client)
self.save_token(token)
self.execute_hook('process_token', self, token=token)
return 200, token, self.TOKEN_RESPONSE_HEADER
authlib-0.15.5/authlib/oauth2/rfc6749/grants/implicit.py 0000664 0000000 0000000 00000022121 14133261713 0022651 0 ustar 00root root 0000000 0000000 import 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-0.15.5/authlib/oauth2/rfc6749/grants/refresh_token.py 0000664 0000000 0000000 00000014547 14133261713 0023712 0 ustar 00root root 0000000 0000000 """
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 token.get_client_id() != client.get_client_id():
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
token = self._validate_request_token(client)
self._validate_token_scope(token)
self.request.credential = 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.
"""
credential = self.request.credential
user = self.authenticate_user(credential)
if not user:
raise InvalidRequestError('There is no "user" for this token.')
client = self.request.client
token = self.issue_token(user, credential)
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(credential)
return 200, token, self.TOKEN_RESPONSE_HEADER
def issue_token(self, user, credential):
expires_in = credential.get_expires_in()
scope = self.request.scope
if not scope:
scope = credential.get_scope()
token = self.generate_token(
user=user,
expires_in=expires_in,
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):
item = Token.get(refresh_token=refresh_token)
if item and item.is_refresh_token_active():
return item
:param refresh_token: The refresh token issued to the client
:return: token
"""
raise NotImplementedError()
def authenticate_user(self, credential):
"""Authenticate the user related to this credential. Developers MUST
implement this method in subclass::
def authenticate_user(self, credential):
return User.query.get(credential.user_id)
:param credential: Token object
:return: user
"""
raise NotImplementedError()
def revoke_old_credential(self, credential):
"""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, credential):
credential.revoked = True
credential.save()
:param credential: Token object
"""
raise NotImplementedError()
authlib-0.15.5/authlib/oauth2/rfc6749/grants/resource_owner_password_credentials.py 0000664 0000000 0000000 00000013203 14133261713 0030400 0 ustar 00root root 0000000 0000000 import 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.request.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-0.15.5/authlib/oauth2/rfc6749/models.py 0000664 0000000 0000000 00000015235 14133261713 0021034 0 ustar 00root root 0000000 0000000 """
authlib.oauth2.rfc6749.models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module defines how to construct Client, AuthorizationCode and Token.
"""
class ClientMixin(object):
"""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 has_client_secret(self):
"""A method returns that if the client has ``client_secret`` value.
If the value is in ``client_secret`` column::
def has_client_secret(self):
return bool(self.client_secret)
: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``::
def check_client_secret(self, client_secret):
return self.client_secret == client_secret
:param client_secret: A string of client secret
:return: bool
"""
raise NotImplementedError()
def check_token_endpoint_auth_method(self, method):
"""Check client ``token_endpoint_auth_method`` defined via `RFC7591`_.
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_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(object):
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(object):
def get_client_id(self):
"""A method to return client_id of the token. 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_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 get_expires_at(self):
"""A method to get the value when this token will be expired. e.g.
it would be::
def get_expires_at(self):
return self.created_at + self.expires_in
:return: timestamp int
"""
raise NotImplementedError()
authlib-0.15.5/authlib/oauth2/rfc6749/parameters.py 0000664 0000000 0000000 00000020116 14133261713 0021706 0 ustar 00root root 0000000 0000000 from 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]:
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()
if state and params.get('state', None) != 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-0.15.5/authlib/oauth2/rfc6749/resource_protector.py 0000664 0000000 0000000 00000002307 14133261713 0023475 0 ustar 00root root 0000000 0000000 """
authlib.oauth2.rfc6749.resource_protector
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implementation of Accessing Protected Resources per `Section 7`_.
.. _`Section 7`: https://tools.ietf.org/html/rfc6749#section-7
"""
from .errors import MissingAuthorizationError, UnsupportedTokenTypeError
class ResourceProtector(object):
def __init__(self):
self._token_validators = {}
def register_token_validator(self, validator):
if validator.TOKEN_TYPE not in self._token_validators:
self._token_validators[validator.TOKEN_TYPE] = validator
def validate_request(self, scope, request, scope_operator='AND'):
auth = request.headers.get('Authorization')
if not auth:
raise MissingAuthorizationError()
# https://tools.ietf.org/html/rfc6749#section-7.1
token_parts = auth.split(None, 1)
if len(token_parts) != 2:
raise UnsupportedTokenTypeError()
token_type, token_string = token_parts
validator = self._token_validators.get(token_type.lower())
if not validator:
raise UnsupportedTokenTypeError()
return validator(token_string, scope, request, scope_operator)
authlib-0.15.5/authlib/oauth2/rfc6749/token_endpoint.py 0000664 0000000 0000000 00000002167 14133261713 0022571 0 ustar 00root root 0000000 0000000 class TokenEndpoint(object):
#: 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=request,
methods=self.CLIENT_AUTH_METHODS,
)
request.client = client
return client
def authenticate_endpoint_credential(self, request, client):
raise NotImplementedError()
def create_endpoint_response(self, request):
raise NotImplementedError()
authlib-0.15.5/authlib/oauth2/rfc6749/util.py 0000664 0000000 0000000 00000002142 14133261713 0020517 0 ustar 00root root 0000000 0000000 import base64
import binascii
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 username, password
return query, None
authlib-0.15.5/authlib/oauth2/rfc6749/wrappers.py 0000664 0000000 0000000 00000005147 14133261713 0021415 0 ustar 00root root 0000000 0000000 import time
from authlib.common.urls import urlparse, url_decode
from .errors import InsecureTransportError
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(OAuth2Token, self).__init__(params)
def is_expired(self):
expires_at = self.get('expires_at')
if not expires_at:
return None
return expires_at < time.time()
@classmethod
def from_dict(cls, token):
if isinstance(token, dict) and not isinstance(token, cls):
token = cls(token)
return token
class OAuth2Request(object):
def __init__(self, method, uri, 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.query = urlparse.urlparse(uri).query
self.args = dict(url_decode(self.query))
self.form = self.body or {}
#: dict of query and body params
data = {}
data.update(self.args)
data.update(self.form)
self.data = data
#: authenticate method
self.auth_method = None
#: authenticated user on this request
self.user = None
#: authorization_code or token model instance
self.credential = None
#: client which sending this request
self.client = None
@property
def client_id(self):
"""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):
return self.data.get('response_type')
@property
def grant_type(self):
return self.data.get('grant_type')
@property
def redirect_uri(self):
return self.data.get('redirect_uri')
@property
def scope(self):
return self.data.get('scope')
@property
def state(self):
return self.data.get('state')
class HttpRequest(object):
def __init__(self, method, uri, data=None, headers=None):
self.method = method
self.uri = uri
self.data = data
self.headers = headers or {}
self.user = None
authlib-0.15.5/authlib/oauth2/rfc6750/ 0000775 0000000 0000000 00000000000 14133261713 0017161 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc6750/__init__.py 0000664 0000000 0000000 00000001141 14133261713 0021267 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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 InvalidRequestError, InvalidTokenError, InsufficientScopeError
from .parameters import add_bearer_token
from .wrappers import BearerToken
from .validator import BearerTokenValidator
__all__ = [
'InvalidRequestError', 'InvalidTokenError', 'InsufficientScopeError',
'add_bearer_token',
'BearerToken',
'BearerTokenValidator',
]
authlib-0.15.5/authlib/oauth2/rfc6750/errors.py 0000664 0000000 0000000 00000006246 14133261713 0021057 0 ustar 00root root 0000000 0000000 """
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
from ..rfc6749.errors import InvalidRequestError
__all__ = [
'InvalidRequestError', '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'
status_code = 401
def __init__(self, description=None, uri=None, status_code=None,
state=None, realm=None):
super(InvalidTokenError, self).__init__(
description, uri, status_code, state)
self.realm = realm
def get_error_description(self):
return self.gettext(
'The access token provided is expired, revoked, malformed, '
'or invalid for other reasons.'
)
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(InvalidTokenError, self).get_headers()
extras = []
if self.realm:
extras.append('realm="{}"'.format(self.realm))
extras.append('error="{}"'.format(self.error))
error_description = self.get_error_description()
extras.append('error_description="{}"'.format(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'
status_code = 403
def __init__(self, token_scope, required_scope):
super(InsufficientScopeError, self).__init__()
self.token_scope = token_scope
self.required_scope = required_scope
def get_error_description(self):
return self.gettext(
'The request requires higher privileges than '
'provided by the access token. '
'Required: "%(required)s", Provided:"%(provided)s"'
) % dict(required=self.required_scope, provided=self.token_scope)
authlib-0.15.5/authlib/oauth2/rfc6750/parameters.py 0000664 0000000 0000000 00000002274 14133261713 0021703 0 ustar 00root root 0000000 0000000 from 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'] = 'Bearer {}'.format(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-0.15.5/authlib/oauth2/rfc6750/validator.py 0000664 0000000 0000000 00000006307 14133261713 0021526 0 ustar 00root root 0000000 0000000 """
authlib.oauth2.rfc6750.validator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Validate Bearer Token for in request, scope and token.
"""
import time
from ..rfc6749.util import scope_to_list
from .errors import (
InvalidRequestError,
InvalidTokenError,
InsufficientScopeError
)
class BearerTokenValidator(object):
TOKEN_TYPE = 'bearer'
def __init__(self, realm=None):
self.realm = realm
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 request_invalid(self, request):
"""Check 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 request_invalid(self, request):
return 'X-Device-Version' in request.headers
Usually, you don't have to detect if the request is valid or not,
you can just return a ``False``.
:param request: instance of HttpRequest
:return: Boolean
"""
raise NotImplementedError()
def token_revoked(self, token):
"""Check if this token is revoked. Developers MUST re-implement this
method. If there is a column called ``revoked`` on the token table::
def token_revoked(self, token):
return token.revoked
:param token: token instance
:return: Boolean
"""
raise NotImplementedError()
def token_expired(self, token):
expires_at = token.get_expires_at()
if not expires_at:
return False
return expires_at < time.time()
def scope_insufficient(self, token, scope, operator='AND'):
if not scope:
return False
token_scopes = scope_to_list(token.get_scope())
if not token_scopes:
return True
token_scopes = set(token_scopes)
resource_scopes = set(scope_to_list(scope))
if operator == 'AND':
return not token_scopes.issuperset(resource_scopes)
if operator == 'OR':
return not token_scopes & resource_scopes
if callable(operator):
return not operator(token_scopes, resource_scopes)
raise ValueError('Invalid operator value')
def __call__(self, token_string, scope, request, scope_operator='AND'):
if self.request_invalid(request):
raise InvalidRequestError()
token = self.authenticate_token(token_string)
if not token:
raise InvalidTokenError(realm=self.realm)
if self.token_expired(token):
raise InvalidTokenError(realm=self.realm)
if self.token_revoked(token):
raise InvalidTokenError(realm=self.realm)
if self.scope_insufficient(token, scope, scope_operator):
raise InsufficientScopeError(token.get_scope(), scope)
return token
authlib-0.15.5/authlib/oauth2/rfc6750/wrappers.py 0000664 0000000 0000000 00000007225 14133261713 0021404 0 ustar 00root root 0000000 0000000
class BearerToken(object):
"""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"
}
:param access_token_generator: a function to generate access_token.
:param refresh_token_generator: a function to generate refresh_token,
if not provided, refresh_token will not be added into token response.
:param expires_generator: The expires_generator can be an int value or a
function. If it is int, all token expires_in will be this value. If it
is function, it can generate expires_in depending on client and
grant_type::
def expires_generator(client, grant_type):
if is_official_client(client):
return 3600 * 1000
if grant_type == 'implicit':
return 3600
return 3600 * 10
:return: Callable
When BearerToken is initialized, it will be callable::
token_generator = BearerToken(access_token_generator)
token = token_generator(client, grant_type, expires_in=None,
scope=None, include_refresh_token=True)
The callable function that BearerToken created accepts these parameters:
:param client: the client that making the request.
:param grant_type: current requested grant_type.
: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
"""
#: 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
def __call__(self, client, grant_type, user=None, scope=None,
expires_in=None, include_refresh_token=True):
access_token = self.access_token_generator(client, grant_type, user, scope)
if expires_in is None:
expires_in = self._get_expires_in(client, grant_type)
token = {
'token_type': 'Bearer',
'access_token': access_token,
'expires_in': expires_in
}
if include_refresh_token and self.refresh_token_generator:
token['refresh_token'] = self.refresh_token_generator(
client, grant_type, user, scope)
if scope:
token['scope'] = scope
return token
authlib-0.15.5/authlib/oauth2/rfc7009/ 0000775 0000000 0000000 00000000000 14133261713 0017157 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc7009/__init__.py 0000664 0000000 0000000 00000000571 14133261713 0021273 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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-0.15.5/authlib/oauth2/rfc7009/parameters.py 0000664 0000000 0000000 00000001526 14133261713 0021700 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/oauth2/rfc7009/revocation.py 0000664 0000000 0000000 00000007203 14133261713 0021704 0 ustar 00root root 0000000 0000000 from authlib.consts import default_json_headers
from ..rfc6749 import TokenEndpoint
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_endpoint_credential(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.
"""
if 'token' not in request.form:
raise InvalidRequestError()
token_type = request.form.get('token_type_hint')
if token_type and token_type not in self.SUPPORTED_TOKEN_TYPES:
raise UnsupportedTokenTypeError()
return self.query_token(request.form['token'], token_type, client)
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
credential = self.authenticate_endpoint_credential(request, client)
# the authorization server invalidates the token
if credential:
self.revoke_token(credential)
self.server.send_signal(
'after_revoke_token',
token=credential,
client=client,
)
return 200, {}, default_json_headers
def query_token(self, token, token_type_hint, client):
"""Get the token from database/storage by the given token string.
Developers should implement this method::
def query_token(self, token, token_type_hint, client):
if token_type_hint == 'access_token':
return Token.query_by_access_token(token, client.client_id)
if token_type_hint == 'refresh_token':
return Token.query_by_refresh_token(token, client.client_id)
return Token.query_by_access_token(token, client.client_id) or \
Token.query_by_refresh_token(token, client.client_id)
"""
raise NotImplementedError()
def revoke_token(self, token):
"""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):
token.revoked = True
token.save()
"""
raise NotImplementedError()
authlib-0.15.5/authlib/oauth2/rfc7521/ 0000775 0000000 0000000 00000000000 14133261713 0017156 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc7521/__init__.py 0000664 0000000 0000000 00000000103 14133261713 0021261 0 ustar 00root root 0000000 0000000 from .client import AssertionClient
__all__ = ['AssertionClient']
authlib-0.15.5/authlib/oauth2/rfc7521/client.py 0000664 0000000 0000000 00000004661 14133261713 0021015 0 ustar 00root root 0000000 0000000 from authlib.common.encoding import to_native
from authlib.oauth2.base import OAuth2Error
class AssertionClient(object):
"""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
def __init__(self, session, token_endpoint, issuer, subject,
audience=None, grant_type=None, claims=None,
token_placement='header', scope=None, **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
@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 _refresh_token(self, data):
resp = self.session.request(
'POST', self.token_endpoint, data=data, withhold_token=True)
token = resp.json()
if 'error' in token:
raise OAuth2Error(
error=token['error'],
description=token.get('error_description')
)
self.token = token
return self.token
authlib-0.15.5/authlib/oauth2/rfc7523/ 0000775 0000000 0000000 00000000000 14133261713 0017160 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc7523/__init__.py 0000664 0000000 0000000 00000001401 14133261713 0021265 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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,
register_session_client_auth_method,
)
__all__ = [
'JWTBearerGrant',
'JWTBearerClientAssertion',
'client_secret_jwt_sign',
'private_key_jwt_sign',
'ClientSecretJWT',
'PrivateKeyJWT',
'register_session_client_auth_method',
]
authlib-0.15.5/authlib/oauth2/rfc7523/assertion.py 0000664 0000000 0000000 00000004134 14133261713 0021543 0 ustar 00root root 0000000 0000000 import 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 is not None:
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, header=None, **kwargs):
return _sign(client_secret, client_id, token_endpoint,
alg, claims=claims, header=header, **kwargs)
def private_key_jwt_sign(private_key, client_id, token_endpoint, alg='RS256',
claims=None, header=None, **kwargs):
return _sign(private_key, client_id, token_endpoint,
alg, claims=claims, header=header, **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-0.15.5/authlib/oauth2/rfc7523/auth.py 0000664 0000000 0000000 00000007435 14133261713 0020504 0 ustar 00root root 0000000 0000000 from authlib.common.urls import add_params_to_qs
from authlib.deprecate import deprecate
from .assertion import client_secret_jwt_sign, private_key_jwt_sign
from .client import ASSERTION_TYPE
class ClientSecretJWT(object):
"""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
"""
name = 'client_secret_jwt'
alg = 'HS256'
def __init__(self, token_endpoint=None, claims=None, header=None):
self.token_endpoint = token_endpoint
self.claims = claims
self.header = header
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.header,
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
"""
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.header,
alg=self.alg,
)
def register_session_client_auth_method(session, token_url=None, **kwargs): # pragma: no cover
"""Register "client_secret_jwt" or "private_key_jwt" token endpoint auth
method to OAuth2Session.
:param session: OAuth2Session instance.
:param token_url: Optional token endpoint url.
"""
deprecate('Use `ClientSecretJWT` and `PrivateKeyJWT` instead', '1.0', 'Jeclj', 'ca')
if session.token_endpoint_auth_method == 'client_secret_jwt':
cls = ClientSecretJWT
elif session.token_endpoint_auth_method == 'private_key_jwt':
cls = PrivateKeyJWT
else:
raise ValueError('Invalid token_endpoint_auth_method')
session.register_client_auth_method(cls(token_url))
authlib-0.15.5/authlib/oauth2/rfc7523/client.py 0000664 0000000 0000000 00000010451 14133261713 0021011 0 ustar 00root root 0000000 0000000 import 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(object):
"""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_token_endpoint_auth_method(self.CLIENT_AUTH_METHOD):
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-0.15.5/authlib/oauth2/rfc7523/jwt_bearer.py 0000664 0000000 0000000 00000012451 14133261713 0021661 0 ustar 00root root 0000000 0000000 import logging
from authlib.jose import jwt
from authlib.jose.errors import JoseError
from ..rfc6749 import BaseGrant, TokenEndpointMixin
from ..rfc6749 import (
UnauthorizedClientError,
InvalidRequestError,
InvalidGrantError
)
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
@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 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
return {
'iss': {'essential': True},
'sub': {'essential': True},
'aud': {'essential': True},
'exp': {'essential': True},
}
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
"""
claims = jwt.decode(
assertion, self.resolve_public_key,
claims_options=self.create_claims_options())
try:
claims.validate()
except JoseError as e:
log.debug('Assertion Error: %r', e)
raise InvalidGrantError(description=e.description)
return claims
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.authenticate_client(claims)
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()
self.request.user = self.authenticate_user(client, claims)
def create_token_response(self):
"""If valid and authorized, the authorization server issues an access
token.
"""
token = self.generate_token(
scope=self.request.scope,
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 authenticate_user(self, client, claims):
"""Authenticate user with the given assertion claims. Developers MUST
implement it in subclass, e.g.::
def authenticate_user(self, client, claims):
user = User.get_by_sub(claims['sub'])
if is_authorized_to_client(user, client):
return user
:param client: OAuth Client instance
:param claims: assertion payload claims
:return: User instance
"""
raise NotImplementedError()
def authenticate_client(self, claims):
"""Authenticate client with the given assertion claims. Developers MUST
implement it in subclass, e.g.::
def authenticate_client(self, claims):
return Client.get_by_iss(claims['iss'])
:param claims: assertion payload claims
:return: Client instance
"""
raise NotImplementedError()
def resolve_public_key(self, headers, payload):
"""Find public key to verify assertion signature. Developers MUST
implement it in subclass, e.g.::
def resolve_public_key(self, headers, payload):
jwk_set = get_jwk_set_by_iss(payload['iss'])
return filter_jwk_set(jwk_set, headers['kid'])
:param headers: JWT headers dict
:param payload: JWT payload dict
:return: A public key
"""
raise NotImplementedError()
authlib-0.15.5/authlib/oauth2/rfc7591/ 0000775 0000000 0000000 00000000000 14133261713 0017165 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc7591/__init__.py 0000664 0000000 0000000 00000001233 14133261713 0021275 0 ustar 00root root 0000000 0000000 """
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-0.15.5/authlib/oauth2/rfc7591/claims.py 0000664 0000000 0000000 00000023121 14133261713 0021006 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/oauth2/rfc7591/endpoint.py 0000664 0000000 0000000 00000016525 14133261713 0021370 0 ustar 00root root 0000000 0000000 import 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.util import scope_to_list
from .claims import ClientMetadataClaims
from .errors import (
InvalidClientMetadataError,
UnapprovedSoftwareStatementError,
InvalidSoftwareStatementError,
)
class ClientRegistrationEndpoint(object):
"""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.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.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):
return response_types_supported.issuperset(set(value))
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):
return grant_types_supported.issuperset(set(value))
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 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-0.15.5/authlib/oauth2/rfc7591/errors.py 0000664 0000000 0000000 00000002112 14133261713 0021047 0 ustar 00root root 0000000 0000000 from ..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-0.15.5/authlib/oauth2/rfc7592/ 0000775 0000000 0000000 00000000000 14133261713 0017166 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc7592/__init__.py 0000664 0000000 0000000 00000000473 14133261713 0021303 0 ustar 00root root 0000000 0000000 """
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-0.15.5/authlib/oauth2/rfc7592/endpoint.py 0000664 0000000 0000000 00000015006 14133261713 0021362 0 ustar 00root root 0000000 0000000 from authlib.consts import default_json_headers
from ..rfc6749 import AccessDeniedError
from ..rfc6750 import InvalidTokenError
class ClientConfigurationEndpoint(object):
ENDPOINT_NAME = 'client_configuration'
def __init__(self, server):
self.server = server
def __call__(self, request):
return self.create_configuration_response(request)
def create_configuration_response(self, request):
token = self.authenticate_token(request)
if not token:
raise InvalidTokenError()
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)
raise InvalidTokenError()
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 AccessDeniedError()
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)
info = self.generate_client_registration_info(client, request)
body.update(info)
return 200, body, default_json_headers
def create_delete_client_response(self, client, request):
"""To deprive itself on the authorization server, the client makes
an HTTP DELETE request to the client configuration endpoint. This
request is authenticated by the registration access token issued to
the client.
The following is a non-normative example request::
DELETE /register/s6BhdRkqt3 HTTP/1.1
Host: server.example.com
Authorization: Bearer reg-23410913-abewfq.123483
"""
self.delete_client(client, request)
headers = [
('Cache-Control', 'no-store'),
('Pragma', 'no-cache'),
]
return 204, '', headers
def create_update_client_response(self, client, request):
""" To update a previously registered client's registration with an
authorization server, the client makes an HTTP PUT request to the
client configuration endpoint with a content type of "application/
json".
The following is a non-normative example request::
PUT /register/s6BhdRkqt3 HTTP/1.1
Accept: application/json
Host: server.example.com
Authorization: Bearer reg-23410913-abewfq.123483
{
"client_id": "s6BhdRkqt3",
"client_secret": "cf136dc3c1fc93f31185e5885805d",
"redirect_uris": [
"https://client.example.org/callback",
"https://client.example.org/alt"
],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "client_secret_basic",
"jwks_uri": "https://client.example.org/my_public_keys.jwks",
"client_name": "My New Example",
"client_name#fr": "Mon Nouvel Exemple",
"logo_uri": "https://client.example.org/newlogo.png",
"logo_uri#fr": "https://client.example.org/fr/newlogo.png"
}
"""
# 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:
return
# The client MUST include its "client_id" field in the request
client_id = request.data.get('client_id')
if not client_id:
raise
if client_id != client.get_client_id():
raise
# 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
client = self.save_client(client, request)
return self.create_read_client_response(client, request)
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."""
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):
raise NotImplementedError()
def revoke_access_token(self, request):
raise NotImplementedError()
def check_permission(self, client, request):
raise NotImplementedError()
def introspect_client(self, client):
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 save_client(self, client, request):
raise NotImplementedError()
authlib-0.15.5/authlib/oauth2/rfc7636/ 0000775 0000000 0000000 00000000000 14133261713 0017165 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc7636/__init__.py 0000664 0000000 0000000 00000000554 14133261713 0021302 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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-0.15.5/authlib/oauth2/rfc7636/challenge.py 0000664 0000000 0000000 00000012006 14133261713 0021460 0 ustar 00root root 0000000 0000000 import re
import hashlib
from authlib.common.encoding import to_bytes, to_unicode, urlsafe_b64encode
from ..rfc6749.errors import InvalidRequestError, InvalidGrantError
CODE_VERIFIER_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(object):
"""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 = grant.request
challenge = request.args.get('code_challenge')
method = request.args.get('code_challenge_method')
if not challenge and not method:
return
if not challenge:
raise InvalidRequestError('Missing "code_challenge"')
if method and method not in self.SUPPORTED_CODE_CHALLENGE_METHOD:
raise InvalidRequestError('Unsupported "code_challenge_method"')
def validate_code_verifier(self, grant):
request = 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.credential
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('No verify method for "{}"'.format(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-0.15.5/authlib/oauth2/rfc7662/ 0000775 0000000 0000000 00000000000 14133261713 0017164 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc7662/__init__.py 0000664 0000000 0000000 00000000555 14133261713 0021302 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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
__all__ = ['IntrospectionEndpoint', 'IntrospectionToken']
authlib-0.15.5/authlib/oauth2/rfc7662/introspection.py 0000664 0000000 0000000 00000011264 14133261713 0022442 0 ustar 00root root 0000000 0000000 import time
from 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_endpoint_credential(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.
"""
params = request.form
if 'token' not in params:
raise InvalidRequestError()
token_type = params.get('token_type_hint')
if token_type and token_type not in self.SUPPORTED_TOKEN_TYPES:
raise UnsupportedTokenTypeError()
return self.query_token(params['token'], token_type, client)
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
credential = self.authenticate_endpoint_credential(request, client)
# the authorization server invalidates the token
body = self.create_introspection_payload(credential)
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}
expires_at = token.get_expires_at()
if expires_at < time.time() or token.revoked:
return {'active': False}
payload = self.introspect_token(token)
if 'active' not in payload:
payload['active'] = True
return payload
def query_token(self, token, token_type_hint, client):
"""Get the token from database/storage by the given token string.
Developers should implement this method::
def query_token(self, token, token_type_hint, client):
if token_type_hint == 'access_token':
tok = Token.query_by_access_token(token)
elif token_type_hint == 'refresh_token':
tok = Token.query_by_refresh_token(token)
else:
tok = Token.query_by_access_token(token)
if not tok:
tok = Token.query_by_refresh_token(token)
if check_client_permission(client, tok):
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):
active = is_token_active(token)
return {
'active': active,
'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-0.15.5/authlib/oauth2/rfc7662/models.py 0000664 0000000 0000000 00000001544 14133261713 0021025 0 ustar 00root root 0000000 0000000 from ..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-0.15.5/authlib/oauth2/rfc8414/ 0000775 0000000 0000000 00000000000 14133261713 0017160 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc8414/__init__.py 0000664 0000000 0000000 00000000601 14133261713 0021266 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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-0.15.5/authlib/oauth2/rfc8414/models.py 0000664 0000000 0000000 00000042122 14133261713 0021016 0 ustar 00root root 0000000 0000000 from 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, 'validate_{}'.format(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('"{}" MUST be JSON array'.format(key))
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('"{}" is required'.format(key))
if value and 'none' in value:
raise ValueError(
'the value "none" MUST NOT be used in "{}"'.format(key))
def validate_array_value(metadata, key):
values = metadata.get(key)
if values is not None and not isinstance(values, list):
raise ValueError('"{}" MUST be JSON array'.format(key))
authlib-0.15.5/authlib/oauth2/rfc8414/well_known.py 0000664 0000000 0000000 00000001351 14133261713 0021711 0 ustar 00root root 0000000 0000000 from 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 = '/.well-known/{}{}'.format(suffix, path)
else:
url_path = '/.well-known/{}'.format(suffix)
if not external:
return url_path
return parsed.scheme + '://' + parsed.netloc + url_path
authlib-0.15.5/authlib/oauth2/rfc8628/ 0000775 0000000 0000000 00000000000 14133261713 0017167 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc8628/__init__.py 0000664 0000000 0000000 00000001276 14133261713 0021306 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
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-0.15.5/authlib/oauth2/rfc8628/device_code.py 0000664 0000000 0000000 00000017745 14133261713 0022010 0 ustar 00root root 0000000 0000000 import time
import logging
from ..rfc6749.errors import (
InvalidRequestError,
InvalidClientError,
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
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')
if not self.request.client_id:
raise InvalidRequestError('Missing "client_id" in payload')
credential = self.query_device_credential(device_code)
if not credential:
raise InvalidRequestError('Invalid "device_code" in payload')
if credential.get_client_id() != self.request.client_id:
raise UnauthorizedClientError()
client = self.authenticate_token_endpoint_client()
if not client.check_grant_type(self.GRANT_TYPE):
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):
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
exp = credential.get_expires_at()
now = time.time()
if exp < now:
raise ExpiredTokenError()
if self.should_slow_down(credential, now):
raise SlowDownError()
raise AuthorizationPendingError()
def authenticate_token_endpoint_client(self):
client = self.server.query_client(self.request.client_id)
if not client:
raise InvalidClientError()
self.server.send_signal(
'after_authenticate_client',
client=client, grant=self)
return client
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.query.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.query.get(user_id)
return user, bool(allowed)
Note, user grant information is saved by verification endpoint.
"""
raise NotImplementedError()
def should_slow_down(self, credential, now):
"""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-0.15.5/authlib/oauth2/rfc8628/endpoint.py 0000664 0000000 0000000 00000014156 14133261713 0021370 0 ustar 00root root 0000000 0000000 from authlib.consts import default_json_headers
from authlib.common.security import generate_token
from authlib.common.urls import add_params_to_uri
from ..rfc6749.errors import InvalidRequestError
class DeviceAuthorizationEndpoint(object):
"""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'
#: 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 create_endpoint_response(self, request):
# https://tools.ietf.org/html/rfc8628#section-3.1
if not request.client_id:
raise InvalidRequestError('Missing "client_id" in payload')
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-0.15.5/authlib/oauth2/rfc8628/errors.py 0000664 0000000 0000000 00000001627 14133261713 0021063 0 ustar 00root root 0000000 0000000 from ..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-0.15.5/authlib/oauth2/rfc8628/models.py 0000664 0000000 0000000 00000001142 14133261713 0021022 0 ustar 00root root 0000000 0000000
class DeviceCredentialMixin(object):
def get_client_id(self):
raise NotImplementedError()
def get_scope(self):
raise NotImplementedError()
def get_user_code(self):
raise NotImplementedError()
def get_expires_at(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_expires_at(self):
return self.get('expires_at')
authlib-0.15.5/authlib/oauth2/rfc8693/ 0000775 0000000 0000000 00000000000 14133261713 0017171 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oauth2/rfc8693/__init__.py 0000664 0000000 0000000 00000000316 14133261713 0021302 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
authlib.oauth2.rfc8693
~~~~~~~~~~~~~~~~~~~~~~
This module represents an implementation of
OAuth 2.0 Token Exchange.
https://tools.ietf.org/html/rfc8693
"""
authlib-0.15.5/authlib/oidc/ 0000775 0000000 0000000 00000000000 14133261713 0015601 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oidc/__init__.py 0000664 0000000 0000000 00000000000 14133261713 0017700 0 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oidc/core/ 0000775 0000000 0000000 00000000000 14133261713 0016531 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oidc/core/__init__.py 0000664 0000000 0000000 00000001156 14133261713 0020645 0 ustar 00root root 0000000 0000000 """
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 OpenIDCode, OpenIDHybridGrant, OpenIDImplicitGrant
__all__ = [
'AuthorizationCodeMixin',
'IDToken', 'CodeIDToken', 'ImplicitIDToken', 'HybridIDToken',
'UserInfo', 'get_claim_cls_by_response_type',
'OpenIDCode', 'OpenIDHybridGrant', 'OpenIDImplicitGrant',
]
authlib-0.15.5/authlib/oidc/core/claims.py 0000664 0000000 0000000 00000023752 14133261713 0020364 0 ustar 00root root 0000000 0000000 import 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):
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(ImplicitIDToken, self).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(HybridIDToken, self).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-0.15.5/authlib/oidc/core/errors.py 0000664 0000000 0000000 00000005503 14133261713 0020422 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/oidc/core/grants/ 0000775 0000000 0000000 00000000000 14133261713 0020027 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oidc/core/grants/__init__.py 0000664 0000000 0000000 00000000302 14133261713 0022133 0 ustar 00root root 0000000 0000000 from .code import OpenIDCode
from .implicit import OpenIDImplicitGrant
from .hybrid import OpenIDHybridGrant
__all__ = [
'OpenIDCode',
'OpenIDImplicitGrant',
'OpenIDHybridGrant',
]
authlib-0.15.5/authlib/oidc/core/grants/code.py 0000664 0000000 0000000 00000007747 14133261713 0021332 0 ustar 00root root 0000000 0000000 """
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 .util import (
is_openid_scope,
validate_nonce,
validate_request_prompt,
generate_id_token,
)
log = logging.getLogger(__name__)
class OpenIDCode(object):
"""An extension from OpenID Connect for "grant_type=code" request.
"""
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 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': 'RS512',
'iss': 'issuer-identity',
'exp': 3600
}
:param grant: AuthorizationCodeGrant instance
:return: dict
"""
raise NotImplementedError()
def generate_user_info(self, user, scope): # pragma: no cover
"""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 = grant.request
credential = request.credential
config = self.get_jwt_config(grant)
config['aud'] = self.get_audiences(request)
config['nonce'] = credential.get_nonce()
config['auth_time'] = credential.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 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-0.15.5/authlib/oidc/core/grants/hybrid.py 0000664 0000000 0000000 00000007101 14133261713 0021661 0 ustar 00root root 0000000 0000000 import logging
from authlib.deprecate import deprecate
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
item = 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,
)
item.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
if hasattr(self, 'create_authorization_code'): # pragma: no cover
deprecate('Use "generate_authorization_code" instead', '1.0')
code = self.create_authorization_code(client, grant_user, self.request)
else:
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-0.15.5/authlib/oidc/core/grants/implicit.py 0000664 0000000 0000000 00000012316 14133261713 0022216 0 ustar 00root root 0000000 0000000 import 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': 'RS512',
'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(
OpenIDImplicitGrant, self).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-0.15.5/authlib/oidc/core/grants/util.py 0000664 0000000 0000000 00000011261 14133261713 0021357 0 ustar 00root root 0000000 0000000 import time
import random
from authlib.oauth2.rfc6749 import InvalidRequestError
from authlib.oauth2.rfc6749.util 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, alg, iss, aud, exp,
nonce=None, auth_time=None, code=None):
payload = _generate_id_token_payload(
alg=alg, iss=iss, aud=aud, exp=exp, nonce=nonce,
auth_time=auth_time, code=code,
access_token=token.get('access_token'),
)
payload.update(user_info)
return _jwt_encode(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'
def _generate_id_token_payload(
alg, iss, aud, exp, nonce=None, auth_time=None,
code=None, access_token=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))
if access_token:
payload['at_hash'] = to_native(create_half_hash(access_token, alg))
return payload
def _jwt_encode(alg, payload, key):
jwt = JWT(algorithms=alg)
header = {'alg': alg}
if isinstance(key, dict):
# JWK set format
if 'keys' in key:
key = random.choice(key['keys'])
header['kid'] = key['kid']
elif 'kid' in key:
header['kid'] = key['kid']
return to_native(jwt.encode(header, payload, key))
authlib-0.15.5/authlib/oidc/core/models.py 0000664 0000000 0000000 00000000635 14133261713 0020372 0 ustar 00root root 0000000 0000000 from 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-0.15.5/authlib/oidc/core/util.py 0000664 0000000 0000000 00000000606 14133261713 0020062 0 ustar 00root root 0000000 0000000 import hashlib
from authlib.common.encoding import to_bytes, urlsafe_b64encode
def create_half_hash(s, alg):
hash_type = 'sha{}'.format(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-0.15.5/authlib/oidc/discovery/ 0000775 0000000 0000000 00000000000 14133261713 0017610 5 ustar 00root root 0000000 0000000 authlib-0.15.5/authlib/oidc/discovery/__init__.py 0000664 0000000 0000000 00000000501 14133261713 0021715 0 ustar 00root root 0000000 0000000 """
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-0.15.5/authlib/oidc/discovery/models.py 0000664 0000000 0000000 00000030503 14133261713 0021446 0 ustar 00root root 0000000 0000000 from 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(OpenIDProviderMetadata, self).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('"{}" MUST be boolean'.format(key))
authlib-0.15.5/authlib/oidc/discovery/well_known.py 0000664 0000000 0000000 00000001076 14133261713 0022345 0 ustar 00root root 0000000 0000000 from 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-0.15.5/docs/ 0000775 0000000 0000000 00000000000 14133261713 0014163 5 ustar 00root root 0000000 0000000 authlib-0.15.5/docs/Makefile 0000664 0000000 0000000 00000001134 14133261713 0015622 0 ustar 00root root 0000000 0000000 # 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-0.15.5/docs/_static/ 0000775 0000000 0000000 00000000000 14133261713 0015611 5 ustar 00root root 0000000 0000000 authlib-0.15.5/docs/_static/authlib.png 0000664 0000000 0000000 00000027215 14133261713 0017756 0 ustar 00root root 0000000 0000000 PNG
IHDR æ$ gAMA a cHRM z&