Pre Merge pull request !4 from tde3cifang/openkylin/nile

This commit is contained in:
tde3cifang 2025-07-02 09:48:10 +00:00 committed by Gitee
commit ee9468ebf0
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
73 changed files with 1741 additions and 460 deletions

View File

@ -1,6 +1,36 @@
Changelog Changelog
========= =========
3.3.1 (2025-06-19):
------------------
OAuth2.0 Client:
* #906: fix regression of expires_in parsing when float in string.
3.3.0 (2025-06-17):
------------------
OAuth2.0 Provider:
* OIDC: #879 Changed in how ui_locales is parsed
* RFC8628: Added OAuth2.0 Device Authorization Grant support
* PKCE: #876, #893 Fixed `create_code_verifier` length
* OIDC: Pre-configured OIDC server to use Refresh Token by default
OAuth2.0 Common:
* OAuth2Error: Allow 0 to be a valid state
OAuth2.0 Client:
* #745: expires_at is forced to be an int
* #899: expires_at clarification
General:
* Removed Python 3.5, 3.6, 3.7 support
* #859, #883: Added Python 3.12, 3.13 Support
* Added dependency-review GitHub Action
* Updated various references of license (SPDX identifier..)
* Added GitHub Action for lint, replaced bandy with ruff, removed isort...
* Migrated to GitHub Actions from Travis
* Added Security Policy
3.2.2 (2022-10-17) 3.2.2 (2022-10-17)
------------------ ------------------
OAuth2.0 Provider: OAuth2.0 Provider:

10
LICENSE
View File

@ -1,4 +1,4 @@
Copyright (c) 2019 The OAuthlib Community Copyright (c) The OAuthlib Community
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@ -11,14 +11,14 @@ modification, are permitted provided that the following conditions are met:
notice, this list of conditions and the following disclaimer in the notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution. documentation and/or other materials provided with the distribution.
3. Neither the name of this project nor the names of its contributors may 3. Neither the name of the copyright holder nor the names of its
be used to endorse or promote products derived from this software without contributors may be used to endorse or promote products derived from this
specific prior written permission. software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER

View File

@ -1,50 +1,65 @@
Metadata-Version: 2.1 Metadata-Version: 2.4
Name: oauthlib Name: oauthlib
Version: 3.2.2 Version: 3.3.1
Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic
Home-page: https://github.com/oauthlib/oauthlib Home-page: https://github.com/oauthlib/oauthlib
Author: The OAuthlib Community Author: The OAuthlib Community
Author-email: idan@gazit.me Maintainer: Jonathan Huot
Maintainer: Ib Lundgren Maintainer-email: jonathan.huot@gmail.com
Maintainer-email: ib.lundgren@gmail.com License: BSD-3-Clause
License: BSD
Platform: any Platform: any
Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: MacOS Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX
Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: Implementation Classifier: Programming Language :: Python :: Implementation
Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.6 Requires-Python: >=3.8
Description-Content-Type: text/x-rst Description-Content-Type: text/x-rst
Provides-Extra: rsa
Provides-Extra: signedtoken
Provides-Extra: signals
License-File: LICENSE License-File: LICENSE
Provides-Extra: rsa
Requires-Dist: cryptography>=3.0.0; extra == "rsa"
Provides-Extra: signedtoken
Requires-Dist: cryptography>=3.0.0; extra == "signedtoken"
Requires-Dist: pyjwt<3,>=2.0.0; extra == "signedtoken"
Provides-Extra: signals
Requires-Dist: blinker>=1.4.0; extra == "signals"
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: maintainer
Dynamic: maintainer-email
Dynamic: platform
Dynamic: provides-extra
Dynamic: requires-python
Dynamic: summary
OAuthLib - Python Framework for OAuth1 & OAuth2 OAuthLib - Python Framework for OAuth1 & OAuth2
=============================================== ===============================================
*A generic, spec-compliant, thorough implementation of the OAuth request-signing *A generic, spec-compliant, thorough implementation of the OAuth request-signing
logic for Python 3.6+.* logic for Python 3.8+*
.. image:: https://app.travis-ci.com/oauthlib/oauthlib.svg?branch=master .. image:: https://github.com/oauthlib/oauthlib/actions/workflows/python-build.yml/badge.svg
:target: https://app.travis-ci.com/oauthlib/oauthlib :target: https://github.com/oauthlib/oauthlib/actions
:alt: Travis :alt: GitHub Actions
.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master
:target: https://coveralls.io/r/oauthlib/oauthlib :target: https://coveralls.io/r/oauthlib/oauthlib
:alt: Coveralls :alt: Coveralls
@ -113,7 +128,9 @@ Which web frameworks are supported?
The following packages provide OAuth support using OAuthLib. The following packages provide OAuth support using OAuthLib.
- For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support. - For Django there is:
- `django-oauth-toolkit`_, which includes `Django REST framework`_ support.
- `django-allauth`_, which includes `Django REST framework`_ as well as `Django Ninja`_ support.
- For Flask there is `flask-oauthlib`_ and `Flask-Dance`_. - For Flask there is `flask-oauthlib`_ and `Flask-Dance`_.
- For Pyramid there is `pyramid-oauthlib`_. - For Pyramid there is `pyramid-oauthlib`_.
- For Bottle there is `bottle-oauthlib`_. - For Bottle there is `bottle-oauthlib`_.
@ -127,6 +144,8 @@ please open a Pull Request, updating the documentation.
.. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance .. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance
.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib .. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib
.. _`django-allauth`: https://allauth.org/
.. _`Django Ninja`: https://django-ninja.dev/
Using OAuthLib? Please get in touch! Using OAuthLib? Please get in touch!
------------------------------------ ------------------------------------
@ -148,7 +167,7 @@ have the pleasure to run into each other, please send a docs pull request =)
License License
------- -------
OAuthLib is yours to use and abuse according to the terms of the BSD license. OAuthLib is yours to use and abuse according to the terms of the BSD-3-Clause license.
Check the LICENSE file for full details. Check the LICENSE file for full details.
Credits Credits

View File

@ -2,11 +2,11 @@ OAuthLib - Python Framework for OAuth1 & OAuth2
=============================================== ===============================================
*A generic, spec-compliant, thorough implementation of the OAuth request-signing *A generic, spec-compliant, thorough implementation of the OAuth request-signing
logic for Python 3.6+.* logic for Python 3.8+*
.. image:: https://app.travis-ci.com/oauthlib/oauthlib.svg?branch=master .. image:: https://github.com/oauthlib/oauthlib/actions/workflows/python-build.yml/badge.svg
:target: https://app.travis-ci.com/oauthlib/oauthlib :target: https://github.com/oauthlib/oauthlib/actions
:alt: Travis :alt: GitHub Actions
.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master
:target: https://coveralls.io/r/oauthlib/oauthlib :target: https://coveralls.io/r/oauthlib/oauthlib
:alt: Coveralls :alt: Coveralls
@ -75,7 +75,9 @@ Which web frameworks are supported?
The following packages provide OAuth support using OAuthLib. The following packages provide OAuth support using OAuthLib.
- For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support. - For Django there is:
- `django-oauth-toolkit`_, which includes `Django REST framework`_ support.
- `django-allauth`_, which includes `Django REST framework`_ as well as `Django Ninja`_ support.
- For Flask there is `flask-oauthlib`_ and `Flask-Dance`_. - For Flask there is `flask-oauthlib`_ and `Flask-Dance`_.
- For Pyramid there is `pyramid-oauthlib`_. - For Pyramid there is `pyramid-oauthlib`_.
- For Bottle there is `bottle-oauthlib`_. - For Bottle there is `bottle-oauthlib`_.
@ -89,6 +91,8 @@ please open a Pull Request, updating the documentation.
.. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance .. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance
.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib .. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib
.. _`django-allauth`: https://allauth.org/
.. _`Django Ninja`: https://django-ninja.dev/
Using OAuthLib? Please get in touch! Using OAuthLib? Please get in touch!
------------------------------------ ------------------------------------
@ -110,7 +114,7 @@ have the pleasure to run into each other, please send a docs pull request =)
License License
------- -------
OAuthLib is yours to use and abuse according to the terms of the BSD license. OAuthLib is yours to use and abuse according to the terms of the BSD-3-Clause license.
Check the LICENSE file for full details. Check the LICENSE file for full details.
Credits Credits

6
debian/changelog vendored
View File

@ -1,3 +1,9 @@
python-oauthlib (3.3.1-ok1) nile; urgency=medium
Initial release for 3.3.1
-- tangtingting <tangtingting@kylinos.cn> Wed, 02 Jul 2025 17:48:02 +0800
python-oauthlib (3.2.2-ok1) nile; urgency=medium python-oauthlib (3.2.2-ok1) nile; urgency=medium
* Build for openKylin. * Build for openKylin.

View File

@ -1,50 +1,65 @@
Metadata-Version: 2.1 Metadata-Version: 2.4
Name: oauthlib Name: oauthlib
Version: 3.2.2 Version: 3.3.1
Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic
Home-page: https://github.com/oauthlib/oauthlib Home-page: https://github.com/oauthlib/oauthlib
Author: The OAuthlib Community Author: The OAuthlib Community
Author-email: idan@gazit.me Maintainer: Jonathan Huot
Maintainer: Ib Lundgren Maintainer-email: jonathan.huot@gmail.com
Maintainer-email: ib.lundgren@gmail.com License: BSD-3-Clause
License: BSD
Platform: any Platform: any
Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: MacOS Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX
Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: Implementation Classifier: Programming Language :: Python :: Implementation
Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.6 Requires-Python: >=3.8
Description-Content-Type: text/x-rst Description-Content-Type: text/x-rst
Provides-Extra: rsa
Provides-Extra: signedtoken
Provides-Extra: signals
License-File: LICENSE License-File: LICENSE
Provides-Extra: rsa
Requires-Dist: cryptography>=3.0.0; extra == "rsa"
Provides-Extra: signedtoken
Requires-Dist: cryptography>=3.0.0; extra == "signedtoken"
Requires-Dist: pyjwt<3,>=2.0.0; extra == "signedtoken"
Provides-Extra: signals
Requires-Dist: blinker>=1.4.0; extra == "signals"
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: maintainer
Dynamic: maintainer-email
Dynamic: platform
Dynamic: provides-extra
Dynamic: requires-python
Dynamic: summary
OAuthLib - Python Framework for OAuth1 & OAuth2 OAuthLib - Python Framework for OAuth1 & OAuth2
=============================================== ===============================================
*A generic, spec-compliant, thorough implementation of the OAuth request-signing *A generic, spec-compliant, thorough implementation of the OAuth request-signing
logic for Python 3.6+.* logic for Python 3.8+*
.. image:: https://app.travis-ci.com/oauthlib/oauthlib.svg?branch=master .. image:: https://github.com/oauthlib/oauthlib/actions/workflows/python-build.yml/badge.svg
:target: https://app.travis-ci.com/oauthlib/oauthlib :target: https://github.com/oauthlib/oauthlib/actions
:alt: Travis :alt: GitHub Actions
.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master
:target: https://coveralls.io/r/oauthlib/oauthlib :target: https://coveralls.io/r/oauthlib/oauthlib
:alt: Coveralls :alt: Coveralls
@ -113,7 +128,9 @@ Which web frameworks are supported?
The following packages provide OAuth support using OAuthLib. The following packages provide OAuth support using OAuthLib.
- For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support. - For Django there is:
- `django-oauth-toolkit`_, which includes `Django REST framework`_ support.
- `django-allauth`_, which includes `Django REST framework`_ as well as `Django Ninja`_ support.
- For Flask there is `flask-oauthlib`_ and `Flask-Dance`_. - For Flask there is `flask-oauthlib`_ and `Flask-Dance`_.
- For Pyramid there is `pyramid-oauthlib`_. - For Pyramid there is `pyramid-oauthlib`_.
- For Bottle there is `bottle-oauthlib`_. - For Bottle there is `bottle-oauthlib`_.
@ -127,6 +144,8 @@ please open a Pull Request, updating the documentation.
.. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance .. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance
.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib .. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib
.. _`django-allauth`: https://allauth.org/
.. _`Django Ninja`: https://django-ninja.dev/
Using OAuthLib? Please get in touch! Using OAuthLib? Please get in touch!
------------------------------------ ------------------------------------
@ -148,7 +167,7 @@ have the pleasure to run into each other, please send a docs pull request =)
License License
------- -------
OAuthLib is yours to use and abuse according to the terms of the BSD license. OAuthLib is yours to use and abuse according to the terms of the BSD-3-Clause license.
Check the LICENSE file for full details. Check the LICENSE file for full details.
Credits Credits

View File

@ -59,8 +59,15 @@ oauthlib/oauth2/rfc6749/grant_types/implicit.py
oauthlib/oauth2/rfc6749/grant_types/refresh_token.py oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
oauthlib/oauth2/rfc8628/__init__.py oauthlib/oauth2/rfc8628/__init__.py
oauthlib/oauth2/rfc8628/errors.py
oauthlib/oauth2/rfc8628/request_validator.py
oauthlib/oauth2/rfc8628/clients/__init__.py oauthlib/oauth2/rfc8628/clients/__init__.py
oauthlib/oauth2/rfc8628/clients/device.py oauthlib/oauth2/rfc8628/clients/device.py
oauthlib/oauth2/rfc8628/endpoints/__init__.py
oauthlib/oauth2/rfc8628/endpoints/device_authorization.py
oauthlib/oauth2/rfc8628/endpoints/pre_configured.py
oauthlib/oauth2/rfc8628/grant_types/__init__.py
oauthlib/oauth2/rfc8628/grant_types/device_code.py
oauthlib/openid/__init__.py oauthlib/openid/__init__.py
oauthlib/openid/connect/__init__.py oauthlib/openid/connect/__init__.py
oauthlib/openid/connect/core/__init__.py oauthlib/openid/connect/core/__init__.py
@ -127,8 +134,14 @@ tests/oauth2/rfc6749/grant_types/test_implicit.py
tests/oauth2/rfc6749/grant_types/test_refresh_token.py tests/oauth2/rfc6749/grant_types/test_refresh_token.py
tests/oauth2/rfc6749/grant_types/test_resource_owner_password.py tests/oauth2/rfc6749/grant_types/test_resource_owner_password.py
tests/oauth2/rfc8628/__init__.py tests/oauth2/rfc8628/__init__.py
tests/oauth2/rfc8628/test_server.py
tests/oauth2/rfc8628/clients/__init__.py tests/oauth2/rfc8628/clients/__init__.py
tests/oauth2/rfc8628/clients/test_device.py tests/oauth2/rfc8628/clients/test_device.py
tests/oauth2/rfc8628/endpoints/__init__.py
tests/oauth2/rfc8628/endpoints/test_device_application_server.py
tests/oauth2/rfc8628/endpoints/test_error_responses.py
tests/oauth2/rfc8628/grant_types/__init__.py
tests/oauth2/rfc8628/grant_types/test_device_code.py
tests/openid/__init__.py tests/openid/__init__.py
tests/openid/connect/__init__.py tests/openid/connect/__init__.py
tests/openid/connect/core/__init__.py tests/openid/connect/core/__init__.py
@ -138,6 +151,7 @@ tests/openid/connect/core/test_tokens.py
tests/openid/connect/core/endpoints/__init__.py tests/openid/connect/core/endpoints/__init__.py
tests/openid/connect/core/endpoints/test_claims_handling.py tests/openid/connect/core/endpoints/test_claims_handling.py
tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py
tests/openid/connect/core/endpoints/test_refresh_token.py
tests/openid/connect/core/endpoints/test_userinfo_endpoint.py tests/openid/connect/core/endpoints/test_userinfo_endpoint.py
tests/openid/connect/core/grant_types/__init__.py tests/openid/connect/core/grant_types/__init__.py
tests/openid/connect/core/grant_types/test_authorization_code.py tests/openid/connect/core/grant_types/test_authorization_code.py

