Bump requests-oauthlib from 1.3.0 to 1.3.1 (#1636)

* Bump requests-oauthlib from 1.3.0 to 1.3.1

Bumps [requests-oauthlib](https://github.com/requests/requests-oauthlib) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/requests/requests-oauthlib/releases)
- [Changelog](https://github.com/requests/requests-oauthlib/blob/master/HISTORY.rst)
- [Commits](https://github.com/requests/requests-oauthlib/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: requests-oauthlib
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update requests-oauthlib==1.3.1

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>

[skip ci]
This commit is contained in:
dependabot[bot] 2022-02-07 21:57:50 -08:00 committed by GitHub
parent 5523d4ba88
commit 61960aa744
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 464 additions and 77 deletions

View file

@ -33,3 +33,4 @@ from .rfc6749.grant_types import (
from .rfc6749.request_validator import RequestValidator
from .rfc6749.tokens import BearerToken, OAuth2Token
from .rfc6749.utils import is_secure_transport
from .rfc8628.clients import DeviceClient

View file

@ -8,6 +8,10 @@ for consuming OAuth 2.0 RFC6749.
"""
import time
import warnings
import secrets
import re
import hashlib
import base64
from oauthlib.common import generate_token
from oauthlib.oauth2.rfc6749 import tokens
@ -61,6 +65,9 @@ class Client:
state=None,
redirect_url=None,
state_generator=generate_token,
code_verifier=None,
code_challenge=None,
code_challenge_method=None,
**kwargs):
"""Initialize a client with commonly used attributes.
@ -99,6 +106,15 @@ class Client:
:param state_generator: A no argument state generation callable. Defaults
to :py:meth:`oauthlib.common.generate_token`.
:param code_verifier: PKCE parameter. A cryptographically random string that is used to correlate the
authorization request to the token request.
:param code_challenge: PKCE parameter. A challenge derived from the code verifier that is sent in the
authorization request, to be verified against later.
:param code_challenge_method: PKCE parameter. A method that was used to derive code challenge.
Defaults to "plain" if not present in the request.
"""
self.client_id = client_id
@ -113,6 +129,9 @@ class Client:
self.state_generator = state_generator
self.state = state
self.redirect_url = redirect_url
self.code_verifier = code_verifier
self.code_challenge = code_challenge
self.code_challenge_method = code_challenge_method
self.code = None
self.expires_in = None
self._expires_at = None
@ -471,6 +490,91 @@ class Client:
raise ValueError("Invalid token placement.")
return uri, headers, body
def create_code_verifier(self, length):
"""Create PKCE **code_verifier** used in computing **code_challenge**.
:param length: REQUIRED. The length of the code_verifier.
The client first creates a code verifier, "code_verifier", for each
OAuth 2.0 [RFC6749] Authorization Request, in the following manner:
code_verifier = high-entropy cryptographic random STRING using the
unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
from Section 2.3 of [RFC3986], with a minimum length of 43 characters
and a maximum length of 128 characters.
.. _`Section 4.1`: https://tools.ietf.org/html/rfc7636#section-4.1
"""
code_verifier = None
if not length >= 43:
raise ValueError("Length must be greater than or equal to 43")
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")
self.code_verifier = code_verifier
return code_verifier
def create_code_challenge(self, code_verifier, code_challenge_method=None):
"""Create PKCE **code_challenge** derived from the **code_verifier**.
:param code_verifier: REQUIRED. The **code_verifier** generated from create_code_verifier().
:param code_challenge_method: OPTIONAL. The method used to derive the **code_challenge**. Acceptable
values include "S256". DEFAULT is "plain".
The client then creates a code challenge derived from the code
verifier by using one of the following transformations on the code
verifier:
plain
code_challenge = code_verifier
S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
If the client is capable of using "S256", it MUST use "S256", as
"S256" is Mandatory To Implement (MTI) on the server. Clients are
permitted to use "plain" only if they cannot support "S256" for some
technical reason and know via out-of-band configuration that the
server supports "plain".
The plain transformation is for compatibility with existing
deployments and for constrained environments that can't use the S256
transformation.
.. _`Section 4.2`: https://tools.ietf.org/html/rfc7636#section-4.2
"""
code_challenge = None
if code_verifier == None:
raise ValueError("Invalid code_verifier")
if code_challenge_method == None:
code_challenge_method = "plain"
self.code_challenge_method = code_challenge_method
code_challenge = code_verifier
self.code_challenge = code_challenge
if code_challenge_method == "S256":
h = hashlib.sha256()
h.update(code_verifier.encode(encoding='ascii'))
sha256_val = h.digest()
code_challenge = bytes.decode(base64.urlsafe_b64encode(sha256_val))
# replace '+' with '-', '/' with '_', and remove trailing '='
code_challenge = code_challenge.replace("+", "-").replace("/", "_").replace("=", "")
self.code_challenge = code_challenge
return code_challenge
def _add_mac_token(self, uri, http_method='GET', body=None,
headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs):
"""Add a MAC token to the request authorization header.
@ -513,7 +617,10 @@ class Client:
self._expires_at = time.time() + int(self.expires_in)
if 'expires_at' in response:
self._expires_at = int(response.get('expires_at'))
try:
self._expires_at = int(response.get('expires_at'))
except:
self._expires_at = None
if 'mac_key' in response:
self.mac_key = response.get('mac_key')

View file

@ -41,7 +41,7 @@ class WebApplicationClient(Client):
self.code = code
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
state=None, **kwargs):
state=None, code_challenge=None, code_challenge_method='plain', **kwargs):
"""Prepare the authorization code request URI
The client constructs the request URI by adding the following
@ -62,6 +62,13 @@ class WebApplicationClient(Client):
to the client. The parameter SHOULD be used for preventing
cross-site request forgery as described in `Section 10.12`_.
:param code_challenge: OPTIONAL. PKCE parameter. REQUIRED if PKCE is enforced.
A challenge derived from the code_verifier that is sent in the
authorization request, to be verified against later.
:param code_challenge_method: OPTIONAL. PKCE parameter. A method that was used to derive code challenge.
Defaults to "plain" if not present in the request.
:param kwargs: Extra arguments to include in the request URI.
In addition to supplied parameters, OAuthLib will append the ``client_id``
@ -76,6 +83,10 @@ class WebApplicationClient(Client):
'https://example.com?client_id=your_id&response_type=code&redirect_uri=https%3A%2F%2Fa.b%2Fcallback'
>>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures'])
'https://example.com?client_id=your_id&response_type=code&scope=profile+pictures'
>>> client.prepare_request_uri('https://example.com', code_challenge='kjasBS523KdkAILD2k78NdcJSk2k3KHG6')
'https://example.com?client_id=your_id&response_type=code&code_challenge=kjasBS523KdkAILD2k78NdcJSk2k3KHG6'
>>> client.prepare_request_uri('https://example.com', code_challenge_method='S256')
'https://example.com?client_id=your_id&response_type=code&code_challenge_method=S256'
>>> client.prepare_request_uri('https://example.com', foo='bar')
'https://example.com?client_id=your_id&response_type=code&foo=bar'
@ -87,10 +98,11 @@ class WebApplicationClient(Client):
"""
scope = self.scope if scope is None else scope
return prepare_grant_uri(uri, self.client_id, 'code',
redirect_uri=redirect_uri, scope=scope, state=state, **kwargs)
redirect_uri=redirect_uri, scope=scope, state=state, code_challenge=code_challenge,
code_challenge_method=code_challenge_method, **kwargs)
def prepare_request_body(self, code=None, redirect_uri=None, body='',
include_client_id=True, **kwargs):
include_client_id=True, code_verifier=None, **kwargs):
"""Prepare the access token request body.
The client makes a request to the token endpoint by adding the
@ -113,6 +125,9 @@ class WebApplicationClient(Client):
authorization server as described in `Section 3.2.1`_.
:type include_client_id: Boolean
:param code_verifier: OPTIONAL. A cryptographically random string that is used to correlate the
authorization request to the token request.
:param kwargs: Extra parameters to include in the token request.
In addition OAuthLib will add the ``grant_type`` parameter set to
@ -127,6 +142,8 @@ class WebApplicationClient(Client):
>>> client = WebApplicationClient('your_id')
>>> client.prepare_request_body(code='sh35ksdf09sf')
'grant_type=authorization_code&code=sh35ksdf09sf'
>>> client.prepare_request_body(code_verifier='KB46DCKJ873NCGXK5GD682NHDKK34GR')
'grant_type=authorization_code&code_verifier=KB46DCKJ873NCGXK5GD682NHDKK34GR'
>>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar')
'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar'
@ -154,7 +171,7 @@ class WebApplicationClient(Client):
kwargs['client_id'] = self.client_id
kwargs['include_client_id'] = include_client_id
return prepare_token_request(self.grant_type, code=code, body=body,
redirect_uri=redirect_uri, **kwargs)
redirect_uri=redirect_uri, code_verifier=code_verifier, **kwargs)
def parse_request_uri_response(self, uri, state=None):
"""Parse the URI query for code and state.

View file

@ -54,7 +54,8 @@ class MetadataEndpoint(BaseEndpoint):
"""Create metadata response
"""
headers = {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
}
return headers, json.dumps(self.claims), 200

View file

@ -103,15 +103,12 @@ class OAuth2Error(Exception):
value "Bearer". This scheme MUST be followed by one or more
auth-param values.
"""
authvalues = [
"Bearer",
'error="{}"'.format(self.error)
]
authvalues = ['error="{}"'.format(self.error)]
if self.description:
authvalues.append('error_description="{}"'.format(self.description))
if self.uri:
authvalues.append('error_uri="{}"'.format(self.uri))
return {"WWW-Authenticate": ", ".join(authvalues)}
return {"WWW-Authenticate": "Bearer " + ", ".join(authvalues)}
return {}

View file

@ -10,6 +10,7 @@ import logging
from oauthlib import common
from .. import errors
from ..utils import is_secure_transport
from .base import GrantTypeBase
log = logging.getLogger(__name__)
@ -272,6 +273,8 @@ class AuthorizationCodeGrant(GrantTypeBase):
grant = self.create_authorization_code(request)
for modifier in self._code_modifiers:
grant = modifier(grant, token_handler, request)
if 'access_token' in grant:
self.request_validator.save_token(grant, request)
log.debug('Saving grant %r for %r.', grant, request)
self.request_validator.save_authorization_code(
request.client_id, grant, request)
@ -310,6 +313,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
self.request_validator.save_token(token, request)
self.request_validator.invalidate_authorization_code(
request.client_id, request.code, request)
headers.update(self._create_cors_headers(request))
return headers, json.dumps(token), 200
def validate_authorization_request(self, request):
@ -543,3 +547,20 @@ class AuthorizationCodeGrant(GrantTypeBase):
if challenge_method in self._code_challenge_methods:
return self._code_challenge_methods[challenge_method](verifier, challenge)
raise NotImplementedError('Unknown challenge_method %s' % challenge_method)
def _create_cors_headers(self, request):
"""If CORS is allowed, create the appropriate headers."""
if 'origin' not in request.headers:
return {}
origin = request.headers['origin']
if not is_secure_transport(origin):
log.debug('Origin "%s" is not HTTPS, CORS not allowed.', origin)
return {}
elif not self.request_validator.is_origin_allowed(
request.client_id, origin, request):
log.debug('Invalid origin "%s", CORS not allowed.', origin)
return {}
else:
log.debug('Valid origin "%s", injecting CORS headers.', origin)
return {'Access-Control-Allow-Origin': origin}

View file

@ -63,7 +63,7 @@ class RefreshTokenGrant(GrantTypeBase):
refresh_token=self.issue_new_refresh_tokens)
for modifier in self._token_modifiers:
token = modifier(token)
token = modifier(token, token_handler, request)
self.request_validator.save_token(token, request)

View file

@ -23,7 +23,7 @@ from .utils import is_secure_transport, list_to_scope, scope_to_list
def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
scope=None, state=None, **kwargs):
scope=None, state=None, code_challenge=None, code_challenge_method='plain', **kwargs):
"""Prepare the authorization grant request URI.
The client constructs the request URI by adding the following
@ -45,6 +45,11 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
back to the client. The parameter SHOULD be used for
preventing cross-site request forgery as described in
`Section 10.12`_.
:param code_challenge: PKCE paramater. A challenge derived from the
code_verifier that is sent in the authorization
request, to be verified against later.
:param code_challenge_method: PKCE parameter. A method that was used to derive the
code_challenge. Defaults to "plain" if not present in the request.
:param kwargs: Extra arguments to embed in the grant/authorization URL.
An example of an authorization code grant authorization URL:
@ -52,6 +57,7 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
.. code-block:: http
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&code_challenge=kjasBS523KdkAILD2k78NdcJSk2k3KHG6&code_challenge_method=S256
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
@ -73,6 +79,9 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
params.append(('scope', list_to_scope(scope)))
if state:
params.append(('state', state))
if code_challenge is not None:
params.append(('code_challenge', code_challenge))
params.append(('code_challenge_method', code_challenge_method))
for k in kwargs:
if kwargs[k]:
@ -81,7 +90,7 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
return add_params_to_uri(uri, params)
def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs):
def prepare_token_request(grant_type, body='', include_client_id=True, code_verifier=None, **kwargs):
"""Prepare the access token request.
The client makes a request to the token endpoint by adding the
@ -116,6 +125,9 @@ def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs)
authorization request as described in
`Section 4.1.1`_, and their values MUST be identical. *
:param code_verifier: PKCE parameter. A cryptographically random string that is used to correlate the
authorization request to the token request.
:param kwargs: Extra arguments to embed in the request body.
Parameters marked with a `*` above are not explicit arguments in the
@ -142,6 +154,10 @@ def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs)
if client_id is not None:
params.append(('client_id', client_id))
# use code_verifier if code_challenge was passed in the authorization request
if code_verifier is not None:
params.append(('code_verifier', code_verifier))
# the kwargs iteration below only supports including boolean truth (truthy)
# values, but some servers may require an empty string for `client_secret`
client_secret = kwargs.pop('client_secret', None)

View file

@ -649,3 +649,28 @@ class RequestValidator:
"""
raise NotImplementedError('Subclasses must implement this method.')
def is_origin_allowed(self, client_id, origin, request, *args, **kwargs):
"""Indicate if the given origin is allowed to access the token endpoint
via Cross-Origin Resource Sharing (CORS). CORS is used by browser-based
clients, such as Single-Page Applications, to perform the Authorization
Code Grant.
(Note: If performing Authorization Code Grant via a public client such
as a browser, you should use PKCE as well.)
If this method returns true, the appropriate CORS headers will be added
to the response. By default this method always returns False, meaning
CORS is disabled.
:param client_id: Unicode client identifier.
:param redirect_uri: Unicode origin.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: bool
Method is used by:
- Authorization Code Grant
"""
return False

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.
"""
import logging
log = logging.getLogger(__name__)

View file

@ -0,0 +1,8 @@
"""
oauthlib.oauth2.rfc8628
~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming OAuth 2.0 Device Authorization RFC8628.
"""
from .device import DeviceClient

View file

@ -0,0 +1,94 @@
"""
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 import BackendApplicationClient, Client
from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
from oauthlib.oauth2.rfc6749.utils import is_secure_transport, list_to_scope
from oauthlib.common import add_params_to_uri
class DeviceClient(Client):
"""A public client utilizing the device authorization workflow.
The client can request an access token using a device code and
a public client id associated with the device code as defined
in RFC8628.
The device authorization grant type can be used to obtain both
access tokens and refresh tokens and is intended to be used in
a scenario where the device being authorized does not have a
user interface that is suitable for performing authentication.
"""
grant_type = 'urn:ietf:params:oauth:grant-type:device_code'
def __init__(self, client_id, **kwargs):
super().__init__(client_id, **kwargs)
self.client_secret = kwargs.get('client_secret')
def prepare_request_uri(self, uri, scope=None, **kwargs):
if not is_secure_transport(uri):
raise InsecureTransportError()
scope = self.scope if scope is None else scope
params = [(('client_id', self.client_id)), (('grant_type', self.grant_type))]
if self.client_secret is not None:
params.append(('client_secret', self.client_secret))
if scope:
params.append(('scope', list_to_scope(scope)))
for k in kwargs:
if kwargs[k]:
params.append((str(k), kwargs[k]))
return add_params_to_uri(uri, params)
def prepare_request_body(self, device_code, body='', scope=None,
include_client_id=False, **kwargs):
"""Add device_code to request body
The client makes a request to the token endpoint by adding the
device_code as a parameter using the
"application/x-www-form-urlencoded" format to the HTTP request
body.
:param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''.
:param scope: The scope of the access request as described by
`Section 3.3`_.
:param include_client_id: `True` to send the `client_id` in the
body of the upstream request. This is required
if the client is not authenticating with the
authorization server as described in
`Section 3.2.1`_. False otherwise (default).
:type include_client_id: Boolean
:param kwargs: Extra credentials to include in the token request.
The prepared body will include all provided device_code as well as
the ``grant_type`` parameter set to
``urn:ietf:params:oauth:grant-type:device_code``::
>>> from oauthlib.oauth2 import DeviceClient
>>> client = DeviceClient('your_id', 'your_code')
>>> client.prepare_request_body(scope=['hello', 'world'])
'grant_type=urn:ietf:params:oauth:grant-type:device_code&scope=hello+world'
.. _`Section 3.4`: https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
"""
kwargs['client_id'] = self.client_id
kwargs['include_client_id'] = include_client_id
scope = self.scope if scope is None else scope
return prepare_token_request(self.grant_type, body=body, device_code=device_code,
scope=scope, **kwargs)