summaryrefslogtreecommitdiff
path: root/oauthlib
diff options
context:
space:
mode:
authorOmer Katz <omer.drow@gmail.com>2019-06-29 10:46:10 +0300
committerGitHub <noreply@github.com>2019-06-29 10:46:10 +0300
commit9bcf5d6720aea23dd8b57e08dca39341dc49314a (patch)
tree2ce81e3c4e7c22e6ab04f2651814a3c641d44f74 /oauthlib
parent73092d039fa67a88d0989e7bf0ae7d0044a0bdc6 (diff)
parent76d8d3426004e3a14d372444c56f764ad71937e0 (diff)
downloadoauthlib-9bcf5d6720aea23dd8b57e08dca39341dc49314a.tar.gz
Merge branch 'master' into 672-fix-null-expires-in
Diffstat (limited to 'oauthlib')
-rw-r--r--oauthlib/oauth1/rfc5849/endpoints/base.py4
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/base.py31
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/introspect.py3
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/revocation.py3
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/token.py10
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/authorization_code.py3
-rw-r--r--oauthlib/oauth2/rfc6749/parameters.py9
-rw-r--r--oauthlib/oauth2/rfc6749/request_validator.py3
-rw-r--r--oauthlib/oauth2/rfc6749/tokens.py4
-rw-r--r--oauthlib/openid/connect/core/grant_types/authorization_code.py20
-rw-r--r--oauthlib/openid/connect/core/grant_types/base.py97
-rw-r--r--oauthlib/openid/connect/core/grant_types/implicit.py4
-rw-r--r--oauthlib/openid/connect/core/request_validator.py76
13 files changed, 245 insertions, 22 deletions
diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py
index 9702939..ecf8a50 100644
--- a/oauthlib/oauth1/rfc5849/endpoints/base.py
+++ b/oauthlib/oauth1/rfc5849/endpoints/base.py
@@ -10,7 +10,7 @@ from __future__ import absolute_import, unicode_literals
import time
-from oauthlib.common import Request, generate_token
+from oauthlib.common import CaseInsensitiveDict, Request, generate_token
from .. import (CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC, SIGNATURE_RSA,
SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY,
@@ -67,7 +67,7 @@ class BaseEndpoint(object):
def _create_request(self, uri, http_method, body, headers):
# Only include body data from x-www-form-urlencoded requests
- headers = headers or {}
+ headers = CaseInsensitiveDict(headers or {})
if ("Content-Type" in headers and
CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]):
request = Request(uri, http_method, body, headers)
diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py
index c0fc726..e39232f 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/base.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/base.py
@@ -15,6 +15,8 @@ from ..errors import (FatalClientError, OAuth2Error, ServerError,
TemporarilyUnavailableError, InvalidRequestError,
InvalidClientError, UnsupportedTokenTypeError)
+from oauthlib.common import CaseInsensitiveDict, urldecode
+
log = logging.getLogger(__name__)
@@ -23,6 +25,18 @@ class BaseEndpoint(object):
def __init__(self):
self._available = True
self._catch_errors = False
+ self._valid_request_methods = None
+
+ @property
+ def valid_request_methods(self):
+ return self._valid_request_methods
+
+ @valid_request_methods.setter
+ def valid_request_methods(self, valid_request_methods):
+ if valid_request_methods is not None:
+ valid_request_methods = [x.upper() for x in valid_request_methods]
+ self._valid_request_methods = valid_request_methods
+
@property
def available(self):
@@ -30,7 +44,7 @@ class BaseEndpoint(object):
@available.setter
def available(self, available):
- self._available = available
+ self._available = available
@property
def catch_errors(self):
@@ -62,6 +76,21 @@ class BaseEndpoint(object):
request.token_type_hint not in self.supported_token_types):
raise UnsupportedTokenTypeError(request=request)
+ def _raise_on_bad_method(self, request):
+ if self.valid_request_methods is None:
+ raise ValueError('Configure "valid_request_methods" property first')
+ if request.http_method.upper() not in self.valid_request_methods:
+ raise InvalidRequestError(request=request,
+ description=('Unsupported request method %s' % request.http_method.upper()))
+
+ def _raise_on_bad_post_request(self, request):
+ """Raise if invalid POST request received
+ """
+ if request.http_method.upper() == 'POST':
+ query_params = request.uri_query or ""
+ if query_params:
+ raise InvalidRequestError(request=request,
+ description=('URL query parameters are not allowed'))
def catch_errors_and_unavailability(f):
@functools.wraps(f)
diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py
index 47022fd..4accbdc 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py
@@ -39,6 +39,7 @@ class IntrospectEndpoint(BaseEndpoint):
"""
valid_token_types = ('access_token', 'refresh_token')
+ valid_request_methods = ('POST',)
def __init__(self, request_validator, supported_token_types=None):
BaseEndpoint.__init__(self)
@@ -117,6 +118,8 @@ class IntrospectEndpoint(BaseEndpoint):
.. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
.. _`RFC6749`: http://tools.ietf.org/html/rfc6749
"""
+ self._raise_on_bad_method(request)
+ self._raise_on_bad_post_request(request)
self._raise_on_missing_token(request)
self._raise_on_invalid_client(request)
self._raise_on_unsupported_token(request)
diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py
index fda3f30..1fabd03 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py
@@ -28,6 +28,7 @@ class RevocationEndpoint(BaseEndpoint):
"""
valid_token_types = ('access_token', 'refresh_token')
+ valid_request_methods = ('POST',)
def __init__(self, request_validator, supported_token_types=None,
enable_jsonp=False):
@@ -121,6 +122,8 @@ class RevocationEndpoint(BaseEndpoint):
.. _`Section 4.1.2`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2
.. _`RFC6749`: https://tools.ietf.org/html/rfc6749
"""
+ self._raise_on_bad_method(request)
+ self._raise_on_bad_post_request(request)
self._raise_on_missing_token(request)
self._raise_on_invalid_client(request)
self._raise_on_unsupported_token(request)
diff --git a/oauthlib/oauth2/rfc6749/endpoints/token.py b/oauthlib/oauth2/rfc6749/endpoints/token.py
index 90fb16f..bc87e9b 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/token.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/token.py
@@ -62,6 +62,8 @@ class TokenEndpoint(BaseEndpoint):
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
"""
+ valid_request_methods = ('POST',)
+
def __init__(self, default_grant_type, default_token_type, grant_types):
BaseEndpoint.__init__(self)
self._grant_types = grant_types
@@ -85,13 +87,13 @@ class TokenEndpoint(BaseEndpoint):
return self._default_token_type
@catch_errors_and_unavailability
- def create_token_response(self, uri, http_method='GET', body=None,
+ def create_token_response(self, uri, http_method='POST', body=None,
headers=None, credentials=None, grant_type_for_scope=None,
claims=None):
"""Extract grant_type and route to the designated handler."""
request = Request(
uri, http_method=http_method, body=body, headers=headers)
-
+ self.validate_token_request(request)
# 'scope' is an allowed Token Request param in both the "Resource Owner Password Credentials Grant"
# and "Client Credentials Grant" flows
# https://tools.ietf.org/html/rfc6749#section-4.3.2
@@ -115,3 +117,7 @@ class TokenEndpoint(BaseEndpoint):
request.grant_type, grant_type_handler)
return grant_type_handler.create_token_response(
request, self.default_token_type)
+
+ def validate_token_request(self, request):
+ self._raise_on_bad_method(request)
+ self._raise_on_bad_post_request(request)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
index 5f03d9c..9b84c4c 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
@@ -405,12 +405,15 @@ class AuthorizationCodeGrant(GrantTypeBase):
raise errors.MissingCodeChallengeError(request=request)
if request.code_challenge is not None:
+ request_info["code_challenge"] = request.code_challenge
+
# 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)
+ request_info["code_challenge_method"] = request.code_challenge_method
# OPTIONAL. The scope of the access request as described by Section 3.3
# https://tools.ietf.org/html/rfc6749#section-3.3
diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py
index f8d42db..14d4c0d 100644
--- a/oauthlib/oauth2/rfc6749/parameters.py
+++ b/oauthlib/oauth2/rfc6749/parameters.py
@@ -264,12 +264,15 @@ def parse_authorization_code_response(uri, state=None):
query = urlparse.urlparse(uri).query
params = dict(urlparse.parse_qsl(query))
- if not 'code' in params:
- raise MissingCodeError("Missing code parameter in response.")
-
if state and params.get('state', None) != state:
raise MismatchingStateError()
+ if 'error' in params:
+ raise_from_error(params.get('error'), params)
+
+ if not 'code' in params:
+ raise MissingCodeError("Missing code parameter in response.")
+
return params
diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py
index d6ec2ab..86509b6 100644
--- a/oauthlib/oauth2/rfc6749/request_validator.py
+++ b/oauthlib/oauth2/rfc6749/request_validator.py
@@ -271,6 +271,9 @@ class RequestValidator(object):
- Code Challenge (``request.code_challenge``) and
- Code Challenge Method (``request.code_challenge_method``)
+ To support OIDC, you MUST associate the code with:
+ - nonce, if present (``code["nonce"]``)
+
The ``code`` argument is actually a dictionary, containing at least a
``code`` key with the actual authorization code:
diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py
index 7973923..3587af4 100644
--- a/oauthlib/oauth2/rfc6749/tokens.py
+++ b/oauthlib/oauth2/rfc6749/tokens.py
@@ -254,7 +254,7 @@ def get_token_from_header(request):
if 'Authorization' in request.headers:
split_header = request.headers.get('Authorization').split()
- if len(split_header) == 2 and split_header[0] == 'Bearer':
+ if len(split_header) == 2 and split_header[0].lower() == 'bearer':
token = split_header[1]
else:
token = request.access_token
@@ -353,7 +353,7 @@ class BearerToken(TokenBase):
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
- if request.headers.get('Authorization', '').split(' ')[0] == 'Bearer':
+ if request.headers.get('Authorization', '').split(' ')[0].lower() == 'bearer':
return 9
elif request.access_token is not None:
return 5
diff --git a/oauthlib/openid/connect/core/grant_types/authorization_code.py b/oauthlib/openid/connect/core/grant_types/authorization_code.py
index b0b1015..becfcfa 100644
--- a/oauthlib/openid/connect/core/grant_types/authorization_code.py
+++ b/oauthlib/openid/connect/core/grant_types/authorization_code.py
@@ -22,3 +22,23 @@ class AuthorizationCodeGrant(GrantTypeBase):
self.custom_validators.post_auth.append(
self.openid_authorization_validator)
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.
+ """
+ # Treat it as normal OAuth 2 auth code request if openid is not present
+ if not request.scopes or 'openid' not in request.scopes:
+ return token
+
+ nonce = self.request_validator.get_authorization_code_nonce(
+ request.client_id,
+ request.code,
+ request.redirect_uri,
+ request
+ )
+ return super(AuthorizationCodeGrant, self).add_id_token(token, token_handler, request, nonce=nonce)
diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py
index 4f5c944..32a21b6 100644
--- a/oauthlib/openid/connect/core/grant_types/base.py
+++ b/oauthlib/openid/connect/core/grant_types/base.py
@@ -1,7 +1,9 @@
from .exceptions import OIDCNoPrompt
-import datetime
+import base64
+import hashlib
import logging
+import time
from json import loads
from oauthlib.oauth2.rfc6749.errors import ConsentRequired, InvalidRequestError, LoginRequired
@@ -49,7 +51,45 @@ class GrantTypeBase(object):
raise InvalidRequestError(description="Malformed claims parameter",
uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter")
- def add_id_token(self, token, token_handler, request):
+ def id_token_hash(self, value, hashfunc=hashlib.sha256):
+ """
+ Its value is the base64url encoding of the left-most half of the
+ hash of the octets of the ASCII representation of the access_token
+ value, where the hash algorithm used is the hash algorithm used in
+ the alg Header Parameter of the ID Token's JOSE Header.
+
+ For instance, if the alg is RS256, hash the access_token value
+ with SHA-256, then take the left-most 128 bits and
+ base64url-encode them.
+ For instance, if the alg is HS512, hash the code value with
+ SHA-512, then take the left-most 256 bits and base64url-encode
+ them. The c_hash value is a case-sensitive string.
+
+ Example of hash from OIDC specification (bound to a JWS using RS256):
+
+ code:
+ Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk
+
+ c_hash:
+ LDktKdoQak3Pk0cnXxCltA
+ """
+ digest = hashfunc(value.encode()).digest()
+ left_most = len(digest) // 2
+ return base64.urlsafe_b64encode(digest[:left_most]).decode().rstrip("=")
+
+ def add_id_token(self, token, token_handler, request, nonce=None):
+ """
+ Construct an initial version of id_token, and let the
+ request_validator sign or encrypt it.
+
+ The initial version can contain the fields below, accordingly
+ to the spec:
+ - aud
+ - iat
+ - nonce
+ - at_hash
+ - c_hash
+ """
# Treat it as normal OAuth 2 auth code request if openid is not present
if not request.scopes or 'openid' not in request.scopes:
return token
@@ -58,13 +98,54 @@ class GrantTypeBase(object):
if request.response_type and 'id_token' not in request.response_type:
return token
- if request.max_age:
- d = datetime.datetime.utcnow()
- token['auth_time'] = d.isoformat("T") + "Z"
-
- # TODO: acr claims (probably better handled by server code using oauthlib in get_id_token)
+ # Implementation mint its own id_token without help.
+ id_token = self.request_validator.get_id_token(token, token_handler, request)
+ if id_token:
+ token['id_token'] = id_token
+ return token
- token['id_token'] = self.request_validator.get_id_token(token, token_handler, request)
+ # Fallback for asking some help from oauthlib framework.
+ # Start with technicals fields bound to the specification.
+ id_token = {}
+ id_token['aud'] = request.client_id
+ id_token['iat'] = int(time.time())
+
+ # nonce is REQUIRED when response_type value is:
+ # - id_token token (Implicit)
+ # - id_token (Implicit)
+ # - code id_token (Hybrid)
+ # - code id_token token (Hybrid)
+ #
+ # nonce is OPTIONAL when response_type value is:
+ # - code (Authorization Code)
+ # - code token (Hybrid)
+ if nonce is not None:
+ id_token["nonce"] = nonce
+
+ # at_hash is REQUIRED when response_type value is:
+ # - id_token token (Implicit)
+ # - code id_token token (Hybrid)
+ #
+ # at_hash is OPTIONAL when:
+ # - code (Authorization code)
+ # - code id_token (Hybrid)
+ # - code token (Hybrid)
+ #
+ # at_hash MAY NOT be used when:
+ # - id_token (Implicit)
+ if "access_token" in token:
+ id_token["at_hash"] = self.id_token_hash(token["access_token"])
+
+ # c_hash is REQUIRED when response_type value is:
+ # - code id_token (Hybrid)
+ # - code id_token token (Hybrid)
+ #
+ # c_hash is OPTIONAL for others.
+ if "code" in token:
+ id_token["c_hash"] = self.id_token_hash(token["code"])
+
+ # Call request_validator to complete/sign/encrypt id_token
+ token['id_token'] = self.request_validator.finalize_id_token(id_token, token, token_handler, request)
return token
diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py
index d3797b2..c2dbc27 100644
--- a/oauthlib/openid/connect/core/grant_types/implicit.py
+++ b/oauthlib/openid/connect/core/grant_types/implicit.py
@@ -27,9 +27,9 @@ class ImplicitGrant(GrantTypeBase):
self.register_token_modifier(self.add_id_token)
def add_id_token(self, token, token_handler, request):
- if 'state' not in token:
+ if 'state' not in token and request.state:
token['state'] = request.state
- return super(ImplicitGrant, self).add_id_token(token, token_handler, request)
+ return super(ImplicitGrant, self).add_id_token(token, token_handler, request, nonce=request.nonce)
def openid_authorization_validator(self, request):
"""Additional validation when following the implicit flow.
diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py
index 1587754..d96c9ef 100644
--- a/oauthlib/openid/connect/core/request_validator.py
+++ b/oauthlib/openid/connect/core/request_validator.py
@@ -24,7 +24,7 @@ class RequestValidator(OAuth2RequestValidator):
id_token in token response only based on authorization code scopes.
Only code param should be sufficient to retrieve grant code from
- any storage you are using, `client_id` and `redirect_uri` can gave a
+ any storage you are using, `client_id` and `redirect_uri` can have a
blank value `""` don't forget to check it before using those values
in a select query if a database is used.
@@ -38,6 +38,30 @@ class RequestValidator(OAuth2RequestValidator):
"""
raise NotImplementedError('Subclasses must implement this method.')
+ def get_authorization_code_nonce(self, client_id, code, redirect_uri, request):
+ """ Extracts nonce from saved authorization code.
+
+ If present in the Authentication Request, Authorization
+ Servers MUST include a nonce Claim in the ID Token with the
+ Claim Value being the nonce value sent in the Authentication
+ Request. Authorization Servers SHOULD perform no other
+ processing on nonce values used. The nonce value is a
+ case-sensitive string.
+
+ Only code param should be sufficient to retrieve grant code from
+ any storage you are using. However, `client_id` and `redirect_uri`
+ have been validated and can be used also.
+
+ :param client_id: Unicode client identifier
+ :param code: Unicode authorization code grant
+ :param redirect_uri: Unicode absolute URI
+ :return: Unicode nonce
+
+ Method is used by:
+ - Authorization Token Grant Dispatcher
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
def get_jwt_bearer_token(self, token, token_handler, request):
"""Get JWT Bearer token or OpenID Connect ID token
@@ -57,6 +81,12 @@ class RequestValidator(OAuth2RequestValidator):
def get_id_token(self, token, token_handler, request):
"""Get OpenID Connect ID token
+ This method is OPTIONAL and is NOT RECOMMENDED.
+ `finalize_id_token` SHOULD be implemented instead. However, if you
+ want a full control over the minting of the `id_token`, you
+ MAY want to override `get_id_token` instead of using
+ `finalize_id_token`.
+
In the OpenID Connect workflows when an ID Token is requested this method is called.
Subclasses should implement the construction, signing and optional encryption of the
ID Token as described in the OpenID Connect spec.
@@ -85,7 +115,49 @@ class RequestValidator(OAuth2RequestValidator):
:type request: oauthlib.common.Request
:return: The ID Token (a JWS signed JWT)
"""
- # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token
+ return None
+
+ def finalize_id_token(self, id_token, token, token_handler, request):
+ """Finalize OpenID Connect ID token & Sign or Encrypt.
+
+ In the OpenID Connect workflows when an ID Token is requested
+ this method is called. Subclasses should implement the
+ construction, signing and optional encryption of the ID Token
+ as described in the OpenID Connect spec.
+
+ The `id_token` parameter is a dict containing a couple of OIDC
+ technical fields related to the specification. Prepopulated
+ attributes are:
+
+ - `aud`, equals to `request.client_id`.
+ - `iat`, equals to current time.
+ - `nonce`, if present, is equals to the `nonce` from the
+ authorization request.
+ - `at_hash`, hash of `access_token`, if relevant.
+ - `c_hash`, hash of `code`, if relevant.
+
+ This method MUST provide required fields as below:
+
+ - `iss`, REQUIRED. Issuer Identifier for the Issuer of the response.
+ - `sub`, REQUIRED. Subject Identifier
+ - `exp`, REQUIRED. Expiration time on or after which the ID
+ Token MUST NOT be accepted by the RP when performing
+ authentication with the OP.
+
+ Additionals 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`_
+
+ .. _`OpenID Connect Core#Claims`: https://openid.net/specs/openid-connect-core-1_0.html#Claims
+
+ :param id_token: A dict containing technical fields of id_token
+ :param token: A Bearer token dict
+ :param token_handler: the token handler (BearerToken class)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: The ID Token (a JWS signed JWT or JWE encrypted JWT)
+ """
raise NotImplementedError('Subclasses must implement this method.')
def validate_jwt_bearer_token(self, token, scopes, request):