View File

@ -5,14 +5,14 @@
A generic, spec-compliant, thorough implementation of the OAuth A generic, spec-compliant, thorough implementation of the OAuth
request-signing logic. request-signing logic.
:copyright: (c) 2019 by The OAuthlib Community :copyright: (c) The OAuthlib Community
:license: BSD, see LICENSE for details. :license: BSD-3-Clause, see LICENSE for details.
""" """
import logging import logging
from logging import NullHandler from logging import NullHandler
__author__ = 'The OAuthlib Community' __author__ = 'The OAuthlib Community'
__version__ = '3.2.2' __version__ = '3.3.1'
logging.getLogger('oauthlib').addHandler(NullHandler()) logging.getLogger('oauthlib').addHandler(NullHandler())
@ -23,7 +23,7 @@ def set_debug(debug_val):
:param debug_val: Value to set. Must be a bool value. :param debug_val: Value to set. Must be a bool value.
""" """
global _DEBUG global _DEBUG # noqa: PLW0603
_DEBUG = debug_val _DEBUG = debug_val
def get_debug(): def get_debug():

View File

@ -34,7 +34,7 @@ INVALID_HEX_PATTERN = re.compile(r'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]')
always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'abcdefghijklmnopqrstuvwxyz' 'abcdefghijklmnopqrstuvwxyz'
'0123456789' '_.-') '0123456789_.-')
log = logging.getLogger('oauthlib') log = logging.getLogger('oauthlib')
@ -198,7 +198,7 @@ def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
def generate_signed_token(private_pem, request): def generate_signed_token(private_pem, request):
import jwt import jwt # noqa: PLC0415
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
@ -216,7 +216,7 @@ def generate_signed_token(private_pem, request):
def verify_signed_token(public_pem, token): def verify_signed_token(public_pem, token):
import jwt import jwt # noqa: PLC0415
return jwt.decode(token, public_pem, algorithms=['RS256']) return jwt.decode(token, public_pem, algorithms=['RS256'])
@ -316,7 +316,7 @@ class CaseInsensitiveDict(dict):
return super().__getitem__(key) return super().__getitem__(key)
def get(self, k, default=None): def get(self, k, default=None):
return self[k] if k in self else default return self[k] if k in self else default # noqa: SIM401
def __setitem__(self, k, v): def __setitem__(self, k, v):
super().__setitem__(k, v) super().__setitem__(k, v)
@ -346,7 +346,8 @@ class Request:
def __init__(self, uri, http_method='GET', body=None, headers=None, def __init__(self, uri, http_method='GET', body=None, headers=None,
encoding='utf-8'): encoding='utf-8'):
# Convert to unicode using encoding if given, else assume unicode # Convert to unicode using encoding if given, else assume unicode
encode = lambda x: to_unicode(x, encoding) if encoding else x def encode(x):
return to_unicode(x, encoding) if encoding else x
self.uri = encode(uri) self.uri = encode(uri)
self.http_method = encode(http_method) self.http_method = encode(http_method)

View File

@ -121,7 +121,8 @@ class Client:
:param timestamp: Use this timestamp instead of using current. (Mainly for testing) :param timestamp: Use this timestamp instead of using current. (Mainly for testing)
""" """
# Convert to unicode using encoding if given, else assume unicode # Convert to unicode using encoding if given, else assume unicode
encode = lambda x: to_unicode(x, encoding) if encoding else x def encode(x):
return to_unicode(x, encoding) if encoding else x
self.client_key = encode(client_key) self.client_key = encode(client_key)
self.client_secret = encode(client_secret) self.client_secret = encode(client_secret)
@ -219,7 +220,7 @@ class Client:
content_type = request.headers.get('Content-Type', None) content_type = request.headers.get('Content-Type', None)
content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0 content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0
if request.body is not None and content_type_eligible: if request.body is not None and content_type_eligible:
params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8'))) params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8'))) # noqa: S324
return params return params

View File

