diff options
Diffstat (limited to 'oauthlib')
-rw-r--r-- | oauthlib/oauth2/__init__.py | 1 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/base.py | 109 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/web_application.py | 25 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/metadata.py | 3 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/errors.py | 7 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/grant_types/authorization_code.py | 21 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/grant_types/refresh_token.py | 2 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/parameters.py | 20 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/request_validator.py | 25 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc8628/__init__.py | 10 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc8628/clients/__init__.py | 8 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc8628/clients/device.py | 94 | ||||
-rw-r--r-- | oauthlib/openid/connect/core/grant_types/__init__.py | 1 | ||||
-rw-r--r-- | oauthlib/openid/connect/core/grant_types/refresh_token.py | 34 | ||||
-rw-r--r-- | oauthlib/openid/connect/core/request_validator.py | 12 |
15 files changed, 358 insertions, 14 deletions
diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index a6e1ccc..deefb1a 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -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 diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 88065ab..bb4c133 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -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') diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index a1f3db1..1d3b2b5 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -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. diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index 81ee1de..d43a824 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -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 diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index b01e247..da24fea 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -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 {} diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index bf42d88..b799823 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -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} diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index 8698a3d..f801de4 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -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) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index f07b8bd..44738bb 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -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) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 817d594..610a708 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -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 diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py new file mode 100644 index 0000000..531929d --- /dev/null +++ b/oauthlib/oauth2/rfc8628/__init__.py @@ -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__) diff --git a/oauthlib/oauth2/rfc8628/clients/__init__.py b/oauthlib/oauth2/rfc8628/clients/__init__.py new file mode 100644 index 0000000..130b52e --- /dev/null +++ b/oauthlib/oauth2/rfc8628/clients/__init__.py @@ -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 diff --git a/oauthlib/oauth2/rfc8628/clients/device.py b/oauthlib/oauth2/rfc8628/clients/device.py new file mode 100644 index 0000000..df7ff68 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/clients/device.py @@ -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 BackendApplicationClient + >>> 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) diff --git a/oauthlib/openid/connect/core/grant_types/__init__.py b/oauthlib/openid/connect/core/grant_types/__init__.py index 887a585..8dad5f6 100644 --- a/oauthlib/openid/connect/core/grant_types/__init__.py +++ b/oauthlib/openid/connect/core/grant_types/__init__.py @@ -10,3 +10,4 @@ from .dispatchers import ( ) from .hybrid import HybridGrant from .implicit import ImplicitGrant +from .refresh_token import RefreshTokenGrant diff --git a/oauthlib/openid/connect/core/grant_types/refresh_token.py b/oauthlib/openid/connect/core/grant_types/refresh_token.py new file mode 100644 index 0000000..43e4499 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/refresh_token.py @@ -0,0 +1,34 @@ +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +import logging + +from oauthlib.oauth2.rfc6749.grant_types.refresh_token import ( + RefreshTokenGrant as OAuth2RefreshTokenGrant, +) + +from .base import GrantTypeBase + +log = logging.getLogger(__name__) + + +class RefreshTokenGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.proxy_target = OAuth2RefreshTokenGrant( + request_validator=request_validator, **kwargs) + self.register_token_modifier(self.add_id_token) + + def add_id_token(self, token, token_handler, request): + """ + Construct an initial version of id_token, and let the + request_validator sign or encrypt it. + + The authorization_code version of this method is used to + retrieve the nonce accordingly to the code storage. + """ + if not self.request_validator.refresh_id_token(request): + return token + + return super().add_id_token(token, token_handler, request) diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index e8f334b..47c4cd9 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -306,3 +306,15 @@ class RequestValidator(OAuth2RequestValidator): Method is used by: UserInfoEndpoint """ + + def refresh_id_token(self, request): + """Whether the id token should be refreshed. Default, True + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :rtype: True or False + + Method is used by: + RefreshTokenGrant + """ + return True |