Pre Merge pull request !4 from tde3cifang/openkylin/nile
This commit is contained in:
commit
ee9468ebf0
|
@ -1,6 +1,36 @@
|
|||
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)
|
||||
------------------
|
||||
OAuth2.0 Provider:
|
||||
|
|
10
LICENSE
10
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2019 The OAuthlib Community
|
||||
Copyright (c) The OAuthlib Community
|
||||
All rights reserved.
|
||||
|
||||
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
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of this project nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 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
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
|
|
59
PKG-INFO
59
PKG-INFO
|
@ -1,50 +1,65 @@
|
|||
Metadata-Version: 2.1
|
||||
Metadata-Version: 2.4
|
||||
Name: oauthlib
|
||||
Version: 3.2.2
|
||||
Version: 3.3.1
|
||||
Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic
|
||||
Home-page: https://github.com/oauthlib/oauthlib
|
||||
Author: The OAuthlib Community
|
||||
Author-email: idan@gazit.me
|
||||
Maintainer: Ib Lundgren
|
||||
Maintainer-email: ib.lundgren@gmail.com
|
||||
License: BSD
|
||||
Maintainer: Jonathan Huot
|
||||
Maintainer-email: jonathan.huot@gmail.com
|
||||
License: BSD-3-Clause
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: MacOS
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Programming Language :: Python
|
||||
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.9
|
||||
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 :: Implementation
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Requires-Python: >=3.6
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/x-rst
|
||||
Provides-Extra: rsa
|
||||
Provides-Extra: signedtoken
|
||||
Provides-Extra: signals
|
||||
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
|
||||
===============================================
|
||||
|
||||
*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
|
||||
:target: https://app.travis-ci.com/oauthlib/oauthlib
|
||||
:alt: Travis
|
||||
.. image:: https://github.com/oauthlib/oauthlib/actions/workflows/python-build.yml/badge.svg
|
||||
:target: https://github.com/oauthlib/oauthlib/actions
|
||||
:alt: GitHub Actions
|
||||
.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/oauthlib/oauthlib
|
||||
:alt: Coveralls
|
||||
|
@ -113,7 +128,9 @@ Which web frameworks are supported?
|
|||
|
||||
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 Pyramid there is `pyramid-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
|
||||
.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-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!
|
||||
------------------------------------
|
||||
|
@ -148,7 +167,7 @@ have the pleasure to run into each other, please send a docs pull request =)
|
|||
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.
|
||||
|
||||
Credits
|
||||
|
|
16
README.rst
16
README.rst
|
@ -2,11 +2,11 @@ OAuthLib - Python Framework for OAuth1 & OAuth2
|
|||
===============================================
|
||||
|
||||
*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
|
||||
:target: https://app.travis-ci.com/oauthlib/oauthlib
|
||||
:alt: Travis
|
||||
.. image:: https://github.com/oauthlib/oauthlib/actions/workflows/python-build.yml/badge.svg
|
||||
:target: https://github.com/oauthlib/oauthlib/actions
|
||||
:alt: GitHub Actions
|
||||
.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/oauthlib/oauthlib
|
||||
:alt: Coveralls
|
||||
|
@ -75,7 +75,9 @@ Which web frameworks are supported?
|
|||
|
||||
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 Pyramid there is `pyramid-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
|
||||
.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-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!
|
||||
------------------------------------
|
||||
|
@ -110,7 +114,7 @@ have the pleasure to run into each other, please send a docs pull request =)
|
|||
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.
|
||||
|
||||
Credits
|
||||
|
|
|
@ -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
|
||||
|
||||
* Build for openKylin.
|
||||
|
|
|
@ -1,50 +1,65 @@
|
|||
Metadata-Version: 2.1
|
||||
Metadata-Version: 2.4
|
||||
Name: oauthlib
|
||||
Version: 3.2.2
|
||||
Version: 3.3.1
|
||||
Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic
|
||||
Home-page: https://github.com/oauthlib/oauthlib
|
||||
Author: The OAuthlib Community
|
||||
Author-email: idan@gazit.me
|
||||
Maintainer: Ib Lundgren
|
||||
Maintainer-email: ib.lundgren@gmail.com
|
||||
License: BSD
|
||||
Maintainer: Jonathan Huot
|
||||
Maintainer-email: jonathan.huot@gmail.com
|
||||
License: BSD-3-Clause
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: MacOS
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Programming Language :: Python
|
||||
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.9
|
||||
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 :: Implementation
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Requires-Python: >=3.6
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/x-rst
|
||||
Provides-Extra: rsa
|
||||
Provides-Extra: signedtoken
|
||||
Provides-Extra: signals
|
||||
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
|
||||
===============================================
|
||||
|
||||
*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
|
||||
:target: https://app.travis-ci.com/oauthlib/oauthlib
|
||||
:alt: Travis
|
||||
.. image:: https://github.com/oauthlib/oauthlib/actions/workflows/python-build.yml/badge.svg
|
||||
:target: https://github.com/oauthlib/oauthlib/actions
|
||||
:alt: GitHub Actions
|
||||
.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/oauthlib/oauthlib
|
||||
:alt: Coveralls
|
||||
|
@ -113,7 +128,9 @@ Which web frameworks are supported?
|
|||
|
||||
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 Pyramid there is `pyramid-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
|
||||
.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-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!
|
||||
------------------------------------
|
||||
|
@ -148,7 +167,7 @@ have the pleasure to run into each other, please send a docs pull request =)
|
|||
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.
|
||||
|
||||
Credits
|
||||
|
|
|
@ -59,8 +59,15 @@ oauthlib/oauth2/rfc6749/grant_types/implicit.py
|
|||
oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
|
||||
oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.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/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/connect/__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_resource_owner_password.py
|
||||
tests/oauth2/rfc8628/__init__.py
|
||||
tests/oauth2/rfc8628/test_server.py
|
||||
tests/oauth2/rfc8628/clients/__init__.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/connect/__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/test_claims_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/grant_types/__init__.py
|
||||
tests/openid/connect/core/grant_types/test_authorization_code.py
|
||||
|
|
|
@ -5,30 +5,30 @@
|
|||
A generic, spec-compliant, thorough implementation of the OAuth
|
||||
request-signing logic.
|
||||
|
||||
:copyright: (c) 2019 by The OAuthlib Community
|
||||
:license: BSD, see LICENSE for details.
|
||||
:copyright: (c) The OAuthlib Community
|
||||
:license: BSD-3-Clause, see LICENSE for details.
|
||||
"""
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
|
||||
__author__ = 'The OAuthlib Community'
|
||||
__version__ = '3.2.2'
|
||||
__version__ = '3.3.1'
|
||||
|
||||
logging.getLogger('oauthlib').addHandler(NullHandler())
|
||||
|
||||
_DEBUG = False
|
||||
|
||||
def set_debug(debug_val):
|
||||
"""Set value of debug flag
|
||||
|
||||
"""Set value of debug flag
|
||||
|
||||
:param debug_val: Value to set. Must be a bool value.
|
||||
"""
|
||||
global _DEBUG
|
||||
_DEBUG = debug_val
|
||||
"""
|
||||
global _DEBUG # noqa: PLW0603
|
||||
_DEBUG = debug_val
|
||||
|
||||
def get_debug():
|
||||
"""Get debug mode value.
|
||||
|
||||
:return: `True` if debug mode is on, `False` otherwise
|
||||
"""
|
||||
return _DEBUG
|
||||
"""Get debug mode value.
|
||||
|
||||
:return: `True` if debug mode is on, `False` otherwise
|
||||
"""
|
||||
return _DEBUG
|
||||
|
|
|
@ -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'
|
||||
'abcdefghijklmnopqrstuvwxyz'
|
||||
'0123456789' '_.-')
|
||||
'0123456789_.-')
|
||||
|
||||
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):
|
||||
import jwt
|
||||
import jwt # noqa: PLC0415
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
|
||||
|
@ -216,7 +216,7 @@ def generate_signed_token(private_pem, request):
|
|||
|
||||
|
||||
def verify_signed_token(public_pem, token):
|
||||
import jwt
|
||||
import jwt # noqa: PLC0415
|
||||
|
||||
return jwt.decode(token, public_pem, algorithms=['RS256'])
|
||||
|
||||
|
@ -316,7 +316,7 @@ class CaseInsensitiveDict(dict):
|
|||
return super().__getitem__(key)
|
||||
|
||||
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):
|
||||
super().__setitem__(k, v)
|
||||
|
@ -346,7 +346,8 @@ class Request:
|
|||
def __init__(self, uri, http_method='GET', body=None, headers=None,
|
||||
encoding='utf-8'):
|
||||
# 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.http_method = encode(http_method)
|
||||
|
|
|
@ -121,7 +121,8 @@ class Client:
|
|||
:param timestamp: Use this timestamp instead of using current. (Mainly for testing)
|
||||
"""
|
||||
# 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_secret = encode(client_secret)
|
||||
|
@ -219,7 +220,7 @@ class Client:
|
|||
content_type = request.headers.get('Content-Type', None)
|
||||
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:
|
||||
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
|
||||
|
||||
|
|
|
@ -69,12 +69,10 @@ class BaseEndpoint:
|
|||
def _create_request(self, uri, http_method, body, headers):
|
||||
# Only include body data from x-www-form-urlencoded requests
|
||||
headers = CaseInsensitiveDict(headers or {})
|
||||
if ("Content-Type" in headers and
|
||||
CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]):
|
||||
if "Content-Type" in headers and CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]: # noqa: SIM108
|
||||
request = Request(uri, http_method, body, headers)
|
||||
else:
|
||||
request = Request(uri, http_method, '', headers)
|
||||
|
||||
signature_type, params, oauth_params = (
|
||||
self._get_signature_type_and_params(request))
|
||||
|
||||
|
@ -129,8 +127,7 @@ class BaseEndpoint:
|
|||
# Considerations section (`Section 4`_) before deciding on which
|
||||
# method to support.
|
||||
# .. _`Section 4`: https://tools.ietf.org/html/rfc5849#section-4
|
||||
if (not request.signature_method in
|
||||
self.request_validator.allowed_signature_methods):
|
||||
if (request.signature_method not in self.request_validator.allowed_signature_methods):
|
||||
raise errors.InvalidSignatureMethodError(
|
||||
description="Invalid signature, {} not in {!r}.".format(
|
||||
request.signature_method,
|
||||
|
@ -180,9 +177,7 @@ class BaseEndpoint:
|
|||
|
||||
def _check_signature(self, request, is_token_request=False):
|
||||
# ---- RSA Signature verification ----
|
||||
if request.signature_method == SIGNATURE_RSA_SHA1 or \
|
||||
request.signature_method == SIGNATURE_RSA_SHA256 or \
|
||||
request.signature_method == SIGNATURE_RSA_SHA512:
|
||||
if request.signature_method in {SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512}:
|
||||
# RSA-based signature method
|
||||
|
||||
# The server verifies the signature per `[RFC3447] section 8.2.2`_
|
||||
|
|
|
@ -45,6 +45,7 @@ import warnings
|
|||
from oauthlib.common import extract_params, safe_string_equals, urldecode
|
||||
|
||||
from . import utils
|
||||
import contextlib
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -188,10 +189,9 @@ def base_string_uri(uri: str, host: str = None) -> str:
|
|||
raise ValueError('missing host')
|
||||
|
||||
# NOTE: Try guessing if we're dealing with IP or hostname
|
||||
try:
|
||||
with contextlib.suppress(ValueError):
|
||||
hostname = ipaddress.ip_address(hostname)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
if isinstance(hostname, ipaddress.IPv6Address):
|
||||
hostname = f"[{hostname}]"
|
||||
|
@ -568,7 +568,7 @@ def _get_jwt_rsa_algorithm(hash_algorithm_name: str):
|
|||
# Not in cache: instantiate a new RSAAlgorithm
|
||||
|
||||
# PyJWT has some nice pycrypto/cryptography abstractions
|
||||
import jwt.algorithms as jwt_algorithms
|
||||
import jwt.algorithms as jwt_algorithms # noqa: PLC0415
|
||||
m = {
|
||||
'SHA-1': jwt_algorithms.hashes.SHA1,
|
||||
'SHA-256': jwt_algorithms.hashes.SHA256,
|
||||
|
|
|
@ -30,7 +30,8 @@ def filter_params(target):
|
|||
|
||||
def filter_oauth_params(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):
|
||||
return list(filter(is_oauth, list(params.items())))
|
||||
else:
|
||||
|
@ -59,7 +60,7 @@ def unescape(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"""
|
||||
# With Python 2.6, parse_http_list handles unicode fine
|
||||
return urllib2.parse_keqv_list(l)
|
||||
|
|
|
@ -5,32 +5,66 @@ oauthlib.oauth2
|
|||
This module is a wrapper for the most recent implementation of OAuth 2.0 Client
|
||||
and Server classes.
|
||||
"""
|
||||
|
||||
from .rfc6749.clients import (
|
||||
BackendApplicationClient, Client, LegacyApplicationClient,
|
||||
MobileApplicationClient, ServiceApplicationClient, WebApplicationClient,
|
||||
BackendApplicationClient,
|
||||
Client,
|
||||
LegacyApplicationClient,
|
||||
MobileApplicationClient,
|
||||
ServiceApplicationClient,
|
||||
WebApplicationClient,
|
||||
)
|
||||
from .rfc6749.endpoints import (
|
||||
AuthorizationEndpoint, BackendApplicationServer, IntrospectEndpoint,
|
||||
LegacyApplicationServer, MetadataEndpoint, MobileApplicationServer,
|
||||
ResourceEndpoint, RevocationEndpoint, Server, TokenEndpoint,
|
||||
AuthorizationEndpoint,
|
||||
BackendApplicationServer,
|
||||
IntrospectEndpoint,
|
||||
LegacyApplicationServer,
|
||||
MetadataEndpoint,
|
||||
MobileApplicationServer,
|
||||
ResourceEndpoint,
|
||||
RevocationEndpoint,
|
||||
Server,
|
||||
TokenEndpoint,
|
||||
WebApplicationServer,
|
||||
)
|
||||
from .rfc6749.errors import (
|
||||
AccessDeniedError, FatalClientError, InsecureTransportError,
|
||||
InvalidClientError, InvalidClientIdError, InvalidGrantError,
|
||||
InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError,
|
||||
InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError,
|
||||
MissingClientIdError, MissingCodeError, MissingRedirectURIError,
|
||||
MissingResponseTypeError, MissingTokenError, MissingTokenTypeError,
|
||||
OAuth2Error, ServerError, TemporarilyUnavailableError, TokenExpiredError,
|
||||
UnauthorizedClientError, UnsupportedGrantTypeError,
|
||||
UnsupportedResponseTypeError, UnsupportedTokenTypeError,
|
||||
AccessDeniedError,
|
||||
FatalClientError,
|
||||
InsecureTransportError,
|
||||
InvalidClientError,
|
||||
InvalidClientIdError,
|
||||
InvalidGrantError,
|
||||
InvalidRedirectURIError,
|
||||
InvalidRequestError,
|
||||
InvalidRequestFatalError,
|
||||
InvalidScopeError,
|
||||
MismatchingRedirectURIError,
|
||||
MismatchingStateError,
|
||||
MissingClientIdError,
|
||||
MissingCodeError,
|
||||
MissingRedirectURIError,
|
||||
MissingResponseTypeError,
|
||||
MissingTokenError,
|
||||
MissingTokenTypeError,
|
||||
OAuth2Error,
|
||||
ServerError,
|
||||
TemporarilyUnavailableError,
|
||||
TokenExpiredError,
|
||||
UnauthorizedClientError,
|
||||
UnsupportedGrantTypeError,
|
||||
UnsupportedResponseTypeError,
|
||||
UnsupportedTokenTypeError,
|
||||
)
|
||||
from .rfc6749.grant_types import (
|
||||
AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant,
|
||||
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
|
||||
AuthorizationCodeGrant,
|
||||
ClientCredentialsGrant,
|
||||
ImplicitGrant,
|
||||
RefreshTokenGrant,
|
||||
ResourceOwnerPasswordCredentialsGrant,
|
||||
)
|
||||
from .rfc6749.request_validator import RequestValidator
|
||||
from .rfc6749.tokens import BearerToken, OAuth2Token
|
||||
from .rfc6749.utils import is_secure_transport
|
||||
from .rfc8628.clients import DeviceClient
|
||||
from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint, DeviceApplicationServer
|
||||
from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant
|
||||
|
|
|
@ -8,17 +8,16 @@ for consuming OAuth 2.0 RFC6749.
|
|||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import re
|
||||
import secrets
|
||||
import time
|
||||
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.errors import (
|
||||
InsecureTransportError, TokenExpiredError,
|
||||
)
|
||||
from oauthlib.oauth2.rfc6749.parameters import (
|
||||
parse_expires,
|
||||
parse_token_response, prepare_token_request,
|
||||
prepare_token_revocation_request,
|
||||
)
|
||||
|
@ -207,7 +206,7 @@ class Client:
|
|||
|
||||
case_insensitive_token_types = {
|
||||
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)
|
||||
|
||||
if not (self.access_token or self.token.get('access_token')):
|
||||
|
@ -466,7 +465,7 @@ class Client:
|
|||
return uri, headers, body
|
||||
|
||||
def create_code_verifier(self, length):
|
||||
"""Create PKCE **code_verifier** used in computing **code_challenge**.
|
||||
"""Create PKCE **code_verifier** used in computing **code_challenge**.
|
||||
See `RFC7636 Section 4.1`_
|
||||
|
||||
:param length: REQUIRED. The length of the code_verifier.
|
||||
|
@ -491,11 +490,7 @@ class Client:
|
|||
if not length <= 128:
|
||||
raise ValueError("Length must be less than or equal to 128")
|
||||
|
||||
allowed_characters = re.compile('^[A-Zaa-z0-9-._~]')
|
||||
code_verifier = secrets.token_urlsafe(length)
|
||||
|
||||
if not re.search(allowed_characters, code_verifier):
|
||||
raise ValueError("code_verifier contains invalid characters")
|
||||
code_verifier = generate_token(length, UNICODE_ASCII_CHARACTER_SET + "-._~")
|
||||
|
||||
self.code_verifier = code_verifier
|
||||
|
||||
|
@ -530,10 +525,10 @@ class Client:
|
|||
"""
|
||||
code_challenge = None
|
||||
|
||||
if code_verifier == None:
|
||||
if code_verifier is None:
|
||||
raise ValueError("Invalid code_verifier")
|
||||
|
||||
if code_challenge_method == None:
|
||||
if code_challenge_method is None:
|
||||
code_challenge_method = "plain"
|
||||
self.code_challenge_method = code_challenge_method
|
||||
code_challenge = code_verifier
|
||||
|
@ -587,15 +582,13 @@ class Client:
|
|||
if 'token_type' in response:
|
||||
self.token_type = response.get('token_type')
|
||||
|
||||
if 'expires_in' in response:
|
||||
self.expires_in = response.get('expires_in')
|
||||
self._expires_at = time.time() + int(self.expires_in)
|
||||
|
||||
if 'expires_at' in response:
|
||||
try:
|
||||
self._expires_at = int(response.get('expires_at'))
|
||||
except:
|
||||
self._expires_at = None
|
||||
vin, vat, v_at = parse_expires(response)
|
||||
if vin:
|
||||
self.expires_in = vin
|
||||
if vat:
|
||||
self.expires_at = vat
|
||||
if v_at:
|
||||
self._expires_at = v_at
|
||||
|
||||
if 'mac_key' in response:
|
||||
self.mac_key = response.get('mac_key')
|
||||
|
|
|
@ -43,7 +43,7 @@ class MobileApplicationClient(Client):
|
|||
redirection URI, it may be exposed to the resource owner and other
|
||||
applications residing on the same device.
|
||||
"""
|
||||
|
||||
|
||||
response_type = 'token'
|
||||
|
||||
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
|
||||
|
|
|
@ -91,7 +91,7 @@ class ServiceApplicationClient(Client):
|
|||
``https://provider.com/oauth2/token``.
|
||||
|
||||
: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.
|
||||
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
|
||||
"""
|
||||
import jwt
|
||||
import jwt # noqa: PLC0415
|
||||
|
||||
key = private_key or self.private_key
|
||||
if not key:
|
||||
|
|
|
@ -33,7 +33,7 @@ class WebApplicationClient(Client):
|
|||
browser) and capable of receiving incoming requests (via redirection)
|
||||
from the authorization server.
|
||||
"""
|
||||
|
||||
|
||||
grant_type = 'authorization_code'
|
||||
|
||||
def __init__(self, client_id, code=None, **kwargs):
|
||||
|
@ -62,8 +62,8 @@ class WebApplicationClient(Client):
|
|||
to the client. The parameter SHOULD be used for preventing
|
||||
cross-site request forgery as described in `Section 10.12`_.
|
||||
|
||||
:param code_challenge: OPTIONAL. PKCE parameter. REQUIRED if PKCE is enforced.
|
||||
A challenge derived from the code_verifier that is sent in the
|
||||
:param code_challenge: OPTIONAL. PKCE parameter. REQUIRED if PKCE is enforced.
|
||||
A challenge derived from the code_verifier that is sent in the
|
||||
authorization request, to be verified against later.
|
||||
|
||||
:param code_challenge_method: OPTIONAL. PKCE parameter. A method that was used to derive code challenge.
|
||||
|
|
|
@ -32,7 +32,7 @@ class BaseEndpoint:
|
|||
if valid_request_methods is not None:
|
||||
valid_request_methods = [x.upper() for x in valid_request_methods]
|
||||
self._valid_request_methods = valid_request_methods
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
|
@ -40,7 +40,7 @@ class BaseEndpoint:
|
|||
|
||||
@available.setter
|
||||
def available(self, available):
|
||||
self._available = available
|
||||
self._available = available
|
||||
|
||||
@property
|
||||
def catch_errors(self):
|
||||
|
|
|
@ -74,7 +74,7 @@ class IntrospectEndpoint(BaseEndpoint):
|
|||
request
|
||||
)
|
||||
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:
|
||||
claims.pop("active")
|
||||
return resp_headers, json.dumps(dict(active=True, **claims)), 200
|
||||
|
|
|
@ -38,9 +38,9 @@ class MetadataEndpoint(BaseEndpoint):
|
|||
"""
|
||||
|
||||
def __init__(self, endpoints, claims={}, raise_errors=True):
|
||||
assert isinstance(claims, dict)
|
||||
assert isinstance(claims, dict) # noqa: S101
|
||||
for endpoint in endpoints:
|
||||
assert isinstance(endpoint, BaseEndpoint)
|
||||
assert isinstance(endpoint, BaseEndpoint) # noqa: S101
|
||||
|
||||
BaseEndpoint.__init__(self)
|
||||
self.raise_errors = raise_errors
|
||||
|
|
|
@ -5,9 +5,13 @@ oauthlib.oauth2.rfc6749.endpoints.pre_configured
|
|||
This module is an implementation of various endpoints needed
|
||||
for providing OAuth 2.0 RFC6749 servers.
|
||||
"""
|
||||
|
||||
from ..grant_types import (
|
||||
AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant,
|
||||
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
|
||||
AuthorizationCodeGrant,
|
||||
ClientCredentialsGrant,
|
||||
ImplicitGrant,
|
||||
RefreshTokenGrant,
|
||||
ResourceOwnerPasswordCredentialsGrant,
|
||||
)
|
||||
from ..tokens import BearerToken
|
||||
from .authorization import AuthorizationEndpoint
|
||||
|
@ -15,16 +19,26 @@ from .introspect import IntrospectEndpoint
|
|||
from .resource import ResourceEndpoint
|
||||
from .revocation import RevocationEndpoint
|
||||
from .token import TokenEndpoint
|
||||
from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant
|
||||
|
||||
|
||||
class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
class Server(
|
||||
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, request_validator, token_expires_in=None,
|
||||
token_generator=None, refresh_token_generator=None,
|
||||
*args, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
request_validator,
|
||||
token_expires_in=None,
|
||||
token_generator=None,
|
||||
refresh_token_generator=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Construct a new all-grants-in-one server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
|
@ -40,43 +54,58 @@ class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
|
|||
"""
|
||||
self.auth_grant = AuthorizationCodeGrant(request_validator)
|
||||
self.implicit_grant = ImplicitGrant(request_validator)
|
||||
self.password_grant = ResourceOwnerPasswordCredentialsGrant(
|
||||
request_validator)
|
||||
self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator)
|
||||
self.credentials_grant = ClientCredentialsGrant(request_validator)
|
||||
self.refresh_grant = RefreshTokenGrant(request_validator)
|
||||
self.device_code_grant = DeviceCodeGrant(request_validator, **kwargs)
|
||||
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
self.bearer = BearerToken(
|
||||
request_validator, token_generator, token_expires_in, refresh_token_generator
|
||||
)
|
||||
|
||||
AuthorizationEndpoint.__init__(self, default_response_type='code',
|
||||
response_types={
|
||||
'code': self.auth_grant,
|
||||
'token': self.implicit_grant,
|
||||
'none': self.auth_grant
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
AuthorizationEndpoint.__init__(
|
||||
self,
|
||||
default_response_type="code",
|
||||
response_types={
|
||||
"code": self.auth_grant,
|
||||
"token": self.implicit_grant,
|
||||
"none": self.auth_grant,
|
||||
},
|
||||
default_token_type=self.bearer,
|
||||
)
|
||||
|
||||
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
|
||||
grant_types={
|
||||
'authorization_code': self.auth_grant,
|
||||
'password': self.password_grant,
|
||||
'client_credentials': self.credentials_grant,
|
||||
'refresh_token': self.refresh_grant,
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
TokenEndpoint.__init__(
|
||||
self,
|
||||
default_grant_type="authorization_code",
|
||||
grant_types={
|
||||
"authorization_code": self.auth_grant,
|
||||
"password": self.password_grant,
|
||||
"client_credentials": self.credentials_grant,
|
||||
"refresh_token": self.refresh_grant,
|
||||
"urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant,
|
||||
},
|
||||
default_token_type=self.bearer,
|
||||
)
|
||||
ResourceEndpoint.__init__(
|
||||
self, default_token="Bearer", token_types={"Bearer": self.bearer}
|
||||
)
|
||||
RevocationEndpoint.__init__(self, request_validator)
|
||||
IntrospectEndpoint.__init__(self, request_validator)
|
||||
|
||||
|
||||
class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
|
||||
class WebApplicationServer(
|
||||
AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, ResourceEndpoint, RevocationEndpoint
|
||||
):
|
||||
"""An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
|
||||
|
||||
def __init__(self, request_validator, token_generator=None,
|
||||
token_expires_in=None, refresh_token_generator=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
request_validator,
|
||||
token_generator=None,
|
||||
token_expires_in=None,
|
||||
refresh_token_generator=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Construct a new web application server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
|
@ -92,30 +121,44 @@ class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpo
|
|||
"""
|
||||
self.auth_grant = AuthorizationCodeGrant(request_validator)
|
||||
self.refresh_grant = RefreshTokenGrant(request_validator)
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
AuthorizationEndpoint.__init__(self, 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={
|
||||
'authorization_code': self.auth_grant,
|
||||
'refresh_token': self.refresh_grant,
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
self.bearer = BearerToken(
|
||||
request_validator, token_generator, token_expires_in, refresh_token_generator
|
||||
)
|
||||
AuthorizationEndpoint.__init__(
|
||||
self,
|
||||
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={
|
||||
"authorization_code": self.auth_grant,
|
||||
"refresh_token": self.refresh_grant,
|
||||
},
|
||||
default_token_type=self.bearer,
|
||||
)
|
||||
ResourceEndpoint.__init__(
|
||||
self, default_token="Bearer", token_types={"Bearer": self.bearer}
|
||||
)
|
||||
RevocationEndpoint.__init__(self, request_validator)
|
||||
IntrospectEndpoint.__init__(self, request_validator)
|
||||
|
||||
|
||||
class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
|
||||
class MobileApplicationServer(
|
||||
AuthorizationEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint
|
||||
):
|
||||
"""An all-in-one endpoint featuring Implicit code grant and Bearer tokens."""
|
||||
|
||||
def __init__(self, request_validator, token_generator=None,
|
||||
token_expires_in=None, refresh_token_generator=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
request_validator,
|
||||
token_generator=None,
|
||||
token_expires_in=None,
|
||||
refresh_token_generator=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Construct a new implicit grant server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
|
@ -130,27 +173,39 @@ class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint,
|
|||
token-, resource-, and revocation-endpoint constructors.
|
||||
"""
|
||||
self.implicit_grant = ImplicitGrant(request_validator)
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
AuthorizationEndpoint.__init__(self, default_response_type='token',
|
||||
response_types={
|
||||
'token': self.implicit_grant},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
RevocationEndpoint.__init__(self, request_validator,
|
||||
supported_token_types=['access_token'])
|
||||
IntrospectEndpoint.__init__(self, request_validator,
|
||||
supported_token_types=['access_token'])
|
||||
self.bearer = BearerToken(
|
||||
request_validator, token_generator, token_expires_in, refresh_token_generator
|
||||
)
|
||||
AuthorizationEndpoint.__init__(
|
||||
self,
|
||||
default_response_type="token",
|
||||
response_types={"token": self.implicit_grant},
|
||||
default_token_type=self.bearer,
|
||||
)
|
||||
ResourceEndpoint.__init__(
|
||||
self, default_token="Bearer", token_types={"Bearer": self.bearer}
|
||||
)
|
||||
RevocationEndpoint.__init__(
|
||||
self, request_validator, supported_token_types=["access_token"]
|
||||
)
|
||||
IntrospectEndpoint.__init__(
|
||||
self, request_validator, supported_token_types=["access_token"]
|
||||
)
|
||||
|
||||
|
||||
class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
|
||||
class LegacyApplicationServer(
|
||||
TokenEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint
|
||||
):
|
||||
"""An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens."""
|
||||
|
||||
def __init__(self, request_validator, token_generator=None,
|
||||
token_expires_in=None, refresh_token_generator=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
request_validator,
|
||||
token_generator=None,
|
||||
token_expires_in=None,
|
||||
refresh_token_generator=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Construct a resource owner password credentials grant server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
|
@ -164,30 +219,40 @@ class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint,
|
|||
:param kwargs: Extra parameters to pass to authorization-,
|
||||
token-, resource-, and revocation-endpoint constructors.
|
||||
"""
|
||||
self.password_grant = ResourceOwnerPasswordCredentialsGrant(
|
||||
request_validator)
|
||||
self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator)
|
||||
self.refresh_grant = RefreshTokenGrant(request_validator)
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
TokenEndpoint.__init__(self, default_grant_type='password',
|
||||
grant_types={
|
||||
'password': self.password_grant,
|
||||
'refresh_token': self.refresh_grant,
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
self.bearer = BearerToken(
|
||||
request_validator, token_generator, token_expires_in, refresh_token_generator
|
||||
)
|
||||
TokenEndpoint.__init__(
|
||||
self,
|
||||
default_grant_type="password",
|
||||
grant_types={
|
||||
"password": self.password_grant,
|
||||
"refresh_token": self.refresh_grant,
|
||||
},
|
||||
default_token_type=self.bearer,
|
||||
)
|
||||
ResourceEndpoint.__init__(
|
||||
self, default_token="Bearer", token_types={"Bearer": self.bearer}
|
||||
)
|
||||
RevocationEndpoint.__init__(self, request_validator)
|
||||
IntrospectEndpoint.__init__(self, request_validator)
|
||||
|
||||
|
||||
class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
|
||||
class BackendApplicationServer(
|
||||
TokenEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint
|
||||
):
|
||||
"""An all-in-one endpoint featuring Client Credentials grant and Bearer tokens."""
|
||||
|
||||
def __init__(self, request_validator, token_generator=None,
|
||||
token_expires_in=None, refresh_token_generator=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
request_validator,
|
||||
token_generator=None,
|
||||
token_expires_in=None,
|
||||
refresh_token_generator=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Construct a client credentials grant server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
|
@ -202,15 +267,21 @@ class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint,
|
|||
token-, resource-, and revocation-endpoint constructors.
|
||||
"""
|
||||
self.credentials_grant = ClientCredentialsGrant(request_validator)
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
TokenEndpoint.__init__(self, default_grant_type='client_credentials',
|
||||
grant_types={
|
||||
'client_credentials': self.credentials_grant},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
RevocationEndpoint.__init__(self, request_validator,
|
||||
supported_token_types=['access_token'])
|
||||
IntrospectEndpoint.__init__(self, request_validator,
|
||||
supported_token_types=['access_token'])
|
||||
self.bearer = BearerToken(
|
||||
request_validator, token_generator, token_expires_in, refresh_token_generator
|
||||
)
|
||||
TokenEndpoint.__init__(
|
||||
self,
|
||||
default_grant_type="client_credentials",
|
||||
grant_types={"client_credentials": self.credentials_grant},
|
||||
default_token_type=self.bearer,
|
||||
)
|
||||
ResourceEndpoint.__init__(
|
||||
self, default_token="Bearer", token_types={"Bearer": self.bearer}
|
||||
)
|
||||
RevocationEndpoint.__init__(
|
||||
self, request_validator, supported_token_types=["access_token"]
|
||||
)
|
||||
IntrospectEndpoint.__init__(
|
||||
self, request_validator, supported_token_types=["access_token"]
|
||||
)
|
||||
|
|
|
@ -81,4 +81,4 @@ class ResourceEndpoint(BaseEndpoint):
|
|||
"""
|
||||
estimates = sorted(((t.estimate_type(request), n)
|
||||
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
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
import json
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from oauthlib.common import add_params_to_uri, urlencode
|
||||
|
||||
|
@ -60,7 +62,7 @@ class OAuth2Error(Exception):
|
|||
self.response_type = request.response_type
|
||||
self.response_mode = request.response_mode
|
||||
self.grant_type = request.grant_type
|
||||
if not state:
|
||||
if state is None:
|
||||
self.state = request.state
|
||||
else:
|
||||
self.redirect_uri = None
|
||||
|
@ -150,7 +152,6 @@ class FatalClientError(OAuth2Error):
|
|||
|
||||
Instead the user should be informed of the error by the provider itself.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRequestFatalError(FatalClientError):
|
||||
|
@ -387,8 +388,6 @@ class CustomOAuth2Error(OAuth2Error):
|
|||
|
||||
|
||||
def raise_from_error(error, params=None):
|
||||
import inspect
|
||||
import sys
|
||||
kwargs = {
|
||||
'description': params.get('error_description'),
|
||||
'uri': params.get('error_uri'),
|
||||
|
|
|
@ -387,7 +387,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
|
|||
raise errors.MissingResponseTypeError(request=request)
|
||||
# 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"
|
||||
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)
|
||||
|
||||
if not self.request_validator.validate_response_type(request.client_id,
|
||||
|
@ -400,9 +400,8 @@ class AuthorizationCodeGrant(GrantTypeBase):
|
|||
|
||||
# OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request"
|
||||
# https://tools.ietf.org/html/rfc6749#section-4.4.1
|
||||
if self.request_validator.is_pkce_required(request.client_id, request) is True:
|
||||
if request.code_challenge is None:
|
||||
raise errors.MissingCodeChallengeError(request=request)
|
||||
if self.request_validator.is_pkce_required(request.client_id, request) is True and request.code_challenge is None:
|
||||
raise errors.MissingCodeChallengeError(request=request)
|
||||
|
||||
if request.code_challenge is not None:
|
||||
request_info["code_challenge"] = request.code_challenge
|
||||
|
|
|
@ -143,7 +143,7 @@ class GrantTypeBase:
|
|||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
# 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
|
||||
|
||||
token.update(token_handler.create_token(request, refresh_token=False))
|
||||
|
@ -199,10 +199,7 @@ class GrantTypeBase:
|
|||
|
||||
if request.response_type == 'none':
|
||||
state = token.get('state', None)
|
||||
if state:
|
||||
token_items = [('state', state)]
|
||||
else:
|
||||
token_items = []
|
||||
token_items = [('state', state)] if state else []
|
||||
|
||||
if request.response_mode == 'query':
|
||||
headers['Location'] = add_params_to_uri(
|
||||
|
|
|
@ -107,11 +107,10 @@ class ClientCredentialsGrant(GrantTypeBase):
|
|||
if not self.request_validator.authenticate_client(request):
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise errors.InvalidClientError(request=request)
|
||||
else:
|
||||
if not hasattr(request.client, 'client_id'):
|
||||
raise NotImplementedError('Authenticate client must set the '
|
||||
'request.client.client_id attribute '
|
||||
'in authenticate_client.')
|
||||
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)
|
||||
|
||||
|
|
|
@ -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!
|
||||
# "id_token token" - return the access token and the id 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)
|
||||
else:
|
||||
token = {}
|
||||
token = token_handler.create_token(request, refresh_token=False) if 'token' in request.response_type.split() else {}
|
||||
|
||||
if request.state is not None:
|
||||
token['state'] = request.state
|
||||
|
|
|
@ -101,6 +101,9 @@ class RefreshTokenGrant(GrantTypeBase):
|
|||
if not self.request_validator.authenticate_client(request):
|
||||
log.debug('Invalid client (%r), denying access.', 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):
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise errors.InvalidClientError(request=request)
|
||||
|
|
|
@ -180,12 +180,11 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
|
|||
request.password, request.client, request):
|
||||
raise errors.InvalidGrantError(
|
||||
'Invalid credentials given.', request=request)
|
||||
else:
|
||||
if not hasattr(request.client, 'client_id'):
|
||||
raise NotImplementedError(
|
||||
'Validate user must set the '
|
||||
'request.client.client_id attribute '
|
||||
'in authenticate_client.')
|
||||
elif not hasattr(request.client, 'client_id'):
|
||||
raise NotImplementedError(
|
||||
'Validate user must set the '
|
||||
'request.client.client_id attribute '
|
||||
'in authenticate_client.')
|
||||
log.debug('Authorizing access to user %r.', request.user)
|
||||
|
||||
# Ensure client is authorized use of this grant type
|
||||
|
|
|
@ -45,10 +45,10 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
|
|||
back to the client. The parameter SHOULD be used for
|
||||
preventing cross-site request forgery as described in
|
||||
`Section 10.12`_.
|
||||
:param code_challenge: PKCE parameter. A challenge derived from the
|
||||
code_verifier that is sent in the authorization
|
||||
:param code_challenge: PKCE parameter. A challenge derived from the
|
||||
code_verifier that is sent in the authorization
|
||||
request, to be verified against later.
|
||||
:param code_challenge_method: PKCE parameter. A method that was used to derive the
|
||||
:param code_challenge_method: PKCE parameter. A method that was used to derive the
|
||||
code_challenge. Defaults to "plain" if not present in the request.
|
||||
:param kwargs: Extra arguments to embed in the grant/authorization URL.
|
||||
|
||||
|
@ -150,9 +150,8 @@ def prepare_token_request(grant_type, body='', include_client_id=True, code_veri
|
|||
|
||||
# pull the `client_id` out of the kwargs.
|
||||
client_id = kwargs.pop('client_id', None)
|
||||
if include_client_id:
|
||||
if client_id is not None:
|
||||
params.append(('client_id', client_id))
|
||||
if include_client_id and client_id is not None:
|
||||
params.append(('client_id', client_id))
|
||||
|
||||
# use code_verifier if code_challenge was passed in the authorization request
|
||||
if code_verifier is not None:
|
||||
|
@ -274,13 +273,13 @@ def parse_authorization_code_response(uri, state=None):
|
|||
query = urlparse.urlparse(uri).query
|
||||
params = dict(urlparse.parse_qsl(query))
|
||||
|
||||
if state and params.get('state', None) != state:
|
||||
if state and params.get('state') != state:
|
||||
raise MismatchingStateError()
|
||||
|
||||
if 'error' in 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.")
|
||||
|
||||
return params
|
||||
|
@ -337,17 +336,20 @@ def parse_implicit_response(uri, state=None, scope=None):
|
|||
fragment = urlparse.urlparse(uri).fragment
|
||||
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:
|
||||
params['scope'] = scope_to_list(params['scope'])
|
||||
|
||||
if 'expires_in' in params:
|
||||
params['expires_at'] = time.time() + int(params['expires_in'])
|
||||
vin, vat, v_at = parse_expires(params)
|
||||
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.")
|
||||
|
||||
params = OAuth2Token(params, old_scope=scope)
|
||||
|
@ -424,18 +426,19 @@ def parse_token_response(body, scope=None):
|
|||
# https://github.com/oauthlib/oauthlib/issues/267
|
||||
|
||||
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:
|
||||
params['scope'] = scope_to_list(params['scope'])
|
||||
|
||||
if 'expires_in' in params:
|
||||
if params['expires_in'] is None:
|
||||
params.pop('expires_in')
|
||||
else:
|
||||
params['expires_at'] = time.time() + int(params['expires_in'])
|
||||
vin, vat, v_at = parse_expires(params)
|
||||
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')
|
||||
|
||||
params = OAuth2Token(params, old_scope=scope)
|
||||
validate_token_parameters(params)
|
||||
|
@ -447,12 +450,11 @@ def validate_token_parameters(params):
|
|||
if 'error' in 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.")
|
||||
|
||||
if not 'token_type' in params:
|
||||
if os.environ.get('OAUTHLIB_STRICT_TOKEN_TYPE'):
|
||||
raise MissingTokenTypeError()
|
||||
if 'token_type' not in params and os.environ.get('OAUTHLIB_STRICT_TOKEN_TYPE'):
|
||||
raise MissingTokenTypeError()
|
||||
|
||||
# If the issued access token scope is different from the one requested by
|
||||
# the client, the authorization server MUST include the "scope" response
|
||||
|
@ -469,3 +471,58 @@ def validate_token_parameters(params):
|
|||
w.old_scope = params.old_scopes
|
||||
w.new_scope = params.scopes
|
||||
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
|
||||
|
|
|
@ -48,12 +48,12 @@ class RequestValidator:
|
|||
Headers may be accesses through request.headers and parameters found in
|
||||
both body and query can be obtained by direct attribute access, i.e.
|
||||
request.client_id for client_id in the URL query.
|
||||
|
||||
|
||||
The authentication process is required to contain the identification of
|
||||
the client (i.e. search the database based on the client_id). In case the
|
||||
client doesn't exist based on the received client_id, this method has to
|
||||
return False and the HTTP response created by the library will contain
|
||||
'invalid_client' message.
|
||||
'invalid_client' message.
|
||||
|
||||
After the client identification succeeds, this method needs to set the
|
||||
client on the request, i.e. request.client = client. A client object's
|
||||
|
|
|
@ -24,7 +24,7 @@ class OAuth2Token(dict):
|
|||
def __init__(self, params, old_scope=None):
|
||||
super().__init__(params)
|
||||
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']))
|
||||
if old_scope is not None:
|
||||
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)
|
||||
|
||||
if query:
|
||||
request_uri = path + '?' + query
|
||||
else:
|
||||
request_uri = path
|
||||
request_uri = path + '?' + query if query else path
|
||||
|
||||
# Hash the body/payload
|
||||
if body is not None and draft == 0:
|
||||
|
@ -305,10 +302,7 @@ class BearerToken(TokenBase):
|
|||
"If you do, call `request_validator.save_token()` instead.",
|
||||
DeprecationWarning)
|
||||
|
||||
if callable(self.expires_in):
|
||||
expires_in = self.expires_in(request)
|
||||
else:
|
||||
expires_in = self.expires_in
|
||||
expires_in = self.expires_in(request) if callable(self.expires_in) else self.expires_in
|
||||
|
||||
request.expires_in = expires_in
|
||||
|
||||
|
|
|
@ -5,6 +5,12 @@ oauthlib.oauth2.rfc8628
|
|||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 Device Authorization RFC8628.
|
||||
"""
|
||||
|
||||
from oauthlib.oauth2.rfc8628.errors import (
|
||||
SlowDownError,
|
||||
AuthorizationPendingError,
|
||||
ExpiredTokenError,
|
||||
)
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
|
@ -45,9 +45,9 @@ class DeviceClient(Client):
|
|||
if scope:
|
||||
params.append(('scope', list_to_scope(scope)))
|
||||
|
||||
for k in kwargs:
|
||||
if kwargs[k]:
|
||||
params.append((str(k), kwargs[k]))
|
||||
for k,v in kwargs.items():
|
||||
if v:
|
||||
params.append((str(k), v))
|
||||
|
||||
return add_params_to_uri(uri, params)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
)
|
|
@ -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"
|
|
@ -0,0 +1 @@
|
|||
from oauthlib.oauth2.rfc8628.grant_types.device_code import DeviceCodeGrant
|
|
@ -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
|
|
@ -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
|
|
@ -5,34 +5,60 @@ oauthlib.openid.connect.core.endpoints.pre_configured
|
|||
This module is an implementation of various endpoints needed
|
||||
for providing OpenID Connect servers.
|
||||
"""
|
||||
|
||||
from oauthlib.oauth2.rfc6749.endpoints import (
|
||||
AuthorizationEndpoint, IntrospectEndpoint, ResourceEndpoint,
|
||||
RevocationEndpoint, TokenEndpoint,
|
||||
AuthorizationEndpoint,
|
||||
IntrospectEndpoint,
|
||||
ResourceEndpoint,
|
||||
RevocationEndpoint,
|
||||
TokenEndpoint,
|
||||
)
|
||||
from oauthlib.oauth2.rfc6749.grant_types import (
|
||||
AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
|
||||
ClientCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant,
|
||||
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
|
||||
ClientCredentialsGrant,
|
||||
ImplicitGrant as OAuth2ImplicitGrant,
|
||||
ResourceOwnerPasswordCredentialsGrant,
|
||||
)
|
||||
from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant
|
||||
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 (
|
||||
AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher,
|
||||
AuthorizationCodeGrantDispatcher,
|
||||
AuthorizationTokenGrantDispatcher,
|
||||
ImplicitTokenGrantDispatcher,
|
||||
)
|
||||
from ..tokens import JWTToken
|
||||
from .userinfo import UserInfoEndpoint
|
||||
|
||||
|
||||
class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint, UserInfoEndpoint):
|
||||
class Server(
|
||||
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, request_validator, token_expires_in=None,
|
||||
token_generator=None, refresh_token_generator=None,
|
||||
*args, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
request_validator,
|
||||
token_expires_in=None,
|
||||
token_generator=None,
|
||||
refresh_token_generator=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Construct a new all-grants-in-one server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
|
@ -48,50 +74,66 @@ class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
|
|||
"""
|
||||
self.auth_grant = OAuth2AuthorizationCodeGrant(request_validator)
|
||||
self.implicit_grant = OAuth2ImplicitGrant(request_validator)
|
||||
self.password_grant = ResourceOwnerPasswordCredentialsGrant(
|
||||
request_validator)
|
||||
self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator)
|
||||
self.credentials_grant = ClientCredentialsGrant(request_validator)
|
||||
self.refresh_grant = RefreshTokenGrant(request_validator)
|
||||
self.openid_connect_auth = AuthorizationCodeGrant(request_validator)
|
||||
self.openid_connect_implicit = ImplicitGrant(request_validator)
|
||||
self.openid_connect_hybrid = HybridGrant(request_validator)
|
||||
self.device_code_grant = DeviceCodeGrant(request_validator, **kwargs)
|
||||
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
self.bearer = BearerToken(
|
||||
request_validator, token_generator, token_expires_in, refresh_token_generator
|
||||
)
|
||||
|
||||
self.jwt = JWTToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
self.jwt = JWTToken(
|
||||
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.implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit)
|
||||
self.auth_grant_choice = AuthorizationCodeGrantDispatcher(
|
||||
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
|
||||
# internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination
|
||||
AuthorizationEndpoint.__init__(self, default_response_type='code',
|
||||
response_types={
|
||||
'code': self.auth_grant_choice,
|
||||
'token': self.implicit_grant_choice,
|
||||
'id_token': self.openid_connect_implicit,
|
||||
'id_token token': self.openid_connect_implicit,
|
||||
'code token': self.openid_connect_hybrid,
|
||||
'code id_token': self.openid_connect_hybrid,
|
||||
'code id_token token': self.openid_connect_hybrid,
|
||||
'none': self.auth_grant
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
AuthorizationEndpoint.__init__(
|
||||
self,
|
||||
default_response_type="code",
|
||||
response_types={
|
||||
"code": self.auth_grant_choice,
|
||||
"token": self.implicit_grant_choice,
|
||||
"id_token": self.openid_connect_implicit,
|
||||
"id_token token": self.openid_connect_implicit,
|
||||
"code token": self.openid_connect_hybrid,
|
||||
"code id_token": self.openid_connect_hybrid,
|
||||
"code id_token token": self.openid_connect_hybrid,
|
||||
"none": self.auth_grant,
|
||||
},
|
||||
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',
|
||||
grant_types={
|
||||
'authorization_code': self.token_grant_choice,
|
||||
'password': self.password_grant,
|
||||
'client_credentials': self.credentials_grant,
|
||||
'refresh_token': self.refresh_grant,
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer, 'JWT': self.jwt})
|
||||
TokenEndpoint.__init__(
|
||||
self,
|
||||
default_grant_type="authorization_code",
|
||||
grant_types={
|
||||
"authorization_code": self.token_grant_choice,
|
||||
"password": self.password_grant,
|
||||
"client_credentials": self.credentials_grant,
|
||||
"refresh_token": self.refresh_grant,
|
||||
"urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant,
|
||||
},
|
||||
default_token_type=self.bearer,
|
||||
)
|
||||
ResourceEndpoint.__init__(
|
||||
self, default_token="Bearer", token_types={"Bearer": self.bearer, "JWT": self.jwt}
|
||||
)
|
||||
RevocationEndpoint.__init__(self, request_validator)
|
||||
IntrospectEndpoint.__init__(self, request_validator)
|
||||
UserInfoEndpoint.__init__(self, request_validator)
|
||||
|
|
|
@ -5,6 +5,9 @@ oauthlib.oauth2.rfc6749.errors
|
|||
Error used both by OAuth 2 clients and providers to represent the spec
|
||||
defined error responses for all four core grant types.
|
||||
"""
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error
|
||||
|
||||
|
||||
|
@ -72,8 +75,8 @@ class InvalidRequestURI(OpenIDClientError):
|
|||
contains invalid data.
|
||||
"""
|
||||
error = 'invalid_request_uri'
|
||||
description = 'The request_uri in the Authorization Request returns an ' \
|
||||
'error or contains invalid data.'
|
||||
description = ('The request_uri in the Authorization Request returns an '
|
||||
'error or contains invalid data.')
|
||||
|
||||
|
||||
class InvalidRequestObject(OpenIDClientError):
|
||||
|
@ -137,8 +140,6 @@ class InsufficientScopeError(OAuth2Error):
|
|||
|
||||
|
||||
def raise_from_error(error, params=None):
|
||||
import inspect
|
||||
import sys
|
||||
kwargs = {
|
||||
'description': params.get('error_description'),
|
||||
'uri': params.get('error_uri'),
|
||||
|
|
|
@ -310,11 +310,15 @@ class GrantTypeBase:
|
|||
msg = "Session user does not match client supplied user."
|
||||
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 = {
|
||||
'display': request.display,
|
||||
'nonce': request.nonce,
|
||||
'prompt': prompt,
|
||||
'ui_locales': request.ui_locales.split() if request.ui_locales else [],
|
||||
'ui_locales': ui_locales,
|
||||
'id_token_hint': request.id_token_hint,
|
||||
'login_hint': request.login_hint,
|
||||
'claims': request.claims
|
||||
|
|
|
@ -80,9 +80,9 @@ class AuthorizationTokenGrantDispatcher(Dispatcher):
|
|||
handler = self.default_grant
|
||||
scopes = ()
|
||||
parameters = dict(request.decoded_body)
|
||||
client_id = parameters.get('client_id', None)
|
||||
code = parameters.get('code', None)
|
||||
redirect_uri = parameters.get('redirect_uri', None)
|
||||
client_id = parameters.get('client_id')
|
||||
code = parameters.get('code')
|
||||
redirect_uri = parameters.get('redirect_uri')
|
||||
|
||||
# If code is not present fallback to `default_grant` which will
|
||||
# raise an error for the missing `code` in `create_token_response` step.
|
||||
|
|
|
@ -54,10 +54,9 @@ class HybridGrant(GrantTypeBase):
|
|||
# Token. Sufficient entropy MUST be present in the `nonce`
|
||||
# values used to prevent attackers from guessing values. For
|
||||
# implementation notes, see Section 15.5.2.
|
||||
if request.response_type in ["code id_token", "code id_token token"]:
|
||||
if not request.nonce:
|
||||
raise InvalidRequestError(
|
||||
request=request,
|
||||
description='Request is missing mandatory nonce parameter.'
|
||||
)
|
||||
if request.response_type in ["code id_token", "code id_token token"] and not request.nonce:
|
||||
raise InvalidRequestError(
|
||||
request=request,
|
||||
description='Request is missing mandatory nonce parameter.'
|
||||
)
|
||||
return request_info
|
||||
|
|
|
@ -143,7 +143,7 @@ class RequestValidator(OAuth2RequestValidator):
|
|||
Token MUST NOT be accepted by the RP when performing
|
||||
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.
|
||||
|
||||
More information can be found at `OpenID Connect Core#Claims`_
|
||||
|
|
|
@ -27,10 +27,7 @@ class JWTToken(TokenBase):
|
|||
def create_token(self, request, refresh_token=False):
|
||||
"""Create a JWT Token, using requestvalidator method."""
|
||||
|
||||
if callable(self.expires_in):
|
||||
expires_in = self.expires_in(request)
|
||||
else:
|
||||
expires_in = self.expires_in
|
||||
expires_in = self.expires_in(request) if callable(self.expires_in) else self.expires_in
|
||||
|
||||
request.expires_in = expires_in
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ signals_available = False
|
|||
try:
|
||||
from blinker import Namespace
|
||||
signals_available = True
|
||||
except ImportError: # noqa
|
||||
except ImportError:
|
||||
class Namespace:
|
||||
def signal(self, name, doc=None):
|
||||
return _FakeSignal(name, doc)
|
||||
|
@ -26,7 +26,8 @@ except ImportError: # noqa
|
|||
raise RuntimeError('signalling support is unavailable '
|
||||
'because the blinker library is '
|
||||
'not installed.')
|
||||
send = lambda *a, **kw: None
|
||||
def send(*a, **kw):
|
||||
return None
|
||||
connect = disconnect = has_receivers_for = receivers_for = \
|
||||
temporarily_connected_to = connected_to = _fail
|
||||
del _fail
|
||||
|
|
|
@ -174,8 +174,7 @@ URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? (?: \# %(fragment)s
|
|||
URI_reference = r"^(?: %(URI)s | %(relative_ref)s )$" % locals()
|
||||
|
||||
# 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):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[metadata]
|
||||
license_file = LICENSE
|
||||
license_files = LICENSE
|
||||
|
||||
[isort]
|
||||
combine_as_imports = true
|
||||
|
|
25
setup.py
25
setup.py
|
@ -1,8 +1,9 @@
|
|||
#!/usr/bin/env python3
|
||||
# Hack because logging + setuptools sucks.
|
||||
try:
|
||||
import contextlib
|
||||
with contextlib.suppress(ImportError):
|
||||
import multiprocessing
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
from os.path import dirname, join
|
||||
|
||||
|
@ -27,14 +28,13 @@ setup(
|
|||
long_description=fread('README.rst'),
|
||||
long_description_content_type='text/x-rst',
|
||||
author='The OAuthlib Community',
|
||||
author_email='idan@gazit.me',
|
||||
maintainer='Ib Lundgren',
|
||||
maintainer_email='ib.lundgren@gmail.com',
|
||||
maintainer='Jonathan Huot',
|
||||
maintainer_email='jonathan.huot@gmail.com',
|
||||
url='https://github.com/oauthlib/oauthlib',
|
||||
platforms='any',
|
||||
license='BSD',
|
||||
packages=find_packages(exclude=('docs', 'tests', 'tests.*')),
|
||||
python_requires='>=3.6',
|
||||
license='BSD-3-Clause',
|
||||
packages=find_packages(exclude=('docs', 'examples', 'tests', 'tests.*')),
|
||||
python_requires='>=3.8',
|
||||
extras_require={
|
||||
'rsa': rsa_require,
|
||||
'signedtoken': signedtoken_require,
|
||||
|
@ -44,18 +44,17 @@ setup(
|
|||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: MacOS',
|
||||
'Operating System :: POSIX',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3.12',
|
||||
'Programming Language :: Python :: 3.13',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: Implementation',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
|
|
|
@ -304,7 +304,7 @@ class ClientValidator(RequestValidator):
|
|||
def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
|
||||
request, request_token=None, access_token=None):
|
||||
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):
|
||||
return client_key in self.clients
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from jwt import InvalidKeyError
|
||||
from oauthlib.oauth1.rfc5849.signature import (
|
||||
base_string_uri, collect_parameters, normalize_parameters,
|
||||
sign_hmac_sha1_with_client, sign_hmac_sha256_with_client,
|
||||
|
@ -82,12 +83,13 @@ class SignatureTests(TestCase):
|
|||
|
||||
# ==== Example test vector =======================================
|
||||
|
||||
eg_signature_base_string =\
|
||||
'POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q' \
|
||||
'%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_' \
|
||||
'key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m' \
|
||||
'ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk' \
|
||||
eg_signature_base_string = (
|
||||
'POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q'
|
||||
'%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_'
|
||||
'key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m'
|
||||
'ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk'
|
||||
'9d7dh3k39sjv7'
|
||||
)
|
||||
|
||||
# The _signature base string_ above is copied from the end of
|
||||
# RFC 5849 section 3.4.1.1.
|
||||
|
@ -101,11 +103,11 @@ class SignatureTests(TestCase):
|
|||
|
||||
eg_base_string_uri = 'http://example.com/request'
|
||||
|
||||
eg_normalized_parameters =\
|
||||
'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' \
|
||||
eg_normalized_parameters = (
|
||||
'a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj'
|
||||
'dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1'
|
||||
'&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7'
|
||||
|
||||
)
|
||||
# The above _normalized parameters_ corresponds to the parameters below.
|
||||
#
|
||||
# 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_authorization_header =\
|
||||
'OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2",' \
|
||||
' oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1",' \
|
||||
' oauth_timestamp="137131201", oauth_nonce="7d8f3e4a",' \
|
||||
eg_authorization_header = (
|
||||
'OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2",'
|
||||
' oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1",'
|
||||
' oauth_timestamp="137131201", oauth_nonce="7d8f3e4a",'
|
||||
' oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"'
|
||||
|
||||
)
|
||||
# ==== Signature base string calculating function tests ==========
|
||||
|
||||
def test_signature_base_string(self):
|
||||
|
@ -465,10 +467,10 @@ class SignatureTests(TestCase):
|
|||
expected_signature_hmac_sha256 = \
|
||||
'wdfdHUKXHbOnOGZP8WFAWMSAmWzN3EVBWWgXGlC/Eo4='
|
||||
|
||||
expected_signature_hmac_sha512 = \
|
||||
'u/vlyZFDxOWOZ9UUXwRBJHvq8/T4jCA74ocRmn2ECnjUBTAeJiZIRU8hDTjS88Tz' \
|
||||
expected_signature_hmac_sha512 = (
|
||||
'u/vlyZFDxOWOZ9UUXwRBJHvq8/T4jCA74ocRmn2ECnjUBTAeJiZIRU8hDTjS88Tz'
|
||||
'1fGONffMpdZxUkUTW3k1kg=='
|
||||
|
||||
)
|
||||
def test_sign_hmac_sha1_with_client(self):
|
||||
"""
|
||||
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
|
||||
# most text editors will add.
|
||||
|
||||
expected_signature_rsa_sha1 = \
|
||||
'mFY2KOEnlYWsTvUA+5kxuBIcvBYXu+ljw9ttVJQxKduMueGSVPCB1tK1PlqVLK738' \
|
||||
'HK0t19ecBJfb6rMxUwrriw+MlBO+jpojkZIWccw1J4cAb4qu4M81DbpUAq4j/1w/Q' \
|
||||
expected_signature_rsa_sha1 = (
|
||||
'mFY2KOEnlYWsTvUA+5kxuBIcvBYXu+ljw9ttVJQxKduMueGSVPCB1tK1PlqVLK738'
|
||||
'HK0t19ecBJfb6rMxUwrriw+MlBO+jpojkZIWccw1J4cAb4qu4M81DbpUAq4j/1w/Q'
|
||||
'yTR4TWCODlEfN7Zfgy8+pf+TjiXfIwRC1jEWbuL1E='
|
||||
|
||||
expected_signature_rsa_sha256 = \
|
||||
'jqKl6m0WS69tiVJV8ZQ6aQEfJqISoZkiPBXRv6Al2+iFSaDpfeXjYm+Hbx6m1azR' \
|
||||
'drZ/35PM3cvuid3LwW/siAkzb0xQcGnTyAPH8YcGWzmnKGY7LsB7fkqThchNxvRK' \
|
||||
)
|
||||
expected_signature_rsa_sha256 = (
|
||||
'jqKl6m0WS69tiVJV8ZQ6aQEfJqISoZkiPBXRv6Al2+iFSaDpfeXjYm+Hbx6m1azR'
|
||||
'drZ/35PM3cvuid3LwW/siAkzb0xQcGnTyAPH8YcGWzmnKGY7LsB7fkqThchNxvRK'
|
||||
'/N7s9M1WMnfZZ+1dQbbwtTs1TG1+iexUcV7r3M7Heec='
|
||||
|
||||
expected_signature_rsa_sha512 = \
|
||||
'jL1CnjlsNd25qoZVHZ2oJft47IRYTjpF5CvCUjL3LY0NTnbEeVhE4amWXUFBe9GL' \
|
||||
'DWdUh/79ZWNOrCirBFIP26cHLApjYdt4ZG7EVK0/GubS2v8wT1QPRsog8zyiMZkm' \
|
||||
)
|
||||
expected_signature_rsa_sha512 = (
|
||||
'jL1CnjlsNd25qoZVHZ2oJft47IRYTjpF5CvCUjL3LY0NTnbEeVhE4amWXUFBe9GL'
|
||||
'DWdUh/79ZWNOrCirBFIP26cHLApjYdt4ZG7EVK0/GubS2v8wT1QPRsog8zyiMZkm'
|
||||
'g4JXdWCGXG8YRvRJTg+QKhXuXwS6TcMNakrgzgFIVhA='
|
||||
|
||||
)
|
||||
def test_sign_rsa_sha1_with_client(self):
|
||||
"""
|
||||
Test sign and verify with RSA-SHA1.
|
||||
|
@ -764,12 +766,17 @@ MmgDHR2tt8KeYTSgfU+BAkBcaVF91EQ7VXhvyABNYjeYP7lU7orOgdWMa/zbLXSU
|
|||
|
||||
# Signing needs a private key
|
||||
|
||||
for bad_value in [None, '', 'foobar']:
|
||||
for bad_value in [None, '']:
|
||||
self.assertRaises(ValueError,
|
||||
sign_rsa_sha1_with_client,
|
||||
self.eg_signature_base_string,
|
||||
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,
|
||||
sign_rsa_sha1_with_client,
|
||||
self.eg_signature_base_string,
|
||||
|
|
|
@ -53,11 +53,11 @@ class UtilsTests(TestCase):
|
|||
# The following is an isolated test function used to test the filter_params decorator.
|
||||
@filter_params
|
||||
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])
|
||||
|
||||
# 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.
|
||||
# Any param that does not start with 'oauth'
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from oauthlib import common
|
||||
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.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):
|
||||
client = Client(self.client_id)
|
||||
length = 43
|
||||
|
@ -339,6 +316,12 @@ class ClientTest(TestCase):
|
|||
code_verifier = client.create_code_verifier(length=length)
|
||||
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):
|
||||
client = Client(self.client_id)
|
||||
code_verifier = client.create_code_verifier(length=128)
|
||||
|
@ -353,3 +336,43 @@ class ClientTest(TestCase):
|
|||
code_verifier = client.create_code_verifier(length=128)
|
||||
code_challenge_s256 = client.create_code_challenge(code_verifier=code_verifier, code_challenge_method='S256')
|
||||
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)
|
||||
|
|
|
@ -166,7 +166,7 @@ mfvGGg3xNjTMO7IdrwIDAQAB
|
|||
@patch('time.time')
|
||||
def test_parse_token_response(self, t):
|
||||
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)
|
||||
|
||||
|
|
|
@ -252,18 +252,34 @@ class WebApplicationClientTest(TestCase):
|
|||
self.assertEqual(r4b_params['client_id'], self.client_id)
|
||||
|
||||
# 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
|
||||
rWarnings1 = client.prepare_request_body(client_id=self.client_id)
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertIsInstance(w[0].message, DeprecationWarning)
|
||||
|
||||
# warning1 - raise a DeprecationWarning if a `client_id` is submitted
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
client.prepare_request_body(client_id=self.client_id)
|
||||
# testing the exact warning message in Python2&Python3 is a pain
|
||||
|
||||
# scenario Exceptions
|
||||
# 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')
|
||||
# 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)
|
||||
|
|
|
@ -20,8 +20,8 @@ class MetadataEndpointTest(TestCase):
|
|||
"introspection_endpoint": "https://foo.bar/introspect",
|
||||
"token_endpoint": "https://foo.bar/token"
|
||||
}
|
||||
from oauthlib.oauth2 import Server as OAuth2Server
|
||||
from oauthlib.openid import Server as OpenIDServer
|
||||
from oauthlib.oauth2 import Server as OAuth2Server # noqa: PLC0415
|
||||
from oauthlib.openid import Server as OpenIDServer # noqa: PLC0415
|
||||
|
||||
endpoint = OAuth2Server(None)
|
||||
metadata = MetadataEndpoint([endpoint], default_claims)
|
||||
|
@ -98,6 +98,7 @@ class MetadataEndpointTest(TestCase):
|
|||
"scopes_supported": ["email", "profile"],
|
||||
"grant_types_supported": [
|
||||
"authorization_code",
|
||||
"urn:ietf:params:oauth:grant-type:device_code",
|
||||
"password",
|
||||
"client_credentials",
|
||||
"refresh_token",
|
||||
|
@ -130,8 +131,8 @@ class MetadataEndpointTest(TestCase):
|
|||
}
|
||||
|
||||
def sort_list(claims):
|
||||
for k in claims.keys():
|
||||
claims[k] = sorted(claims[k])
|
||||
for key, value in claims.items():
|
||||
claims[key] = sorted(value)
|
||||
|
||||
sort_list(metadata.claims)
|
||||
sort_list(expected_claims)
|
||||
|
|
|
@ -130,6 +130,22 @@ class RefreshTokenGrantTest(TestCase):
|
|||
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):
|
||||
self.request.grant_type = 'wrong_type'
|
||||
self.assertRaises(errors.UnsupportedGrantTypeError,
|
||||
|
@ -168,7 +184,7 @@ class RefreshTokenGrantTest(TestCase):
|
|||
# all ok but without request.scope
|
||||
del self.request.scope
|
||||
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
|
||||
|
||||
|
|
|
@ -302,3 +302,30 @@ class ParameterTests(TestCase):
|
|||
finally:
|
||||
signals.scope_changed.disconnect(record_scope_change)
|
||||
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))
|
||||
|
|
|
@ -76,7 +76,7 @@ class TokenTest(TestCase):
|
|||
bearer_uri = 'http://server.example.com/resource?access_token=vF9dft4qmT'
|
||||
|
||||
def _mocked_validate_bearer_token(self, token, scopes, request):
|
||||
if not token:
|
||||
if not token: # noqa: SIM103
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ class UtilsTests(TestCase):
|
|||
for x in string_list:
|
||||
assert x in set_scope
|
||||
|
||||
self.assertRaises(ValueError, list_to_scope, object())
|
||||
self.assertRaises(ValueError, list_to_scope, object())
|
||||
|
||||
def test_scope_to_list(self):
|
||||
expected = ['foo', 'bar', 'baz']
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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"],
|
||||
)
|
|
@ -28,7 +28,8 @@ class OpenIDConnectEndpointTest(TestCase):
|
|||
'redirect_uri': 'https://a.b/cb',
|
||||
'response_type': 'code',
|
||||
'client_id': 'abcdef',
|
||||
'scope': 'hello openid'
|
||||
'scope': 'hello openid',
|
||||
'ui_locales': 'en-US'
|
||||
}
|
||||
self.url = 'http://a.b/path?' + urlencode(params)
|
||||
|
||||
|
@ -76,3 +77,4 @@ class OpenIDConnectEndpointTest(TestCase):
|
|||
self.assertEqual(creds['prompt'], {'consent'})
|
||||
self.assertEqual(creds['nonce'], 'abcd')
|
||||
self.assertEqual(creds['display'], 'touch')
|
||||
self.assertEqual(creds['ui_locales'], ['en-US'])
|
||||
|
|
|
@ -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))
|
|
@ -1,4 +1,6 @@
|
|||
from datetime import datetime
|
||||
import unittest
|
||||
|
||||
from oauthlib.uri_validate import is_absolute_uri
|
||||
|
||||
from tests.unittest import TestCase
|
||||
|
@ -76,7 +78,6 @@ class UriValidateTest(TestCase):
|
|||
self.assertIsNone(is_absolute_uri('http://[abcd:efgh::1]/'))
|
||||
|
||||
def test_recursive_regex(self):
|
||||
from datetime import datetime
|
||||
t0 = datetime.now()
|
||||
is_absolute_uri('http://[::::::::::::::::::::::::::]/path')
|
||||
t1 = datetime.now()
|
||||
|
|
Loading…
Reference in New Issue