summaryrefslogtreecommitdiff
path: root/oauthlib/oauth2/rfc6749
diff options
context:
space:
mode:
authorOmer Katz <omer.drow@gmail.com>2018-12-17 17:00:15 +0200
committerGitHub <noreply@github.com>2018-12-17 17:00:15 +0200
commitc99b9c34f4736050da497aa14de93168d767e501 (patch)
tree2d5ac271168e97df53428b1e273c92ff4501b847 /oauthlib/oauth2/rfc6749
parent79c667eedae4a4d447e8229e37eb844e3af05374 (diff)
parente9c6f01bc6f89e6b90f2c9b61e6a9878d5612147 (diff)
downloadoauthlib-c99b9c34f4736050da497aa14de93168d767e501.tar.gz
Merge branch 'master' into dry-up-code
Diffstat (limited to 'oauthlib/oauth2/rfc6749')
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/introspect.py13
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/metadata.py31
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/revocation.py8
-rw-r--r--oauthlib/oauth2/rfc6749/errors.py61
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/authorization_code.py111
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/client_credentials.py1
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/refresh_token.py2
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py1
-rw-r--r--oauthlib/oauth2/rfc6749/request_validator.py108
9 files changed, 308 insertions, 28 deletions
diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py
index 25dae1f..b10a845 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py
@@ -56,24 +56,25 @@ class IntrospectEndpoint(BaseEndpoint):
an introspection response indicating the token is not active
as described in Section 2.2.
"""
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
request = Request(uri, http_method, body, headers)
try:
self.validate_introspect_request(request)
log.debug('Token introspect valid for %r.', request)
except OAuth2Error as e:
log.debug('Client error during validation of %r. %r.', request, e)
- return {}, e.json, e.status_code
+ headers.update(e.headers)
+ return headers, e.json, e.status_code
claims = self.request_validator.introspect_token(
request.token,
request.token_type_hint,
request
)
- headers = {
- 'Content-Type': 'application/json',
- 'Cache-Control': 'no-store',
- 'Pragma': 'no-cache',
- }
if claims is None:
return headers, json.dumps(dict(active=False)), 200
if "active" in claims:
diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py
index 6d77b9f..60c846b 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py
@@ -19,6 +19,7 @@ from .authorization import AuthorizationEndpoint
from .introspect import IntrospectEndpoint
from .token import TokenEndpoint
from .revocation import RevocationEndpoint
+from .. import grant_types
log = logging.getLogger(__name__)
@@ -89,21 +90,39 @@ class MetadataEndpoint(BaseEndpoint):
raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem))
def validate_metadata_token(self, claims, endpoint):
- claims.setdefault("grant_types_supported", list(endpoint._grant_types.keys()))
+ """
+ If the token endpoint is used in the grant type, the value of this
+ parameter MUST be the same as the value of the "grant_type"
+ parameter passed to the token endpoint defined in the grant type
+ definition.
+ """
+ self._grant_types.extend(endpoint._grant_types.keys())
claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"])
- self.validate_metadata(claims, "grant_types_supported", is_list=True)
self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True)
self.validate_metadata(claims, "token_endpoint_auth_signing_alg_values_supported", is_list=True)
self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True)
def validate_metadata_authorization(self, claims, endpoint):
- claims.setdefault("response_types_supported", list(self._response_types.keys()))
+ claims.setdefault("response_types_supported",
+ list(filter(lambda x: x != "none", endpoint._response_types.keys())))
claims.setdefault("response_modes_supported", ["query", "fragment"])
+ # The OAuth2.0 Implicit flow is defined as a "grant type" but it is not
+ # using the "token" endpoint, as such, we have to add it explicitly to
+ # the list of "grant_types_supported" when enabled.
+ if "token" in claims["response_types_supported"]:
+ self._grant_types.append("implicit")
+
self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True)
self.validate_metadata(claims, "response_modes_supported", is_list=True)
if "code" in claims["response_types_supported"]:
+ code_grant = endpoint._response_types["code"]
+ if not isinstance(code_grant, grant_types.AuthorizationCodeGrant) and hasattr(code_grant, "default_grant"):
+ code_grant = code_grant.default_grant
+
+ claims.setdefault("code_challenge_methods_supported",
+ list(code_grant._code_challenge_methods.keys()))
self.validate_metadata(claims, "code_challenge_methods_supported", is_list=True)
self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True)
@@ -181,6 +200,7 @@ class MetadataEndpoint(BaseEndpoint):
self.validate_metadata(claims, "op_policy_uri", is_url=True)
self.validate_metadata(claims, "op_tos_uri", is_url=True)
+ self._grant_types = []
for endpoint in self.endpoints:
if isinstance(endpoint, TokenEndpoint):
self.validate_metadata_token(claims, endpoint)
@@ -190,4 +210,9 @@ class MetadataEndpoint(BaseEndpoint):
self.validate_metadata_revocation(claims, endpoint)
if isinstance(endpoint, IntrospectEndpoint):
self.validate_metadata_introspection(claims, endpoint)
+
+ # "grant_types_supported" is a combination of all OAuth2 grant types
+ # allowed in the current provider implementation.
+ claims.setdefault("grant_types_supported", self._grant_types)
+ self.validate_metadata(claims, "grant_types_supported", is_list=True)
return claims
diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py
index f9a5648..00da64a 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py
@@ -58,6 +58,11 @@ class RevocationEndpoint(BaseEndpoint):
An invalid token type hint value is ignored by the authorization server
and does not influence the revocation response.
"""
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
request = Request(
uri, http_method=http_method, body=body, headers=headers)
try:
@@ -68,7 +73,8 @@ class RevocationEndpoint(BaseEndpoint):
response_body = e.json
if self.enable_jsonp and request.callback:
response_body = '%s(%s);' % (request.callback, response_body)
- return {}, response_body, e.status_code
+ headers.update(e.headers)
+ return headers, response_body, e.status_code
self.request_validator.revoke_token(request.token,
request.token_type_hint, request)
diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py
index 678fcff..d2a1402 100644
--- a/oauthlib/oauth2/rfc6749/errors.py
+++ b/oauthlib/oauth2/rfc6749/errors.py
@@ -96,6 +96,27 @@ class OAuth2Error(Exception):
def json(self):
return json.dumps(dict(self.twotuples))
+ @property
+ def headers(self):
+ if self.status_code == 401:
+ """
+ https://tools.ietf.org/html/rfc6750#section-3
+
+ All challenges defined by this specification MUST use the auth-scheme
+ value "Bearer". This scheme MUST be followed by one or more
+ auth-param values.
+ """
+ authvalues = [
+ "Bearer",
+ '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 {}
+
class TokenExpiredError(OAuth2Error):
error = 'token_expired'
@@ -180,12 +201,31 @@ class MissingResponseTypeError(InvalidRequestError):
description = 'Missing response_type parameter.'
+class MissingCodeChallengeError(InvalidRequestError):
+ """
+ If the server requires Proof Key for Code Exchange (PKCE) by OAuth
+ public clients and the client does not send the "code_challenge" in
+ the request, the authorization endpoint MUST return the authorization
+ error response with the "error" value set to "invalid_request". The
+ "error_description" or the response of "error_uri" SHOULD explain the
+ nature of error, e.g., code challenge required.
+ """
+ description = 'Code challenge required.'
+
+
+class MissingCodeVerifierError(InvalidRequestError):
+ """
+ The request to the token endpoint, when PKCE is enabled, has
+ the parameter `code_verifier` REQUIRED.
+ """
+ description = 'Code verifier required.'
+
+
class AccessDeniedError(OAuth2Error):
"""
The resource owner or authorization server denied the request.
"""
error = 'access_denied'
- status_code = 401
class UnsupportedResponseTypeError(OAuth2Error):
@@ -196,14 +236,26 @@ class UnsupportedResponseTypeError(OAuth2Error):
error = 'unsupported_response_type'
+class UnsupportedCodeChallengeMethodError(InvalidRequestError):
+ """
+ If the server supporting PKCE does not support the requested
+ transformation, the authorization endpoint MUST return the
+ authorization error response with "error" value set to
+ "invalid_request". The "error_description" or the response of
+ "error_uri" SHOULD explain the nature of error, e.g., transform
+ algorithm not supported.
+ """
+ description = 'Transform algorithm not supported.'
+
+
class InvalidScopeError(OAuth2Error):
"""
- The requested scope is invalid, unknown, or malformed.
+ The requested scope is invalid, unknown, or malformed, or
+ exceeds the scope granted by the resource owner.
https://tools.ietf.org/html/rfc6749#section-5.2
"""
error = 'invalid_scope'
- status_code = 400
class ServerError(OAuth2Error):
@@ -261,7 +313,6 @@ class UnauthorizedClientError(OAuth2Error):
grant type.
"""
error = 'unauthorized_client'
- status_code = 401
class UnsupportedGrantTypeError(OAuth2Error):
@@ -318,7 +369,6 @@ class ConsentRequired(OAuth2Error):
completed without displaying a user interface for End-User consent.
"""
error = 'consent_required'
- status_code = 401
class LoginRequired(OAuth2Error):
@@ -330,7 +380,6 @@ class LoginRequired(OAuth2Error):
completed without displaying a user interface for End-User authentication.
"""
error = 'login_required'
- status_code = 401
class CustomOAuth2Error(OAuth2Error):
diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
index 0cbcb8c..6463391 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
@@ -5,6 +5,8 @@ oauthlib.oauth2.rfc6749.grant_types
"""
from __future__ import absolute_import, unicode_literals
+import base64
+import hashlib
import json
import logging
@@ -16,6 +18,52 @@ from .base import GrantTypeBase
log = logging.getLogger(__name__)
+def code_challenge_method_s256(verifier, challenge):
+ """
+ If the "code_challenge_method" from `Section 4.3`_ was "S256", the
+ received "code_verifier" is hashed by SHA-256, base64url-encoded, and
+ then compared to the "code_challenge", i.e.:
+
+ BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
+
+ How to implement a base64url-encoding
+ function without padding, based upon the standard base64-encoding
+ function that uses padding.
+
+ To be concrete, example C# code implementing these functions is shown
+ below. Similar code could be used in other languages.
+
+ static string base64urlencode(byte [] arg)
+ {
+ string s = Convert.ToBase64String(arg); // Regular base64 encoder
+ s = s.Split('=')[0]; // Remove any trailing '='s
+ s = s.Replace('+', '-'); // 62nd char of encoding
+ s = s.Replace('/', '_'); // 63rd char of encoding
+ return s;
+ }
+
+ In python urlsafe_b64encode is already replacing '+' and '/', but preserve
+ the trailing '='. So we have to remove it.
+
+ .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
+ """
+ return base64.urlsafe_b64encode(
+ hashlib.sha256(verifier.encode()).digest()
+ ).decode().rstrip('=') == challenge
+
+
+def code_challenge_method_plain(verifier, challenge):
+ """
+ If the "code_challenge_method" from `Section 4.3`_ was "plain", they are
+ compared directly, i.e.:
+
+ code_verifier == code_challenge.
+
+ .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
+ """
+ return verifier == challenge
+
+
class AuthorizationCodeGrant(GrantTypeBase):
"""`Authorization Code Grant`_
@@ -90,12 +138,28 @@ class AuthorizationCodeGrant(GrantTypeBase):
step (C). If valid, the authorization server responds back with
an access token and, optionally, a refresh token.
+ OAuth 2.0 public clients utilizing the Authorization Code Grant are
+ susceptible to the authorization code interception attack.
+
+ A technique to mitigate against the threat through the use of Proof Key for Code
+ Exchange (PKCE, pronounced "pixy") is implemented in the current oauthlib
+ implementation.
+
.. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1
+ .. _`PKCE`: https://tools.ietf.org/html/rfc7636
"""
default_response_mode = 'query'
response_types = ['code']
+ # This dict below is private because as RFC mention it:
+ # "S256" is Mandatory To Implement (MTI) on the server.
+ #
+ _code_challenge_methods = {
+ 'plain': code_challenge_method_plain,
+ 'S256': code_challenge_method_s256
+ }
+
def create_authorization_code(self, request):
"""
Generates an authorization grant represented as a dictionary.
@@ -238,6 +302,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
log.debug('Token request validation ok for %r.', request)
except errors.OAuth2Error as e:
log.debug('Client error during validation of %r. %r.', request, e)
+ headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request, refresh_token=self.refresh_token, save_token=False)
@@ -331,6 +396,20 @@ class AuthorizationCodeGrant(GrantTypeBase):
request.client_id, request.response_type)
raise errors.UnauthorizedClientError(request=request)
+ # OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request"
+ # https://tools.ietf.org/html/rfc6749#section-4.4.1
+ if self.request_validator.is_pkce_required(request.client_id, request) is True:
+ if request.code_challenge is None:
+ raise errors.MissingCodeChallengeError(request=request)
+
+ if request.code_challenge is not None:
+ # OPTIONAL, defaults to "plain" if not present in the request.
+ if request.code_challenge_method is None:
+ request.code_challenge_method = "plain"
+
+ if request.code_challenge_method not in self._code_challenge_methods:
+ raise errors.UnsupportedCodeChallengeMethodError(request=request)
+
# OPTIONAL. The scope of the access request as described by Section 3.3
# https://tools.ietf.org/html/rfc6749#section-3.3
self.validate_scopes(request)
@@ -403,6 +482,33 @@ class AuthorizationCodeGrant(GrantTypeBase):
request.client_id, request.client, request.scopes)
raise errors.InvalidGrantError(request=request)
+ # OPTIONAL. Validate PKCE code_verifier
+ challenge = self.request_validator.get_code_challenge(request.code, request)
+
+ if challenge is not None:
+ if request.code_verifier is None:
+ raise errors.MissingCodeVerifierError(request=request)
+
+ challenge_method = self.request_validator.get_code_challenge_method(request.code, request)
+ if challenge_method is None:
+ raise errors.InvalidGrantError(request=request, description="Challenge method not found")
+
+ if challenge_method not in self._code_challenge_methods:
+ raise errors.ServerError(
+ description="code_challenge_method {} is not supported.".format(challenge_method),
+ request=request
+ )
+
+ if not self.validate_code_challenge(challenge,
+ challenge_method,
+ request.code_verifier):
+ log.debug('request provided a invalid code_verifier.')
+ raise errors.InvalidGrantError(request=request)
+ elif self.request_validator.is_pkce_required(request.client_id, request) is True:
+ if request.code_verifier is None:
+ raise errors.MissingCodeVerifierError(request=request)
+ raise errors.InvalidGrantError(request=request, description="Challenge not found")
+
for attr in ('user', 'scopes'):
if getattr(request, attr, None) is None:
log.debug('request.%s was not set on code validation.', attr)
@@ -430,3 +536,8 @@ class AuthorizationCodeGrant(GrantTypeBase):
for validator in self.custom_validators.post_token:
validator(request)
+
+ def validate_code_challenge(self, challenge, challenge_method, verifier):
+ 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)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
index 5e8fdc0..c966795 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
@@ -73,6 +73,7 @@ class ClientCredentialsGrant(GrantTypeBase):
self.validate_token_request(request)
except errors.OAuth2Error as e:
log.debug('Client error in token request. %s.', e)
+ headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request, refresh_token=False, save_token=False)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
index 78963c3..bd519e8 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
@@ -59,6 +59,8 @@ class RefreshTokenGrant(GrantTypeBase):
log.debug('Validating refresh token request, %r.', request)
self.validate_token_request(request)
except errors.OAuth2Error as e:
+ log.debug('Client error in token request, %s.', e)
+ headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request,
diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
index 95082af..f765d91 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
@@ -101,6 +101,7 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
self.validate_token_request(request)
except errors.OAuth2Error as e:
log.debug('Client error in token request, %s.', e)
+ headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request, self.refresh_token, save_token=False)
diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py
index 2cf1b82..193a9e1 100644
--- a/oauthlib/oauth2/rfc6749/request_validator.py
+++ b/oauthlib/oauth2/rfc6749/request_validator.py
@@ -262,25 +262,29 @@ class RequestValidator(object):
"""Persist the authorization_code.
The code should at minimum be stored with:
- - the client_id (client_id)
- - the redirect URI used (request.redirect_uri)
- - a resource owner / user (request.user)
- - the authorized scopes (request.scopes)
- - the client state, if given (code.get('state'))
+ - the client_id (``client_id``)
+ - the redirect URI used (``request.redirect_uri``)
+ - a resource owner / user (``request.user``)
+ - the authorized scopes (``request.scopes``)
+ - the client state, if given (``code.get('state')``)
+
+ To support PKCE, you MUST associate the code with:
+ - Code Challenge (``request.code_challenge``) and
+ - Code Challenge Method (``request.code_challenge_method``)
- The 'code' argument is actually a dictionary, containing at least a
- 'code' key with the actual authorization code:
+ The ``code`` argument is actually a dictionary, containing at least a
+ ``code`` key with the actual authorization code:
- {'code': 'sdf345jsdf0934f'}
+ ``{'code': 'sdf345jsdf0934f'}``
- It may also have a 'state' key containing a nonce for the client, if it
+ It may also have a ``state`` key containing a nonce for the client, if it
chose to send one. That value should be saved and used in
- 'validate_code'.
+ ``.validate_code``.
- It may also have a 'claims' parameter which, when present, will be a dict
+ It may also have a ``claims`` parameter which, when present, will be a dict
deserialized from JSON as described at
http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
- This value should be saved in this method and used again in 'validate_code'.
+ This value should be saved in this method and used again in ``.validate_code``.
:param client_id: Unicode client identifier.
:param code: A dict of the authorization code grant and, optionally, state.
@@ -564,6 +568,11 @@ class RequestValidator(object):
The request.claims property, if it was given, should assigned a dict.
+ If PKCE is enabled (see 'is_pkce_required' and 'save_authorization_code')
+ you MUST set the following based on the information stored:
+ - request.code_challenge
+ - request.code_challenge_method
+
:param client_id: Unicode client identifier.
:param code: Unicode authorization code.
:param client: Client object set by you, see ``.authenticate_client``.
@@ -742,3 +751,78 @@ class RequestValidator(object):
- OpenIDConnectHybrid
"""
raise NotImplementedError('Subclasses must implement this method.')
+
+ def is_pkce_required(self, client_id, request):
+ """Determine if current request requires PKCE. Default, False.
+ This is called for both "authorization" and "token" requests.
+
+ Override this method by ``return True`` to enable PKCE for everyone.
+ You might want to enable it only for public clients.
+ Note that PKCE can also be used in addition of a client authentication.
+
+ OAuth 2.0 public clients utilizing the Authorization Code Grant are
+ susceptible to the authorization code interception attack. This
+ specification describes the attack as well as a technique to mitigate
+ against the threat through the use of Proof Key for Code Exchange
+ (PKCE, pronounced "pixy"). See `RFC7636`_.
+
+ :param client_id: Client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+
+ .. _`RFC7636`: https://tools.ietf.org/html/rfc7636
+ """
+ return False
+
+ def get_code_challenge(self, code, request):
+ """Is called for every "token" requests.
+
+ When the server issues the authorization code in the authorization
+ response, it MUST associate the ``code_challenge`` and
+ ``code_challenge_method`` values with the authorization code so it can
+ be verified later.
+
+ Typically, the ``code_challenge`` and ``code_challenge_method`` values
+ are stored in encrypted form in the ``code`` itself but could
+ alternatively be stored on the server associated with the code. The
+ server MUST NOT include the ``code_challenge`` value in client requests
+ in a form that other entities can extract.
+
+ Return the ``code_challenge`` associated to the code.
+ If ``None`` is returned, code is considered to not be associated to any
+ challenges.
+
+ :param code: Authorization code.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: code_challenge string
+
+ Method is used by:
+ - Authorization Code Grant - when PKCE is active
+
+ """
+ return None
+
+ def get_code_challenge_method(self, code, request):
+ """Is called during the "token" request processing, when a
+ ``code_verifier`` and a ``code_challenge`` has been provided.
+
+ See ``.get_code_challenge``.
+
+ Must return ``plain`` or ``S256``. You can return a custom value if you have
+ implemented your own ``AuthorizationCodeGrant`` class.
+
+ :param code: Authorization code.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: code_challenge_method string
+
+ Method is used by:
+ - Authorization Code Grant - when PKCE is active
+
+ """
+ raise NotImplementedError('Subclasses must implement this method.')