@ -69,12 +69,10 @@ class BaseEndpoint:
def _create_request(self, uri, http_method, body, headers): def _create_request(self, uri, http_method, body, headers):
# Only include body data from x-www-form-urlencoded requests # Only include body data from x-www-form-urlencoded requests
headers = CaseInsensitiveDict(headers or {}) headers = CaseInsensitiveDict(headers or {})
if ("Content-Type" in headers and if "Content-Type" in headers and CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]: # noqa: SIM108
CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]):
request = Request(uri, http_method, body, headers) request = Request(uri, http_method, body, headers)
else: else:
request = Request(uri, http_method, '', headers) request = Request(uri, http_method, '', headers)
signature_type, params, oauth_params = ( signature_type, params, oauth_params = (
self._get_signature_type_and_params(request)) self._get_signature_type_and_params(request))
@ -129,8 +127,7 @@ class BaseEndpoint:
# Considerations section (`Section 4`_) before deciding on which # Considerations section (`Section 4`_) before deciding on which
# method to support. # method to support.
# .. _`Section 4`: https://tools.ietf.org/html/rfc5849#section-4 # .. _`Section 4`: https://tools.ietf.org/html/rfc5849#section-4
if (not request.signature_method in if (request.signature_method not in self.request_validator.allowed_signature_methods):
self.request_validator.allowed_signature_methods):
raise errors.InvalidSignatureMethodError( raise errors.InvalidSignatureMethodError(
description="Invalid signature, {} not in {!r}.".format( description="Invalid signature, {} not in {!r}.".format(
request.signature_method, request.signature_method,
@ -180,9 +177,7 @@ class BaseEndpoint:
def _check_signature(self, request, is_token_request=False): def _check_signature(self, request, is_token_request=False):
# ---- RSA Signature verification ---- # ---- RSA Signature verification ----
if request.signature_method == SIGNATURE_RSA_SHA1 or \ if request.signature_method in {SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512}:
request.signature_method == SIGNATURE_RSA_SHA256 or \
request.signature_method == SIGNATURE_RSA_SHA512:
# RSA-based signature method # RSA-based signature method
# The server verifies the signature per `[RFC3447] section 8.2.2`_ # The server verifies the signature per `[RFC3447] section 8.2.2`_

View File

@ -45,6 +45,7 @@ import warnings
from oauthlib.common import extract_params, safe_string_equals, urldecode from oauthlib.common import extract_params, safe_string_equals, urldecode
from . import utils from . import utils
import contextlib
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -188,10 +189,9 @@ def base_string_uri(uri: str, host: str = None) -> str:
raise ValueError('missing host') raise ValueError('missing host')
# NOTE: Try guessing if we're dealing with IP or hostname # NOTE: Try guessing if we're dealing with IP or hostname
try: with contextlib.suppress(ValueError):
hostname = ipaddress.ip_address(hostname) hostname = ipaddress.ip_address(hostname)
except ValueError:
pass
if isinstance(hostname, ipaddress.IPv6Address): if isinstance(hostname, ipaddress.IPv6Address):
hostname = f"[{hostname}]" hostname = f"[{hostname}]"
@ -568,7 +568,7 @@ def _get_jwt_rsa_algorithm(hash_algorithm_name: str):
# Not in cache: instantiate a new RSAAlgorithm # Not in cache: instantiate a new RSAAlgorithm
# PyJWT has some nice pycrypto/cryptography abstractions # PyJWT has some nice pycrypto/cryptography abstractions
import jwt.algorithms as jwt_algorithms import jwt.algorithms as jwt_algorithms # noqa: PLC0415
m = { m = {
'SHA-1': jwt_algorithms.hashes.SHA1, 'SHA-1': jwt_algorithms.hashes.SHA1,
'SHA-256': jwt_algorithms.hashes.SHA256, 'SHA-256': jwt_algorithms.hashes.SHA256,

View File

@ -30,7 +30,8 @@ def filter_params(target):
def filter_oauth_params(params): def filter_oauth_params(params):
"""Removes all non oauth parameters from a dict or a list of params.""" """Removes all non oauth parameters from a dict or a list of params."""
is_oauth = lambda kv: kv[0].startswith("oauth_") def is_oauth(kv):
return kv[0].startswith('oauth_')
if isinstance(params, dict): if isinstance(params, dict):
return list(filter(is_oauth, list(params.items()))) return list(filter(is_oauth, list(params.items())))
else: else:
@ -59,7 +60,7 @@ def unescape(u):
return unquote(u) return unquote(u)
def parse_keqv_list(l): def parse_keqv_list(l): # noqa: E741
"""A unicode-safe version of urllib2.parse_keqv_list""" """A unicode-safe version of urllib2.parse_keqv_list"""
# With Python 2.6, parse_http_list handles unicode fine # With Python 2.6, parse_http_list handles unicode fine
return urllib2.parse_keqv_list(l) return urllib2.parse_keqv_list(l)

View File

@ -5,32 +5,66 @@ oauthlib.oauth2
This module is a wrapper for the most recent implementation of OAuth 2.0 Client This module is a wrapper for the most recent implementation of OAuth 2.0 Client
and Server classes. and Server classes.
""" """
from .rfc6749.clients import ( from .rfc6749.clients import (
BackendApplicationClient, Client, LegacyApplicationClient, BackendApplicationClient,
MobileApplicationClient, ServiceApplicationClient, WebApplicationClient, Client,
LegacyApplicationClient,
MobileApplicationClient,
ServiceApplicationClient,
WebApplicationClient,
) )
from .rfc6749.endpoints import ( from .rfc6749.endpoints import (
AuthorizationEndpoint, BackendApplicationServer, IntrospectEndpoint, AuthorizationEndpoint,
LegacyApplicationServer, MetadataEndpoint, MobileApplicationServer, BackendApplicationServer,
ResourceEndpoint, RevocationEndpoint, Server, TokenEndpoint, IntrospectEndpoint,
LegacyApplicationServer,
MetadataEndpoint,
MobileApplicationServer,
ResourceEndpoint,
RevocationEndpoint,
Server,
TokenEndpoint,
WebApplicationServer, WebApplicationServer,
) )
from .rfc6749.errors import ( from .rfc6749.errors import (
AccessDeniedError, FatalClientError, InsecureTransportError, AccessDeniedError,
InvalidClientError, InvalidClientIdError, InvalidGrantError, FatalClientError,
InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InsecureTransportError,
InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError, InvalidClientError,
MissingClientIdError, MissingCodeError, MissingRedirectURIError, InvalidClientIdError,
MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, InvalidGrantError,
OAuth2Error, ServerError, TemporarilyUnavailableError, TokenExpiredError, InvalidRedirectURIError,
UnauthorizedClientError, UnsupportedGrantTypeError, InvalidRequestError,
UnsupportedResponseTypeError, UnsupportedTokenTypeError, InvalidRequestFatalError,
InvalidScopeError,
MismatchingRedirectURIError,
MismatchingStateError,
MissingClientIdError,
MissingCodeError,
MissingRedirectURIError,
MissingResponseTypeError,
MissingTokenError,
MissingTokenTypeError,
OAuth2Error,
ServerError,
TemporarilyUnavailableError,
TokenExpiredError,
UnauthorizedClientError,
UnsupportedGrantTypeError,
UnsupportedResponseTypeError,
UnsupportedTokenTypeError,
) )
from .rfc6749.grant_types import ( from .rfc6749.grant_types import (
AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant, AuthorizationCodeGrant,
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant,
ImplicitGrant,
RefreshTokenGrant,
ResourceOwnerPasswordCredentialsGrant,
) )
from .rfc6749.request_validator import RequestValidator from .rfc6749.request_validator import RequestValidator
from .rfc6749.tokens import BearerToken, OAuth2Token from .rfc6749.tokens import BearerToken, OAuth2Token
from .rfc6749.utils import is_secure_transport from .rfc6749.utils import is_secure_transport
from .rfc8628.clients import DeviceClient from .rfc8628.clients import DeviceClient
from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint, DeviceApplicationServer
from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant

View File

@ -8,17 +8,16 @@ for consuming OAuth 2.0 RFC6749.
""" """
import base64 import base64
import hashlib import hashlib
import re
import secrets
import time import time
import warnings import warnings
from oauthlib.common import generate_token from oauthlib.common import UNICODE_ASCII_CHARACTER_SET, generate_token
from oauthlib.oauth2.rfc6749 import tokens from oauthlib.oauth2.rfc6749 import tokens
from oauthlib.oauth2.rfc6749.errors import ( from oauthlib.oauth2.rfc6749.errors import (
InsecureTransportError, TokenExpiredError, InsecureTransportError, TokenExpiredError,
) )
from oauthlib.oauth2.rfc6749.parameters import ( from oauthlib.oauth2.rfc6749.parameters import (
parse_expires,
parse_token_response, prepare_token_request, parse_token_response, prepare_token_request,
prepare_token_revocation_request, prepare_token_revocation_request,
) )
@ -207,7 +206,7 @@ class Client:
case_insensitive_token_types = { case_insensitive_token_types = {
k.lower(): v for k, v in self.token_types.items()} k.lower(): v for k, v in self.token_types.items()}
if not self.token_type.lower() in case_insensitive_token_types: if self.token_type.lower() not in case_insensitive_token_types:
raise ValueError("Unsupported token type: %s" % self.token_type) raise ValueError("Unsupported token type: %s" % self.token_type)
if not (self.access_token or self.token.get('access_token')): if not (self.access_token or self.token.get('access_token')):
@ -491,11 +490,7 @@ class Client:
if not length <= 128: if not length <= 128:
raise ValueError("Length must be less than or equal to 128") raise ValueError("Length must be less than or equal to 128")
allowed_characters = re.compile('^[A-Zaa-z0-9-._~]') code_verifier = generate_token(length, UNICODE_ASCII_CHARACTER_SET + "-._~")
code_verifier = secrets.token_urlsafe(length)
if not re.search(allowed_characters, code_verifier):
raise ValueError("code_verifier contains invalid characters")
self.code_verifier = code_verifier self.code_verifier = code_verifier
@ -530,10 +525,10 @@ class Client:
""" """
code_challenge = None code_challenge = None
if code_verifier == None: if code_verifier is None:
raise ValueError("Invalid code_verifier") raise ValueError("Invalid code_verifier")
if code_challenge_method == None: if code_challenge_method is None:
code_challenge_method = "plain" code_challenge_method = "plain"
self.code_challenge_method = code_challenge_method self.code_challenge_method = code_challenge_method
code_challenge = code_verifier code_challenge = code_verifier
@ -587,15 +582,13 @@ class Client:
if 'token_type' in response: if 'token_type' in response:
self.token_type = response.get('token_type') self.token_type = response.get('token_type')
if 'expires_in' in response: vin, vat, v_at = parse_expires(response)
self.expires_in = response.get('expires_in') if vin:
self._expires_at = time.time() + int(self.expires_in) self.expires_in = vin
if vat:
if 'expires_at' in response: self.expires_at = vat
try: if v_at:
self._expires_at = int(response.get('expires_at')) self._expires_at = v_at
except:
self._expires_at = None
if 'mac_key' in response: if 'mac_key' in response:
self.mac_key = response.get('mac_key') self.mac_key = response.get('mac_key')

View File

@ -91,7 +91,7 @@ class ServiceApplicationClient(Client):
``https://provider.com/oauth2/token``. ``https://provider.com/oauth2/token``.
:param expires_at: A unix expiration timestamp for the JWT. Defaults :param expires_at: A unix expiration timestamp for the JWT. Defaults
to an hour from now, i.e. ``time.time() + 3600``. to an hour from now, i.e. ``round(time.time()) + 3600``.
:param issued_at: A unix timestamp of when the JWT was created. :param issued_at: A unix timestamp of when the JWT was created.
Defaults to now, i.e. ``time.time()``. Defaults to now, i.e. ``time.time()``.
@ -149,7 +149,7 @@ class ServiceApplicationClient(Client):
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
""" """
import jwt import jwt # noqa: PLC0415
key = private_key or self.private_key key = private_key or self.private_key
if not key: if not key:

View File

@ -74,7 +74,7 @@ class IntrospectEndpoint(BaseEndpoint):
request request
) )
if claims is None: if claims is None:
return resp_headers, json.dumps(dict(active=False)), 200 return resp_headers, json.dumps({'active': False}), 200
if "active" in claims: if "active" in claims:
claims.pop("active") claims.pop("active")
return resp_headers, json.dumps(dict(active=True, **claims)), 200 return resp_headers, json.dumps(dict(active=True, **claims)), 200

View File

@ -38,9 +38,9 @@ class MetadataEndpoint(BaseEndpoint):
""" """
def __init__(self, endpoints, claims={}, raise_errors=True): def __init__(self, endpoints, claims={}, raise_errors=True):
assert isinstance(claims, dict) assert isinstance(claims, dict) # noqa: S101
for endpoint in endpoints: for endpoint in endpoints:
assert isinstance(endpoint, BaseEndpoint) assert isinstance(endpoint, BaseEndpoint) # noqa: S101
BaseEndpoint.__init__(self) BaseEndpoint.__init__(self)
self.raise_errors = raise_errors self.raise_errors = raise_errors

View File

@ -5,9 +5,13 @@ oauthlib.oauth2.rfc6749.endpoints.pre_configured
This module is an implementation of various endpoints needed This module is an implementation of various endpoints needed
for providing OAuth 2.0 RFC6749 servers. for providing OAuth 2.0 RFC6749 servers.
""" """
from ..grant_types import ( from ..grant_types import (
AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant, AuthorizationCodeGrant,
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant,
ImplicitGrant,
RefreshTokenGrant,
ResourceOwnerPasswordCredentialsGrant,
) )
from ..tokens import BearerToken from ..tokens import BearerToken
from .authorization import AuthorizationEndpoint from .authorization import AuthorizationEndpoint
@ -15,16 +19,26 @@ from .introspect import IntrospectEndpoint
from .resource import ResourceEndpoint from .resource import ResourceEndpoint
from .revocation import RevocationEndpoint from .revocation import RevocationEndpoint
from .token import TokenEndpoint from .token import TokenEndpoint
from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant
class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, class Server(
ResourceEndpoint, RevocationEndpoint): AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, ResourceEndpoint, RevocationEndpoint
):
"""
An all-in-one endpoint featuring all four major grant types
and extension grants.
"""
"""An all-in-one endpoint featuring all four major grant types.""" def __init__(
self,
def __init__(self, request_validator, token_expires_in=None, request_validator,
token_generator=None, refresh_token_generator=None, token_expires_in=None,
*args, **kwargs): token_generator=None,
refresh_token_generator=None,
*args,
**kwargs,
):
"""Construct a new all-grants-in-one server. """Construct a new all-grants-in-one server.
:param request_validator: An implementation of :param request_validator: An implementation of
@ -40,43 +54,58 @@ class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
""" """
self.auth_grant = AuthorizationCodeGrant(request_validator) self.auth_grant = AuthorizationCodeGrant(request_validator)
self.implicit_grant = ImplicitGrant(request_validator) self.implicit_grant = ImplicitGrant(request_validator)
self.password_grant = ResourceOwnerPasswordCredentialsGrant( self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator)
request_validator)
self.credentials_grant = ClientCredentialsGrant(request_validator) self.credentials_grant = ClientCredentialsGrant(request_validator)
self.refresh_grant = RefreshTokenGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator)
self.device_code_grant = DeviceCodeGrant(request_validator, **kwargs)
self.bearer = BearerToken(request_validator, token_generator, self.bearer = BearerToken(
token_expires_in, refresh_token_generator) request_validator, token_generator, token_expires_in, refresh_token_generator
)
AuthorizationEndpoint.__init__(self, default_response_type='code', AuthorizationEndpoint.__init__(
self,
default_response_type="code",
response_types={ response_types={
'code': self.auth_grant, "code": self.auth_grant,
'token': self.implicit_grant, "token": self.implicit_grant,
'none': self.auth_grant "none": self.auth_grant,
}, },
default_token_type=self.bearer) default_token_type=self.bearer,
)
TokenEndpoint.__init__(self, default_grant_type='authorization_code', TokenEndpoint.__init__(
self,
default_grant_type="authorization_code",
grant_types={ grant_types={
'authorization_code': self.auth_grant, "authorization_code": self.auth_grant,
'password': self.password_grant, "password": self.password_grant,
'client_credentials': self.credentials_grant, "client_credentials": self.credentials_grant,
'refresh_token': self.refresh_grant, "refresh_token": self.refresh_grant,
"urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant,
}, },
default_token_type=self.bearer) default_token_type=self.bearer,
ResourceEndpoint.__init__(self, default_token='Bearer', )
token_types={'Bearer': self.bearer}) ResourceEndpoint.__init__(
self, default_token="Bearer", token_types={"Bearer": self.bearer}
)
RevocationEndpoint.__init__(self, request_validator) RevocationEndpoint.__init__(self, request_validator)
IntrospectEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator)
class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, class WebApplicationServer(
ResourceEndpoint, RevocationEndpoint): AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, ResourceEndpoint, RevocationEndpoint
):
"""An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" """An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
def __init__(self, request_validator, token_generator=None, def __init__(
token_expires_in=None, refresh_token_generator=None, **kwargs): self,
request_validator,
token_generator=None,
token_expires_in=None,
refresh_token_generator=None,
**kwargs,
):
"""Construct a new web application server. """Construct a new web application server.
:param request_validator: An implementation of :param request_validator: An implementation of
@ -92,30 +121,44 @@ class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpo
""" """
self.auth_grant = AuthorizationCodeGrant(request_validator) self.auth_grant = AuthorizationCodeGrant(request_validator)
self.refresh_grant = RefreshTokenGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator, self.bearer = BearerToken(
token_expires_in, refresh_token_generator) request_validator, token_generator, token_expires_in, refresh_token_generator
AuthorizationEndpoint.__init__(self, default_response_type='code', )
response_types={'code': self.auth_grant}, AuthorizationEndpoint.__init__(
default_token_type=self.bearer) self,
TokenEndpoint.__init__(self, default_grant_type='authorization_code', default_response_type="code",
response_types={"code": self.auth_grant},
default_token_type=self.bearer,
)
TokenEndpoint.__init__(
self,
default_grant_type="authorization_code",
grant_types={ grant_types={
'authorization_code': self.auth_grant, "authorization_code": self.auth_grant,
'refresh_token': self.refresh_grant, "refresh_token": self.refresh_grant,
}, },
default_token_type=self.bearer) default_token_type=self.bearer,
ResourceEndpoint.__init__(self, default_token='Bearer', )
token_types={'Bearer': self.bearer}) ResourceEndpoint.__init__(
self, default_token="Bearer", token_types={"Bearer": self.bearer}
)
RevocationEndpoint.__init__(self, request_validator) RevocationEndpoint.__init__(self, request_validator)
IntrospectEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator)
class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, class MobileApplicationServer(
ResourceEndpoint, RevocationEndpoint): AuthorizationEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint
):
"""An all-in-one endpoint featuring Implicit code grant and Bearer tokens.""" """An all-in-one endpoint featuring Implicit code grant and Bearer tokens."""
def __init__(self, request_validator, token_generator=None, def __init__(
token_expires_in=None, refresh_token_generator=None, **kwargs): self,
request_validator,
token_generator=None,
token_expires_in=None,
refresh_token_generator=None,
**kwargs,
):
"""Construct a new implicit grant server. """Construct a new implicit grant server.
:param request_validator: An implementation of :param request_validator: An implementation of
@ -130,27 +173,39 @@ class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint,
token-, resource-, and revocation-endpoint constructors. token-, resource-, and revocation-endpoint constructors.
""" """
self.implicit_grant = ImplicitGrant(request_validator) self.implicit_grant = ImplicitGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator, self.bearer = BearerToken(
token_expires_in, refresh_token_generator) request_validator, token_generator, token_expires_in, refresh_token_generator
AuthorizationEndpoint.__init__(self, default_response_type='token', )
response_types={ AuthorizationEndpoint.__init__(
'token': self.implicit_grant}, self,
default_token_type=self.bearer) default_response_type="token",
ResourceEndpoint.__init__(self, default_token='Bearer', response_types={"token": self.implicit_grant},
token_types={'Bearer': self.bearer}) default_token_type=self.bearer,
RevocationEndpoint.__init__(self, request_validator, )
supported_token_types=['access_token']) ResourceEndpoint.__init__(
IntrospectEndpoint.__init__(self, request_validator, self, default_token="Bearer", token_types={"Bearer": self.bearer}
supported_token_types=['access_token']) )
RevocationEndpoint.__init__(
self, request_validator, supported_token_types=["access_token"]
)
IntrospectEndpoint.__init__(
self, request_validator, supported_token_types=["access_token"]
)
class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, class LegacyApplicationServer(
ResourceEndpoint, RevocationEndpoint): TokenEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint
):
"""An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens.""" """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens."""
def __init__(self, request_validator, token_generator=None, def __init__(
token_expires_in=None, refresh_token_generator=None, **kwargs): self,
request_validator,
token_generator=None,
token_expires_in=None,
refresh_token_generator=None,
**kwargs,
):
"""Construct a resource owner password credentials grant server. """Construct a resource owner password credentials grant server.
:param request_validator: An implementation of :param request_validator: An implementation of
@ -164,30 +219,40 @@ class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint,
:param kwargs: Extra parameters to pass to authorization-, :param kwargs: Extra parameters to pass to authorization-,
token-, resource-, and revocation-endpoint constructors. token-, resource-, and revocation-endpoint constructors.
""" """
self.password_grant = ResourceOwnerPasswordCredentialsGrant( self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator)
request_validator)
self.refresh_grant = RefreshTokenGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator, self.bearer = BearerToken(
token_expires_in, refresh_token_generator) request_validator, token_generator, token_expires_in, refresh_token_generator
TokenEndpoint.__init__(self, default_grant_type='password', )
TokenEndpoint.__init__(
self,
default_grant_type="password",
grant_types={ grant_types={
'password': self.password_grant, "password": self.password_grant,
'refresh_token': self.refresh_grant, "refresh_token": self.refresh_grant,
}, },
default_token_type=self.bearer) default_token_type=self.bearer,
ResourceEndpoint.__init__(self, default_token='Bearer', )
token_types={'Bearer': self.bearer}) ResourceEndpoint.__init__(
self, default_token="Bearer", token_types={"Bearer": self.bearer}
)
RevocationEndpoint.__init__(self, request_validator) RevocationEndpoint.__init__(self, request_validator)
IntrospectEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator)
class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint, class BackendApplicationServer(
ResourceEndpoint, RevocationEndpoint): TokenEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint
):
"""An all-in-one endpoint featuring Client Credentials grant and Bearer tokens.""" """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens."""
def __init__(self, request_validator, token_generator=None, def __init__(
token_expires_in=None, refresh_token_generator=None, **kwargs): self,
request_validator,
token_generator=None,
token_expires_in=None,
refresh_token_generator=None,
**kwargs,
):
"""Construct a client credentials grant server. """Construct a client credentials grant server.
:param request_validator: An implementation of :param request_validator: An implementation of
@ -202,15 +267,21 @@ class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint,
token-, resource-, and revocation-endpoint constructors. token-, resource-, and revocation-endpoint constructors.
""" """
self.credentials_grant = ClientCredentialsGrant(request_validator) self.credentials_grant = ClientCredentialsGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator, self.bearer = BearerToken(
token_expires_in, refresh_token_generator) request_validator, token_generator, token_expires_in, refresh_token_generator
TokenEndpoint.__init__(self, default_grant_type='client_credentials', )
grant_types={ TokenEndpoint.__init__(
'client_credentials': self.credentials_grant}, self,
default_token_type=self.bearer) default_grant_type="client_credentials",
ResourceEndpoint.__init__(self, default_token='Bearer', grant_types={"client_credentials": self.credentials_grant},
token_types={'Bearer': self.bearer}) default_token_type=self.bearer,
RevocationEndpoint.__init__(self, request_validator, )
supported_token_types=['access_token']) ResourceEndpoint.__init__(
IntrospectEndpoint.__init__(self, request_validator, self, default_token="Bearer", token_types={"Bearer": self.bearer}
supported_token_types=['access_token']) )
RevocationEndpoint.__init__(
self, request_validator, supported_token_types=["access_token"]
)
IntrospectEndpoint.__init__(
self, request_validator, supported_token_types=["access_token"]
)

View File

@ -81,4 +81,4 @@ class ResourceEndpoint(BaseEndpoint):
""" """
estimates = sorted(((t.estimate_type(request), n) estimates = sorted(((t.estimate_type(request), n)
for n, t in self.tokens.items()), reverse=True) for n, t in self.tokens.items()), reverse=True)
return estimates[0][1] if len(estimates) else None return estimates[0][1] if estimates else None

View File

