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

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

View File

@ -1,6 +1,36 @@
Changelog
=========
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
View File

@ -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

View File

@ -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

View File

@ -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

6
debian/changelog vendored
View File

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

View File

@ -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

View File

@ -59,8 +59,15 @@ oauthlib/oauth2/rfc6749/grant_types/implicit.py
oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
oauthlib/oauth2/rfc6749/grant_types/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

View File

@ -5,14 +5,14 @@
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())
@ -23,7 +23,7 @@ def set_debug(debug_val):
:param debug_val: Value to set. Must be a bool value.
"""
global _DEBUG
global _DEBUG # noqa: PLW0603
_DEBUG = debug_val
def get_debug():

View File

@ -34,7 +34,7 @@ INVALID_HEX_PATTERN = re.compile(r'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]')
always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'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)

View File

@ -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

View File

@ -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`_

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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')):
@ -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')

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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',
AuthorizationEndpoint.__init__(
self,
default_response_type="code",
response_types={
'code': self.auth_grant,
'token': self.implicit_grant,
'none': self.auth_grant
"code": self.auth_grant,
"token": self.implicit_grant,
"none": self.auth_grant,
},
default_token_type=self.bearer)
default_token_type=self.bearer,
)
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
TokenEndpoint.__init__(
self,
default_grant_type="authorization_code",
grant_types={
'authorization_code': self.auth_grant,
'password': self.password_grant,
'client_credentials': self.credentials_grant,
'refresh_token': self.refresh_grant,
"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})
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',
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,
"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})
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',
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,
"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})
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"]
)

View File

@ -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

View File

@ -6,6 +6,8 @@ Error used both by OAuth 2 clients and providers to represent the spec
defined error responses for all four core grant types.
"""
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'),

View File

@ -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,8 +400,7 @@ 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:
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:

View File

@ -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(

View File

@ -107,8 +107,7 @@ 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'):
elif not hasattr(request.client, 'client_id'):
raise NotImplementedError('Authenticate client must set the '
'request.client.client_id attribute '
'in authenticate_client.')

View File

@ -233,10 +233,7 @@ class ImplicitGrant(GrantTypeBase):
# In OIDC implicit flow it is possible to have a request_type that does not include the access_token!
# "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

View File

@ -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)

View File

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

View File

@ -150,8 +150,7 @@ def prepare_token_request(grant_type, body='', include_client_id=True, code_veri
# pull the `client_id` out of the kwargs.
client_id = kwargs.pop('client_id', None)
if include_client_id:
if client_id is not None:
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
@ -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:
vin, vat, v_at = parse_expires(params)
if vin:
params['expires_in'] = vin
elif 'expires_in' in params:
params.pop('expires_in')
else:
params['expires_at'] = time.time() + int(params['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,11 +450,10 @@ 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'):
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
@ -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

View File

@ -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

View File

@ -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__)

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,55 @@
from oauthlib.oauth2.rfc6749.errors import OAuth2Error
"""
oauthlib.oauth2.rfc8628.errors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Error used both by OAuth2 clients and providers to represent the spec
defined error responses specific to the the device grant
"""
class AuthorizationPendingError(OAuth2Error):
"""
For the device authorization grant;
The authorization request is still pending as the end user hasn't
yet completed the user-interaction steps (Section 3.3). The
client SHOULD repeat the access token request to the token
endpoint (a process known as polling). Before each new request,
the client MUST wait at least the number of seconds specified by
the "interval" parameter of the device authorization response,
or 5 seconds if none was provided, and respect any
increase in the polling interval required by the "slow_down"
error.
"""
error = "authorization_pending"
class SlowDownError(OAuth2Error):
"""
A variant of "authorization_pending", the authorization request is
still pending and polling should continue, but the interval MUST
be increased by 5 seconds for this and all subsequent requests.
"""
error = "slow_down"
class ExpiredTokenError(OAuth2Error):
"""
The "device_code" has expired, and the device authorization
session has concluded. The client MAY commence a new device
authorization request but SHOULD wait for user interaction before
restarting to avoid unnecessary polling.
"""
error = "expired_token"
class AccessDenied(OAuth2Error):
"""
The authorization request was denied.
"""
error = "access_denied"

View File

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

View File

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

View File

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

View File

@ -5,34 +5,60 @@ oauthlib.openid.connect.core.endpoints.pre_configured
This module is an implementation of various endpoints needed
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',
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
"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)
default_token_type=self.bearer,
)
self.token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth)
self.token_grant_choice = AuthorizationTokenGrantDispatcher(
request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth
)
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
TokenEndpoint.__init__(
self,
default_grant_type="authorization_code",
grant_types={
'authorization_code': self.token_grant_choice,
'password': self.password_grant,
'client_credentials': self.credentials_grant,
'refresh_token': self.refresh_grant,
"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})
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)

View File

@ -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'),

View File

@ -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

View File

@ -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.

View File

@ -54,8 +54,7 @@ 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:
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.'

View File

@ -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`_

View File

@ -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

View File

@ -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

View File

@ -174,8 +174,7 @@ URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? (?: \# %(fragment)s
URI_reference = r"^(?: %(URI)s | %(relative_ref)s )$" % locals()
# 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):

View File

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

View File

@ -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',

View File

@ -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

View File

@ -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,

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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)
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)

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,8 @@ class OpenIDConnectEndpointTest(TestCase):
'redirect_uri': 'https://a.b/cb',
'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'])

View File

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

View File

@ -1,4 +1,6 @@
from datetime import datetime
import unittest
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()