summaryrefslogtreecommitdiff
path: root/oauthlib/oauth2/rfc6749
diff options
context:
space:
mode:
authorJonathan Huot <jonathan.huot@thomsonreuters.com>2018-11-29 16:43:00 +0100
committerJonathan Huot <jonathan.huot@thomsonreuters.com>2018-11-29 16:43:00 +0100
commit01b3c4c20178b292d470eead153b91feaa05c057 (patch)
tree2d96e87f21c474dd29e959cd41d17324e2347068 /oauthlib/oauth2/rfc6749
parent719a5f3bc62465ab8502442458c72016bf91b7cd (diff)
downloadoauthlib-01b3c4c20178b292d470eead153b91feaa05c057.tar.gz
Initial OAuth2.0/PKCE Provider support
Diffstat (limited to 'oauthlib/oauth2/rfc6749')
-rw-r--r--oauthlib/oauth2/rfc6749/errors.py32
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/authorization_code.py110
-rw-r--r--oauthlib/oauth2/rfc6749/request_validator.py108
3 files changed, 238 insertions, 12 deletions
diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py
index 7ead3d4..f7fac5c 100644
--- a/oauthlib/oauth2/rfc6749/errors.py
+++ b/oauthlib/oauth2/rfc6749/errors.py
@@ -180,6 +180,26 @@ 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.
@@ -196,6 +216,18 @@ 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.
diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
index 8ebae49..0df7c6c 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
@@ -17,6 +19,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`_
@@ -91,12 +139,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.
@@ -350,6 +414,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)
@@ -422,6 +500,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)
@@ -449,3 +554,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/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.')