@ -6,6 +6,8 @@ Error used both by OAuth 2 clients and providers to represent the spec
defined error responses for all four core grant types. defined error responses for all four core grant types.
""" """
import json import json
import inspect
import sys
from oauthlib.common import add_params_to_uri, urlencode from oauthlib.common import add_params_to_uri, urlencode
@ -60,7 +62,7 @@ class OAuth2Error(Exception):
self.response_type = request.response_type self.response_type = request.response_type
self.response_mode = request.response_mode self.response_mode = request.response_mode
self.grant_type = request.grant_type self.grant_type = request.grant_type
if not state: if state is None:
self.state = request.state self.state = request.state
else: else:
self.redirect_uri = None self.redirect_uri = None
@ -150,7 +152,6 @@ class FatalClientError(OAuth2Error):
Instead the user should be informed of the error by the provider itself. Instead the user should be informed of the error by the provider itself.
""" """
pass
class InvalidRequestFatalError(FatalClientError): class InvalidRequestFatalError(FatalClientError):
@ -387,8 +388,6 @@ class CustomOAuth2Error(OAuth2Error):
def raise_from_error(error, params=None): def raise_from_error(error, params=None):
import inspect
import sys
kwargs = { kwargs = {
'description': params.get('error_description'), 'description': params.get('error_description'),
'uri': params.get('error_uri'), 'uri': params.get('error_uri'),

View File

@ -387,7 +387,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
raise errors.MissingResponseTypeError(request=request) raise errors.MissingResponseTypeError(request=request)
# Value MUST be set to "code" or one of the OpenID authorization code including # Value MUST be set to "code" or one of the OpenID authorization code including
# response_types "code token", "code id_token", "code token id_token" # response_types "code token", "code id_token", "code token id_token"
elif not 'code' in request.response_type and request.response_type != 'none': elif 'code' not in request.response_type and request.response_type != 'none':
raise errors.UnsupportedResponseTypeError(request=request) raise errors.UnsupportedResponseTypeError(request=request)
if not self.request_validator.validate_response_type(request.client_id, if not self.request_validator.validate_response_type(request.client_id,
@ -400,8 +400,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
# OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request" # OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request"
# https://tools.ietf.org/html/rfc6749#section-4.4.1 # https://tools.ietf.org/html/rfc6749#section-4.4.1
if self.request_validator.is_pkce_required(request.client_id, request) is True: if self.request_validator.is_pkce_required(request.client_id, request) is True and request.code_challenge is None:
if request.code_challenge is None:
raise errors.MissingCodeChallengeError(request=request) raise errors.MissingCodeChallengeError(request=request)
if request.code_challenge is not None: if request.code_challenge is not None:

View File

@ -143,7 +143,7 @@ class GrantTypeBase:
:type request: oauthlib.common.Request :type request: oauthlib.common.Request
""" """
# Only add a hybrid access token on auth step if asked for # Only add a hybrid access token on auth step if asked for
if not request.response_type in ["token", "code token", "id_token token", "code id_token token"]: if request.response_type not in ["token", "code token", "id_token token", "code id_token token"]:
return token return token
token.update(token_handler.create_token(request, refresh_token=False)) token.update(token_handler.create_token(request, refresh_token=False))
@ -199,10 +199,7 @@ class GrantTypeBase:
if request.response_type == 'none': if request.response_type == 'none':
state = token.get('state', None) state = token.get('state', None)
if state: token_items = [('state', state)] if state else []
token_items = [('state', state)]
else:
token_items = []
if request.response_mode == 'query': if request.response_mode == 'query':
headers['Location'] = add_params_to_uri( headers['Location'] = add_params_to_uri(

View File

@ -107,8 +107,7 @@ class ClientCredentialsGrant(GrantTypeBase):
if not self.request_validator.authenticate_client(request): if not self.request_validator.authenticate_client(request):
log.debug('Client authentication failed, %r.', request) log.debug('Client authentication failed, %r.', request)
raise errors.InvalidClientError(request=request) raise errors.InvalidClientError(request=request)
else: elif not hasattr(request.client, 'client_id'):
if not hasattr(request.client, 'client_id'):
raise NotImplementedError('Authenticate client must set the ' raise NotImplementedError('Authenticate client must set the '
'request.client.client_id attribute ' 'request.client.client_id attribute '
'in authenticate_client.') 'in authenticate_client.')

View File

@ -233,10 +233,7 @@ class ImplicitGrant(GrantTypeBase):
# In OIDC implicit flow it is possible to have a request_type that does not include the access_token! # In OIDC implicit flow it is possible to have a request_type that does not include the access_token!
# "id_token token" - return the access token and the id token # "id_token token" - return the access token and the id token
# "id_token" - don't return the access token # "id_token" - don't return the access token
if "token" in request.response_type.split(): token = token_handler.create_token(request, refresh_token=False) if 'token' in request.response_type.split() else {}
token = token_handler.create_token(request, refresh_token=False)
else:
token = {}
if request.state is not None: if request.state is not None:
token['state'] = request.state token['state'] = request.state

View File

@ -101,6 +101,9 @@ class RefreshTokenGrant(GrantTypeBase):
if not self.request_validator.authenticate_client(request): if not self.request_validator.authenticate_client(request):
log.debug('Invalid client (%r), denying access.', request) log.debug('Invalid client (%r), denying access.', request)
raise errors.InvalidClientError(request=request) raise errors.InvalidClientError(request=request)
# Ensure that request.client_id is set.
if request.client_id is None and request.client is not None:
request.client_id = request.client.client_id
elif not self.request_validator.authenticate_client_id(request.client_id, request): elif not self.request_validator.authenticate_client_id(request.client_id, request):
log.debug('Client authentication failed, %r.', request) log.debug('Client authentication failed, %r.', request)
raise errors.InvalidClientError(request=request) raise errors.InvalidClientError(request=request)

View File

@ -180,8 +180,7 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
request.password, request.client, request): request.password, request.client, request):
raise errors.InvalidGrantError( raise errors.InvalidGrantError(
'Invalid credentials given.', request=request) 'Invalid credentials given.', request=request)
else: elif not hasattr(request.client, 'client_id'):
if not hasattr(request.client, 'client_id'):
raise NotImplementedError( raise NotImplementedError(
'Validate user must set the ' 'Validate user must set the '
'request.client.client_id attribute ' 'request.client.client_id attribute '

View File

@ -150,8 +150,7 @@ def prepare_token_request(grant_type, body='', include_client_id=True, code_veri
# pull the `client_id` out of the kwargs. # pull the `client_id` out of the kwargs.
client_id = kwargs.pop('client_id', None) client_id = kwargs.pop('client_id', None)
if include_client_id: if include_client_id and client_id is not None:
if client_id is not None:
params.append(('client_id', client_id)) params.append(('client_id', client_id))
# use code_verifier if code_challenge was passed in the authorization request # use code_verifier if code_challenge was passed in the authorization request
@ -274,13 +273,13 @@ def parse_authorization_code_response(uri, state=None):
query = urlparse.urlparse(uri).query query = urlparse.urlparse(uri).query
params = dict(urlparse.parse_qsl(query)) params = dict(urlparse.parse_qsl(query))
if state and params.get('state', None) != state: if state and params.get('state') != state:
raise MismatchingStateError() raise MismatchingStateError()
if 'error' in params: if 'error' in params:
raise_from_error(params.get('error'), params) raise_from_error(params.get('error'), params)
if not 'code' in params: if 'code' not in params:
raise MissingCodeError("Missing code parameter in response.") raise MissingCodeError("Missing code parameter in response.")
return params return params
@ -337,17 +336,20 @@ def parse_implicit_response(uri, state=None, scope=None):
fragment = urlparse.urlparse(uri).fragment fragment = urlparse.urlparse(uri).fragment
params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True)) params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
for key in ('expires_in',):
if key in params: # cast things to int
params[key] = int(params[key])
if 'scope' in params: if 'scope' in params:
params['scope'] = scope_to_list(params['scope']) params['scope'] = scope_to_list(params['scope'])
if 'expires_in' in params: vin, vat, v_at = parse_expires(params)
params['expires_at'] = time.time() + int(params['expires_in']) if vin:
params['expires_in'] = vin
elif 'expires_in' in params:
params.pop('expires_in')
if vat:
params['expires_at'] = vat
elif 'expires_at' in params:
params.pop('expires_at')
if state and params.get('state', None) != state: if state and params.get('state') != state:
raise ValueError("Mismatching or missing state in params.") raise ValueError("Mismatching or missing state in params.")
params = OAuth2Token(params, old_scope=scope) params = OAuth2Token(params, old_scope=scope)
@ -424,18 +426,19 @@ def parse_token_response(body, scope=None):
# https://github.com/oauthlib/oauthlib/issues/267 # https://github.com/oauthlib/oauthlib/issues/267
params = dict(urlparse.parse_qsl(body)) params = dict(urlparse.parse_qsl(body))
for key in ('expires_in',):
if key in params: # cast things to int
params[key] = int(params[key])
if 'scope' in params: if 'scope' in params:
params['scope'] = scope_to_list(params['scope']) params['scope'] = scope_to_list(params['scope'])
if 'expires_in' in params: vin, vat, v_at = parse_expires(params)
if params['expires_in'] is None: if vin:
params['expires_in'] = vin
elif 'expires_in' in params:
params.pop('expires_in') params.pop('expires_in')
else: if vat:
params['expires_at'] = time.time() + int(params['expires_in']) params['expires_at'] = vat
elif 'expires_at' in params:
params.pop('expires_at')
params = OAuth2Token(params, old_scope=scope) params = OAuth2Token(params, old_scope=scope)
validate_token_parameters(params) validate_token_parameters(params)
@ -447,11 +450,10 @@ def validate_token_parameters(params):
if 'error' in params: if 'error' in params:
raise_from_error(params.get('error'), params) raise_from_error(params.get('error'), params)
if not 'access_token' in params: if 'access_token' not in params:
raise MissingTokenError(description="Missing access token parameter.") raise MissingTokenError(description="Missing access token parameter.")
if not 'token_type' in params: if 'token_type' not in params and os.environ.get('OAUTHLIB_STRICT_TOKEN_TYPE'):
if os.environ.get('OAUTHLIB_STRICT_TOKEN_TYPE'):
raise MissingTokenTypeError() raise MissingTokenTypeError()
# If the issued access token scope is different from the one requested by # If the issued access token scope is different from the one requested by
@ -469,3 +471,58 @@ def validate_token_parameters(params):
w.old_scope = params.old_scopes w.old_scope = params.old_scopes
w.new_scope = params.scopes w.new_scope = params.scopes
raise w raise w
def parse_expires(params):
"""Parse `expires_in`, `expires_at` fields from params
Parse following these rules:
- `expires_in` must be either integer, float or None. If a float, it is converted into an integer.
- `expires_at` is not in specification so it does its best to:
- convert into a int, else
- convert into a float, else
- reuse the same type as-is (usually string)
- `_expires_at` is a special internal value returned to be always an `int`, based
either on the presence of `expires_at`, or reuse the current time plus
`expires_in`. This is typically used to validate token expiry.
:param params: Dict with expires_in and expires_at optionally set
:return: Tuple of `expires_in`, `expires_at`, and `_expires_at`. None if not set.
"""
expires_in = None
expires_at = None
_expires_at = None
if 'expires_in' in params:
if isinstance(params.get('expires_in'), int):
expires_in = params.get('expires_in')
elif isinstance(params.get('expires_in'), float):
expires_in = int(params.get('expires_in'))
elif isinstance(params.get('expires_in'), str):
try:
# Attempt to convert to int
expires_in = int(params.get('expires_in'))
except ValueError:
raise ValueError("expires_in must be an int")
elif params.get('expires_in') is not None:
raise ValueError("expires_in must be an int")
if 'expires_at' in params:
if isinstance(params.get('expires_at'), (float, int)):
expires_at = params.get('expires_at')
_expires_at = expires_at
elif isinstance(params.get('expires_at'), str):
try:
# Attempt to convert to int first, then float if int fails
expires_at = int(params.get('expires_at'))
_expires_at = expires_at
except ValueError:
try:
expires_at = float(params.get('expires_at'))
_expires_at = expires_at
except ValueError:
# no change from str
expires_at = params.get('expires_at')
if _expires_at is None and expires_in:
expires_at = round(time.time()) + expires_in
_expires_at = expires_at
return expires_in, expires_at, _expires_at

View File

@ -24,7 +24,7 @@ class OAuth2Token(dict):
def __init__(self, params, old_scope=None): def __init__(self, params, old_scope=None):
super().__init__(params) super().__init__(params)
self._new_scope = None self._new_scope = None
if 'scope' in params and params['scope']: if params.get('scope'):
self._new_scope = set(utils.scope_to_list(params['scope'])) self._new_scope = set(utils.scope_to_list(params['scope']))
if old_scope is not None: if old_scope is not None:
self._old_scope = set(utils.scope_to_list(old_scope)) self._old_scope = set(utils.scope_to_list(old_scope))
@ -123,10 +123,7 @@ def prepare_mac_header(token, uri, key, http_method,
sch, net, path, par, query, fra = urlparse(uri) sch, net, path, par, query, fra = urlparse(uri)
if query: request_uri = path + '?' + query if query else path
request_uri = path + '?' + query
else:
request_uri = path
# Hash the body/payload # Hash the body/payload
if body is not None and draft == 0: if body is not None and draft == 0:
@ -305,10 +302,7 @@ class BearerToken(TokenBase):
"If you do, call `request_validator.save_token()` instead.", "If you do, call `request_validator.save_token()` instead.",
DeprecationWarning) DeprecationWarning)
if callable(self.expires_in): expires_in = self.expires_in(request) if callable(self.expires_in) else self.expires_in
expires_in = self.expires_in(request)
else:
expires_in = self.expires_in
request.expires_in = expires_in request.expires_in = expires_in

View File

@ -5,6 +5,12 @@ oauthlib.oauth2.rfc8628
This module is an implementation of various logic needed This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 Device Authorization RFC8628. for consuming and providing OAuth 2.0 Device Authorization RFC8628.
""" """
from oauthlib.oauth2.rfc8628.errors import (
SlowDownError,
AuthorizationPendingError,
ExpiredTokenError,
)
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@ -45,9 +45,9 @@ class DeviceClient(Client):
if scope: if scope:
params.append(('scope', list_to_scope(scope))) params.append(('scope', list_to_scope(scope)))
for k in kwargs: for k,v in kwargs.items():
if kwargs[k]: if v:
params.append((str(k), kwargs[k])) params.append((str(k), v))
return add_params_to_uri(uri, params) return add_params_to_uri(uri, params)

View File

@ -0,0 +1,10 @@
"""
oauthlib.oauth2.rfc8628
~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 Device Authorization RFC8628.
"""
from .device_authorization import DeviceAuthorizationEndpoint
from .pre_configured import DeviceApplicationServer

View File

@ -0,0 +1,232 @@
"""
oauthlib.oauth2.rfc8628
~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC8628.
"""
import logging
from typing import Callable
from oauthlib.common import Request, generate_token
from oauthlib.oauth2.rfc6749 import errors
from oauthlib.oauth2.rfc6749.endpoints.base import (
BaseEndpoint,
catch_errors_and_unavailability,
)
log = logging.getLogger(__name__)
class DeviceAuthorizationEndpoint(BaseEndpoint):
"""DeviceAuthorization endpoint - used by the client to initiate
the authorization flow by requesting a set of verification codes
from the authorization server by making an HTTP "POST" request to
the device authorization endpoint.
The client authentication requirements of Section 3.2.1 of [RFC6749]
apply to requests on this endpoint, which means that confidential
clients (those that have established client credentials) authenticate
in the same manner as when making requests to the token endpoint, and
public clients provide the "client_id" parameter to identify
themselves.
"""
def __init__(
self,
request_validator,
verification_uri,
expires_in=1800,
interval=None,
verification_uri_complete=None,
user_code_generator: Callable[[None], str] = None,
):
"""
:param request_validator: An instance of RequestValidator.
:type request_validator: oauthlib.oauth2.rfc6749.RequestValidator.
:param verification_uri: a string containing the URL that can be polled by the client application
:param expires_in: a number that represents the lifetime of the `user_code` and `device_code`
:param interval: an option number that represents the number of seconds between each poll requests
:param verification_uri_complete: a string of a function that can be called with `user_data` as parameter
:param user_code_generator: a callable that returns a configurable user code
"""
self.request_validator = request_validator
self._expires_in = expires_in
self._interval = interval
self._verification_uri = verification_uri
self._verification_uri_complete = verification_uri_complete
self.user_code_generator = user_code_generator
BaseEndpoint.__init__(self)
@property
def interval(self):
"""The minimum amount of time in seconds that the client
SHOULD wait between polling requests to the token endpoint. If no
value is provided, clients MUST use 5 as the default.
"""
return self._interval
@property
def expires_in(self):
"""The lifetime in seconds of the "device_code" and "user_code"."""
return self._expires_in
@property
def verification_uri(self):
"""The end-user verification URI on the authorization
server. The URI should be short and easy to remember as end users
will be asked to manually type it into their user agent.
"""
return self._verification_uri
def verification_uri_complete(self, user_code):
if not self._verification_uri_complete:
return None
if isinstance(self._verification_uri_complete, str):
return self._verification_uri_complete.format(user_code=user_code)
if callable(self._verification_uri_complete):
return self._verification_uri_complete(user_code)
return None
@catch_errors_and_unavailability
def validate_device_authorization_request(self, request):
"""Validate the device authorization request.
The client_id is 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]`_.
.. _`Section 3.2.1. of [RFC6749]`: https://www.rfc-editor.org/rfc/rfc6749#section-3.2.1
.. _`Section 2.2 of [RFC6749]`: https://www.rfc-editor.org/rfc/rfc6749#section-2.2
"""
# First check duplicate parameters
for param in ("client_id", "scope"):
try:
duplicate_params = request.duplicate_params
except ValueError:
raise errors.InvalidRequestFatalError(
description="Unable to parse query string", request=request
)
if param in duplicate_params:
raise errors.InvalidRequestFatalError(
description="Duplicate %s parameter." % param, request=request
)
# the "application/x-www-form-urlencoded" format, per Appendix B of [RFC6749]
# https://www.rfc-editor.org/rfc/rfc6749#appendix-B
if request.headers["Content-Type"] != "application/x-www-form-urlencoded":
raise errors.InvalidRequestError(
"Content-Type must be application/x-www-form-urlencoded",
request=request,
)
# REQUIRED. The client identifier as described in Section 2.2.
# https://tools.ietf.org/html/rfc6749#section-2.2
# TODO: extract client_id an helper validation function.
if not request.client_id:
raise errors.MissingClientIdError(request=request)
if not self.request_validator.validate_client_id(request.client_id, request):
raise errors.InvalidClientIdError(request=request)
# The client authentication requirements of Section 3.2.1 of [RFC6749]
# apply to requests on this endpoint, which means that confidential
# clients (those that have established client credentials) authenticate
# in the same manner as when making requests to the token endpoint, and
# public clients provide the "client_id" parameter to identify
# themselves.
self._raise_on_invalid_client(request)
@catch_errors_and_unavailability
def create_device_authorization_response(
self, uri, http_method="POST", body=None, headers=None
):
"""
Generate a unique device verification code and an end-user code that are valid for a limited time.
Include them in the HTTP response body using the "application/json" format [RFC8259] with a
200 (OK) status code, as described in `Section-3.2`_.
:param uri: The full URI of the token request.
:type uri: str
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param user_code_generator:
A callable that returns a string for the user code.
This allows the caller to decide how the `user_code` should be formatted.
:type user_code_generator: Callable[[], str]
:return: A tuple of three elements:
1. A dict of headers to set on the response.
2. The response body as a string.
3. The response status code as an integer.
:rtype: tuple
The response contains the following parameters:
device_code
**REQUIRED.** The device verification code.
user_code
**REQUIRED.** The end-user verification code.
verification_uri
**REQUIRED.** The end-user verification URI on the authorization server.
The URI should be short and easy to remember as end users will be asked
to manually type it into their user agent.
verification_uri_complete
**OPTIONAL.** A verification URI that includes the `user_code` (or
other information with the same function as the `user_code`), which is
designed for non-textual transmission.
expires_in
**REQUIRED.** The lifetime in seconds of the `device_code` and `user_code`.
interval
**OPTIONAL.** The minimum amount of time in seconds that the client
SHOULD wait between polling requests to the token endpoint. If no
value is provided, clients MUST use 5 as the default.
**For example:**
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "https://example.com/device",
"verification_uri_complete":
"https://example.com/device?user_code=WDJB-MJHT",
"expires_in": 1800,
"interval": 5
}
.. _`Section-3.2`: https://www.rfc-editor.org/rfc/rfc8628#section-3.2
"""
request = Request(uri, http_method, body, headers)
self.validate_device_authorization_request(request)
log.debug("Pre resource owner authorization validation ok for %r.", request)
headers = {}
user_code = self.user_code_generator() if self.user_code_generator else generate_token()
data = {
"verification_uri": self.verification_uri,
"expires_in": self.expires_in,
"user_code": user_code,
"device_code": generate_token(),
}
if self.interval is not None:
data["interval"] = self.interval
verification_uri_complete = self.verification_uri_complete(user_code)
if verification_uri_complete:
data["verification_uri_complete"] = verification_uri_complete
return headers, data, 200

View File

@ -0,0 +1,36 @@
from oauthlib.oauth2.rfc8628.endpoints.device_authorization import (
DeviceAuthorizationEndpoint,
)
from typing import Callable, Optional
from oauthlib.openid.connect.core.request_validator import RequestValidator
class DeviceApplicationServer(DeviceAuthorizationEndpoint):
"""An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
def __init__(
self,
request_validator: RequestValidator,
verification_uri: str,
interval: int = 5,
verification_uri_complete: Optional[str] = None, # noqa: FA100
user_code_generator: Callable[[None], str] = None,
**kwargs,
):
"""Construct a new web application server.
:param request_validator: An implementation of
oauthlib.oauth2.rfc8626.RequestValidator.
:param interval: How long the device needs to wait before polling the server
:param verification_uri: the verification_uri to be send back.
:param user_code_generator: a callable that allows the user code to be configured.
"""
DeviceAuthorizationEndpoint.__init__(
self,
request_validator,
interval=interval,
verification_uri=verification_uri,
user_code_generator=user_code_generator,
verification_uri_complete=verification_uri_complete,
)

View File

@ -0,0 +1,55 @@
from oauthlib.oauth2.rfc6749.errors import OAuth2Error
"""
oauthlib.oauth2.rfc8628.errors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Error used both by OAuth2 clients and providers to represent the spec
defined error responses specific to the the device grant
"""
class AuthorizationPendingError(OAuth2Error):
"""
For the device authorization grant;
The authorization request is still pending as the end user hasn't
yet completed the user-interaction steps (Section 3.3). The
client SHOULD repeat the access token request to the token
endpoint (a process known as polling). Before each new request,
the client MUST wait at least the number of seconds specified by
the "interval" parameter of the device authorization response,
or 5 seconds if none was provided, and respect any
increase in the polling interval required by the "slow_down"
error.
"""
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"
class AccessDenied(OAuth2Error):
"""
The authorization request was denied.
"""
error = "access_denied"

View File

@ -0,0 +1 @@
from oauthlib.oauth2.rfc8628.grant_types.device_code import DeviceCodeGrant

View File

@ -0,0 +1,111 @@
from __future__ import annotations
import json
from typing import Callable
from oauthlib import common # noqa: TC001
from oauthlib.oauth2.rfc6749 import errors as rfc6749_errors
from oauthlib.oauth2.rfc6749.grant_types.base import GrantTypeBase
class DeviceCodeGrant(GrantTypeBase):
def create_authorization_response(
self, request: common.Request, token_handler: Callable
) -> tuple[dict, str, int]:
"""
Validate the device flow request -> create the access token
-> persist the token -> return the token.
"""
headers = self._get_default_headers()
try:
self.validate_token_request(request)
except rfc6749_errors.OAuth2Error as e:
headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request, refresh_token=False)
for modifier in self._token_modifiers:
token = modifier(token)
self.request_validator.save_token(token, request)
return self.create_token_response(request, token_handler)
def validate_token_request(self, request: common.Request) -> None:
"""
Performs the necessary check against the request to ensure
it's allowed to retrieve a token.
"""
for validator in self.custom_validators.pre_token:
validator(request)
if not getattr(request, "grant_type", None):
raise rfc6749_errors.InvalidRequestError(
"Request is missing grant type.", request=request
)
if request.grant_type != "urn:ietf:params:oauth:grant-type:device_code":
raise rfc6749_errors.UnsupportedGrantTypeError(request=request)
for param in ("grant_type", "scope"):
if param in request.duplicate_params:
raise rfc6749_errors.InvalidRequestError(
description=f"Duplicate {param} parameter.", request=request
)
if not self.request_validator.authenticate_client(request):
raise rfc6749_errors.InvalidClientError(request=request)
elif not hasattr(request.client, "client_id"):
raise NotImplementedError(
"Authenticate client must set the "
"request.client.client_id attribute "
"in authenticate_client."
)
# Ensure client is authorized use of this grant type
self.validate_grant_type(request)
request.client_id = request.client_id or request.client.client_id
self.validate_scopes(request)
for validator in self.custom_validators.post_token:
validator(request)
def create_token_response(
self, request: common.Request, token_handler: Callable
) -> tuple[dict, str, int]:
"""Return token or error in json format.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
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`_.
.. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
"""
headers = self._get_default_headers()
try:
if self.request_validator.client_authentication_required(
request
) and not self.request_validator.authenticate_client(request):
raise rfc6749_errors.InvalidClientError(request=request)
self.validate_token_request(request)
except rfc6749_errors.OAuth2Error as e:
headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request, self.refresh_token)
self.request_validator.save_token(token, request)
return headers, json.dumps(token), 200

View File

@ -0,0 +1,25 @@
from oauthlib.oauth2 import RequestValidator as OAuth2RequestValidator
class RequestValidator(OAuth2RequestValidator):
def client_authentication_required(self, request, *args, **kwargs):
"""Determine if client authentication is required for current request.
According to the rfc8628, client authentication is required in the following cases:
- Device Authorization Request follows the, the client authentication requirements
of Section 3.2.1 of [RFC6749] apply to requests on this endpoint, which means that
confidential clients (those that have established client credentials) authenticate
in the same manner as when making requests to the token endpoint, and
public clients provide the "client_id" parameter to identify themselves,
see `Section 3.1`_.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
- Device Authorization Request
.. _`Section 3.1`: https://www.rfc-editor.org/rfc/rfc8628#section-3.1
"""
return True

View File

@ -5,34 +5,60 @@ oauthlib.openid.connect.core.endpoints.pre_configured
This module is an implementation of various endpoints needed This module is an implementation of various endpoints needed
for providing OpenID Connect servers. for providing OpenID Connect servers.
""" """
from oauthlib.oauth2.rfc6749.endpoints import ( from oauthlib.oauth2.rfc6749.endpoints import (
AuthorizationEndpoint, IntrospectEndpoint, ResourceEndpoint, AuthorizationEndpoint,
RevocationEndpoint, TokenEndpoint, IntrospectEndpoint,
ResourceEndpoint,
RevocationEndpoint,
TokenEndpoint,
) )
from oauthlib.oauth2.rfc6749.grant_types import ( from oauthlib.oauth2.rfc6749.grant_types import (
AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
ClientCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant, ClientCredentialsGrant,
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant,
ResourceOwnerPasswordCredentialsGrant,
) )
from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant
from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.oauth2.rfc6749.tokens import BearerToken
from ..grant_types import AuthorizationCodeGrant, HybridGrant, ImplicitGrant from ..grant_types import (
AuthorizationCodeGrant,
HybridGrant,
ImplicitGrant,
RefreshTokenGrant,
)
from ..grant_types.dispatchers import ( from ..grant_types.dispatchers import (
AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher, AuthorizationCodeGrantDispatcher,
AuthorizationTokenGrantDispatcher,
ImplicitTokenGrantDispatcher, ImplicitTokenGrantDispatcher,
) )
from ..tokens import JWTToken from ..tokens import JWTToken
from .userinfo import UserInfoEndpoint from .userinfo import UserInfoEndpoint
class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, class Server(
ResourceEndpoint, RevocationEndpoint, UserInfoEndpoint): AuthorizationEndpoint,
IntrospectEndpoint,
TokenEndpoint,
ResourceEndpoint,
RevocationEndpoint,
UserInfoEndpoint,
):
"""
An all-in-one endpoint featuring all four major grant types
and extension grants.
"""
"""An all-in-one endpoint featuring all four major grant types.""" def __init__(
self,
def __init__(self, request_validator, token_expires_in=None, request_validator,
token_generator=None, refresh_token_generator=None, token_expires_in=None,
*args, **kwargs): token_generator=None,
refresh_token_generator=None,
*args,
**kwargs,
):
"""Construct a new all-grants-in-one server. """Construct a new all-grants-in-one server.
:param request_validator: An implementation of :param request_validator: An implementation of
@ -48,50 +74,66 @@ class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
""" """
self.auth_grant = OAuth2AuthorizationCodeGrant(request_validator) self.auth_grant = OAuth2AuthorizationCodeGrant(request_validator)
self.implicit_grant = OAuth2ImplicitGrant(request_validator) self.implicit_grant = OAuth2ImplicitGrant(request_validator)
self.password_grant = ResourceOwnerPasswordCredentialsGrant( self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator)
request_validator)
self.credentials_grant = ClientCredentialsGrant(request_validator) self.credentials_grant = ClientCredentialsGrant(request_validator)
self.refresh_grant = RefreshTokenGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator)
self.openid_connect_auth = AuthorizationCodeGrant(request_validator) self.openid_connect_auth = AuthorizationCodeGrant(request_validator)
self.openid_connect_implicit = ImplicitGrant(request_validator) self.openid_connect_implicit = ImplicitGrant(request_validator)
self.openid_connect_hybrid = HybridGrant(request_validator) self.openid_connect_hybrid = HybridGrant(request_validator)
self.device_code_grant = DeviceCodeGrant(request_validator, **kwargs)
self.bearer = BearerToken(request_validator, token_generator, self.bearer = BearerToken(
token_expires_in, refresh_token_generator) request_validator, token_generator, token_expires_in, refresh_token_generator
)
self.jwt = JWTToken(request_validator, token_generator, self.jwt = JWTToken(
token_expires_in, refresh_token_generator) request_validator, token_generator, token_expires_in, refresh_token_generator
)
self.auth_grant_choice = AuthorizationCodeGrantDispatcher(default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth) self.auth_grant_choice = AuthorizationCodeGrantDispatcher(
self.implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit) default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth
)
self.implicit_grant_choice = ImplicitTokenGrantDispatcher(
default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit
)
# See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations
# internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination
AuthorizationEndpoint.__init__(self, default_response_type='code', AuthorizationEndpoint.__init__(
self,
default_response_type="code",
response_types={ response_types={
'code': self.auth_grant_choice, "code": self.auth_grant_choice,
'token': self.implicit_grant_choice, "token": self.implicit_grant_choice,
'id_token': self.openid_connect_implicit, "id_token": self.openid_connect_implicit,
'id_token token': self.openid_connect_implicit, "id_token token": self.openid_connect_implicit,
'code token': self.openid_connect_hybrid, "code token": self.openid_connect_hybrid,
'code id_token': self.openid_connect_hybrid, "code id_token": self.openid_connect_hybrid,
'code id_token token': self.openid_connect_hybrid, "code id_token token": self.openid_connect_hybrid,
'none': self.auth_grant "none": self.auth_grant,
}, },
default_token_type=self.bearer) default_token_type=self.bearer,
)
self.token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth) self.token_grant_choice = AuthorizationTokenGrantDispatcher(
request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth
)
TokenEndpoint.__init__(self, default_grant_type='authorization_code', TokenEndpoint.__init__(
self,
default_grant_type="authorization_code",
grant_types={ grant_types={
'authorization_code': self.token_grant_choice, "authorization_code": self.token_grant_choice,
'password': self.password_grant, "password": self.password_grant,
'client_credentials': self.credentials_grant, "client_credentials": self.credentials_grant,
'refresh_token': self.refresh_grant, "refresh_token": self.refresh_grant,
"urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant,
}, },
default_token_type=self.bearer) default_token_type=self.bearer,
ResourceEndpoint.__init__(self, default_token='Bearer', )
token_types={'Bearer': self.bearer, 'JWT': self.jwt}) ResourceEndpoint.__init__(
self, default_token="Bearer", token_types={"Bearer": self.bearer, "JWT": self.jwt}
)
RevocationEndpoint.__init__(self, request_validator) RevocationEndpoint.__init__(self, request_validator)
IntrospectEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator)
UserInfoEndpoint.__init__(self, request_validator) UserInfoEndpoint.__init__(self, request_validator)

View File

@ -5,6 +5,9 @@ oauthlib.oauth2.rfc6749.errors
Error used both by OAuth 2 clients and providers to represent the spec Error used both by OAuth 2 clients and providers to represent the spec
defined error responses for all four core grant types. defined error responses for all four core grant types.
""" """
import inspect
import sys
from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error
@ -72,8 +75,8 @@ class InvalidRequestURI(OpenIDClientError):
contains invalid data. contains invalid data.
""" """
error = 'invalid_request_uri' error = 'invalid_request_uri'
description = 'The request_uri in the Authorization Request returns an ' \ description = ('The request_uri in the Authorization Request returns an '
'error or contains invalid data.' 'error or contains invalid data.')
class InvalidRequestObject(OpenIDClientError): class InvalidRequestObject(OpenIDClientError):
@ -137,8 +140,6 @@ class InsufficientScopeError(OAuth2Error):
def raise_from_error(error, params=None): def raise_from_error(error, params=None):
import inspect
import sys
kwargs = { kwargs = {
'description': params.get('error_description'), 'description': params.get('error_description'),
'uri': params.get('error_uri'), 'uri': params.get('error_uri'),

View File

@ -310,11 +310,15 @@ class GrantTypeBase:
msg = "Session user does not match client supplied user." msg = "Session user does not match client supplied user."
raise LoginRequired(request=request, description=msg) raise LoginRequired(request=request, description=msg)
ui_locales = request.ui_locales if request.ui_locales else []
if hasattr(ui_locales, 'split'):
ui_locales = ui_locales.strip().split()
request_info = { request_info = {
'display': request.display, 'display': request.display,
'nonce': request.nonce, 'nonce': request.nonce,
'prompt': prompt, 'prompt': prompt,
'ui_locales': request.ui_locales.split() if request.ui_locales else [], 'ui_locales': ui_locales,
'id_token_hint': request.id_token_hint, 'id_token_hint': request.id_token_hint,
'login_hint': request.login_hint, 'login_hint': request.login_hint,
'claims': request.claims 'claims': request.claims

View File

@ -80,9 +80,9 @@ class AuthorizationTokenGrantDispatcher(Dispatcher):
handler = self.default_grant handler = self.default_grant
scopes = () scopes = ()
parameters = dict(request.decoded_body) parameters = dict(request.decoded_body)
client_id = parameters.get('client_id', None) client_id = parameters.get('client_id')
code = parameters.get('code', None) code = parameters.get('code')
redirect_uri = parameters.get('redirect_uri', None) redirect_uri = parameters.get('redirect_uri')
# If code is not present fallback to `default_grant` which will # If code is not present fallback to `default_grant` which will
# raise an error for the missing `code` in `create_token_response` step. # raise an error for the missing `code` in `create_token_response` step.

View File

@ -54,8 +54,7 @@ class HybridGrant(GrantTypeBase):
# Token. Sufficient entropy MUST be present in the `nonce` # Token. Sufficient entropy MUST be present in the `nonce`
# values used to prevent attackers from guessing values. For # values used to prevent attackers from guessing values. For
# implementation notes, see Section 15.5.2. # implementation notes, see Section 15.5.2.
if request.response_type in ["code id_token", "code id_token token"]: if request.response_type in ["code id_token", "code id_token token"] and not request.nonce:
if not request.nonce:
raise InvalidRequestError( raise InvalidRequestError(
request=request, request=request,
description='Request is missing mandatory nonce parameter.' description='Request is missing mandatory nonce parameter.'

View File

@ -143,7 +143,7 @@ class RequestValidator(OAuth2RequestValidator):
Token MUST NOT be accepted by the RP when performing Token MUST NOT be accepted by the RP when performing
authentication with the OP. authentication with the OP.
Additionals claims must be added, note that `request.scope` Additional claims must be added, note that `request.scope`
should be used to determine the list of claims. should be used to determine the list of claims.
More information can be found at `OpenID Connect Core#Claims`_ More information can be found at `OpenID Connect Core#Claims`_

View File

@ -27,10 +27,7 @@ class JWTToken(TokenBase):
def create_token(self, request, refresh_token=False): def create_token(self, request, refresh_token=False):
"""Create a JWT Token, using requestvalidator method.""" """Create a JWT Token, using requestvalidator method."""
if callable(self.expires_in): expires_in = self.expires_in(request) if callable(self.expires_in) else self.expires_in
expires_in = self.expires_in(request)
else:
expires_in = self.expires_in
request.expires_in = expires_in request.expires_in = expires_in

View File

@ -7,7 +7,7 @@ signals_available = False
try: try:
from blinker import Namespace from blinker import Namespace
signals_available = True signals_available = True
except ImportError: # noqa except ImportError:
class Namespace: class Namespace:
def signal(self, name, doc=None): def signal(self, name, doc=None):
return _FakeSignal(name, doc) return _FakeSignal(name, doc)
@ -26,7 +26,8 @@ except ImportError: # noqa
raise RuntimeError('signalling support is unavailable ' raise RuntimeError('signalling support is unavailable '
'because the blinker library is ' 'because the blinker library is '
'not installed.') 'not installed.')
send = lambda *a, **kw: None def send(*a, **kw):
return None
connect = disconnect = has_receivers_for = receivers_for = \ connect = disconnect = has_receivers_for = receivers_for = \
temporarily_connected_to = connected_to = _fail temporarily_connected_to = connected_to = _fail
del _fail del _fail

View File

@ -174,8 +174,7 @@ URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? (?: \# %(fragment)s
URI_reference = r"^(?: %(URI)s | %(relative_ref)s )$" % locals() URI_reference = r"^(?: %(URI)s | %(relative_ref)s )$" % locals()
# absolute-URI = scheme ":" hier-part [ "?" query ] # absolute-URI = scheme ":" hier-part [ "?" query ]
absolute_URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? )$" % locals( absolute_URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? )$" % locals() # noqa: N816
)
def is_uri(uri): def is_uri(uri):

View File

@ -1,5 +1,5 @@
[metadata] [metadata]
license_file = LICENSE license_files = LICENSE
[isort] [isort]
combine_as_imports = true combine_as_imports = true

View File

@ -1,8 +1,9 @@
#!/usr/bin/env python3
# Hack because logging + setuptools sucks. # Hack because logging + setuptools sucks.
try: import contextlib
with contextlib.suppress(ImportError):
import multiprocessing import multiprocessing
except ImportError:
pass
from os.path import dirname, join from os.path import dirname, join
@ -27,14 +28,13 @@ setup(
long_description=fread('README.rst'), long_description=fread('README.rst'),
long_description_content_type='text/x-rst', long_description_content_type='text/x-rst',
author='The OAuthlib Community', author='The OAuthlib Community',
author_email='idan@gazit.me', maintainer='Jonathan Huot',
maintainer='Ib Lundgren', maintainer_email='jonathan.huot@gmail.com',
maintainer_email='ib.lundgren@gmail.com',
url='https://github.com/oauthlib/oauthlib', url='https://github.com/oauthlib/oauthlib',
platforms='any', platforms='any',
license='BSD', license='BSD-3-Clause',
packages=find_packages(exclude=('docs', 'tests', 'tests.*')), packages=find_packages(exclude=('docs', 'examples', 'tests', 'tests.*')),
python_requires='>=3.6', python_requires='>=3.8',
extras_require={ extras_require={
'rsa': rsa_require, 'rsa': rsa_require,
'signedtoken': signedtoken_require, 'signedtoken': signedtoken_require,
@ -44,18 +44,17 @@ setup(
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment', 'Environment :: Web Environment',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved',
'License :: OSI Approved :: BSD License',
'Operating System :: MacOS', 'Operating System :: MacOS',
'Operating System :: POSIX', 'Operating System :: POSIX',
'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: Linux',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation',
'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: CPython',

View File

@ -304,7 +304,7 @@ class ClientValidator(RequestValidator):
def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
request, request_token=None, access_token=None): request, request_token=None, access_token=None):
resource_owner_key = request_token if request_token else access_token resource_owner_key = request_token if request_token else access_token
return not (client_key, nonce, timestamp, resource_owner_key) in self.nonces return (client_key, nonce, timestamp, resource_owner_key) not in self.nonces
def validate_client_key(self, client_key): def validate_client_key(self, client_key):
return client_key in self.clients return client_key in self.clients

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from jwt import InvalidKeyError
from oauthlib.oauth1.rfc5849.signature import ( from oauthlib.oauth1.rfc5849.signature import (
base_string_uri, collect_parameters, normalize_parameters, base_string_uri, collect_parameters, normalize_parameters,
sign_hmac_sha1_with_client, sign_hmac_sha256_with_client, sign_hmac_sha1_with_client, sign_hmac_sha256_with_client,
@ -82,12 +83,13 @@ class SignatureTests(TestCase):
# ==== Example test vector ======================================= # ==== Example test vector =======================================
eg_signature_base_string =\ eg_signature_base_string = (
'POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q' \ '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_' \ '%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_'
'key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m' \ 'key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m'
'ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk' \ 'ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk'
'9d7dh3k39sjv7' '9d7dh3k39sjv7'
)
# The _signature base string_ above is copied from the end of # The _signature base string_ above is copied from the end of
# RFC 5849 section 3.4.1.1. # RFC 5849 section 3.4.1.1.
@ -101,11 +103,11 @@ class SignatureTests(TestCase):
eg_base_string_uri = 'http://example.com/request' eg_base_string_uri = 'http://example.com/request'
eg_normalized_parameters =\ eg_normalized_parameters = (
'a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj' \ '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' \ 'dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1'
'&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7' '&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7'
)
# The above _normalized parameters_ corresponds to the parameters below. # The above _normalized parameters_ corresponds to the parameters below.
# #
# The parameters below is copied from the table at the end of # The parameters below is copied from the table at the end of
@ -133,12 +135,12 @@ class SignatureTests(TestCase):
eg_body = 'c2&a3=2+q' eg_body = 'c2&a3=2+q'
eg_authorization_header =\ eg_authorization_header = (
'OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2",' \ 'OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2",'
' oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1",' \ ' oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1",'
' oauth_timestamp="137131201", oauth_nonce="7d8f3e4a",' \ ' oauth_timestamp="137131201", oauth_nonce="7d8f3e4a",'
' oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"' ' oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"'
)
# ==== Signature base string calculating function tests ========== # ==== Signature base string calculating function tests ==========
def test_signature_base_string(self): def test_signature_base_string(self):
@ -465,10 +467,10 @@ class SignatureTests(TestCase):
expected_signature_hmac_sha256 = \ expected_signature_hmac_sha256 = \
'wdfdHUKXHbOnOGZP8WFAWMSAmWzN3EVBWWgXGlC/Eo4=' 'wdfdHUKXHbOnOGZP8WFAWMSAmWzN3EVBWWgXGlC/Eo4='
expected_signature_hmac_sha512 = \ expected_signature_hmac_sha512 = (
'u/vlyZFDxOWOZ9UUXwRBJHvq8/T4jCA74ocRmn2ECnjUBTAeJiZIRU8hDTjS88Tz' \ 'u/vlyZFDxOWOZ9UUXwRBJHvq8/T4jCA74ocRmn2ECnjUBTAeJiZIRU8hDTjS88Tz'
'1fGONffMpdZxUkUTW3k1kg==' '1fGONffMpdZxUkUTW3k1kg=='
)
def test_sign_hmac_sha1_with_client(self): def test_sign_hmac_sha1_with_client(self):
""" """
Test sign and verify with HMAC-SHA1. Test sign and verify with HMAC-SHA1.
@ -632,21 +634,21 @@ GLYT3Jw1Lfb1bbuck9Y0JsRJO7uydWUbxXyZ+8YaDfE2NMw7sh2vAgMBAAE=
# Note: the "echo -n" is needed to remove the last newline character, which # Note: the "echo -n" is needed to remove the last newline character, which
# most text editors will add. # most text editors will add.
expected_signature_rsa_sha1 = \ expected_signature_rsa_sha1 = (
'mFY2KOEnlYWsTvUA+5kxuBIcvBYXu+ljw9ttVJQxKduMueGSVPCB1tK1PlqVLK738' \ 'mFY2KOEnlYWsTvUA+5kxuBIcvBYXu+ljw9ttVJQxKduMueGSVPCB1tK1PlqVLK738'
'HK0t19ecBJfb6rMxUwrriw+MlBO+jpojkZIWccw1J4cAb4qu4M81DbpUAq4j/1w/Q' \ 'HK0t19ecBJfb6rMxUwrriw+MlBO+jpojkZIWccw1J4cAb4qu4M81DbpUAq4j/1w/Q'
'yTR4TWCODlEfN7Zfgy8+pf+TjiXfIwRC1jEWbuL1E=' 'yTR4TWCODlEfN7Zfgy8+pf+TjiXfIwRC1jEWbuL1E='
)
expected_signature_rsa_sha256 = \ expected_signature_rsa_sha256 = (
'jqKl6m0WS69tiVJV8ZQ6aQEfJqISoZkiPBXRv6Al2+iFSaDpfeXjYm+Hbx6m1azR' \ 'jqKl6m0WS69tiVJV8ZQ6aQEfJqISoZkiPBXRv6Al2+iFSaDpfeXjYm+Hbx6m1azR'
'drZ/35PM3cvuid3LwW/siAkzb0xQcGnTyAPH8YcGWzmnKGY7LsB7fkqThchNxvRK' \ 'drZ/35PM3cvuid3LwW/siAkzb0xQcGnTyAPH8YcGWzmnKGY7LsB7fkqThchNxvRK'
'/N7s9M1WMnfZZ+1dQbbwtTs1TG1+iexUcV7r3M7Heec=' '/N7s9M1WMnfZZ+1dQbbwtTs1TG1+iexUcV7r3M7Heec='
)
expected_signature_rsa_sha512 = \ expected_signature_rsa_sha512 = (
'jL1CnjlsNd25qoZVHZ2oJft47IRYTjpF5CvCUjL3LY0NTnbEeVhE4amWXUFBe9GL' \ 'jL1CnjlsNd25qoZVHZ2oJft47IRYTjpF5CvCUjL3LY0NTnbEeVhE4amWXUFBe9GL'
'DWdUh/79ZWNOrCirBFIP26cHLApjYdt4ZG7EVK0/GubS2v8wT1QPRsog8zyiMZkm' \ 'DWdUh/79ZWNOrCirBFIP26cHLApjYdt4ZG7EVK0/GubS2v8wT1QPRsog8zyiMZkm'
'g4JXdWCGXG8YRvRJTg+QKhXuXwS6TcMNakrgzgFIVhA=' 'g4JXdWCGXG8YRvRJTg+QKhXuXwS6TcMNakrgzgFIVhA='
)
def test_sign_rsa_sha1_with_client(self): def test_sign_rsa_sha1_with_client(self):
""" """
Test sign and verify with RSA-SHA1. Test sign and verify with RSA-SHA1.
@ -764,12 +766,17 @@ MmgDHR2tt8KeYTSgfU+BAkBcaVF91EQ7VXhvyABNYjeYP7lU7orOgdWMa/zbLXSU
# Signing needs a private key # Signing needs a private key
for bad_value in [None, '', 'foobar']: for bad_value in [None, '']:
self.assertRaises(ValueError, self.assertRaises(ValueError,
sign_rsa_sha1_with_client, sign_rsa_sha1_with_client,
self.eg_signature_base_string, self.eg_signature_base_string,
MockClient(rsa_key=bad_value)) MockClient(rsa_key=bad_value))
self.assertRaises(InvalidKeyError,
sign_rsa_sha1_with_client,
self.eg_signature_base_string,
MockClient(rsa_key='foobar'))
self.assertRaises(AttributeError, self.assertRaises(AttributeError,
sign_rsa_sha1_with_client, sign_rsa_sha1_with_client,
self.eg_signature_base_string, self.eg_signature_base_string,

View File

@ -53,11 +53,11 @@ class UtilsTests(TestCase):
# The following is an isolated test function used to test the filter_params decorator. # The following is an isolated test function used to test the filter_params decorator.
@filter_params @filter_params
def special_test_function(params, realm=None): def special_test_function(params, realm=None):
""" I am a special test function """ """I am a special test function"""
return 'OAuth ' + ','.join(['='.join([k, v]) for k, v in params]) return 'OAuth ' + ','.join(['='.join([k, v]) for k, v in params])
# check that the docstring got through # check that the docstring got through
self.assertEqual(special_test_function.__doc__, " I am a special test function ") self.assertEqual(special_test_function.__doc__, "I am a special test function")
# Check that the decorator filtering works as per design. # Check that the decorator filtering works as per design.
# Any param that does not start with 'oauth' # Any param that does not start with 'oauth'

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime import datetime
import json
from unittest.mock import patch
from oauthlib import common from oauthlib import common
from oauthlib.oauth2 import Client, InsecureTransportError, TokenExpiredError from oauthlib.oauth2 import Client, InsecureTransportError, TokenExpiredError
@ -302,31 +304,6 @@ class ClientTest(TestCase):
self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'}) self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'})
self.assertFormBodyEqual(b, 'grant_type=refresh_token&scope={}&refresh_token={}'.format(scope, token)) self.assertFormBodyEqual(b, 'grant_type=refresh_token&scope={}&refresh_token={}'.format(scope, token))
def test_parse_token_response_invalid_expires_at(self):
token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",'
' "token_type":"example",'
' "expires_at":"2006-01-02T15:04:05Z",'
' "scope":"/profile",'
' "example_parameter":"example_value"}')
token = {
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_at": "2006-01-02T15:04:05Z",
"scope": ["/profile"],
"example_parameter": "example_value"
}
client = Client(self.client_id)
# Parse code and state
response = client.parse_request_body_response(token_json, scope=["/profile"])
self.assertEqual(response, token)
self.assertEqual(None, client._expires_at)
self.assertEqual(client.access_token, response.get("access_token"))
self.assertEqual(client.refresh_token, response.get("refresh_token"))
self.assertEqual(client.token_type, response.get("token_type"))
def test_create_code_verifier_min_length(self): def test_create_code_verifier_min_length(self):
client = Client(self.client_id) client = Client(self.client_id)
length = 43 length = 43
@ -339,6 +316,12 @@ class ClientTest(TestCase):
code_verifier = client.create_code_verifier(length=length) code_verifier = client.create_code_verifier(length=length)
self.assertEqual(client.code_verifier, code_verifier) self.assertEqual(client.code_verifier, code_verifier)
def test_create_code_verifier_length(self):
client = Client(self.client_id)
length = 96
code_verifier = client.create_code_verifier(length=length)
self.assertEqual(len(code_verifier), length)
def test_create_code_challenge_plain(self): def test_create_code_challenge_plain(self):
client = Client(self.client_id) client = Client(self.client_id)
code_verifier = client.create_code_verifier(length=128) code_verifier = client.create_code_verifier(length=128)
@ -353,3 +336,43 @@ class ClientTest(TestCase):
code_verifier = client.create_code_verifier(length=128) code_verifier = client.create_code_verifier(length=128)
code_challenge_s256 = client.create_code_challenge(code_verifier=code_verifier, code_challenge_method='S256') code_challenge_s256 = client.create_code_challenge(code_verifier=code_verifier, code_challenge_method='S256')
self.assertEqual(code_challenge_s256, client.code_challenge) self.assertEqual(code_challenge_s256, client.code_challenge)
def test_parse_token_response_expires_at_types(self):
for title, fieldjson, expected, generated in [
('int', 1661185148, 1661185148, 1661185148),
('float', 1661185148.6437678, 1661185148.6437678, 1661185148.6437678),
('str', "\"2006-01-02T15:04:05Z\"", "2006-01-02T15:04:05Z", None),
('str-as-int', "\"1661185148\"", 1661185148, 1661185148),
('str-as-float', "\"1661185148.42\"", 1661185148.42, 1661185148.42),
]:
with self.subTest(msg=title):
token_json = ('{{ "access_token":"2YotnFZFEjr1zCsicMWpAA",'
' "token_type":"example",'
' "expires_at":{expires_at},'
' "scope":"/profile",'
' "example_parameter":"example_value"}}'.format(expires_at=fieldjson))
client = Client(self.client_id)
response = client.parse_request_body_response(token_json, scope=["/profile"])
self.assertEqual(response['expires_at'], expected, "response attribute wrong")
self.assertEqual(client.expires_at, expected, "client attribute wrong")
if generated:
self.assertEqual(client._expires_at, generated, "internal expiration wrong")
@patch('time.time')
def test_parse_token_response_generated_expires_at_is_int(self, t):
t.return_value = 1661185148.6437678
expected_expires_at = round(t.return_value) + 3600
token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",'
' "token_type":"example",'
' "expires_in":3600,'
' "scope":"/profile",'
' "example_parameter":"example_value"}')
client = Client(self.client_id)
response = client.parse_request_body_response(token_json, scope=["/profile"])
self.assertEqual(response['expires_at'], expected_expires_at)
self.assertEqual(client._expires_at, expected_expires_at)

View File

@ -166,7 +166,7 @@ mfvGGg3xNjTMO7IdrwIDAQAB
@patch('time.time') @patch('time.time')
def test_parse_token_response(self, t): def test_parse_token_response(self, t):
t.return_value = time() t.return_value = time()
self.token['expires_at'] = self.token['expires_in'] + t.return_value self.token['expires_at'] = self.token['expires_in'] + round(t.return_value)
client = ServiceApplicationClient(self.client_id) client = ServiceApplicationClient(self.client_id)

View File

@ -252,18 +252,34 @@ class WebApplicationClientTest(TestCase):
self.assertEqual(r4b_params['client_id'], self.client_id) self.assertEqual(r4b_params['client_id'], self.client_id)
# scenario Warnings # scenario Warnings
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always") # catch all
# warning1 - raise a DeprecationWarning if a `client_id` is submitted # warning1 - raise a DeprecationWarning if a `client_id` is submitted
rWarnings1 = client.prepare_request_body(client_id=self.client_id) with self.assertWarns(DeprecationWarning):
self.assertEqual(len(w), 1) client.prepare_request_body(client_id=self.client_id)
self.assertIsInstance(w[0].message, DeprecationWarning)
# testing the exact warning message in Python2&Python3 is a pain # testing the exact warning message in Python2&Python3 is a pain
# scenario Exceptions # scenario Exceptions
# exception1 - raise a ValueError if the a different `client_id` is submitted # exception1 - raise a ValueError if the a different `client_id` is submitted
with self.assertRaises(ValueError) as cm: with self.assertWarns(DeprecationWarning), self.assertRaises(ValueError):
client.prepare_request_body(client_id='different_client_id') client.prepare_request_body(client_id='different_client_id')
# testing the exact exception message in Python2&Python3 is a pain # testing the exact exception message in Python2&Python3 is a pain
def test_expires_in_as_str(self):
"""
see regression issue #906
"""
client = WebApplicationClient(
client_id="dummy",
token={"access_token": "xyz", "expires_in": "3600"}
)
self.assertIsNotNone(client)
client = WebApplicationClient(
client_id="dummy",
token={"access_token": "xyz", "expires_in": 3600}
)
self.assertIsNotNone(client)
client = WebApplicationClient(
client_id="dummy",
token={"access_token": "xyz", "expires_in": 3600.12}
)
self.assertIsNotNone(client)

View File

@ -20,8 +20,8 @@ class MetadataEndpointTest(TestCase):
"introspection_endpoint": "https://foo.bar/introspect", "introspection_endpoint": "https://foo.bar/introspect",
"token_endpoint": "https://foo.bar/token" "token_endpoint": "https://foo.bar/token"
} }
from oauthlib.oauth2 import Server as OAuth2Server from oauthlib.oauth2 import Server as OAuth2Server # noqa: PLC0415
from oauthlib.openid import Server as OpenIDServer from oauthlib.openid import Server as OpenIDServer # noqa: PLC0415
endpoint = OAuth2Server(None) endpoint = OAuth2Server(None)
metadata = MetadataEndpoint([endpoint], default_claims) metadata = MetadataEndpoint([endpoint], default_claims)
@ -98,6 +98,7 @@ class MetadataEndpointTest(TestCase):
"scopes_supported": ["email", "profile"], "scopes_supported": ["email", "profile"],
"grant_types_supported": [ "grant_types_supported": [
"authorization_code", "authorization_code",
"urn:ietf:params:oauth:grant-type:device_code",
"password", "password",
"client_credentials", "client_credentials",
"refresh_token", "refresh_token",
@ -130,8 +131,8 @@ class MetadataEndpointTest(TestCase):
} }
def sort_list(claims): def sort_list(claims):
for k in claims.keys(): for key, value in claims.items():
claims[k] = sorted(claims[k]) claims[key] = sorted(value)
sort_list(metadata.claims) sort_list(metadata.claims)
sort_list(expected_claims) sort_list(expected_claims)

View File

@ -130,6 +130,22 @@ class RefreshTokenGrantTest(TestCase):
self.request) self.request)
self.mock_validator.client_authentication_required.assert_called_once_with(self.request) self.mock_validator.client_authentication_required.assert_called_once_with(self.request)
def test_authentication_required_populate_client_id(self):
"""
Make sure that request.client_id is populated from
request.client.client_id if None.
"""
self.mock_validator.client_authentication_required.return_value = True
self.mock_validator.authenticate_client.return_value = True
# self.mock_validator.authenticate_client_id.return_value = False
# self.request.code = 'waffles'
self.request.client_id = None
self.request.client.client_id = 'foobar'
self.auth.validate_token_request(self.request)
self.request.client_id = 'foobar'
def test_invalid_grant_type(self): def test_invalid_grant_type(self):
self.request.grant_type = 'wrong_type' self.request.grant_type = 'wrong_type'
self.assertRaises(errors.UnsupportedGrantTypeError, self.assertRaises(errors.UnsupportedGrantTypeError,
@ -168,7 +184,7 @@ class RefreshTokenGrantTest(TestCase):
# all ok but without request.scope # all ok but without request.scope
del self.request.scope del self.request.scope
self.auth.validate_token_request(self.request) self.auth.validate_token_request(self.request)
self.assertEqual(self.request.scopes, 'foo bar baz'.split()) self.assertEqual(self.request.scopes, ['foo', 'bar', 'baz'])
# CORS # CORS

View File

@ -302,3 +302,30 @@ class ParameterTests(TestCase):
finally: finally:
signals.scope_changed.disconnect(record_scope_change) signals.scope_changed.disconnect(record_scope_change)
del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
def test_parse_expires(self):
for title, arg, expected in [
('none', (None, None), (None, None, None)),
('expires_in only', (3600, None), (3600, 4600, 4600)),
('expires_in and expires_at', (3600, 200), (3600, 200, 200)),
('expires_in and expires_at float', (3600, 200.42), (3600, 200.42, 200.42)),
('expires_in and expires_at str-int', (3600, "200"), (3600, 200, 200)),
('expires_in and expires_at str-float', (3600, "200.42"), (3600, 200.42, 200.42)),
('expires_in float only', (3600.12, None), (3600, 4600, 4600)),
('expires_in float and expires_at', (3600.12, 200), (3600, 200, 200)),
('expires_in float and expires_at float', (3600.12, 200.42), (3600, 200.42, 200.42)),
('expires_in float and expires_at str-int', (3600.12, "200"), (3600, 200, 200)),
('expires_in float and expires_at str-float', (3600.12, "200.42"), (3600, 200.42, 200.42)),
('expires_in str only', ("3600", None), (3600, 4600, 4600)),
('expires_in str and expires_at', ("3600", 200), (3600, 200, 200)),
('expires_in str and expires_at float', ("3600", 200.42), (3600, 200.42, 200.42)),
('expires_in str and expires_at str-int', ("3600", "200"), (3600, 200, 200)),
('expires_in str and expires_at str-float', ("3600", "200.42"), (3600, 200.42, 200.42)),
]:
with self.subTest(msg=title):
params = {
"expires_in": arg[0],
"expires_at": arg[1]
}
self.assertEqual(expected, parse_expires(params))

View File

@ -76,7 +76,7 @@ class TokenTest(TestCase):
bearer_uri = 'http://server.example.com/resource?access_token=vF9dft4qmT' bearer_uri = 'http://server.example.com/resource?access_token=vF9dft4qmT'
def _mocked_validate_bearer_token(self, token, scopes, request): def _mocked_validate_bearer_token(self, token, scopes, request):
if not token: if not token: # noqa: SIM103
return False return False
return True return True

View File

@ -0,0 +1,26 @@
import json
from unittest import TestCase, mock
from oauthlib.common import Request, urlencode
from oauthlib.oauth2.rfc6749 import errors
from oauthlib.oauth2.rfc8628.endpoints.pre_configured import DeviceApplicationServer
from oauthlib.oauth2.rfc8628.request_validator import RequestValidator
def test_server_set_up_device_endpoint_instance_attributes_correctly():
"""
Simple test that just instantiates DeviceApplicationServer
and asserts the important attributes are present
"""
validator = mock.MagicMock(spec=RequestValidator)
validator.get_default_redirect_uri.return_value = None
validator.get_code_challenge.return_value = None
verification_uri = "test.com/device"
verification_uri_complete = "test.com/device?user_code=123"
device = DeviceApplicationServer(validator, verification_uri=verification_uri, verification_uri_complete=verification_uri_complete)
device_vars = vars(device)
assert device_vars["_verification_uri_complete"] == "test.com/device?user_code=123"
assert device_vars["_verification_uri"] == "test.com/device"
assert device_vars["_expires_in"] == 1800
assert device_vars["_interval"] == 5

View File

@ -0,0 +1,95 @@
import json
from unittest import TestCase, mock
from oauthlib.common import Request, urlencode
from oauthlib.oauth2.rfc6749 import errors
from oauthlib.oauth2.rfc8628.endpoints.pre_configured import DeviceApplicationServer
from oauthlib.oauth2.rfc8628.request_validator import RequestValidator
class ErrorResponseTest(TestCase):
def set_client(self, request):
request.client = mock.MagicMock()
request.client.client_id = "mocked"
return True
def build_request(self, uri="https://example.com/device_authorize", client_id="foo"):
body = ""
if client_id:
body = f"client_id={client_id}"
return Request(
uri,
http_method="POST",
body=body,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
def assert_request_raises(self, error, request):
"""Test that the request fails similarly on the validation and response endpoint."""
self.assertRaises(
error,
self.device.validate_device_authorization_request,
request,
)
self.assertRaises(
error,
self.device.create_device_authorization_response,
uri=request.uri,
http_method=request.http_method,
body=request.body,
headers=request.headers,
)
def setUp(self):
self.validator = mock.MagicMock(spec=RequestValidator)
self.validator.get_default_redirect_uri.return_value = None
self.validator.get_code_challenge.return_value = None
self.device = DeviceApplicationServer(self.validator, "https://example.com/verify")
def test_missing_client_id(self):
# Device code grant
request = self.build_request(client_id=None)
self.assert_request_raises(errors.MissingClientIdError, request)
def test_empty_client_id(self):
# Device code grant
self.assertRaises(
errors.MissingClientIdError,
self.device.create_device_authorization_response,
"https://i.l/",
"POST",
"client_id=",
{"Content-Type": "application/x-www-form-urlencoded"},
)
def test_invalid_client_id(self):
request = self.build_request(client_id="foo")
# Device code grant
self.validator.validate_client_id.return_value = False
self.assert_request_raises(errors.InvalidClientIdError, request)
def test_duplicate_client_id(self):
request = self.build_request()
request.body = "client_id=foo&client_id=bar"
# Device code grant
self.validator.validate_client_id.return_value = False
self.assert_request_raises(errors.InvalidRequestFatalError, request)
def test_unauthenticated_confidential_client(self):
self.validator.client_authentication_required.return_value = True
self.validator.authenticate_client.return_value = False
request = self.build_request()
self.assert_request_raises(errors.InvalidClientError, request)
def test_unauthenticated_public_client(self):
self.validator.client_authentication_required.return_value = False
self.validator.authenticate_client_id.return_value = False
request = self.build_request()
self.assert_request_raises(errors.InvalidClientError, request)
def test_duplicate_scope_parameter(self):
request = self.build_request()
request.body = "client_id=foo&scope=foo&scope=bar"
# Device code grant
self.validator.validate_client_id.return_value = False
self.assert_request_raises(errors.InvalidRequestFatalError, request)

View File

@ -0,0 +1,172 @@
import json
from unittest import mock
import pytest
from oauthlib import common
from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant
from oauthlib.oauth2.rfc6749.tokens import BearerToken
def create_request(body: str = "") -> common.Request:
request = common.Request("http://a.b/path", body=body or None)
request.scopes = ("hello", "world")
request.expires_in = 1800
request.client = "batman"
request.client_id = "abcdef"
request.code = "1234"
request.response_type = "code"
request.grant_type = "urn:ietf:params:oauth:grant-type:device_code"
request.redirect_uri = "https://a.b/"
return request
def create_device_code_grant(mock_validator: mock.MagicMock) -> DeviceCodeGrant:
return DeviceCodeGrant(request_validator=mock_validator)
def test_custom_auth_validators_unsupported():
custom_validator = mock.Mock()
validator = mock.MagicMock()
expected = (
"DeviceCodeGrant does not "
"support authorization validators. Use token validators instead."
)
with pytest.raises(ValueError, match=expected):
DeviceCodeGrant(validator, pre_auth=[custom_validator])
with pytest.raises(ValueError, match=expected):
DeviceCodeGrant(validator, post_auth=[custom_validator])
expected = "'tuple' object has no attribute 'append'"
auth = DeviceCodeGrant(validator)
with pytest.raises(AttributeError, match=expected):
auth.custom_validators.pre_auth.append(custom_validator)
def test_custom_pre_and_post_token_validators():
client = mock.MagicMock()
validator = mock.MagicMock()
pre_token_validator = mock.Mock()
post_token_validator = mock.Mock()
request: common.Request = create_request()
request.client = client
auth = DeviceCodeGrant(validator)
auth.custom_validators.pre_token.append(pre_token_validator)
auth.custom_validators.post_token.append(post_token_validator)
bearer = BearerToken(validator)
auth.create_token_response(request, bearer)
pre_token_validator.assert_called()
post_token_validator.assert_called()
def test_create_token_response():
validator = mock.MagicMock()
request: common.Request = create_request()
request.client = mock.Mock()
auth = DeviceCodeGrant(validator)
bearer = BearerToken(validator)
headers, body, status_code = auth.create_token_response(request, bearer)
token = json.loads(body)
assert headers == {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"Pragma": "no-cache",
}
# when a custom token generator callable isn't used
# the random generator is used as default for the access token
assert token == {
"access_token": mock.ANY,
"expires_in": 3600,
"token_type": "Bearer",
"scope": "hello world",
"refresh_token": mock.ANY,
}
assert status_code == 200
validator.save_token.assert_called_once()
def test_invalid_client_error():
validator = mock.MagicMock()
request: common.Request = create_request()
request.client = mock.Mock()
auth = DeviceCodeGrant(validator)
bearer = BearerToken(validator)
validator.authenticate_client.return_value = False
headers, body, status_code = auth.create_token_response(request, bearer)
body = json.loads(body)
assert headers == {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"Pragma": "no-cache",
"WWW-Authenticate": 'Bearer error="invalid_client"',
}
assert body == {"error": "invalid_client"}
assert status_code == 401
validator.save_token.assert_not_called()
def test_invalid_grant_type_error():
validator = mock.MagicMock()
request: common.Request = create_request()
request.client = mock.Mock()
request.grant_type = "not_device_code"
auth = DeviceCodeGrant(validator)
bearer = BearerToken(validator)
headers, body, status_code = auth.create_token_response(request, bearer)
body = json.loads(body)
assert headers == {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"Pragma": "no-cache",
}
assert body == {"error": "unsupported_grant_type"}
assert status_code == 400
validator.save_token.assert_not_called()
def test_duplicate_params_error():
validator = mock.MagicMock()
request: common.Request = create_request(
"client_id=123&scope=openid&scope=openid"
)
request.client = mock.Mock()
auth = DeviceCodeGrant(validator)
bearer = BearerToken(validator)
headers, body, status_code = auth.create_token_response(request, bearer)
body = json.loads(body)
assert headers == {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"Pragma": "no-cache",
}
assert body == {"error": "invalid_request", "error_description": "Duplicate scope parameter."}
assert status_code == 400
validator.save_token.assert_not_called()

View File

@ -0,0 +1,113 @@
import json
from unittest import mock
from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint
from oauthlib.oauth2.rfc8628.request_validator import RequestValidator
from tests.unittest import TestCase
class DeviceAuthorizationEndpointTest(TestCase):
def _configure_endpoint(
self, interval=None, verification_uri_complete=None, user_code_generator=None
):
self.endpoint = DeviceAuthorizationEndpoint(
request_validator=mock.MagicMock(spec=RequestValidator),
verification_uri=self.verification_uri,
interval=interval,
verification_uri_complete=verification_uri_complete,
user_code_generator=user_code_generator,
)
def setUp(self):
self.request_validator = mock.MagicMock(spec=RequestValidator)
self.verification_uri = "http://i.b/l/verify"
self.uri = "http://i.b/l"
self.http_method = "POST"
self.body = "client_id=abc"
self.headers = {"Content-Type": "application/x-www-form-urlencoded"}
self._configure_endpoint()
def response_payload(self):
return self.uri, self.http_method, self.body, self.headers
@mock.patch("oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token")
def test_device_authorization_grant(self, generate_token):
generate_token.side_effect = ["abc", "def"]
_, body, status_code = self.endpoint.create_device_authorization_response(
*self.response_payload()
)
expected_payload = {
"verification_uri": "http://i.b/l/verify",
"user_code": "abc",
"device_code": "def",
"expires_in": 1800,
}
self.assertEqual(200, status_code)
self.assertEqual(body, expected_payload)
@mock.patch(
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
lambda: "abc",
)
def test_device_authorization_grant_interval(self):
self._configure_endpoint(interval=5)
_, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload())
self.assertEqual(5, body["interval"])
@mock.patch(
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
lambda: "abc",
)
def test_device_authorization_grant_interval_with_zero(self):
self._configure_endpoint(interval=0)
_, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload())
self.assertEqual(0, body["interval"])
@mock.patch(
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
lambda: "abc",
)
def test_device_authorization_grant_verify_url_complete_string(self):
self._configure_endpoint(verification_uri_complete="http://i.l/v?user_code={user_code}")
_, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload())
self.assertEqual(
"http://i.l/v?user_code=abc",
body["verification_uri_complete"],
)
@mock.patch(
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
lambda: "abc",
)
def test_device_authorization_grant_verify_url_complete_callable(self):
self._configure_endpoint(verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}")
_, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload())
self.assertEqual(
"http://i.l/v?user_code=abc",
body["verification_uri_complete"],
)
@mock.patch(
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
lambda: "abc",
)
def test_device_authorization_grant_user_gode_generator(self):
def user_code():
"""
A friendly user code the device can display and the user
can type in. It's up to the device how
this code should be displayed. e.g 123-456
"""
return "123456"
self._configure_endpoint(
verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}",
user_code_generator=user_code,
)
_, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload())
self.assertEqual(
"http://i.l/v?user_code=123456",
body["verification_uri_complete"],
)

View File

@ -28,7 +28,8 @@ class OpenIDConnectEndpointTest(TestCase):
'redirect_uri': 'https://a.b/cb', 'redirect_uri': 'https://a.b/cb',
'response_type': 'code', 'response_type': 'code',
'client_id': 'abcdef', 'client_id': 'abcdef',
'scope': 'hello openid' 'scope': 'hello openid',
'ui_locales': 'en-US'
} }
self.url = 'http://a.b/path?' + urlencode(params) self.url = 'http://a.b/path?' + urlencode(params)
@ -76,3 +77,4 @@ class OpenIDConnectEndpointTest(TestCase):
self.assertEqual(creds['prompt'], {'consent'}) self.assertEqual(creds['prompt'], {'consent'})
self.assertEqual(creds['nonce'], 'abcd') self.assertEqual(creds['nonce'], 'abcd')
self.assertEqual(creds['display'], 'touch') self.assertEqual(creds['display'], 'touch')
self.assertEqual(creds['ui_locales'], ['en-US'])

View File

@ -0,0 +1,32 @@
"""Ensure that the server correctly uses the OIDC flavor of
the Refresh token grant type when appropriate.
When the OpenID scope is provided, the refresh token response
should include a fresh ID token.
"""
import json
from unittest import mock
from oauthlib.openid import RequestValidator
from oauthlib.openid.connect.core.endpoints.pre_configured import Server
from tests.unittest import TestCase
class TestRefreshToken(TestCase):
def setUp(self):
self.validator = mock.MagicMock(spec=RequestValidator)
self.validator.get_id_token.return_value='id_token'
self.server = Server(self.validator)
def test_refresh_token_with_openid(self):
request_body = 'scope=openid+test_scope&grant_type=refresh_token&refresh_token=abc'
headers, body, status = self.server.create_token_response('', body=request_body)
self.assertIn('id_token', json.loads(body))
def test_refresh_token_no_openid(self):
request_body = 'scope=test_scope&grant_type=refresh_token&refresh_token=abc'
headers, body, status = self.server.create_token_response('', body=request_body)
self.assertNotIn('id_token', json.loads(body))

View File

@ -1,4 +1,6 @@
from datetime import datetime
import unittest import unittest
from oauthlib.uri_validate import is_absolute_uri from oauthlib.uri_validate import is_absolute_uri
from tests.unittest import TestCase from tests.unittest import TestCase
@ -76,7 +78,6 @@ class UriValidateTest(TestCase):
self.assertIsNone(is_absolute_uri('http://[abcd:efgh::1]/')) self.assertIsNone(is_absolute_uri('http://[abcd:efgh::1]/'))
def test_recursive_regex(self): def test_recursive_regex(self):
from datetime import datetime
t0 = datetime.now() t0 = datetime.now()
is_absolute_uri('http://[::::::::::::::::::::::::::]/path') is_absolute_uri('http://[::::::::::::::::::::::::::]/path')
t1 = datetime.now() t1 = datetime.now()