Update oauthlib-3.1.1

This commit is contained in:
JonnyWong16 2021-10-14 22:34:45 -07:00
parent e58aa40099
commit d76838a607
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
64 changed files with 4329 additions and 1421 deletions

View file

@ -5,21 +5,30 @@
A generic, spec-compliant, thorough implementation of the OAuth
request-signing logic.
:copyright: (c) 2011 by Idan Gazit.
:copyright: (c) 2019 by The OAuthlib Community
:license: BSD, see LICENSE for details.
"""
__author__ = 'Idan Gazit <idan@gazit.me>'
__version__ = '1.1.1'
import logging
try: # Python 2.7+
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
from logging import NullHandler
def emit(self, record):
pass
__author__ = 'The OAuthlib Community'
__version__ = '3.1.1'
logging.getLogger('oauthlib').addHandler(NullHandler())
_DEBUG = False
def set_debug(debug_val):
"""Set value of debug flag
:param debug_val: Value to set. Must be a bool value.
"""
global _DEBUG
_DEBUG = debug_val
def get_debug():
"""Get debug mode value.
:return: `True` if debug mode is on, `False` otherwise
"""
return _DEBUG

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.common
~~~~~~~~~~~~~~
@ -6,28 +5,24 @@ oauthlib.common
This module provides data structures and utilities common
to all implementations of OAuth.
"""
from __future__ import absolute_import, unicode_literals
import collections
import datetime
import logging
import random
import re
import sys
import time
import urllib.parse as urlparse
from urllib.parse import (
quote as _quote, unquote as _unquote, urlencode as _urlencode,
)
from . import get_debug
try:
from urllib import quote as _quote
from urllib import unquote as _unquote
from urllib import urlencode as _urlencode
from secrets import randbits
from secrets import SystemRandom
except ImportError:
from urllib.parse import quote as _quote
from urllib.parse import unquote as _unquote
from urllib.parse import urlencode as _urlencode
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from random import getrandbits as randbits
from random import SystemRandom
UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
@ -45,23 +40,14 @@ always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
log = logging.getLogger('oauthlib')
PY3 = sys.version_info[0] == 3
if PY3:
unicode_type = str
bytes_type = bytes
else:
unicode_type = unicode
bytes_type = str
# 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either)
def quote(s, safe=b'/'):
s = s.encode('utf-8') if isinstance(s, unicode_type) else s
s = s.encode('utf-8') if isinstance(s, str) else s
s = _quote(s, safe)
# PY3 always returns unicode. PY2 may return either, depending on whether
# it had to modify the string.
if isinstance(s, bytes_type):
if isinstance(s, bytes):
s = s.decode('utf-8')
return s
@ -71,7 +57,7 @@ def unquote(s):
# PY3 always returns unicode. PY2 seems to always return what you give it,
# which differs from quote's behavior. Just to be safe, make sure it is
# unicode before we return.
if isinstance(s, bytes_type):
if isinstance(s, bytes):
s = s.decode('utf-8')
return s
@ -79,7 +65,7 @@ def unquote(s):
def urlencode(params):
utf8_params = encode_params_utf8(params)
urlencoded = _urlencode(utf8_params)
if isinstance(urlencoded, unicode_type): # PY3 returns unicode
if isinstance(urlencoded, str):
return urlencoded
else:
return urlencoded.decode("utf-8")
@ -92,8 +78,8 @@ def encode_params_utf8(params):
encoded = []
for k, v in params:
encoded.append((
k.encode('utf-8') if isinstance(k, unicode_type) else k,
v.encode('utf-8') if isinstance(v, unicode_type) else v))
k.encode('utf-8') if isinstance(k, str) else k,
v.encode('utf-8') if isinstance(v, str) else v))
return encoded
@ -104,12 +90,12 @@ def decode_params_utf8(params):
decoded = []
for k, v in params:
decoded.append((
k.decode('utf-8') if isinstance(k, bytes_type) else k,
v.decode('utf-8') if isinstance(v, bytes_type) else v))
k.decode('utf-8') if isinstance(k, bytes) else k,
v.decode('utf-8') if isinstance(v, bytes) else v))
return decoded
urlencoded = set(always_safe) | set('=&;%+~,*@!()/?')
urlencoded = set(always_safe) | set('=&;:%+~,*@!()/?\'$')
def urldecode(query):
@ -137,22 +123,6 @@ def urldecode(query):
if INVALID_HEX_PATTERN.search(query):
raise ValueError('Invalid hex encoding in query string.')
# We encode to utf-8 prior to parsing because parse_qsl behaves
# differently on unicode input in python 2 and 3.
# Python 2.7
# >>> urlparse.parse_qsl(u'%E5%95%A6%E5%95%A6')
# u'\xe5\x95\xa6\xe5\x95\xa6'
# Python 2.7, non unicode input gives the same
# >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6')
# '\xe5\x95\xa6\xe5\x95\xa6'
# but now we can decode it to unicode
# >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6').decode('utf-8')
# u'\u5566\u5566'
# Python 3.3 however
# >>> urllib.parse.parse_qsl(u'%E5%95%A6%E5%95%A6')
# u'\u5566\u5566'
query = query.encode(
'utf-8') if not PY3 and isinstance(query, unicode_type) else query
# We want to allow queries such as "c2" whereas urlparse.parse_qsl
# with the strict_parsing flag will not.
params = urlparse.parse_qsl(query, keep_blank_values=True)
@ -169,7 +139,7 @@ def extract_params(raw):
empty list of parameters. Any other input will result in a return
value of None.
"""
if isinstance(raw, bytes_type) or isinstance(raw, unicode_type):
if isinstance(raw, (bytes, str)):
try:
params = urldecode(raw)
except ValueError:
@ -199,10 +169,10 @@ def generate_nonce():
A random 64-bit number is appended to the epoch timestamp for both
randomness and to decrease the likelihood of collisions.
.. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
.. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
.. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
.. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3
"""
return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp())
return str(str(randbits(64)) + generate_timestamp())
def generate_timestamp():
@ -211,10 +181,10 @@ def generate_timestamp():
Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
Per `section 3.2.1`_ of the MAC Access Authentication spec.
.. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
.. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
.. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
.. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3
"""
return unicode_type(int(time.time()))
return str(int(time.time()))
def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
@ -225,7 +195,7 @@ def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
and entropy when generating the random characters is important. Which is
why SystemRandom is used instead of the default random.choice method.
"""
rand = random.SystemRandom()
rand = SystemRandom()
return ''.join(rand.choice(chars) for x in range(length))
@ -257,7 +227,7 @@ def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET):
"""Generates an OAuth client_id
OAuth 2 specify the format of client_id in
http://tools.ietf.org/html/rfc6749#appendix-A.
https://tools.ietf.org/html/rfc6749#appendix-A.
"""
return generate_token(length, chars)
@ -301,11 +271,11 @@ def safe_string_equals(a, b):
def to_unicode(data, encoding='UTF-8'):
"""Convert a number of different types of objects to unicode."""
if isinstance(data, unicode_type):
if isinstance(data, str):
return data
if isinstance(data, bytes_type):
return unicode_type(data, encoding=encoding)
if isinstance(data, bytes):
return str(data, encoding=encoding)
if hasattr(data, '__iter__'):
try:
@ -319,7 +289,7 @@ def to_unicode(data, encoding='UTF-8'):
# We support 2.6 which lacks dict comprehensions
if hasattr(data, 'items'):
data = data.items()
return dict(((to_unicode(k, encoding), to_unicode(v, encoding)) for k, v in data))
return {to_unicode(k, encoding): to_unicode(v, encoding) for k, v in data}
return data
@ -331,7 +301,7 @@ class CaseInsensitiveDict(dict):
proxy = {}
def __init__(self, data):
self.proxy = dict((k.lower(), k) for k in data)
self.proxy = {k.lower(): k for k in data}
for k in data:
self[k] = data[k]
@ -340,22 +310,27 @@ class CaseInsensitiveDict(dict):
def __delitem__(self, k):
key = self.proxy[k.lower()]
super(CaseInsensitiveDict, self).__delitem__(key)
super().__delitem__(key)
del self.proxy[k.lower()]
def __getitem__(self, k):
key = self.proxy[k.lower()]
return super(CaseInsensitiveDict, self).__getitem__(key)
return super().__getitem__(key)
def get(self, k, default=None):
return self[k] if k in self else default
def __setitem__(self, k, v):
super(CaseInsensitiveDict, self).__setitem__(k, v)
super().__setitem__(k, v)
self.proxy[k.lower()] = k
def update(self, *args, **kwargs):
super().update(*args, **kwargs)
for k in dict(*args, **kwargs):
self.proxy[k.lower()] = k
class Request(object):
class Request:
"""A malleable representation of a signable HTTP request.
@ -389,6 +364,9 @@ class Request(object):
"client_id": None,
"client_secret": None,
"code": None,
"code_challenge": None,
"code_challenge_method": None,
"code_verifier": None,
"extra_credentials": None,
"grant_type": None,
"redirect_uri": None,
@ -401,10 +379,21 @@ class Request(object):
"token": None,
"user": None,
"token_type_hint": None,
# OpenID Connect
"response_mode": None,
"nonce": None,
"display": None,
"prompt": None,
"claims": None,
"max_age": None,
"ui_locales": None,
"id_token_hint": None,
"login_hint": None,
"acr_values": None
}
self._params.update(dict(urldecode(self.uri_query)))
self._params.update(dict(self.decoded_body or []))
self._params.update(self.headers)
def __getattr__(self, name):
if name in self._params:
@ -413,13 +402,15 @@ class Request(object):
raise AttributeError(name)
def __repr__(self):
if not get_debug():
return "<oauthlib.Request SANITIZED>"
body = self.body
headers = self.headers.copy()
if body:
body = SANITIZE_PATTERN.sub('\1<SANITIZED>', body)
body = SANITIZE_PATTERN.sub('\1<SANITIZED>', str(body))
if 'Authorization' in headers:
headers['Authorization'] = '<SANITIZED>'
return '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
return '<oauthlib.Request url="{}", http_method="{}", headers="{}", body="{}">'.format(
self.uri, self.http_method, headers, body)
@property

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth1
~~~~~~~~~~~~~~
@ -6,14 +5,24 @@ oauthlib.oauth1
This module is a wrapper for the most recent implementation of OAuth 1.0 Client
and Server classes.
"""
from __future__ import absolute_import, unicode_literals
from .rfc5849 import Client
from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT
from .rfc5849 import (SIGNATURE_HMAC,
SIGNATURE_HMAC_SHA1,
SIGNATURE_HMAC_SHA256,
SIGNATURE_HMAC_SHA512,
SIGNATURE_RSA,
SIGNATURE_RSA_SHA1,
SIGNATURE_RSA_SHA256,
SIGNATURE_RSA_SHA512,
SIGNATURE_PLAINTEXT)
from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY
from .rfc5849 import SIGNATURE_TYPE_BODY
from .rfc5849.request_validator import RequestValidator
from .rfc5849.endpoints import RequestTokenEndpoint, AuthorizationEndpoint
from .rfc5849.endpoints import AccessTokenEndpoint, ResourceEndpoint
from .rfc5849.endpoints import SignatureOnlyEndpoint, WebApplicationServer
from .rfc5849.errors import *
from .rfc5849.errors import (InsecureTransportError,
InvalidClientError,
InvalidRequestError,
InvalidSignatureMethodError,
OAuth1Error)

View file

@ -1,36 +1,68 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth1.rfc5849
~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for signing and checking OAuth 1.0 RFC 5849 requests.
It supports all three standard signature methods defined in RFC 5849:
- HMAC-SHA1
- RSA-SHA1
- PLAINTEXT
It also supports signature methods that are not defined in RFC 5849. These are
based on the standard ones but replace SHA-1 with the more secure SHA-256:
- HMAC-SHA256
- RSA-SHA256
"""
from __future__ import absolute_import, unicode_literals
import base64
import hashlib
import logging
log = logging.getLogger(__name__)
import urllib.parse as urlparse
import sys
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from oauthlib.common import (
Request, generate_nonce, generate_timestamp, to_unicode, urlencode,
)
if sys.version_info[0] == 3:
bytes_type = bytes
else:
bytes_type = str
from oauthlib.common import Request, urlencode, generate_nonce
from oauthlib.common import generate_timestamp, to_unicode
from . import parameters, signature
SIGNATURE_HMAC = "HMAC-SHA1"
SIGNATURE_RSA = "RSA-SHA1"
log = logging.getLogger(__name__)
# Available signature methods
#
# Note: SIGNATURE_HMAC and SIGNATURE_RSA are kept for backward compatibility
# with previous versions of this library, when it the only HMAC-based and
# RSA-based signature methods were HMAC-SHA1 and RSA-SHA1. But now that it
# supports other hashing algorithms besides SHA1, explicitly identifying which
# hashing algorithm is being used is recommended.
#
# Note: if additional values are defined here, don't forget to update the
# imports in "../__init__.py" so they are available outside this module.
SIGNATURE_HMAC_SHA1 = "HMAC-SHA1"
SIGNATURE_HMAC_SHA256 = "HMAC-SHA256"
SIGNATURE_HMAC_SHA512 = "HMAC-SHA512"
SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1 # deprecated variable for HMAC-SHA1
SIGNATURE_RSA_SHA1 = "RSA-SHA1"
SIGNATURE_RSA_SHA256 = "RSA-SHA256"
SIGNATURE_RSA_SHA512 = "RSA-SHA512"
SIGNATURE_RSA = SIGNATURE_RSA_SHA1 # deprecated variable for RSA-SHA1
SIGNATURE_PLAINTEXT = "PLAINTEXT"
SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
SIGNATURE_METHODS = (
SIGNATURE_HMAC_SHA1,
SIGNATURE_HMAC_SHA256,
SIGNATURE_HMAC_SHA512,
SIGNATURE_RSA_SHA1,
SIGNATURE_RSA_SHA256,
SIGNATURE_RSA_SHA512,
SIGNATURE_PLAINTEXT
)
SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
SIGNATURE_TYPE_QUERY = 'QUERY'
@ -39,12 +71,16 @@ SIGNATURE_TYPE_BODY = 'BODY'
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
class Client(object):
class Client:
"""A client used to sign OAuth 1.0 RFC 5849 requests."""
SIGNATURE_METHODS = {
SIGNATURE_HMAC: signature.sign_hmac_sha1_with_client,
SIGNATURE_RSA: signature.sign_rsa_sha1_with_client,
SIGNATURE_HMAC_SHA1: signature.sign_hmac_sha1_with_client,
SIGNATURE_HMAC_SHA256: signature.sign_hmac_sha256_with_client,
SIGNATURE_HMAC_SHA512: signature.sign_hmac_sha512_with_client,
SIGNATURE_RSA_SHA1: signature.sign_rsa_sha1_with_client,
SIGNATURE_RSA_SHA256: signature.sign_rsa_sha256_with_client,
SIGNATURE_RSA_SHA512: signature.sign_rsa_sha512_with_client,
SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client
}
@ -57,7 +93,7 @@ class Client(object):
resource_owner_key=None,
resource_owner_secret=None,
callback_uri=None,
signature_method=SIGNATURE_HMAC,
signature_method=SIGNATURE_HMAC_SHA1,
signature_type=SIGNATURE_TYPE_AUTH_HEADER,
rsa_key=None, verifier=None, realm=None,
encoding='utf-8', decoding=None,
@ -105,10 +141,11 @@ class Client(object):
def __repr__(self):
attrs = vars(self).copy()
attrs['client_secret'] = '****' if attrs['client_secret'] else None
attrs['rsa_key'] = '****' if attrs['rsa_key'] else None
attrs[
'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None
attribute_str = ', '.join('%s=%s' % (k, v) for k, v in attrs.items())
return '<%s %s>' % (self.__class__.__name__, attribute_str)
attribute_str = ', '.join('{}={}'.format(k, v) for k, v in attrs.items())
return '<{} {}>'.format(self.__class__.__name__, attribute_str)
def get_oauth_signature(self, request):
"""Get an OAuth signature to be used in signing a request
@ -118,7 +155,7 @@ class Client(object):
replace any netloc part of the request argument's uri attribute
value.
.. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
.. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
"""
if self.signature_method == SIGNATURE_PLAINTEXT:
# fast-path
@ -131,25 +168,24 @@ class Client(object):
uri_query=urlparse.urlparse(uri).query,
body=body,
headers=headers)
log.debug("Collected params: {0}".format(collected_params))
log.debug("Collected params: {}".format(collected_params))
normalized_params = signature.normalize_parameters(collected_params)
normalized_uri = signature.normalize_base_string_uri(uri,
headers.get('Host', None))
log.debug("Normalized params: {0}".format(normalized_params))
log.debug("Normalized URI: {0}".format(normalized_uri))
normalized_uri = signature.base_string_uri(uri, headers.get('Host', None))
log.debug("Normalized params: {}".format(normalized_params))
log.debug("Normalized URI: {}".format(normalized_uri))
base_string = signature.construct_base_string(request.http_method,
base_string = signature.signature_base_string(request.http_method,
normalized_uri, normalized_params)
log.debug("Base signing string: {0}".format(base_string))
log.debug("Signing: signature base string: {}".format(base_string))
if self.signature_method not in self.SIGNATURE_METHODS:
raise ValueError('Invalid signature method.')
sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
log.debug("Signature: {0}".format(sig))
log.debug("Signature: {}".format(sig))
return sig
def get_oauth_params(self, request):
@ -174,10 +210,12 @@ class Client(object):
params.append(('oauth_verifier', self.verifier))
# providing body hash for requests other than x-www-form-urlencoded
# as described in http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
# as described in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-4.1.1
# 4.1.1. When to include the body hash
# * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies
# * [...] SHOULD include the oauth_body_hash parameter on all other requests.
# Note that SHA-1 is vulnerable. The spec acknowledges that in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-6.2
# At this time, no further effort has been made to replace SHA-1 for the OAuth Request Body Hash extension.
content_type = request.headers.get('Content-Type', None)
content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0
if request.body is not None and content_type_eligible:
@ -278,8 +316,8 @@ class Client(object):
# header field set to "application/x-www-form-urlencoded".
elif not should_have_params and has_params:
raise ValueError(
"Body contains parameters but Content-Type header was {0} "
"instead of {1}".format(content_type or "not set",
"Body contains parameters but Content-Type header was {} "
"instead of {}".format(content_type or "not set",
CONTENT_TYPE_FORM_URLENCODED))
# 3.5.2. Form-Encoded Body
@ -296,7 +334,7 @@ class Client(object):
raise ValueError(
'Body signatures may only be used with form-urlencoded content')
# We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
# We amend https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
# with the clause that parameters from body should only be included
# in non GET or HEAD requests. Extracting the request body parameters
# and including them in the signature base string would give semantic

View file

@ -1,9 +1,8 @@
from __future__ import absolute_import
from .access_token import AccessTokenEndpoint
from .authorization import AuthorizationEndpoint
from .base import BaseEndpoint
from .request_token import RequestTokenEndpoint
from .authorization import AuthorizationEndpoint
from .access_token import AccessTokenEndpoint
from .resource import ResourceEndpoint
from .signature_only import SignatureOnlyEndpoint
from .pre_configured import WebApplicationServer
from .pre_configured import WebApplicationServer # isort:skip

View file

@ -8,14 +8,12 @@ OAuth 1.0 RFC 5849. It validates the correctness of access token requests,
creates and persists tokens as well as create the proper response to be
returned to the client.
"""
from __future__ import absolute_import, unicode_literals
import logging
from oauthlib.common import urlencode
from .base import BaseEndpoint
from .. import errors
from .base import BaseEndpoint
log = logging.getLogger(__name__)
@ -37,7 +35,8 @@ class AccessTokenEndpoint(BaseEndpoint):
Similar to OAuth 2, indication of granted scopes will be included as a
space separated list in ``oauth_authorized_realms``.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: The token as an urlencoded string.
"""
request.realms = self.request_validator.get_realms(
@ -120,7 +119,8 @@ class AccessTokenEndpoint(BaseEndpoint):
def validate_access_token_request(self, request):
"""Validate an access token request.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:raises: OAuth1Error if the request is invalid.
:returns: A tuple of 2 elements.
1. The validation result (True or False).
@ -180,7 +180,7 @@ class AccessTokenEndpoint(BaseEndpoint):
# token credentials to the client, and ensure that the temporary
# credentials have not expired or been used before. The server MUST
# also verify the verification code received from the client.
# .. _`Section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2
# .. _`Section 3.2`: https://tools.ietf.org/html/rfc5849#section-3.2
#
# Note that early exit would enable resource owner authorization
# verifier enumertion.

View file

@ -6,16 +6,12 @@ oauthlib.oauth1.rfc5849.endpoints.authorization
This module is an implementation of various logic needed
for signing and checking OAuth 1.0 RFC 5849 requests.
"""
from __future__ import absolute_import, unicode_literals
from urllib.parse import urlencode
from oauthlib.common import Request, add_params_to_uri
from oauthlib.common import add_params_to_uri
from .base import BaseEndpoint
from .. import errors
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from .base import BaseEndpoint
class AuthorizationEndpoint(BaseEndpoint):
@ -41,7 +37,8 @@ class AuthorizationEndpoint(BaseEndpoint):
def create_verifier(self, request, credentials):
"""Create and save a new request token.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param credentials: A dict of extra token credentials.
:returns: The verifier as a dict.
"""

View file

@ -6,21 +6,20 @@ oauthlib.oauth1.rfc5849.endpoints.base
This module is an implementation of various logic needed
for signing and checking OAuth 1.0 RFC 5849 requests.
"""
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 signature, utils, errors
from .. import CONTENT_TYPE_FORM_URLENCODED
from .. import SIGNATURE_HMAC, SIGNATURE_RSA
from .. import SIGNATURE_TYPE_AUTH_HEADER
from .. import SIGNATURE_TYPE_QUERY
from .. import SIGNATURE_TYPE_BODY
from .. import (
CONTENT_TYPE_FORM_URLENCODED,
SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_HMAC_SHA512,
SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512,
SIGNATURE_PLAINTEXT,
SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY,
SIGNATURE_TYPE_QUERY, errors, signature, utils)
class BaseEndpoint(object):
class BaseEndpoint:
def __init__(self, request_validator, token_generator=None):
self.request_validator = request_validator
@ -70,7 +69,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)
@ -130,11 +129,11 @@ class BaseEndpoint(object):
# specification. Implementers should review the Security
# Considerations section (`Section 4`_) before deciding on which
# method to support.
# .. _`Section 4`: http://tools.ietf.org/html/rfc5849#section-4
# .. _`Section 4`: https://tools.ietf.org/html/rfc5849#section-4
if (not request.signature_method in
self.request_validator.allowed_signature_methods):
raise errors.InvalidSignatureMethodError(
description="Invalid signature, %s not in %r." % (
description="Invalid signature, {} not in {!r}.".format(
request.signature_method,
self.request_validator.allowed_signature_methods))
@ -182,35 +181,65 @@ class BaseEndpoint(object):
def _check_signature(self, request, is_token_request=False):
# ---- RSA Signature verification ----
if request.signature_method == SIGNATURE_RSA:
if request.signature_method == SIGNATURE_RSA_SHA1 or \
request.signature_method == SIGNATURE_RSA_SHA256 or \
request.signature_method == SIGNATURE_RSA_SHA512:
# RSA-based signature method
# The server verifies the signature per `[RFC3447] section 8.2.2`_
# .. _`[RFC3447] section 8.2.2`: http://tools.ietf.org/html/rfc3447#section-8.2.1
# .. _`[RFC3447] section 8.2.2`: https://tools.ietf.org/html/rfc3447#section-8.2.1
rsa_key = self.request_validator.get_rsa_key(
request.client_key, request)
valid_signature = signature.verify_rsa_sha1(request, rsa_key)
if request.signature_method == SIGNATURE_RSA_SHA1:
valid_signature = signature.verify_rsa_sha1(request, rsa_key)
elif request.signature_method == SIGNATURE_RSA_SHA256:
valid_signature = signature.verify_rsa_sha256(request, rsa_key)
elif request.signature_method == SIGNATURE_RSA_SHA512:
valid_signature = signature.verify_rsa_sha512(request, rsa_key)
else:
valid_signature = False
# ---- HMAC or Plaintext Signature verification ----
else:
# Non-RSA based signature method
# Servers receiving an authenticated request MUST validate it by:
# Recalculating the request signature independently as described in
# `Section 3.4`_ and comparing it to the value received from the
# client via the "oauth_signature" parameter.
# .. _`Section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
# .. _`Section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
client_secret = self.request_validator.get_client_secret(
request.client_key, request)
resource_owner_secret = None
if request.resource_owner_key:
if is_token_request:
resource_owner_secret = self.request_validator.get_request_token_secret(
request.client_key, request.resource_owner_key, request)
resource_owner_secret = \
self.request_validator.get_request_token_secret(
request.client_key, request.resource_owner_key,
request)
else:
resource_owner_secret = self.request_validator.get_access_token_secret(
request.client_key, request.resource_owner_key, request)
resource_owner_secret = \
self.request_validator.get_access_token_secret(
request.client_key, request.resource_owner_key,
request)
if request.signature_method == SIGNATURE_HMAC:
valid_signature = signature.verify_hmac_sha1(request,
client_secret, resource_owner_secret)
if request.signature_method == SIGNATURE_HMAC_SHA1:
valid_signature = signature.verify_hmac_sha1(
request, client_secret, resource_owner_secret)
elif request.signature_method == SIGNATURE_HMAC_SHA256:
valid_signature = signature.verify_hmac_sha256(
request, client_secret, resource_owner_secret)
elif request.signature_method == SIGNATURE_HMAC_SHA512:
valid_signature = signature.verify_hmac_sha512(
request, client_secret, resource_owner_secret)
elif request.signature_method == SIGNATURE_PLAINTEXT:
valid_signature = signature.verify_plaintext(
request, client_secret, resource_owner_secret)
else:
valid_signature = signature.verify_plaintext(request,
client_secret, resource_owner_secret)
valid_signature = False
return valid_signature

View file

@ -1,7 +1,7 @@
from __future__ import absolute_import, unicode_literals
from . import RequestTokenEndpoint, AuthorizationEndpoint
from . import AccessTokenEndpoint, ResourceEndpoint
from . import (
AccessTokenEndpoint, AuthorizationEndpoint, RequestTokenEndpoint,
ResourceEndpoint,
)
class WebApplicationServer(RequestTokenEndpoint, AuthorizationEndpoint,

View file

@ -8,14 +8,12 @@ OAuth 1.0 RFC 5849. It validates the correctness of request token requests,
creates and persists tokens as well as create the proper response to be
returned to the client.
"""
from __future__ import absolute_import, unicode_literals
import logging
from oauthlib.common import urlencode
from .base import BaseEndpoint
from .. import errors
from .base import BaseEndpoint
log = logging.getLogger(__name__)
@ -34,7 +32,8 @@ class RequestTokenEndpoint(BaseEndpoint):
def create_request_token(self, request, credentials):
"""Create and save a new request token.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param credentials: A dict of extra token credentials.
:returns: The token as an urlencoded string.
"""
@ -111,7 +110,8 @@ class RequestTokenEndpoint(BaseEndpoint):
def validate_request_token_request(self, request):
"""Validate a request token request.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:raises: OAuth1Error if the request is invalid.
:returns: A tuple of 2 elements.
1. The validation result (True or False).
@ -127,7 +127,7 @@ class RequestTokenEndpoint(BaseEndpoint):
request.client_key, request)
if not self.request_validator.check_realms(request.realms):
raise errors.InvalidRequestError(
description='Invalid realm %s. Allowed are %r.' % (
description='Invalid realm {}. Allowed are {!r}.'.format(
request.realms, self.request_validator.realms))
if not request.redirect_uri:
@ -156,7 +156,7 @@ class RequestTokenEndpoint(BaseEndpoint):
# However they could be seen as a scope or realm to which the
# client has access and as such every client should be checked
# to ensure it is authorized access to that scope or realm.
# .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2
# .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2
#
# Note that early exit would enable client realm access enumeration.
#
@ -178,7 +178,7 @@ class RequestTokenEndpoint(BaseEndpoint):
# Callback is normally never required, except for requests for
# a Temporary Credential as described in `Section 2.1`_
# .._`Section 2.1`: http://tools.ietf.org/html/rfc5849#section-2.1
# .._`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1
valid_redirect = self.request_validator.validate_redirect_uri(
request.client_key, request.redirect_uri, request)
if not request.redirect_uri:

View file

@ -6,12 +6,10 @@ oauthlib.oauth1.rfc5849.endpoints.resource
This module is an implementation of the resource protection provider logic of
OAuth 1.0 RFC 5849.
"""
from __future__ import absolute_import, unicode_literals
import logging
from .base import BaseEndpoint
from .. import errors
from .base import BaseEndpoint
log = logging.getLogger(__name__)
@ -119,7 +117,7 @@ class ResourceEndpoint(BaseEndpoint):
# However they could be seen as a scope or realm to which the
# client has access and as such every client should be checked
# to ensure it is authorized access to that scope or realm.
# .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2
# .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2
#
# Note that early exit would enable client realm access enumeration.
#

View file

@ -6,12 +6,10 @@ oauthlib.oauth1.rfc5849.endpoints.signature_only
This module is an implementation of the signing logic of OAuth 1.0 RFC 5849.
"""
from __future__ import absolute_import, unicode_literals
import logging
from .base import BaseEndpoint
from .. import errors
from .base import BaseEndpoint
log = logging.getLogger(__name__)
@ -34,17 +32,22 @@ class SignatureOnlyEndpoint(BaseEndpoint):
"""
try:
request = self._create_request(uri, http_method, body, headers)
except errors.OAuth1Error:
except errors.OAuth1Error as err:
log.info(
'Exception caught while validating request, %s.' % err)
return False, None
try:
self._check_transport_security(request)
self._check_mandatory_parameters(request)
except errors.OAuth1Error:
except errors.OAuth1Error as err:
log.info(
'Exception caught while validating request, %s.' % err)
return False, request
if not self.request_validator.validate_timestamp_and_nonce(
request.client_key, request.timestamp, request.nonce, request):
log.debug('[Failure] verification failed: timestamp/nonce')
return False, request
# The server SHOULD return a 401 (Unauthorized) status code when

View file

@ -1,4 +1,3 @@
# coding=utf-8
"""
oauthlib.oauth1.rfc5849.errors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -6,9 +5,7 @@ oauthlib.oauth1.rfc5849.errors
Error used both by OAuth 1 clients and provicers to represent the spec
defined error responses for all four core grant types.
"""
from __future__ import unicode_literals
from oauthlib.common import urlencode, add_params_to_uri
from oauthlib.common import add_params_to_uri, urlencode
class OAuth1Error(Exception):
@ -37,10 +34,10 @@ class OAuth1Error(Exception):
request: Oauthlib Request object
"""
self.description = description or self.description
message = '(%s) %s' % (self.error, self.description)
message = '({}) {}'.format(self.error, self.description)
if request:
message += ' ' + repr(request)
super(OAuth1Error, self).__init__(message)
super().__init__(message)
self.uri = uri
self.status_code = status_code

View file

@ -1,21 +1,17 @@
# -*- coding: utf-8 -*-
"""
oauthlib.parameters
~~~~~~~~~~~~~~~~~~~
This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec.
.. _`section 3.5`: http://tools.ietf.org/html/rfc5849#section-3.5
.. _`section 3.5`: https://tools.ietf.org/html/rfc5849#section-3.5
"""
from __future__ import absolute_import, unicode_literals
from urllib.parse import urlparse, urlunparse
try:
from urlparse import urlparse, urlunparse
except ImportError:
from urllib.parse import urlparse, urlunparse
from . import utils
from oauthlib.common import extract_params, urlencode
from . import utils
# TODO: do we need filter_params now that oauth_params are handled by Request?
# We can easily pass in just oauth protocol params.
@ -40,8 +36,8 @@ def prepare_headers(oauth_params, headers=None, realm=None):
oauth_version="1.0"
.. _`section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
.. _`RFC2617`: http://tools.ietf.org/html/rfc2617
.. _`section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1
.. _`RFC2617`: https://tools.ietf.org/html/rfc2617
"""
headers = headers or {}
@ -52,28 +48,28 @@ def prepare_headers(oauth_params, headers=None, realm=None):
# 1. Parameter names and values are encoded per Parameter Encoding
# (`Section 3.6`_)
#
# .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
# .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
escaped_name = utils.escape(oauth_parameter_name)
escaped_value = utils.escape(value)
# 2. Each parameter's name is immediately followed by an "=" character
# (ASCII code 61), a """ character (ASCII code 34), the parameter
# value (MAY be empty), and another """ character (ASCII code 34).
part = '{0}="{1}"'.format(escaped_name, escaped_value)
part = '{}="{}"'.format(escaped_name, escaped_value)
authorization_header_parameters_parts.append(part)
# 3. Parameters are separated by a "," character (ASCII code 44) and
# OPTIONAL linear whitespace per `RFC2617`_.
#
# .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
# .. _`RFC2617`: https://tools.ietf.org/html/rfc2617
authorization_header_parameters = ', '.join(
authorization_header_parameters_parts)
# 4. The OPTIONAL "realm" parameter MAY be added and interpreted per
# `RFC2617 section 1.2`_.
#
# .. _`RFC2617 section 1.2`: http://tools.ietf.org/html/rfc2617#section-1.2
# .. _`RFC2617 section 1.2`: https://tools.ietf.org/html/rfc2617#section-1.2
if realm:
# NOTE: realm should *not* be escaped
authorization_header_parameters = ('realm="%s", ' % realm +
@ -96,8 +92,8 @@ def _append_params(oauth_params, params):
Per `section 3.5.2`_ and `3.5.3`_ of the spec.
.. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
.. _`3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
.. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2
.. _`3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3
"""
merged = list(params)
@ -115,7 +111,7 @@ def prepare_form_encoded_body(oauth_params, body):
Per `section 3.5.2`_ of the spec.
.. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
.. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2
"""
# append OAuth params to the existing body
@ -127,7 +123,7 @@ def prepare_request_uri_query(oauth_params, uri):
Per `section 3.5.3`_ of the spec.
.. _`section 3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
.. _`section 3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3
"""
# append OAuth params to the existing set of query components

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth1.rfc5849
~~~~~~~~~~~~~~
@ -6,12 +5,10 @@ oauthlib.oauth1.rfc5849
This module is an implementation of various logic needed
for signing and checking OAuth 1.0 RFC 5849 requests.
"""
from __future__ import absolute_import, unicode_literals
from . import SIGNATURE_METHODS, utils
class RequestValidator(object):
class RequestValidator:
"""A validator/datastore interaction base class for OAuth 1 providers.
@ -107,7 +104,7 @@ class RequestValidator(object):
their use more straightforward and as such it could be worth reading what
follows in chronological order.
.. _`whitelisting or blacklisting`: http://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html
.. _`whitelisting or blacklisting`: https://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html
"""
def __init__(self):
@ -195,7 +192,15 @@ class RequestValidator(object):
def check_realms(self, realms):
"""Check that the realm is one of a set allowed realms."""
return all((r in self.realms for r in realms))
return all(r in self.realms for r in realms)
def _subclass_must_implement(self, fn):
"""
Returns a NotImplementedError for a function that should be implemented.
:param fn: name of the function
"""
m = "Missing function implementation in {}: {}".format(type(self), fn)
return NotImplementedError(m)
@property
def dummy_client(self):
@ -219,7 +224,7 @@ class RequestValidator(object):
* ResourceEndpoint
* SignatureOnlyEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("dummy_client")
@property
def dummy_request_token(self):
@ -235,7 +240,7 @@ class RequestValidator(object):
* AccessTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("dummy_request_token")
@property
def dummy_access_token(self):
@ -251,13 +256,14 @@ class RequestValidator(object):
* ResourceEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("dummy_access_token")
def get_client_secret(self, client_key, request):
"""Retrieves the client secret associated with the client key.
:param client_key: The client/consumer key.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: The client secret as a string.
This method must allow the use of a dummy client_key value.
@ -286,14 +292,15 @@ class RequestValidator(object):
* ResourceEndpoint
* SignatureOnlyEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement('get_client_secret')
def get_request_token_secret(self, client_key, token, request):
"""Retrieves the shared secret associated with the request token.
:param client_key: The client/consumer key.
:param token: The request token string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: The token secret as a string.
This method must allow the use of a dummy values and the running time
@ -318,14 +325,15 @@ class RequestValidator(object):
* AccessTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement('get_request_token_secret')
def get_access_token_secret(self, client_key, token, request):
"""Retrieves the shared secret associated with the access token.
:param client_key: The client/consumer key.
:param token: The access token string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: The token secret as a string.
This method must allow the use of a dummy values and the running time
@ -350,13 +358,14 @@ class RequestValidator(object):
* ResourceEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("get_access_token_secret")
def get_default_realms(self, client_key, request):
"""Get the default realms for a client.
:param client_key: The client/consumer key.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: The list of default realms associated with the client.
The list of default realms will be set during client registration and
@ -366,13 +375,14 @@ class RequestValidator(object):
* RequestTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("get_default_realms")
def get_realms(self, token, request):
"""Get realms associated with a request token.
:param token: The request token string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: The list of realms associated with the request token.
This method is used by
@ -380,13 +390,14 @@ class RequestValidator(object):
* AuthorizationEndpoint
* AccessTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("get_realms")
def get_redirect_uri(self, token, request):
"""Get the redirect URI associated with a request token.
:param token: The request token string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: The redirect URI associated with the request token.
It may be desirable to return a custom URI if the redirect is set to "oob".
@ -397,13 +408,14 @@ class RequestValidator(object):
* AuthorizationEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("get_redirect_uri")
def get_rsa_key(self, client_key, request):
"""Retrieves a previously stored client provided RSA key.
:param client_key: The client/consumer key.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: The rsa public key as a string.
This method must allow the use of a dummy client_key value. Fetching
@ -420,14 +432,15 @@ class RequestValidator(object):
* ResourceEndpoint
* SignatureOnlyEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("get_rsa_key")
def invalidate_request_token(self, client_key, request_token, request):
"""Invalidates a used request token.
:param client_key: The client/consumer key.
:param request_token: The request token string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: None
Per `Section 2.3`__ of the spec:
@ -435,7 +448,7 @@ class RequestValidator(object):
"The server MUST (...) ensure that the temporary
credentials have not expired or been used before."
.. _`Section 2.3`: http://tools.ietf.org/html/rfc5849#section-2.3
.. _`Section 2.3`: https://tools.ietf.org/html/rfc5849#section-2.3
This method should ensure that provided token won't validate anymore.
It can be simply removing RequestToken from storage or setting
@ -446,13 +459,14 @@ class RequestValidator(object):
* AccessTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("invalidate_request_token")
def validate_client_key(self, client_key, request):
"""Validates that supplied client key is a registered and valid client.
:param client_key: The client/consumer key.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: True or False
Note that if the dummy client is supplied it should validate in same
@ -482,14 +496,15 @@ class RequestValidator(object):
* ResourceEndpoint
* SignatureOnlyEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("validate_client_key")
def validate_request_token(self, client_key, token, request):
"""Validates that supplied request token is registered and valid.
:param client_key: The client/consumer key.
:param token: The request token string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: True or False
Note that if the dummy request_token is supplied it should validate in
@ -516,14 +531,15 @@ class RequestValidator(object):
* AccessTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("validate_request_token")
def validate_access_token(self, client_key, token, request):
"""Validates that supplied access token is registered and valid.
:param client_key: The client/consumer key.
:param token: The access token string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: True or False
Note that if the dummy access token is supplied it should validate in
@ -550,7 +566,7 @@ class RequestValidator(object):
* ResourceEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("validate_access_token")
def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
request, request_token=None, access_token=None):
@ -561,7 +577,8 @@ class RequestValidator(object):
:param nonce: The ``oauth_nonce`` parameter.
:param request_token: Request token string, if any.
:param access_token: Access token string, if any.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: True or False
Per `Section 3.3`_ of the spec.
@ -572,7 +589,7 @@ class RequestValidator(object):
channel. The nonce value MUST be unique across all requests with the
same timestamp, client credentials, and token combinations."
.. _`Section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
.. _`Section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3
One of the first validation checks that will be made is for the validity
of the nonce and timestamp, which are associated with a client key and
@ -600,7 +617,7 @@ class RequestValidator(object):
* ResourceEndpoint
* SignatureOnlyEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("validate_timestamp_and_nonce")
def validate_redirect_uri(self, client_key, redirect_uri, request):
"""Validates the client supplied redirection URI.
@ -608,7 +625,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param redirect_uri: The URI the client which to redirect back to after
authorization is successful.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: True or False
It is highly recommended that OAuth providers require their clients
@ -633,14 +651,15 @@ class RequestValidator(object):
* RequestTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("validate_redirect_uri")
def validate_requested_realms(self, client_key, realms, request):
"""Validates that the client may request access to the realm.
:param client_key: The client/consumer key.
:param realms: The list of realms that client is requesting access to.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: True or False
This method is invoked when obtaining a request token and should
@ -651,7 +670,7 @@ class RequestValidator(object):
* RequestTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("validate_requested_realms")
def validate_realms(self, client_key, token, request, uri=None,
realms=None):
@ -659,7 +678,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param token: A request token string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param uri: The URI the realms is protecting.
:param realms: A list of realms that must have been granted to
the access token.
@ -685,7 +705,7 @@ class RequestValidator(object):
* ResourceEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("validate_realms")
def validate_verifier(self, client_key, token, verifier, request):
"""Validates a verification code.
@ -693,7 +713,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param token: A request token string.
:param verifier: The authorization verifier string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: True or False
OAuth providers issue a verification code to clients after the
@ -716,13 +737,14 @@ class RequestValidator(object):
* AccessTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("validate_verifier")
def verify_request_token(self, token, request):
"""Verify that the given OAuth1 request token is valid.
:param token: A request token string.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: True or False
This method is used only in AuthorizationEndpoint to check whether the
@ -734,14 +756,15 @@ class RequestValidator(object):
* AuthorizationEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("verify_request_token")
def verify_realms(self, token, realms, request):
"""Verify authorized realms to see if they match those given to token.
:param token: An access token string.
:param realms: A list of realms the client attempts to access.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:returns: True or False
This prevents the list of authorized realms sent by the client during
@ -757,13 +780,14 @@ class RequestValidator(object):
* AuthorizationEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("verify_realms")
def save_access_token(self, token, request):
"""Save an OAuth1 access token.
:param token: A dict with token credentials.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
The token dictionary will at minimum include
@ -780,13 +804,14 @@ class RequestValidator(object):
* AccessTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("save_access_token")
def save_request_token(self, token, request):
"""Save an OAuth1 request token.
:param token: A dict with token credentials.
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
The token dictionary will at minimum include
@ -800,7 +825,7 @@ class RequestValidator(object):
* RequestTokenEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("save_request_token")
def save_verifier(self, token, verifier, request):
"""Associate an authorization verifier with a request token.
@ -808,7 +833,8 @@ class RequestValidator(object):
:param token: A request token string.
:param verifier A dictionary containing the oauth_verifier and
oauth_token
:param request: An oauthlib.common.Request object.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
We need to associate verifiers with tokens for validation during the
access token request.
@ -820,4 +846,4 @@ class RequestValidator(object):
* AuthorizationEndpoint
"""
raise NotImplementedError("Subclasses must implement this function.")
raise self._subclass_must_implement("save_verifier")

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.utils
~~~~~~~~~~~~~~
@ -6,14 +5,9 @@ oauthlib.utils
This module contains utility methods used by various parts of the OAuth
spec.
"""
from __future__ import absolute_import, unicode_literals
import urllib.request as urllib2
try:
import urllib2
except ImportError:
import urllib.request as urllib2
from oauthlib.common import quote, unquote, bytes_type, unicode_type
from oauthlib.common import quote, unquote
UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
@ -48,19 +42,19 @@ def escape(u):
Per `section 3.6`_ of the spec.
.. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
.. _`section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
"""
if not isinstance(u, unicode_type):
if not isinstance(u, str):
raise ValueError('Only unicode objects are escapable. ' +
'Got %s of type %s.' % (u, type(u)))
'Got {!r} of type {}.'.format(u, type(u)))
# Letters, digits, and the characters '_.-' are already treated as safe
# by urllib.quote(). We need to add '~' to fully support rfc5849.
return quote(u, safe=b'~')
def unescape(u):
if not isinstance(u, unicode_type):
if not isinstance(u, str):
raise ValueError('Only unicode objects are unescapable.')
return unquote(u)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2
~~~~~~~~~~~~~~
@ -6,29 +5,31 @@ oauthlib.oauth2
This module is a wrapper for the most recent implementation of OAuth 2.0 Client
and Server classes.
"""
from __future__ import absolute_import, unicode_literals
from .rfc6749.clients import Client
from .rfc6749.clients import WebApplicationClient
from .rfc6749.clients import MobileApplicationClient
from .rfc6749.clients import LegacyApplicationClient
from .rfc6749.clients import BackendApplicationClient
from .rfc6749.clients import ServiceApplicationClient
from .rfc6749.endpoints import AuthorizationEndpoint
from .rfc6749.endpoints import TokenEndpoint
from .rfc6749.endpoints import ResourceEndpoint
from .rfc6749.endpoints import RevocationEndpoint
from .rfc6749.endpoints import Server
from .rfc6749.endpoints import WebApplicationServer
from .rfc6749.endpoints import MobileApplicationServer
from .rfc6749.endpoints import LegacyApplicationServer
from .rfc6749.endpoints import BackendApplicationServer
from .rfc6749.errors import *
from .rfc6749.grant_types import AuthorizationCodeGrant
from .rfc6749.grant_types import ImplicitGrant
from .rfc6749.grant_types import ResourceOwnerPasswordCredentialsGrant
from .rfc6749.grant_types import ClientCredentialsGrant
from .rfc6749.grant_types import RefreshTokenGrant
from .rfc6749.clients import (
BackendApplicationClient, Client, LegacyApplicationClient,
MobileApplicationClient, ServiceApplicationClient, WebApplicationClient,
)
from .rfc6749.endpoints import (
AuthorizationEndpoint, BackendApplicationServer, IntrospectEndpoint,
LegacyApplicationServer, MetadataEndpoint, MobileApplicationServer,
ResourceEndpoint, RevocationEndpoint, Server, TokenEndpoint,
WebApplicationServer,
)
from .rfc6749.errors import (
AccessDeniedError, FatalClientError, InsecureTransportError,
InvalidClientError, InvalidClientIdError, InvalidGrantError,
InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError,
InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError,
MissingClientIdError, MissingCodeError, MissingRedirectURIError,
MissingResponseTypeError, MissingTokenError, MissingTokenTypeError,
OAuth2Error, ServerError, TemporarilyUnavailableError, TokenExpiredError,
UnauthorizedClientError, UnsupportedGrantTypeError,
UnsupportedResponseTypeError, UnsupportedTokenTypeError,
)
from .rfc6749.grant_types import (
AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant,
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
)
from .rfc6749.request_validator import RequestValidator
from .rfc6749.tokens import BearerToken, OAuth2Token
from .rfc6749.utils import is_secure_transport

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749
~~~~~~~~~~~~~~~~~~~~~~~
@ -6,61 +5,12 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
import functools
import logging
from .errors import TemporarilyUnavailableError, ServerError
from .errors import FatalClientError, OAuth2Error
from .endpoints.base import BaseEndpoint, catch_errors_and_unavailability
from .errors import (
FatalClientError, OAuth2Error, ServerError, TemporarilyUnavailableError,
)
log = logging.getLogger(__name__)
class BaseEndpoint(object):
def __init__(self):
self._available = True
self._catch_errors = False
@property
def available(self):
return self._available
@available.setter
def available(self, available):
self._available = available
@property
def catch_errors(self):
return self._catch_errors
@catch_errors.setter
def catch_errors(self, catch_errors):
self._catch_errors = catch_errors
def catch_errors_and_unavailability(f):
@functools.wraps(f)
def wrapper(endpoint, uri, *args, **kwargs):
if not endpoint.available:
e = TemporarilyUnavailableError()
log.info('Endpoint unavailable, ignoring request %s.' % uri)
return {}, e.json, 503
if endpoint.catch_errors:
try:
return f(endpoint, uri, *args, **kwargs)
except OAuth2Error:
raise
except FatalClientError:
raise
except Exception as e:
error = ServerError()
log.warning(
'Exception caught while processing request, %s.' % e)
return {}, error.json, 500
else:
return f(endpoint, uri, *args, **kwargs)
return wrapper

View file

@ -6,11 +6,9 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
from .base import *
from .web_application import WebApplicationClient
from .mobile_application import MobileApplicationClient
from .legacy_application import LegacyApplicationClient
from .backend_application import BackendApplicationClient
from .base import AUTH_HEADER, BODY, URI_QUERY, Client
from .legacy_application import LegacyApplicationClient
from .mobile_application import MobileApplicationClient
from .service_application import ServiceApplicationClient
from .web_application import WebApplicationClient

View file

@ -6,11 +6,8 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
from .base import Client
from ..parameters import prepare_token_request
from ..parameters import parse_token_response
from .base import Client
class BackendApplicationClient(Client):
@ -31,15 +28,28 @@ class BackendApplicationClient(Client):
no additional authorization request is needed.
"""
def prepare_request_body(self, body='', scope=None, **kwargs):
grant_type = 'client_credentials'
def prepare_request_body(self, body='', scope=None,
include_client_id=False, **kwargs):
"""Add the client credentials to the request body.
The client makes a request to the token endpoint by adding the
following parameters using the "application/x-www-form-urlencoded"
format per `Appendix B`_ in the HTTP request entity-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 client MUST authenticate with the authorization server as
@ -53,9 +63,12 @@ class BackendApplicationClient(Client):
>>> client.prepare_request_body(scope=['hello', 'world'])
'grant_type=client_credentials&scope=hello+world'
.. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
return prepare_token_request('client_credentials', body=body,
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,
scope=scope, **kwargs)

View file

@ -6,20 +6,20 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
import time
import warnings
from oauthlib.common import generate_token
from oauthlib.oauth2.rfc6749 import tokens
from oauthlib.oauth2.rfc6749.parameters import parse_token_response
from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
from oauthlib.oauth2.rfc6749.parameters import prepare_token_revocation_request
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
from oauthlib.oauth2.rfc6749.errors import (
InsecureTransportError, TokenExpiredError,
)
from oauthlib.oauth2.rfc6749.parameters import (
parse_token_response, prepare_token_request,
prepare_token_revocation_request,
)
from oauthlib.oauth2.rfc6749.utils import is_secure_transport
AUTH_HEADER = 'auth_header'
URI_QUERY = 'query'
BODY = 'body'
@ -28,8 +28,8 @@ FORM_ENC_HEADERS = {
'Content-Type': 'application/x-www-form-urlencoded'
}
class Client(object):
class Client:
"""Base OAuth2 client responsible for access token management.
This class also acts as a generic interface providing methods common to all
@ -47,6 +47,7 @@ class Client(object):
Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`.
"""
refresh_token_key = 'refresh_token'
def __init__(self, client_id,
default_token_placement=AUTH_HEADER,
@ -80,7 +81,7 @@ class Client(object):
``token`` dict parameter.
:param refresh_token: A refresh token (string) used to refresh expired
tokens. Can also be supplide inside the ``token`` dict parameter.
tokens. Can also be supplied inside the ``token`` dict parameter.
:param mac_key: Encryption key used with MAC tokens.
@ -112,8 +113,10 @@ class Client(object):
self.state_generator = state_generator
self.state = state
self.redirect_url = redirect_url
self.code = None
self.expires_in = None
self._expires_at = None
self._populate_attributes(self.token)
self.populate_token_attributes(self.token)
@property
def token_types(self):
@ -141,6 +144,7 @@ class Client(object):
def parse_request_uri_response(self, *args, **kwargs):
"""Abstract method used to parse redirection responses."""
raise NotImplementedError("Must be implemented by inheriting classes.")
def add_token(self, uri, http_method='GET', body=None, headers=None,
token_placement=None, **kwargs):
@ -174,20 +178,20 @@ class Client(object):
nonce="274312:dj83hs9s",
mac="kDZvddkndxvhGRXZhvuDjEWhGeE="
.. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/rfc6749#section-12.2
.. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/rfc6749#section-12.2
.. _`I-D.ietf-oauth-v2-bearer`: https://tools.ietf.org/html/rfc6749#section-12.2
.. _`I-D.ietf-oauth-v2-http-mac`: https://tools.ietf.org/html/rfc6749#section-12.2
"""
if not is_secure_transport(uri):
raise InsecureTransportError()
token_placement = token_placement or self.default_token_placement
case_insensitive_token_types = dict(
(k.lower(), v) for k, v in self.token_types.items())
case_insensitive_token_types = {
k.lower(): v for k, v in self.token_types.items()}
if not self.token_type.lower() in case_insensitive_token_types:
raise ValueError("Unsupported token type: %s" % self.token_type)
if not self.access_token:
if not (self.access_token or self.token.get('access_token')):
raise ValueError("Missing access token.")
if self._expires_at and self._expires_at < time.time():
@ -197,7 +201,7 @@ class Client(object):
headers, token_placement, **kwargs)
def prepare_authorization_request(self, authorization_url, state=None,
redirect_url=None, scope=None, **kwargs):
redirect_url=None, scope=None, **kwargs):
"""Prepare the authorization request.
This is the first step in many OAuth flows in which the user is
@ -218,6 +222,11 @@ class Client(object):
the provider. If provided then it must also be provided in the
token request.
:param scope: List of scopes to request. Must be equal to
or a subset of the scopes granted when obtaining the refresh
token. If none is provided, the ones provided in the constructor are
used.
:param kwargs: Additional parameters to included in the request.
:returns: The prepared request tuple with (url, headers, body).
@ -227,14 +236,15 @@ class Client(object):
self.state = state or self.state_generator()
self.redirect_url = redirect_url or self.redirect_url
self.scope = scope or self.scope
# do not assign scope to self automatically anymore
scope = self.scope if scope is None else scope
auth_url = self.prepare_request_uri(
authorization_url, redirect_uri=self.redirect_url,
scope=self.scope, state=self.state, **kwargs)
authorization_url, redirect_uri=self.redirect_url,
scope=scope, state=self.state, **kwargs)
return auth_url, FORM_ENC_HEADERS, ''
def prepare_token_request(self, token_url, authorization_response=None,
redirect_url=None, state=None, body='', **kwargs):
redirect_url=None, state=None, body='', **kwargs):
"""Prepare a token creation request.
Note that these requests usually require client authentication, either
@ -251,7 +261,10 @@ class Client(object):
:param redirect_url: The redirect_url supplied with the authorization
request (if there was one).
:param body: Request body (URL encoded string).
:param state:
:param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''.
:param kwargs: Additional parameters to included in the request.
@ -263,15 +276,15 @@ class Client(object):
state = state or self.state
if authorization_response:
self.parse_request_uri_response(
authorization_response, state=state)
authorization_response, state=state)
self.redirect_url = redirect_url or self.redirect_url
body = self.prepare_request_body(body=body,
redirect_uri=self.redirect_url, **kwargs)
redirect_uri=self.redirect_url, **kwargs)
return token_url, FORM_ENC_HEADERS, body
def prepare_refresh_token_request(self, token_url, refresh_token=None,
body='', scope=None, **kwargs):
body='', scope=None, **kwargs):
"""Prepare an access token refresh request.
Expired access tokens can be replaced by new access tokens without
@ -283,11 +296,13 @@ class Client(object):
:param refresh_token: Refresh token string.
:param body: Request body (URL encoded string).
:param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''.
:param scope: List of scopes to request. Must be equal to
or a subset of the scopes granted when obtaining the refresh
token.
token. If none is provided, the ones provided in the constructor are
used.
:param kwargs: Additional parameters to included in the request.
@ -296,13 +311,14 @@ class Client(object):
if not is_secure_transport(token_url):
raise InsecureTransportError()
self.scope = scope or self.scope
# do not assign scope to self automatically anymore
scope = self.scope if scope is None else scope
body = self.prepare_refresh_body(body=body,
refresh_token=refresh_token, scope=self.scope, **kwargs)
refresh_token=refresh_token, scope=scope, **kwargs)
return token_url, FORM_ENC_HEADERS, body
def prepare_token_revocation_request(self, revocation_url, token,
token_type_hint="access_token", body='', callback=None, **kwargs):
token_type_hint="access_token", body='', callback=None, **kwargs):
"""Prepare a token revocation request.
:param revocation_url: Provider token revocation endpoint URL.
@ -313,6 +329,8 @@ class Client(object):
``"refresh_token"``. This is optional and if you wish to not pass it you
must provide ``token_type_hint=None``.
:param body:
:param callback: A jsonp callback such as ``package.callback`` to be invoked
upon receiving the response. Not that it should not include a () suffix.
@ -357,8 +375,8 @@ class Client(object):
raise InsecureTransportError()
return prepare_token_revocation_request(revocation_url, token,
token_type_hint=token_type_hint, body=body, callback=callback,
**kwargs)
token_type_hint=token_type_hint, body=body, callback=callback,
**kwargs)
def parse_request_body_response(self, body, scope=None, **kwargs):
"""Parse the JSON response body.
@ -370,7 +388,8 @@ class Client(object):
returns an error response as described in `Section 5.2`_.
:param body: The response body from the token request.
:param scope: Scopes originally requested.
:param scope: Scopes originally requested. If none is provided, the ones
provided in the constructor are used.
:return: Dictionary of token parameters.
:raises: Warning if scope has changed. OAuth2Error if response is invalid.
@ -398,16 +417,17 @@ class Client(object):
If omitted, the authorization server SHOULD provide the
expiration time via other means or document the default value.
**scope**
**scope**
Providers may supply this in all responses but are required to only
if it has changed since the authorization request.
.. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
.. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
.. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
.. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
"""
scope = self.scope if scope is None else scope
self.token = parse_token_response(body, scope=scope)
self._populate_attributes(self.token)
self.populate_token_attributes(self.token)
return self.token
def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs):
@ -427,10 +447,12 @@ class Client(object):
Section 3.3. The requested scope MUST NOT include any scope
not originally granted by the resource owner, and if omitted is
treated as equal to the scope originally granted by the
resource owner.
resource owner. Note that if none is provided, the ones provided
in the constructor are used if any.
"""
refresh_token = refresh_token or self.refresh_token
return prepare_token_request('refresh_token', body=body, scope=scope,
scope = self.scope if scope is None else scope
return prepare_token_request(self.refresh_token_key, body=body, scope=scope,
refresh_token=refresh_token, **kwargs)
def _add_bearer_token(self, uri, http_method='GET', body=None,
@ -455,13 +477,27 @@ class Client(object):
Warning: MAC token support is experimental as the spec is not yet stable.
"""
if token_placement != AUTH_HEADER:
raise ValueError("Invalid token placement.")
headers = tokens.prepare_mac_header(self.access_token, uri,
self.mac_key, http_method, headers=headers, body=body, ext=ext,
hash_algorithm=self.mac_algorithm, **kwargs)
return uri, headers, body
def _populate_attributes(self, response):
"""Add commonly used values such as access_token to self."""
warnings.warn("Please switch to the public method "
"populate_token_attributes.", DeprecationWarning)
return self.populate_token_attributes(response)
def populate_code_attributes(self, response):
"""Add attributes from an auth code response to self."""
if 'code' in response:
self.code = response.get('code')
def populate_token_attributes(self, response):
"""Add attributes from a token exchange response to self."""
if 'access_token' in response:
self.access_token = response.get('access_token')
@ -479,12 +515,8 @@ class Client(object):
if 'expires_at' in response:
self._expires_at = int(response.get('expires_at'))
if 'code' in response:
self.code = response.get('code')
if 'mac_key' in response:
self.mac_key = response.get('mac_key')
if 'mac_algorithm' in response:
self.mac_algorithm = response.get('mac_algorithm')

View file

@ -6,11 +6,8 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
from .base import Client
from ..parameters import prepare_token_request
from ..parameters import parse_token_response
from .base import Client
class LegacyApplicationClient(Client):
@ -36,10 +33,13 @@ class LegacyApplicationClient(Client):
MUST discard the credentials once an access token has been obtained.
"""
def __init__(self, client_id, **kwargs):
super(LegacyApplicationClient, self).__init__(client_id, **kwargs)
grant_type = 'password'
def prepare_request_body(self, username, password, body='', scope=None, **kwargs):
def __init__(self, client_id, **kwargs):
super().__init__(client_id, **kwargs)
def prepare_request_body(self, username, password, body='', scope=None,
include_client_id=False, **kwargs):
"""Add the resource owner password and username to the request body.
The client makes a request to the token endpoint by adding the
@ -48,8 +48,16 @@ class LegacyApplicationClient(Client):
:param username: The resource owner username.
:param password: The resource owner password.
: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.
If the client type is confidential or the client was issued client
@ -65,9 +73,12 @@ class LegacyApplicationClient(Client):
>>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world'])
'grant_type=password&username=foo&scope=hello+world&password=bar'
.. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
return prepare_token_request('password', body=body, username=username,
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, username=username,
password=password, scope=scope, **kwargs)

View file

@ -6,11 +6,8 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
from ..parameters import parse_implicit_response, prepare_grant_uri
from .base import Client
from ..parameters import prepare_grant_uri
from ..parameters import parse_implicit_response
class MobileApplicationClient(Client):
@ -46,6 +43,8 @@ class MobileApplicationClient(Client):
redirection URI, it may be exposed to the resource owner and other
applications residing on the same device.
"""
response_type = 'token'
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
state=None, **kwargs):
@ -86,13 +85,14 @@ class MobileApplicationClient(Client):
>>> client.prepare_request_uri('https://example.com', foo='bar')
'https://example.com?client_id=your_id&response_type=token&foo=bar'
.. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
"""
return prepare_grant_uri(uri, self.client_id, 'token',
scope = self.scope if scope is None else scope
return prepare_grant_uri(uri, self.client_id, self.response_type,
redirect_uri=redirect_uri, state=state, scope=scope, **kwargs)
def parse_request_uri_response(self, uri, state=None, scope=None):
@ -165,9 +165,10 @@ class MobileApplicationClient(Client):
>>> client.parse_request_body_response(response_body, scope=['other'])
('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world'])
.. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
"""
scope = self.scope if scope is None else scope
self.token = parse_implicit_response(uri, state=state, scope=scope)
self._populate_attributes(self.token)
self.populate_token_attributes(self.token)
return self.token

View file

@ -6,15 +6,12 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
import time
from oauthlib.common import to_unicode
from .base import Client
from ..parameters import prepare_token_request
from ..parameters import parse_token_response
from .base import Client
class ServiceApplicationClient(Client):
@ -42,38 +39,39 @@ class ServiceApplicationClient(Client):
:param private_key: Private key used for signing and encrypting.
Must be given as a string.
:param subject: The principal that is the subject of the JWT, i.e.
:param subject: The principal that is the subject of the JWT, i.e.
which user is the token requested on behalf of.
For example, ``foo@example.com.
:param issuer: The JWT MUST contain an "iss" (issuer) claim that
contains a unique identifier for the entity that issued
the JWT. For example, ``your-client@provider.com``.
the JWT. For example, ``your-client@provider.com``.
:param audience: A value identifying the authorization server as an
intended audience, e.g.
``https://provider.com/oauth2/token``.
:param kwargs: Additional arguments to pass to base client, such as
state and token. See Client.__init__.__doc__ for
state and token. See ``Client.__init__.__doc__`` for
details.
"""
super(ServiceApplicationClient, self).__init__(client_id, **kwargs)
super().__init__(client_id, **kwargs)
self.private_key = private_key
self.subject = subject
self.issuer = issuer
self.audience = audience
def prepare_request_body(self,
def prepare_request_body(self,
private_key=None,
subject=None,
issuer=None,
audience=None,
expires_at=None,
subject=None,
issuer=None,
audience=None,
expires_at=None,
issued_at=None,
extra_claims=None,
body='',
scope=None,
body='',
scope=None,
include_client_id=False,
**kwargs):
"""Create and add a JWT assertion to the request body.
@ -86,7 +84,7 @@ class ServiceApplicationClient(Client):
:param issuer: (iss) The JWT MUST contain an "iss" (issuer) claim that
contains a unique identifier for the entity that issued
the JWT. For example, ``your-client@provider.com``.
the JWT. For example, ``your-client@provider.com``.
:param audience: (aud) A value identifying the authorization server as an
intended audience, e.g.
@ -98,26 +96,38 @@ class ServiceApplicationClient(Client):
:param issued_at: A unix timestamp of when the JWT was created.
Defaults to now, i.e. ``time.time()``.
:param not_before: A unix timestamp after which the JWT may be used.
Not included unless provided.
:param jwt_id: A unique JWT token identifier. Not included unless
provided.
:param extra_claims: A dict of additional claims to include in the JWT.
: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.
:param body: Request body (string) with extra parameters.
: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 not_before: A unix timestamp after which the JWT may be used.
Not included unless provided. *
:param jwt_id: A unique JWT token identifier. Not included unless
provided. *
:param kwargs: Extra credentials to include in the token request.
Parameters marked with a `*` above are not explicit arguments in the
function signature, but are specially documented arguments for items
appearing in the generic `**kwargs` keyworded input.
The "scope" parameter may be used, as defined in the Assertion
Framework for OAuth 2.0 Client Authentication and Authorization Grants
[I-D.ietf-oauth-assertions] specification, to indicate the requested
scope.
Authentication of the client is optional, as described in
Authentication of the client is optional, as described in
`Section 3.2.1`_ of OAuth 2.0 [RFC6749] and consequently, the
"client_id" is only needed when a form of client authentication that
relies on the parameter is used.
@ -137,7 +147,7 @@ class ServiceApplicationClient(Client):
eyJpc3Mi[...omitted for brevity...].
J9l-ZhwP[...omitted for brevity...]
.. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
import jwt
@ -147,8 +157,8 @@ class ServiceApplicationClient(Client):
' token requests.')
claim = {
'iss': issuer or self.issuer,
'aud': audience or self.issuer,
'sub': subject or self.issuer,
'aud': audience or self.audience,
'sub': subject or self.subject,
'exp': int(expires_at or time.time() + 3600),
'iat': int(issued_at or time.time()),
}
@ -169,8 +179,11 @@ class ServiceApplicationClient(Client):
assertion = jwt.encode(claim, key, 'RS256')
assertion = to_unicode(assertion)
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,
assertion=assertion,
scope=scope,
scope=scope,
**kwargs)

View file

@ -6,12 +6,13 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
import warnings
from ..parameters import (
parse_authorization_code_response, prepare_grant_uri,
prepare_token_request,
)
from .base import Client
from ..parameters import prepare_grant_uri, prepare_token_request
from ..parameters import parse_authorization_code_response
from ..parameters import parse_token_response
class WebApplicationClient(Client):
@ -32,9 +33,11 @@ class WebApplicationClient(Client):
browser) and capable of receiving incoming requests (via redirection)
from the authorization server.
"""
grant_type = 'authorization_code'
def __init__(self, client_id, code=None, **kwargs):
super(WebApplicationClient, self).__init__(client_id, **kwargs)
super().__init__(client_id, **kwargs)
self.code = code
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
@ -76,26 +79,24 @@ class WebApplicationClient(Client):
>>> client.prepare_request_uri('https://example.com', foo='bar')
'https://example.com?client_id=your_id&response_type=code&foo=bar'
.. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
"""
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)
def prepare_request_body(self, client_id=None, code=None, body='',
redirect_uri=None, **kwargs):
def prepare_request_body(self, code=None, redirect_uri=None, body='',
include_client_id=True, **kwargs):
"""Prepare the access token request body.
The client makes a request to the token endpoint by adding the
following parameters using the "application/x-www-form-urlencoded"
format in the HTTP request entity-body:
:param client_id: REQUIRED, if the client is not authenticating with the
authorization server as described in `Section 3.2.1`_.
:param code: REQUIRED. The authorization code received from the
authorization server.
@ -103,6 +104,15 @@ class WebApplicationClient(Client):
authorization request as described in `Section 4.1.1`_, and their
values MUST be identical.
:param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra paramters. Default ''.
:param include_client_id: `True` (default) 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`_.
:type include_client_id: Boolean
:param kwargs: Extra parameters to include in the token request.
In addition OAuthLib will add the ``grant_type`` parameter set to
@ -120,12 +130,31 @@ class WebApplicationClient(Client):
>>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar')
'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar'
.. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1
.. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
`Section 3.2.1` also states:
In the "authorization_code" "grant_type" request to the token
endpoint, an unauthenticated client MUST send its "client_id" to
prevent itself from inadvertently accepting a code intended for a
client with a different "client_id". This protects the client from
substitution of the authentication code. (It provides no additional
security for the protected resource.)
.. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
code = code or self.code
return prepare_token_request('authorization_code', code=code, body=body,
client_id=self.client_id, redirect_uri=redirect_uri, **kwargs)
if 'client_id' in kwargs:
warnings.warn("`client_id` has been deprecated in favor of "
"`include_client_id`, a boolean value which will "
"include the already configured `self.client_id`.",
DeprecationWarning)
if kwargs['client_id'] != self.client_id:
raise ValueError("`client_id` was supplied as an argument, but "
"it does not match `self.client_id`")
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)
def parse_request_uri_response(self, uri, state=None):
"""Parse the URI query for code and state.
@ -172,5 +201,5 @@ class WebApplicationClient(Client):
oauthlib.oauth2.rfc6749.errors.MismatchingStateError
"""
response = parse_authorization_code_response(uri, state=state)
self._populate_attributes(response)
self.populate_code_attributes(response)
return response

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749
~~~~~~~~~~~~~~~~~~~~~~~
@ -6,14 +5,13 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
from .authorization import AuthorizationEndpoint
from .token import TokenEndpoint
from .introspect import IntrospectEndpoint
from .metadata import MetadataEndpoint
from .pre_configured import (
BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer,
Server, WebApplicationServer,
)
from .resource import ResourceEndpoint
from .revocation import RevocationEndpoint
from .pre_configured import Server
from .pre_configured import WebApplicationServer
from .pre_configured import MobileApplicationServer
from .pre_configured import LegacyApplicationServer
from .pre_configured import BackendApplicationServer
from .token import TokenEndpoint

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749
~~~~~~~~~~~~~~~~~~~~~~~
@ -6,11 +5,10 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
import logging
from oauthlib.common import Request
from oauthlib.oauth2.rfc6749 import utils
from .base import BaseEndpoint, catch_errors_and_unavailability
@ -58,7 +56,7 @@ class AuthorizationEndpoint(BaseEndpoint):
# Enforced through the design of oauthlib.common.Request
.. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
"""
def __init__(self, default_response_type, default_token_type,
@ -108,7 +106,9 @@ class AuthorizationEndpoint(BaseEndpoint):
"""Extract response_type and route to the designated handler."""
request = Request(
uri, http_method=http_method, body=body, headers=headers)
request.scopes = None
request.scopes = utils.scope_to_list(request.scope)
response_type_handler = self.response_types.get(
request.response_type, self.default_response_type_handler)
return response_type_handler.validate_authorization_request(request)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749
~~~~~~~~~~~~~~~~~~~~~~~
@ -6,22 +5,34 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
import functools
import logging
from ..errors import TemporarilyUnavailableError, ServerError
from ..errors import FatalClientError, OAuth2Error
from ..errors import (
FatalClientError, InvalidClientError, InvalidRequestError, OAuth2Error,
ServerError, TemporarilyUnavailableError, UnsupportedTokenTypeError,
)
log = logging.getLogger(__name__)
class BaseEndpoint(object):
class BaseEndpoint:
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):
@ -29,7 +40,7 @@ class BaseEndpoint(object):
@available.setter
def available(self, available):
self._available = available
self._available = available
@property
def catch_errors(self):
@ -39,6 +50,43 @@ class BaseEndpoint(object):
def catch_errors(self, catch_errors):
self._catch_errors = catch_errors
def _raise_on_missing_token(self, request):
"""Raise error on missing token."""
if not request.token:
raise InvalidRequestError(request=request,
description='Missing token parameter.')
def _raise_on_invalid_client(self, request):
"""Raise on failed client authentication."""
if self.request_validator.client_authentication_required(request):
if not self.request_validator.authenticate_client(request):
log.debug('Client authentication failed, %r.', request)
raise InvalidClientError(request=request)
elif not self.request_validator.authenticate_client_id(request.client_id, request):
log.debug('Client authentication failed, %r.', request)
raise InvalidClientError(request=request)
def _raise_on_unsupported_token(self, request):
"""Raise on unsupported tokens."""
if (request.token_type_hint and
request.token_type_hint in self.valid_token_types and
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)

View file

@ -0,0 +1,122 @@
"""
oauthlib.oauth2.rfc6749.endpoint.introspect
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An implementation of the OAuth 2.0 `Token Introspection`.
.. _`Token Introspection`: https://tools.ietf.org/html/rfc7662
"""
import json
import logging
from oauthlib.common import Request
from ..errors import OAuth2Error
from .base import BaseEndpoint, catch_errors_and_unavailability
log = logging.getLogger(__name__)
class IntrospectEndpoint(BaseEndpoint):
"""Introspect token endpoint.
This endpoint defines a method to query an OAuth 2.0 authorization
server to determine the active state of an OAuth 2.0 token and to
determine meta-information about this token. OAuth 2.0 deployments
can use this method to convey information about the authorization
context of the token from the authorization server to the protected
resource.
To prevent the values of access tokens from leaking into
server-side logs via query parameters, an authorization server
offering token introspection MAY disallow the use of HTTP GET on
the introspection endpoint and instead require the HTTP POST method
to be used at the introspection endpoint.
"""
valid_token_types = ('access_token', 'refresh_token')
valid_request_methods = ('POST',)
def __init__(self, request_validator, supported_token_types=None):
BaseEndpoint.__init__(self)
self.request_validator = request_validator
self.supported_token_types = (
supported_token_types or self.valid_token_types)
@catch_errors_and_unavailability
def create_introspect_response(self, uri, http_method='POST', body=None,
headers=None):
"""Create introspect valid or invalid response
If the authorization server is unable to determine the state
of the token without additional information, it SHOULD return
an introspection response indicating the token is not active
as described in Section 2.2.
"""
resp_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)
resp_headers.update(e.headers)
return resp_headers, e.json, e.status_code
claims = self.request_validator.introspect_token(
request.token,
request.token_type_hint,
request
)
if claims is None:
return resp_headers, json.dumps(dict(active=False)), 200
if "active" in claims:
claims.pop("active")
return resp_headers, json.dumps(dict(active=True, **claims)), 200
def validate_introspect_request(self, request):
"""Ensure the request is valid.
The protected resource calls the introspection endpoint using
an HTTP POST request with parameters sent as
"application/x-www-form-urlencoded".
token REQUIRED. The string value of the token.
token_type_hint OPTIONAL.
A hint about the type of the token submitted for
introspection. The protected resource MAY pass this parameter to
help the authorization server optimize the token lookup. If the
server is unable to locate the token using the given hint, it MUST
extend its search across all of its supported token types. An
authorization server MAY ignore this parameter, particularly if it
is able to detect the token type automatically.
* access_token: An Access Token as defined in [`RFC6749`],
`section 1.4`_
* refresh_token: A Refresh Token as defined in [`RFC6749`],
`section 1.5`_
The introspection endpoint MAY accept other OPTIONAL
parameters to provide further context to the query. For
instance, an authorization server may desire to know the IP
address of the client accessing the protected resource to
determine if the correct client is likely to be presenting the
token. The definition of this or any other parameters are
outside the scope of this specification, to be defined by
service documentation or extensions to this specification.
.. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
.. _`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)

View file

@ -0,0 +1,237 @@
"""
oauthlib.oauth2.rfc6749.endpoint.metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An implementation of the `OAuth 2.0 Authorization Server Metadata`.
.. _`OAuth 2.0 Authorization Server Metadata`: https://tools.ietf.org/html/rfc8414
"""
import copy
import json
import logging
from .. import grant_types
from .authorization import AuthorizationEndpoint
from .base import BaseEndpoint, catch_errors_and_unavailability
from .introspect import IntrospectEndpoint
from .revocation import RevocationEndpoint
from .token import TokenEndpoint
log = logging.getLogger(__name__)
class MetadataEndpoint(BaseEndpoint):
"""OAuth2.0 Authorization Server Metadata endpoint.
This specification generalizes the metadata format defined by
`OpenID Connect Discovery 1.0` in a way that is compatible
with OpenID Connect Discovery while being applicable to a wider set
of OAuth 2.0 use cases. This is intentionally parallel to the way
that OAuth 2.0 Dynamic Client Registration Protocol [`RFC7591`_]
generalized the dynamic client registration mechanisms defined by
OpenID Connect Dynamic Client Registration 1.0
in a way that is compatible with it.
.. _`OpenID Connect Discovery 1.0`: https://openid.net/specs/openid-connect-discovery-1_0.html
.. _`RFC7591`: https://tools.ietf.org/html/rfc7591
"""
def __init__(self, endpoints, claims={}, raise_errors=True):
assert isinstance(claims, dict)
for endpoint in endpoints:
assert isinstance(endpoint, BaseEndpoint)
BaseEndpoint.__init__(self)
self.raise_errors = raise_errors
self.endpoints = endpoints
self.initial_claims = claims
self.claims = self.validate_metadata_server()
@catch_errors_and_unavailability
def create_metadata_response(self, uri, http_method='GET', body=None,
headers=None):
"""Create metadata response
"""
headers = {
'Content-Type': 'application/json'
}
return headers, json.dumps(self.claims), 200
def validate_metadata(self, array, key, is_required=False, is_list=False, is_url=False, is_issuer=False):
if not self.raise_errors:
return
if key not in array:
if is_required:
raise ValueError("key {} is a mandatory metadata.".format(key))
elif is_issuer:
if not array[key].startswith("https"):
raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key]))
if "?" in array[key] or "&" in array[key] or "#" in array[key]:
raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key]))
elif is_url:
if not array[key].startswith("http"):
raise ValueError("key {}: {} must be an URL".format(key, array[key]))
elif is_list:
if not isinstance(array[key], list):
raise ValueError("key {}: {} must be an Array".format(key, array[key]))
for elem in array[key]:
if not isinstance(elem, str):
raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem))
def validate_metadata_token(self, claims, endpoint):
"""
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, "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(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)
def validate_metadata_revocation(self, claims, endpoint):
claims.setdefault("revocation_endpoint_auth_methods_supported",
["client_secret_post", "client_secret_basic"])
self.validate_metadata(claims, "revocation_endpoint_auth_methods_supported", is_list=True)
self.validate_metadata(claims, "revocation_endpoint_auth_signing_alg_values_supported", is_list=True)
self.validate_metadata(claims, "revocation_endpoint", is_required=True, is_url=True)
def validate_metadata_introspection(self, claims, endpoint):
claims.setdefault("introspection_endpoint_auth_methods_supported",
["client_secret_post", "client_secret_basic"])
self.validate_metadata(claims, "introspection_endpoint_auth_methods_supported", is_list=True)
self.validate_metadata(claims, "introspection_endpoint_auth_signing_alg_values_supported", is_list=True)
self.validate_metadata(claims, "introspection_endpoint", is_required=True, is_url=True)
def validate_metadata_server(self):
"""
Authorization servers can have metadata describing their
configuration. The following authorization server metadata values
are used by this specification. More details can be found in
`RFC8414 section 2`_ :
issuer
REQUIRED
authorization_endpoint
URL of the authorization server's authorization endpoint
[`RFC6749#Authorization`_]. This is REQUIRED unless no grant types are supported
that use the authorization endpoint.
token_endpoint
URL of the authorization server's token endpoint [`RFC6749#Token`_]. This
is REQUIRED unless only the implicit grant type is supported.
scopes_supported
RECOMMENDED.
response_types_supported
REQUIRED.
Other OPTIONAL fields:
jwks_uri,
registration_endpoint,
response_modes_supported
grant_types_supported
OPTIONAL. JSON array containing a list of the OAuth 2.0 grant
type values that this authorization server supports. The array
values used are the same as those used with the "grant_types"
parameter defined by "OAuth 2.0 Dynamic Client Registration
Protocol" [`RFC7591`_]. If omitted, the default value is
"["authorization_code", "implicit"]".
token_endpoint_auth_methods_supported
token_endpoint_auth_signing_alg_values_supported
service_documentation
ui_locales_supported
op_policy_uri
op_tos_uri
revocation_endpoint
revocation_endpoint_auth_methods_supported
revocation_endpoint_auth_signing_alg_values_supported
introspection_endpoint
introspection_endpoint_auth_methods_supported
introspection_endpoint_auth_signing_alg_values_supported
code_challenge_methods_supported
Additional authorization server metadata parameters MAY also be used.
Some are defined by other specifications, such as OpenID Connect
Discovery 1.0 [`OpenID.Discovery`_].
.. _`RFC8414 section 2`: https://tools.ietf.org/html/rfc8414#section-2
.. _`RFC6749#Authorization`: https://tools.ietf.org/html/rfc6749#section-3.1
.. _`RFC6749#Token`: https://tools.ietf.org/html/rfc6749#section-3.2
.. _`RFC7591`: https://tools.ietf.org/html/rfc7591
.. _`OpenID.Discovery`: https://openid.net/specs/openid-connect-discovery-1_0.html
"""
claims = copy.deepcopy(self.initial_claims)
self.validate_metadata(claims, "issuer", is_required=True, is_issuer=True)
self.validate_metadata(claims, "jwks_uri", is_url=True)
self.validate_metadata(claims, "scopes_supported", is_list=True)
self.validate_metadata(claims, "service_documentation", is_url=True)
self.validate_metadata(claims, "ui_locales_supported", is_list=True)
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)
if isinstance(endpoint, AuthorizationEndpoint):
self.validate_metadata_authorization(claims, endpoint)
if isinstance(endpoint, RevocationEndpoint):
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

View file

@ -1,28 +1,24 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749
~~~~~~~~~~~~~~~~~~~~~~~
oauthlib.oauth2.rfc6749.endpoints.pre_configured
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
This module is an implementation of various endpoints needed
for providing OAuth 2.0 RFC6749 servers.
"""
from __future__ import absolute_import, unicode_literals
from ..grant_types import (
AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant,
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
)
from ..tokens import BearerToken
from ..grant_types import AuthorizationCodeGrant
from ..grant_types import ImplicitGrant
from ..grant_types import ResourceOwnerPasswordCredentialsGrant
from ..grant_types import ClientCredentialsGrant
from ..grant_types import RefreshTokenGrant
from .authorization import AuthorizationEndpoint
from .token import TokenEndpoint
from .introspect import IntrospectEndpoint
from .resource import ResourceEndpoint
from .revocation import RevocationEndpoint
from .token import TokenEndpoint
class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
RevocationEndpoint):
class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring all four major grant types."""
@ -42,35 +38,40 @@ class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
:param kwargs: Extra parameters to pass to authorization-,
token-, resource-, and revocation-endpoint constructors.
"""
auth_grant = AuthorizationCodeGrant(request_validator)
implicit_grant = ImplicitGrant(request_validator)
password_grant = ResourceOwnerPasswordCredentialsGrant(
self.auth_grant = AuthorizationCodeGrant(request_validator)
self.implicit_grant = ImplicitGrant(request_validator)
self.password_grant = ResourceOwnerPasswordCredentialsGrant(
request_validator)
credentials_grant = ClientCredentialsGrant(request_validator)
refresh_grant = RefreshTokenGrant(request_validator)
bearer = BearerToken(request_validator, token_generator,
self.credentials_grant = ClientCredentialsGrant(request_validator)
self.refresh_grant = RefreshTokenGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator,
token_expires_in, refresh_token_generator)
AuthorizationEndpoint.__init__(self, default_response_type='code',
response_types={
'code': auth_grant,
'token': implicit_grant,
'code': self.auth_grant,
'token': self.implicit_grant,
'none': self.auth_grant
},
default_token_type=bearer)
default_token_type=self.bearer)
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
grant_types={
'authorization_code': auth_grant,
'password': password_grant,
'client_credentials': credentials_grant,
'refresh_token': refresh_grant,
'authorization_code': self.auth_grant,
'password': self.password_grant,
'client_credentials': self.credentials_grant,
'refresh_token': self.refresh_grant,
},
default_token_type=bearer)
default_token_type=self.bearer)
ResourceEndpoint.__init__(self, default_token='Bearer',
token_types={'Bearer': bearer})
token_types={'Bearer': self.bearer})
RevocationEndpoint.__init__(self, request_validator)
IntrospectEndpoint.__init__(self, request_validator)
class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
RevocationEndpoint):
class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
@ -89,26 +90,27 @@ class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoin
:param kwargs: Extra parameters to pass to authorization-,
token-, resource-, and revocation-endpoint constructors.
"""
auth_grant = AuthorizationCodeGrant(request_validator)
refresh_grant = RefreshTokenGrant(request_validator)
bearer = BearerToken(request_validator, token_generator,
self.auth_grant = AuthorizationCodeGrant(request_validator)
self.refresh_grant = RefreshTokenGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator,
token_expires_in, refresh_token_generator)
AuthorizationEndpoint.__init__(self, default_response_type='code',
response_types={'code': auth_grant},
default_token_type=bearer)
response_types={'code': self.auth_grant},
default_token_type=self.bearer)
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
grant_types={
'authorization_code': auth_grant,
'refresh_token': refresh_grant,
'authorization_code': self.auth_grant,
'refresh_token': self.refresh_grant,
},
default_token_type=bearer)
default_token_type=self.bearer)
ResourceEndpoint.__init__(self, default_token='Bearer',
token_types={'Bearer': bearer})
token_types={'Bearer': self.bearer})
RevocationEndpoint.__init__(self, request_validator)
IntrospectEndpoint.__init__(self, request_validator)
class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint,
RevocationEndpoint):
class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint,
ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring Implicit code grant and Bearer tokens."""
@ -127,21 +129,23 @@ class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint,
:param kwargs: Extra parameters to pass to authorization-,
token-, resource-, and revocation-endpoint constructors.
"""
implicit_grant = ImplicitGrant(request_validator)
bearer = BearerToken(request_validator, token_generator,
self.implicit_grant = ImplicitGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator,
token_expires_in, refresh_token_generator)
AuthorizationEndpoint.__init__(self, default_response_type='token',
response_types={
'token': implicit_grant},
default_token_type=bearer)
'token': self.implicit_grant},
default_token_type=self.bearer)
ResourceEndpoint.__init__(self, default_token='Bearer',
token_types={'Bearer': bearer})
token_types={'Bearer': self.bearer})
RevocationEndpoint.__init__(self, request_validator,
supported_token_types=['access_token'])
IntrospectEndpoint.__init__(self, request_validator,
supported_token_types=['access_token'])
class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint,
RevocationEndpoint):
class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint,
ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens."""
@ -160,24 +164,25 @@ class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint,
:param kwargs: Extra parameters to pass to authorization-,
token-, resource-, and revocation-endpoint constructors.
"""
password_grant = ResourceOwnerPasswordCredentialsGrant(
self.password_grant = ResourceOwnerPasswordCredentialsGrant(
request_validator)
refresh_grant = RefreshTokenGrant(request_validator)
bearer = BearerToken(request_validator, token_generator,
self.refresh_grant = RefreshTokenGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator,
token_expires_in, refresh_token_generator)
TokenEndpoint.__init__(self, default_grant_type='password',
grant_types={
'password': password_grant,
'refresh_token': refresh_grant,
'password': self.password_grant,
'refresh_token': self.refresh_grant,
},
default_token_type=bearer)
default_token_type=self.bearer)
ResourceEndpoint.__init__(self, default_token='Bearer',
token_types={'Bearer': bearer})
token_types={'Bearer': self.bearer})
RevocationEndpoint.__init__(self, request_validator)
IntrospectEndpoint.__init__(self, request_validator)
class BackendApplicationServer(TokenEndpoint, ResourceEndpoint,
RevocationEndpoint):
class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint,
ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring Client Credentials grant and Bearer tokens."""
@ -196,14 +201,16 @@ class BackendApplicationServer(TokenEndpoint, ResourceEndpoint,
:param kwargs: Extra parameters to pass to authorization-,
token-, resource-, and revocation-endpoint constructors.
"""
credentials_grant = ClientCredentialsGrant(request_validator)
bearer = BearerToken(request_validator, token_generator,
self.credentials_grant = ClientCredentialsGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator,
token_expires_in, refresh_token_generator)
TokenEndpoint.__init__(self, default_grant_type='client_credentials',
grant_types={
'client_credentials': credentials_grant},
default_token_type=bearer)
'client_credentials': self.credentials_grant},
default_token_type=self.bearer)
ResourceEndpoint.__init__(self, default_token='Bearer',
token_types={'Bearer': bearer})
token_types={'Bearer': self.bearer})
RevocationEndpoint.__init__(self, request_validator,
supported_token_types=['access_token'])
IntrospectEndpoint.__init__(self, request_validator,
supported_token_types=['access_token'])

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749
~~~~~~~~~~~~~~~~~~~~~~~
@ -6,8 +5,6 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
import logging
from oauthlib.common import Request
@ -83,5 +80,5 @@ class ResourceEndpoint(BaseEndpoint):
to give an estimation based on the request.
"""
estimates = sorted(((t.estimate_type(request), n)
for n, t in self.tokens.items()))
for n, t in self.tokens.items()), reverse=True)
return estimates[0][1] if len(estimates) else None

View file

@ -1,21 +1,17 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.endpoint.revocation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An implementation of the OAuth 2 `Token Revocation`_ spec (draft 11).
.. _`Token Revocation`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11
.. _`Token Revocation`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11
"""
from __future__ import absolute_import, unicode_literals
import logging
from oauthlib.common import Request
from ..errors import OAuth2Error
from .base import BaseEndpoint, catch_errors_and_unavailability
from ..errors import InvalidClientError, UnsupportedTokenTypeError
from ..errors import InvalidRequestError, OAuth2Error
log = logging.getLogger(__name__)
@ -29,6 +25,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):
@ -59,6 +56,11 @@ class RevocationEndpoint(BaseEndpoint):
An invalid token type hint value is ignored by the authorization server
and does not influence the revocation response.
"""
resp_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,8 +70,9 @@ class RevocationEndpoint(BaseEndpoint):
log.debug('Client error during validation of %r. %r.', request, e)
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
response_body = '{}({});'.format(request.callback, response_body)
resp_headers.update(e.headers)
return resp_headers, response_body, e.status_code
self.request_validator.revoke_token(request.token,
request.token_type_hint, request)
@ -110,21 +113,14 @@ class RevocationEndpoint(BaseEndpoint):
The client also includes its authentication credentials as described in
`Section 2.3`_. of [`RFC6749`_].
.. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
.. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
.. _`section 2.3`: http://tools.ietf.org/html/rfc6749#section-2.3
.. _`Section 4.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2
.. _`RFC6749`: http://tools.ietf.org/html/rfc6749
.. _`section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4
.. _`section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5
.. _`section 2.3`: https://tools.ietf.org/html/rfc6749#section-2.3
.. _`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
"""
if not request.token:
raise InvalidRequestError(request=request,
description='Missing token parameter.')
if self.request_validator.client_authentication_required(request):
if not self.request_validator.authenticate_client(request):
raise InvalidClientError(request=request)
if (request.token_type_hint and
request.token_type_hint in self.valid_token_types and
request.token_type_hint not in self.supported_token_types):
raise UnsupportedTokenTypeError(request=request)
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)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749
~~~~~~~~~~~~~~~~~~~~~~~
@ -6,15 +5,13 @@ oauthlib.oauth2.rfc6749
This module is an implementation of various logic needed
for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
import logging
from oauthlib.common import Request
from oauthlib.oauth2.rfc6749 import utils
from .base import BaseEndpoint, catch_errors_and_unavailability
log = logging.getLogger(__name__)
@ -39,7 +36,6 @@ class TokenEndpoint(BaseEndpoint):
https://example.com/path?query=component # OK
https://example.com/path?query=component#fragment # Not OK
Since requests to the authorization endpoint result in user
Since requests to the token endpoint result in the transmission of
clear-text credentials (in the HTTP request and response), the
authorization server MUST require the use of TLS as described in
@ -59,9 +55,11 @@ class TokenEndpoint(BaseEndpoint):
# Delegated to each grant type.
.. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
.. _`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,16 +83,37 @@ class TokenEndpoint(BaseEndpoint):
return self._default_token_type
@catch_errors_and_unavailability
def create_token_response(self, uri, http_method='GET', body=None,
headers=None, credentials=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)
request.scopes = None
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
# https://tools.ietf.org/html/rfc6749#section-4.4.2
request.scopes = utils.scope_to_list(request.scope)
request.extra_credentials = credentials
if grant_type_for_scope:
request.grant_type = grant_type_for_scope
# OpenID Connect claims, if provided. The server using oauthlib might choose
# to implement the claims parameter of the Authorization Request. In this case
# it should retrieve those claims and pass them via the claims argument here,
# as a dict.
if claims:
request.claims = claims
grant_type_handler = self.grant_types.get(request.grant_type,
self.default_grant_type_handler)
log.debug('Dispatching grant_type %s request to %r.',
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)

View file

@ -1,4 +1,3 @@
# coding=utf-8
"""
oauthlib.oauth2.rfc6749.errors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -6,9 +5,9 @@ oauthlib.oauth2.rfc6749.errors
Error used both by OAuth 2 clients and providers to represent the spec
defined error responses for all four core grant types.
"""
from __future__ import unicode_literals
import json
from oauthlib.common import urlencode, add_params_to_uri
from oauthlib.common import add_params_to_uri, urlencode
class OAuth2Error(Exception):
@ -16,32 +15,37 @@ class OAuth2Error(Exception):
status_code = 400
description = ''
def __init__(self, description=None, uri=None, state=None, status_code=None,
request=None):
def __init__(self, description=None, uri=None, state=None,
status_code=None, request=None):
"""
description: A human-readable ASCII [USASCII] text providing
additional information, used to assist the client
developer in understanding the error that occurred.
Values for the "error_description" parameter MUST NOT
include characters outside the set
x20-21 / x23-5B / x5D-7E.
:param description: A human-readable ASCII [USASCII] text providing
additional information, used to assist the client
developer in understanding the error that occurred.
Values for the "error_description" parameter
MUST NOT include characters outside the set
x20-21 / x23-5B / x5D-7E.
uri: A URI identifying a human-readable web page with information
about the error, used to provide the client developer with
additional information about the error. Values for the
"error_uri" parameter MUST conform to the URI- Reference
syntax, and thus MUST NOT include characters outside the set
x21 / x23-5B / x5D-7E.
:param uri: A URI identifying a human-readable web page with information
about the error, used to provide the client developer with
additional information about the error. Values for the
"error_uri" parameter MUST conform to the URI- Reference
syntax, and thus MUST NOT include characters outside the set
x21 / x23-5B / x5D-7E.
state: A CSRF protection value received from the client.
:param state: A CSRF protection value received from the client.
request: Oauthlib Request object
:param status_code:
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
self.description = description or self.description
message = '(%s) %s' % (self.error, self.description)
if description is not None:
self.description = description
message = '({}) {}'.format(self.error, self.description)
if request:
message += ' ' + repr(request)
super(OAuth2Error, self).__init__(message)
super().__init__(message)
self.uri = uri
self.state = state
@ -54,12 +58,21 @@ class OAuth2Error(Exception):
self.client_id = request.client_id
self.scopes = request.scopes
self.response_type = request.response_type
self.response_mode = request.response_mode
self.grant_type = request.grant_type
if not state:
self.state = request.state
else:
self.redirect_uri = None
self.client_id = None
self.scopes = None
self.response_type = None
self.response_mode = None
self.grant_type = None
def in_uri(self, uri):
return add_params_to_uri(uri, self.twotuples)
fragment = self.response_mode == "fragment"
return add_params_to_uri(uri, self.twotuples, fragment)
@property
def twotuples(self):
@ -80,6 +93,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'
@ -108,8 +142,8 @@ class MissingTokenTypeError(OAuth2Error):
class FatalClientError(OAuth2Error):
"""Errors during authorization where user should not be redirected back.
"""
Errors during authorization where user should not be redirected back.
If the request fails due to a missing, invalid, or mismatching
redirection URI, or if the client identifier is missing or invalid,
@ -123,7 +157,8 @@ class FatalClientError(OAuth2Error):
class InvalidRequestFatalError(FatalClientError):
"""For fatal errors, the request is missing a required parameter, includes
"""
For fatal errors, the request is missing a required parameter, includes
an invalid parameter value, includes a parameter more than once, or is
otherwise malformed.
"""
@ -151,8 +186,8 @@ class MissingClientIdError(InvalidRequestFatalError):
class InvalidRequestError(OAuth2Error):
"""The request is missing a required parameter, includes an invalid
"""
The request is missing a required parameter, includes an invalid
parameter value, includes a parameter more than once, or is
otherwise malformed.
"""
@ -163,31 +198,66 @@ class MissingResponseTypeError(InvalidRequestError):
description = 'Missing response_type parameter.'
class AccessDeniedError(OAuth2Error):
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.'
"""The resource owner or authorization server denied the request."""
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):
"""The authorization server does not support obtaining an authorization
"""
The authorization server does not support obtaining an authorization
code using this method.
"""
error = 'unsupported_response_type'
class InvalidScopeError(OAuth2Error):
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.'
"""The requested scope is invalid, unknown, or malformed."""
class InvalidScopeError(OAuth2Error):
"""
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 = 401
class ServerError(OAuth2Error):
"""The authorization server encountered an unexpected condition that
"""
The authorization server encountered an unexpected condition that
prevented it from fulfilling the request. (This error code is needed
because a 500 Internal Server Error HTTP status code cannot be returned
to the client via a HTTP redirect.)
@ -196,8 +266,8 @@ class ServerError(OAuth2Error):
class TemporarilyUnavailableError(OAuth2Error):
"""The authorization server is currently unable to handle the request
"""
The authorization server is currently unable to handle the request
due to a temporary overloading or maintenance of the server.
(This error code is needed because a 503 Service Unavailable HTTP
status code cannot be returned to the client via a HTTP redirect.)
@ -205,9 +275,9 @@ class TemporarilyUnavailableError(OAuth2Error):
error = 'temporarily_unavailable'
class InvalidClientError(OAuth2Error):
"""Client authentication failed (e.g. unknown client, no client
class InvalidClientError(FatalClientError):
"""
Client authentication failed (e.g. unknown client, no client
authentication included, or unsupported authentication method).
The authorization server MAY return an HTTP 401 (Unauthorized) status
code to indicate which HTTP authentication schemes are supported.
@ -222,42 +292,103 @@ class InvalidClientError(OAuth2Error):
class InvalidGrantError(OAuth2Error):
"""The provided authorization grant (e.g. authorization code, resource
"""
The provided authorization grant (e.g. authorization code, resource
owner credentials) or refresh token is invalid, expired, revoked, does
not match the redirection URI used in the authorization request, or was
issued to another client.
https://tools.ietf.org/html/rfc6749#section-5.2
"""
error = 'invalid_grant'
status_code = 401
status_code = 400
class UnauthorizedClientError(OAuth2Error):
"""The authenticated client is not authorized to use this authorization
"""
The authenticated client is not authorized to use this authorization
grant type.
"""
error = 'unauthorized_client'
status_code = 401
class UnsupportedGrantTypeError(OAuth2Error):
"""The authorization grant type is not supported by the authorization
"""
The authorization grant type is not supported by the authorization
server.
"""
error = 'unsupported_grant_type'
class UnsupportedTokenTypeError(OAuth2Error):
"""The authorization server does not support the revocation of the
"""
The authorization server does not support the hint of the
presented token type. I.e. the client tried to revoke an access token
on a server not supporting this feature.
"""
error = 'unsupported_token_type'
class InvalidTokenError(OAuth2Error):
"""
The access token provided is expired, revoked, malformed, or
invalid for other reasons. The resource SHOULD respond with
the HTTP 401 (Unauthorized) status code. The client MAY
request a new access token and retry the protected resource
request.
"""
error = 'invalid_token'
status_code = 401
description = ("The access token provided is expired, revoked, malformed, "
"or invalid for other reasons.")
class InsufficientScopeError(OAuth2Error):
"""
The request requires higher privileges than provided by the
access token. The resource server SHOULD respond with the HTTP
403 (Forbidden) status code and MAY include the "scope"
attribute with the scope necessary to access the protected
resource.
"""
error = 'insufficient_scope'
status_code = 403
description = ("The request requires higher privileges than provided by "
"the access token.")
class ConsentRequired(OAuth2Error):
"""
The Authorization Server requires End-User consent.
This error MAY be returned when the prompt parameter value in the
Authentication Request is none, but the Authentication Request cannot be
completed without displaying a user interface for End-User consent.
"""
error = 'consent_required'
class LoginRequired(OAuth2Error):
"""
The Authorization Server requires End-User authentication.
This error MAY be returned when the prompt parameter value in the
Authentication Request is none, but the Authentication Request cannot be
completed without displaying a user interface for End-User authentication.
"""
error = 'login_required'
class CustomOAuth2Error(OAuth2Error):
"""
This error is a placeholder for all custom errors not described by the RFC.
Some of the popular OAuth2 providers are using custom errors.
"""
def __init__(self, error, *args, **kwargs):
self.error = error
super().__init__(*args, **kwargs)
def raise_from_error(error, params=None):
import inspect
import sys
@ -269,3 +400,4 @@ def raise_from_error(error, params=None):
for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
if cls.error == error:
raise cls(**kwargs)
raise CustomOAuth2Error(error=error, **kwargs)

View file

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import unicode_literals, absolute_import
from .authorization_code import AuthorizationCodeGrant
from .implicit import ImplicitGrant
from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant
from .client_credentials import ClientCredentialsGrant
from .implicit import ImplicitGrant
from .refresh_token import RefreshTokenGrant
from .resource_owner_password_credentials import (
ResourceOwnerPasswordCredentialsGrant,
)

View file

@ -1,23 +1,66 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import unicode_literals, absolute_import
import base64
import hashlib
import json
import logging
from oauthlib import common
from oauthlib.uri_validate import is_absolute_uri
from .base import GrantTypeBase
from .. import errors
from ..request_validator import RequestValidator
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`_
@ -92,15 +135,35 @@ class AuthorizationCodeGrant(GrantTypeBase):
step (C). If valid, the authorization server responds back with
an access token and, optionally, a refresh token.
.. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1
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
"""
def __init__(self, request_validator=None, refresh_token=True):
self.request_validator = request_validator or RequestValidator()
self.refresh_token = refresh_token
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."""
"""
Generates an authorization grant represented as a dictionary.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
grant = {'code': common.generate_token()}
if hasattr(request, 'state') and request.state:
grant['state'] = request.state
@ -115,7 +178,10 @@ class AuthorizationCodeGrant(GrantTypeBase):
using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
response_type
REQUIRED. Value MUST be set to "code".
REQUIRED. Value MUST be set to "code" for standard OAuth2
authorization flow. For OpenID Connect it must be one of
"code token", "code id_token", or "code token id_token" - we
essentially test that "code" appears in the response_type.
client_id
REQUIRED. The client identifier as described in `Section 2.2`_.
redirect_uri
@ -134,12 +200,12 @@ class AuthorizationCodeGrant(GrantTypeBase):
HTTP redirection response, or by other means available to it via the
user-agent.
:param request: oauthlib.commong.Request
:param token_handler: A token handler instace, for example of type
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
:returns: headers, body, status
:raises: FatalClientError on invalid redirect URI or client id.
ValueError if scopes are not set on the request object.
A few examples::
@ -150,12 +216,6 @@ class AuthorizationCodeGrant(GrantTypeBase):
>>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken
>>> token = BearerToken(your_validator)
>>> grant = AuthorizationCodeGrant(your_validator)
>>> grant.create_authorization_response(request, token)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "oauthlib/oauth2/rfc6749/grant_types.py", line 513, in create_authorization_response
raise ValueError('Scopes must be set on post auth.')
ValueError: Scopes must be set on post auth.
>>> request.scopes = ['authorized', 'in', 'some', 'form']
>>> grant.create_authorization_response(request, token)
(u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400)
@ -174,18 +234,13 @@ class AuthorizationCodeGrant(GrantTypeBase):
File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request
oauthlib.oauth2.rfc6749.errors.InvalidClientIdError
.. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
"""
try:
# request.scopes is only mandated in post auth and both pre and
# post auth use validate_authorization_request
if not request.scopes:
raise ValueError('Scopes must be set on post auth.')
self.validate_authorization_request(request)
log.debug('Pre resource owner authorization validation ok for %r.',
request)
@ -205,17 +260,23 @@ class AuthorizationCodeGrant(GrantTypeBase):
# the authorization server informs the client by adding the following
# parameters to the query component of the redirection URI using the
# "application/x-www-form-urlencoded" format, per Appendix B:
# http://tools.ietf.org/html/rfc6749#appendix-B
# https://tools.ietf.org/html/rfc6749#appendix-B
except errors.OAuth2Error as e:
log.debug('Client error during validation of %r. %r.', request, e)
request.redirect_uri = request.redirect_uri or self.error_uri
return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples)}, None, 302
redirect_uri = common.add_params_to_uri(
request.redirect_uri, e.twotuples,
fragment=request.response_mode == "fragment")
return {'Location': redirect_uri}, None, 302
grant = self.create_authorization_code(request)
for modifier in self._code_modifiers:
grant = modifier(grant, token_handler, request)
log.debug('Saving grant %r for %r.', grant, request)
self.request_validator.save_authorization_code(
request.client_id, grant, request)
return {'Location': common.add_params_to_uri(request.redirect_uri, grant.items())}, None, 302
return self.prepare_authorization_response(
request, grant, {}, None, 302)
def create_token_response(self, request, token_handler):
"""Validate the authorization code.
@ -225,20 +286,28 @@ class AuthorizationCodeGrant(GrantTypeBase):
MUST deny the request and SHOULD revoke (when possible) all tokens
previously issued based on that authorization code. The authorization
code is bound to the client identifier and redirection URI.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
"""
headers = {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'Pragma': 'no-cache',
}
headers = self._get_default_headers()
try:
self.validate_token_request(request)
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)
for modifier in self._token_modifiers:
token = modifier(token, token_handler, request)
self.request_validator.save_token(token, request)
self.request_validator.invalidate_authorization_code(
request.client_id, request.code, request)
return headers, json.dumps(token), 200
@ -255,6 +324,9 @@ class AuthorizationCodeGrant(GrantTypeBase):
missing. These must be caught by the provider and handled, how this
is done is outside of the scope of OAuthLib but showing an error
page describing the issue is a good idea.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
# First check for fatal errors
@ -275,7 +347,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
# REQUIRED. The client identifier as described in Section 2.2.
# http://tools.ietf.org/html/rfc6749#section-2.2
# https://tools.ietf.org/html/rfc6749#section-2.2
if not request.client_id:
raise errors.MissingClientIdError(request=request)
@ -283,25 +355,13 @@ class AuthorizationCodeGrant(GrantTypeBase):
raise errors.InvalidClientIdError(request=request)
# OPTIONAL. As described in Section 3.1.2.
# http://tools.ietf.org/html/rfc6749#section-3.1.2
# https://tools.ietf.org/html/rfc6749#section-3.1.2
log.debug('Validating redirection uri %s for client %s.',
request.redirect_uri, request.client_id)
if request.redirect_uri is not None:
request.using_default_redirect_uri = False
log.debug('Using provided redirect_uri %s', request.redirect_uri)
if not is_absolute_uri(request.redirect_uri):
raise errors.InvalidRedirectURIError(request=request)
if not self.request_validator.validate_redirect_uri(
request.client_id, request.redirect_uri, request):
raise errors.MismatchingRedirectURIError(request=request)
else:
request.redirect_uri = self.request_validator.get_default_redirect_uri(
request.client_id, request)
request.using_default_redirect_uri = True
log.debug('Using default redirect_uri %s.', request.redirect_uri)
if not request.redirect_uri:
raise errors.MissingRedirectURIError(request=request)
# OPTIONAL. As described in Section 3.1.2.
# https://tools.ietf.org/html/rfc6749#section-3.1.2
self._handle_redirects(request)
# Then check for normal errors.
@ -310,16 +370,21 @@ class AuthorizationCodeGrant(GrantTypeBase):
# the authorization server informs the client by adding the following
# parameters to the query component of the redirection URI using the
# "application/x-www-form-urlencoded" format, per Appendix B.
# http://tools.ietf.org/html/rfc6749#appendix-B
# https://tools.ietf.org/html/rfc6749#appendix-B
# Note that the correct parameters to be added are automatically
# populated through the use of specific exceptions.
request_info = {}
for validator in self.custom_validators.pre_auth:
request_info.update(validator(request))
# REQUIRED.
if request.response_type is None:
raise errors.MissingResponseTypeError(request=request)
# Value MUST be set to "code".
elif request.response_type != 'code':
# Value MUST be set to "code" or one of the OpenID authorization code including
# response_types "code token", "code id_token", "code token id_token"
elif not 'code' in request.response_type and request.response_type != 'none':
raise errors.UnsupportedResponseTypeError(request=request)
if not self.request_validator.validate_response_type(request.client_id,
@ -330,23 +395,52 @@ 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:
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
# http://tools.ietf.org/html/rfc6749#section-3.3
# https://tools.ietf.org/html/rfc6749#section-3.3
self.validate_scopes(request)
return request.scopes, {
request_info.update({
'client_id': request.client_id,
'redirect_uri': request.redirect_uri,
'response_type': request.response_type,
'state': request.state,
'request': request,
}
'request': request
})
for validator in self.custom_validators.post_auth:
request_info.update(validator(request))
return request.scopes, request_info
def validate_token_request(self, request):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
# REQUIRED. Value MUST be set to "authorization_code".
if request.grant_type != 'authorization_code':
if request.grant_type not in ('authorization_code', 'openid'):
raise errors.UnsupportedGrantTypeError(request=request)
for validator in self.custom_validators.pre_token:
validator(request)
if request.code is None:
raise errors.InvalidRequestError(
description='Missing code parameter.', request=request)
@ -361,14 +455,14 @@ class AuthorizationCodeGrant(GrantTypeBase):
# credentials (or assigned other authentication requirements), the
# client MUST authenticate with the authorization server as described
# in Section 3.2.1.
# http://tools.ietf.org/html/rfc6749#section-3.2.1
# https://tools.ietf.org/html/rfc6749#section-3.2.1
if not self.request_validator.authenticate_client(request):
log.debug('Client authentication failed, %r.', request)
raise errors.InvalidClientError(request=request)
elif not self.request_validator.authenticate_client_id(request.client_id, request):
# REQUIRED, if the client is not authenticating with the
# authorization server as described in Section 3.2.1.
# http://tools.ietf.org/html/rfc6749#section-3.2.1
# https://tools.ietf.org/html/rfc6749#section-3.2.1
log.debug('Client authentication failed, %r.', request)
raise errors.InvalidClientError(request=request)
@ -377,6 +471,8 @@ class AuthorizationCodeGrant(GrantTypeBase):
'request.client.client_id attribute '
'in authenticate_client.')
request.client_id = request.client_id or request.client.client_id
# Ensure client is authorized use of this grant type
self.validate_grant_type(request)
@ -388,6 +484,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)
@ -395,8 +518,28 @@ class AuthorizationCodeGrant(GrantTypeBase):
# REQUIRED, if the "redirect_uri" parameter was included in the
# authorization request as described in Section 4.1.1, and their
# values MUST be identical.
if request.redirect_uri is None:
request.using_default_redirect_uri = True
request.redirect_uri = self.request_validator.get_default_redirect_uri(
request.client_id, request)
log.debug('Using default redirect_uri %s.', request.redirect_uri)
if not request.redirect_uri:
raise errors.MissingRedirectURIError(request=request)
else:
request.using_default_redirect_uri = False
log.debug('Using provided redirect_uri %s', request.redirect_uri)
if not self.request_validator.confirm_redirect_uri(request.client_id, request.code,
request.redirect_uri, request.client):
request.redirect_uri, request.client,
request):
log.debug('Redirect_uri (%r) invalid for client %r (%r).',
request.redirect_uri, request.client_id, request.client)
raise errors.AccessDeniedError(request=request)
raise errors.MismatchingRedirectURIError(request=request)
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)

View file

@ -1,28 +1,158 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import unicode_literals, absolute_import
import logging
from itertools import chain
from oauthlib.common import add_params_to_uri
from oauthlib.oauth2.rfc6749 import errors, utils
from oauthlib.uri_validate import is_absolute_uri
from ..request_validator import RequestValidator
log = logging.getLogger(__name__)
class GrantTypeBase(object):
class ValidatorsContainer:
"""
Container object for holding custom validator callables to be invoked
as part of the grant type `validate_authorization_request()` or
`validate_authorization_request()` methods on the various grant types.
Authorization validators must be callables that take a request object and
return a dict, which may contain items to be added to the `request_info`
returned from the grant_type after validation.
Token validators must be callables that take a request object and
return None.
Both authorization validators and token validators may raise OAuth2
exceptions if validation conditions fail.
Authorization validators added to `pre_auth` will be run BEFORE
the standard validations (but after the critical ones that raise
fatal errors) as part of `validate_authorization_request()`
Authorization validators added to `post_auth` will be run AFTER
the standard validations as part of `validate_authorization_request()`
Token validators added to `pre_token` will be run BEFORE
the standard validations as part of `validate_token_request()`
Token validators added to `post_token` will be run AFTER
the standard validations as part of `validate_token_request()`
For example:
>>> def my_auth_validator(request):
... return {'myval': True}
>>> auth_code_grant = AuthorizationCodeGrant(request_validator)
>>> auth_code_grant.custom_validators.pre_auth.append(my_auth_validator)
>>> def my_token_validator(request):
... if not request.everything_okay:
... raise errors.OAuth2Error("uh-oh")
>>> auth_code_grant.custom_validators.post_token.append(my_token_validator)
"""
def __init__(self, post_auth, post_token,
pre_auth, pre_token):
self.pre_auth = pre_auth
self.post_auth = post_auth
self.pre_token = pre_token
self.post_token = post_token
@property
def all_pre(self):
return chain(self.pre_auth, self.pre_token)
@property
def all_post(self):
return chain(self.post_auth, self.post_token)
class GrantTypeBase:
error_uri = None
request_validator = None
default_response_mode = 'fragment'
refresh_token = True
response_types = ['code']
def __init__(self, request_validator=None, **kwargs):
self.request_validator = request_validator or RequestValidator()
# Transforms class variables into instance variables:
self.response_types = self.response_types
self.refresh_token = self.refresh_token
self._setup_custom_validators(kwargs)
self._code_modifiers = []
self._token_modifiers = []
for kw, val in kwargs.items():
setattr(self, kw, val)
def _setup_custom_validators(self, kwargs):
post_auth = kwargs.get('post_auth', [])
post_token = kwargs.get('post_token', [])
pre_auth = kwargs.get('pre_auth', [])
pre_token = kwargs.get('pre_token', [])
if not hasattr(self, 'validate_authorization_request'):
if post_auth or pre_auth:
msg = ("{} does not support authorization validators. Use "
"token validators instead.").format(self.__class__.__name__)
raise ValueError(msg)
# Using tuples here because they can't be appended to:
post_auth, pre_auth = (), ()
self.custom_validators = ValidatorsContainer(post_auth, post_token,
pre_auth, pre_token)
def register_response_type(self, response_type):
self.response_types.append(response_type)
def register_code_modifier(self, modifier):
self._code_modifiers.append(modifier)
def register_token_modifier(self, modifier):
self._token_modifiers.append(modifier)
def create_authorization_response(self, request, token_handler):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
"""
raise NotImplementedError('Subclasses must implement this method.')
def create_token_response(self, request, token_handler):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
"""
raise NotImplementedError('Subclasses must implement this method.')
def add_token(self, token, token_handler, request):
"""
:param token:
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
# Only add a hybrid access token on auth step if asked for
if not request.response_type in ["token", "code token", "id_token token", "code id_token token"]:
return token
token.update(token_handler.create_token(request, refresh_token=False))
return token
def validate_grant_type(self, request):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
client_id = getattr(request, 'client_id', None)
if not self.request_validator.validate_grant_type(client_id,
request.grant_type, request.client, request):
@ -31,6 +161,10 @@ class GrantTypeBase(object):
raise errors.UnauthorizedClientError(request=request)
def validate_scopes(self, request):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
if not request.scopes:
request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list(
self.request_validator.get_default_scopes(request.client_id, request))
@ -39,3 +173,78 @@ class GrantTypeBase(object):
if not self.request_validator.validate_scopes(request.client_id,
request.scopes, request.client, request):
raise errors.InvalidScopeError(request=request)
def prepare_authorization_response(self, request, token, headers, body, status):
"""Place token according to response mode.
Base classes can define a default response mode for their authorization
response by overriding the static `default_response_mode` member.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token:
:param headers:
:param body:
:param status:
"""
request.response_mode = request.response_mode or self.default_response_mode
if request.response_mode not in ('query', 'fragment'):
log.debug('Overriding invalid response mode %s with %s',
request.response_mode, self.default_response_mode)
request.response_mode = self.default_response_mode
token_items = token.items()
if request.response_type == 'none':
state = token.get('state', None)
if state:
token_items = [('state', state)]
else:
token_items = []
if request.response_mode == 'query':
headers['Location'] = add_params_to_uri(
request.redirect_uri, token_items, fragment=False)
return headers, body, status
if request.response_mode == 'fragment':
headers['Location'] = add_params_to_uri(
request.redirect_uri, token_items, fragment=True)
return headers, body, status
raise NotImplementedError(
'Subclasses must set a valid default_response_mode')
def _get_default_headers(self):
"""Create default headers for grant responses."""
return {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'Pragma': 'no-cache',
}
def _handle_redirects(self, request):
if request.redirect_uri is not None:
request.using_default_redirect_uri = False
log.debug('Using provided redirect_uri %s', request.redirect_uri)
if not is_absolute_uri(request.redirect_uri):
raise errors.InvalidRedirectURIError(request=request)
# The authorization server MUST verify that the redirection URI
# to which it will redirect the access token matches a
# redirection URI registered by the client as described in
# Section 3.1.2.
# https://tools.ietf.org/html/rfc6749#section-3.1.2
if not self.request_validator.validate_redirect_uri(
request.client_id, request.redirect_uri, request):
raise errors.MismatchingRedirectURIError(request=request)
else:
request.redirect_uri = self.request_validator.get_default_redirect_uri(
request.client_id, request)
request.using_default_redirect_uri = True
log.debug('Using default redirect_uri %s.', request.redirect_uri)
if not request.redirect_uri:
raise errors.MissingRedirectURIError(request=request)
if not is_absolute_uri(request.redirect_uri):
raise errors.InvalidRedirectURIError(request=request)

View file

@ -1,16 +1,12 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import unicode_literals, absolute_import
import json
import logging
from .base import GrantTypeBase
from .. import errors
from ..request_validator import RequestValidator
from .base import GrantTypeBase
log = logging.getLogger(__name__)
@ -47,42 +43,54 @@ class ClientCredentialsGrant(GrantTypeBase):
(B) The authorization server authenticates the client, and if valid,
issues an access token.
.. _`Client Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.4
.. _`Client Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.4
"""
def __init__(self, request_validator=None):
self.request_validator = request_validator or RequestValidator()
def create_token_response(self, request, token_handler):
"""Return token or error in JSON format.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
If the access token request is valid and authorized, the
authorization server issues an access token as described in
`Section 5.1`_. A refresh token SHOULD NOT be included. If the request
failed client authentication or is invalid, the authorization server
returns an error response as described in `Section 5.2`_.
.. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
.. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
"""
headers = {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'Pragma': 'no-cache',
}
headers = self._get_default_headers()
try:
log.debug('Validating access 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, refresh_token=False)
for modifier in self._token_modifiers:
token = modifier(token)
self.request_validator.save_token(token, request)
log.debug('Issuing token to client id %r (%r), %r.',
request.client_id, request.client, token)
return headers, json.dumps(token), 200
def validate_token_request(self, request):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
for validator in self.custom_validators.pre_token:
validator(request)
if not getattr(request, 'grant_type', None):
raise errors.InvalidRequestError('Request is missing grant type.',
request=request)
@ -107,6 +115,9 @@ class ClientCredentialsGrant(GrantTypeBase):
# Ensure client is authorized use of this grant type
self.validate_grant_type(request)
log.debug('Authorizing access to user %r.', request.user)
request.client_id = request.client_id or request.client.client_id
log.debug('Authorizing access to client %r.', request.client_id)
self.validate_scopes(request)
for validator in self.custom_validators.post_token:
validator(request)

View file

@ -1,18 +1,13 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import unicode_literals, absolute_import
import logging
from oauthlib import common
from oauthlib.uri_validate import is_absolute_uri
from .base import GrantTypeBase
from .. import errors
from ..request_validator import RequestValidator
from .base import GrantTypeBase
log = logging.getLogger(__name__)
@ -112,22 +107,29 @@ class ImplicitGrant(GrantTypeBase):
See `Section 10.3`_ and `Section 10.16`_ for important security considerations
when using the implicit grant.
.. _`Implicit Grant`: http://tools.ietf.org/html/rfc6749#section-4.2
.. _`Section 10.3`: http://tools.ietf.org/html/rfc6749#section-10.3
.. _`Section 10.16`: http://tools.ietf.org/html/rfc6749#section-10.16
.. _`Implicit Grant`: https://tools.ietf.org/html/rfc6749#section-4.2
.. _`Section 10.3`: https://tools.ietf.org/html/rfc6749#section-10.3
.. _`Section 10.16`: https://tools.ietf.org/html/rfc6749#section-10.16
"""
def __init__(self, request_validator=None):
self.request_validator = request_validator or RequestValidator()
response_types = ['token']
grant_allows_refresh_token = False
def create_authorization_response(self, request, token_handler):
"""Create an authorization response.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
The client constructs the request URI by adding the following
parameters to the query component of the authorization endpoint URI
using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
response_type
REQUIRED. Value MUST be set to "token".
REQUIRED. Value MUST be set to "token" for standard OAuth2 implicit flow
or "id_token token" or just "id_token" for OIDC implicit flow
client_id
REQUIRED. The client identifier as described in `Section 2.2`_.
@ -152,17 +154,22 @@ class ImplicitGrant(GrantTypeBase):
access token matches a redirection URI registered by the client as
described in `Section 3.1.2`_.
.. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
.. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
"""
return self.create_token_response(request, token_handler)
def create_token_response(self, request, token_handler):
"""Return token or error embedded in the URI fragment.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
If the resource owner grants the access request, the authorization
server issues an access token and delivers it to the client by adding
the following parameters to the fragment component of the redirection
@ -195,16 +202,11 @@ class ImplicitGrant(GrantTypeBase):
The authorization server MUST NOT issue a refresh token.
.. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
"""
try:
# request.scopes is only mandated in post auth and both pre and
# post auth use validate_authorization_request
if not request.scopes:
raise ValueError('Scopes must be set on post auth.')
self.validate_token_request(request)
# If the request fails due to a missing, invalid, or mismatching
@ -222,22 +224,47 @@ class ImplicitGrant(GrantTypeBase):
# the authorization server informs the client by adding the following
# parameters to the fragment component of the redirection URI using the
# "application/x-www-form-urlencoded" format, per Appendix B:
# http://tools.ietf.org/html/rfc6749#appendix-B
# https://tools.ietf.org/html/rfc6749#appendix-B
except errors.OAuth2Error as e:
log.debug('Client error during validation of %r. %r.', request, e)
return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples,
fragment=True)}, None, 302
token = token_handler.create_token(request, refresh_token=False)
return {'Location': common.add_params_to_uri(request.redirect_uri, token.items(),
fragment=True)}, None, 302
# In OIDC implicit flow it is possible to have a request_type that does not include the access_token!
# "id_token token" - return the access token and the id token
# "id_token" - don't return the access token
if "token" in request.response_type.split():
token = token_handler.create_token(request, refresh_token=False)
else:
token = {}
if request.state is not None:
token['state'] = request.state
for modifier in self._token_modifiers:
token = modifier(token, token_handler, request)
# In OIDC implicit flow it is possible to have a request_type that does
# not include the access_token! In this case there is no need to save a token.
if "token" in request.response_type.split():
self.request_validator.save_token(token, request)
return self.prepare_authorization_response(
request, token, {}, None, 302)
def validate_authorization_request(self, request):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
return self.validate_token_request(request)
def validate_token_request(self, request):
"""Check the token request for normal and fatal errors.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
This method is very similar to validate_authorization_request in
the AuthorizationCodeGrant but differ in a few subtle areas.
@ -270,7 +297,7 @@ class ImplicitGrant(GrantTypeBase):
raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
# REQUIRED. The client identifier as described in Section 2.2.
# http://tools.ietf.org/html/rfc6749#section-2.2
# https://tools.ietf.org/html/rfc6749#section-2.2
if not request.client_id:
raise errors.MissingClientIdError(request=request)
@ -278,39 +305,20 @@ class ImplicitGrant(GrantTypeBase):
raise errors.InvalidClientIdError(request=request)
# OPTIONAL. As described in Section 3.1.2.
# http://tools.ietf.org/html/rfc6749#section-3.1.2
if request.redirect_uri is not None:
request.using_default_redirect_uri = False
log.debug('Using provided redirect_uri %s', request.redirect_uri)
if not is_absolute_uri(request.redirect_uri):
raise errors.InvalidRedirectURIError(request=request)
# The authorization server MUST verify that the redirection URI
# to which it will redirect the access token matches a
# redirection URI registered by the client as described in
# Section 3.1.2.
# http://tools.ietf.org/html/rfc6749#section-3.1.2
if not self.request_validator.validate_redirect_uri(
request.client_id, request.redirect_uri, request):
raise errors.MismatchingRedirectURIError(request=request)
else:
request.redirect_uri = self.request_validator.get_default_redirect_uri(
request.client_id, request)
request.using_default_redirect_uri = True
log.debug('Using default redirect_uri %s.', request.redirect_uri)
if not request.redirect_uri:
raise errors.MissingRedirectURIError(request=request)
if not is_absolute_uri(request.redirect_uri):
raise errors.InvalidRedirectURIError(request=request)
# https://tools.ietf.org/html/rfc6749#section-3.1.2
self._handle_redirects(request)
# Then check for normal errors.
request_info = self._run_custom_validators(request,
self.custom_validators.all_pre)
# If the resource owner denies the access request or if the request
# fails for reasons other than a missing or invalid redirection URI,
# the authorization server informs the client by adding the following
# parameters to the fragment component of the redirection URI using the
# "application/x-www-form-urlencoded" format, per Appendix B.
# http://tools.ietf.org/html/rfc6749#appendix-B
# https://tools.ietf.org/html/rfc6749#appendix-B
# Note that the correct parameters to be added are automatically
# populated through the use of specific exceptions
@ -318,8 +326,8 @@ class ImplicitGrant(GrantTypeBase):
# REQUIRED.
if request.response_type is None:
raise errors.MissingResponseTypeError(request=request)
# Value MUST be set to "token".
elif request.response_type != 'token':
# Value MUST be one of our registered types: "token" by default or if using OIDC "id_token" or "id_token token"
elif not set(request.response_type.split()).issubset(self.response_types):
raise errors.UnsupportedResponseTypeError(request=request)
log.debug('Validating use of response_type token for client %r (%r).',
@ -333,13 +341,36 @@ class ImplicitGrant(GrantTypeBase):
raise errors.UnauthorizedClientError(request=request)
# OPTIONAL. The scope of the access request as described by Section 3.3
# http://tools.ietf.org/html/rfc6749#section-3.3
# https://tools.ietf.org/html/rfc6749#section-3.3
self.validate_scopes(request)
return request.scopes, {
request_info.update({
'client_id': request.client_id,
'redirect_uri': request.redirect_uri,
'response_type': request.response_type,
'state': request.state,
'request': request,
}
})
request_info = self._run_custom_validators(
request,
self.custom_validators.all_post,
request_info
)
return request.scopes, request_info
def _run_custom_validators(self,
request,
validations,
request_info=None):
# Make a copy so we don't modify the existing request_info dict
request_info = {} if request_info is None else request_info.copy()
# For implicit grant, auth_validators and token_validators are
# basically equivalent since the token is returned from the
# authorization endpoint.
for validator in validations:
result = validator(request)
if result is not None:
request_info.update(result)
return request_info

View file

@ -1,16 +1,12 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import unicode_literals, absolute_import
import json
import logging
from .base import GrantTypeBase
from .. import errors, utils
from ..request_validator import RequestValidator
from .base import GrantTypeBase
log = logging.getLogger(__name__)
@ -19,16 +15,25 @@ class RefreshTokenGrant(GrantTypeBase):
"""`Refresh token grant`_
.. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6
.. _`Refresh token grant`: https://tools.ietf.org/html/rfc6749#section-6
"""
def __init__(self, request_validator=None, issue_new_refresh_tokens=True):
self.request_validator = request_validator or RequestValidator()
self.issue_new_refresh_tokens = issue_new_refresh_tokens
def __init__(self, request_validator=None,
issue_new_refresh_tokens=True,
**kwargs):
super().__init__(
request_validator,
issue_new_refresh_tokens=issue_new_refresh_tokens,
**kwargs)
def create_token_response(self, request, token_handler):
"""Create a new access token from a refresh_token.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
If valid and authorized, the authorization server issues an access
token as described in `Section 5.1`_. If the request failed
verification or is invalid, the authorization server returns an error
@ -42,31 +47,42 @@ class RefreshTokenGrant(GrantTypeBase):
identical to that of the refresh token included by the client in the
request.
.. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
.. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
"""
headers = {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'Pragma': 'no-cache',
}
headers = self._get_default_headers()
try:
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,
refresh_token=self.issue_new_refresh_tokens)
for modifier in self._token_modifiers:
token = modifier(token)
self.request_validator.save_token(token, request)
log.debug('Issuing new token to client id %r (%r), %r.',
request.client_id, request.client, token)
return headers, json.dumps(token), 200
def validate_token_request(self, request):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
# REQUIRED. Value MUST be set to "refresh_token".
if request.grant_type != 'refresh_token':
raise errors.UnsupportedGrantTypeError(request=request)
for validator in self.custom_validators.pre_token:
validator(request)
if request.refresh_token is None:
raise errors.InvalidRequestError(
description='Missing refresh token parameter.',
@ -78,7 +94,7 @@ class RefreshTokenGrant(GrantTypeBase):
# the client was issued client credentials (or assigned other
# authentication requirements), the client MUST authenticate with the
# authorization server as described in Section 3.2.1.
# http://tools.ietf.org/html/rfc6749#section-3.2.1
# https://tools.ietf.org/html/rfc6749#section-3.2.1
if self.request_validator.client_authentication_required(request):
log.debug('Authenticating client, %r.', request)
if not self.request_validator.authenticate_client(request):
@ -106,7 +122,7 @@ class RefreshTokenGrant(GrantTypeBase):
if request.scope:
request.scopes = utils.scope_to_list(request.scope)
if (not all((s in original_scopes for s in request.scopes))
if (not all(s in original_scopes for s in request.scopes)
and not self.request_validator.is_within_original_scope(
request.scopes, request.refresh_token, request)):
log.debug('Refresh token %s lack requested scopes, %r.',
@ -114,3 +130,6 @@ class RefreshTokenGrant(GrantTypeBase):
raise errors.InvalidScopeError(request=request)
else:
request.scopes = original_scopes
for validator in self.custom_validators.post_token:
validator(request)

View file

@ -1,16 +1,12 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import unicode_literals, absolute_import
import json
import logging
from .base import GrantTypeBase
from .. import errors
from ..request_validator import RequestValidator
from .base import GrantTypeBase
log = logging.getLogger(__name__)
@ -67,34 +63,27 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
the resource owner credentials, and if valid, issues an access
token.
.. _`Resource Owner Password Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.3
.. _`Resource Owner Password Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.3
"""
def __init__(self, request_validator=None, refresh_token=True):
"""
If the refresh_token keyword argument is False, do not return
a refresh token in the response.
"""
self.request_validator = request_validator or RequestValidator()
self.refresh_token = refresh_token
def create_token_response(self, request, token_handler):
"""Return token or error in json format.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
If the access token request is valid and authorized, the
authorization server issues an access token and optional refresh
token as described in `Section 5.1`_. If the request failed client
authentication or is invalid, the authorization server returns an
error response as described in `Section 5.2`_.
.. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
.. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
"""
headers = {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'Pragma': 'no-cache',
}
headers = self._get_default_headers()
try:
if self.request_validator.client_authentication_required(request):
log.debug('Authenticating client, %r.', request)
@ -108,15 +97,25 @@ 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)
for modifier in self._token_modifiers:
token = modifier(token)
self.request_validator.save_token(token, request)
log.debug('Issuing token %r to client id %r (%r) and username %s.',
token, request.client_id, request.client, request.username)
return headers, json.dumps(token), 200
def validate_token_request(self, request):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
The client makes a request to the token endpoint by adding the
following parameters using the "application/x-www-form-urlencoded"
format per Appendix B with a character encoding of UTF-8 in the HTTP
@ -156,9 +155,12 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
brute force attacks (e.g., using rate-limitation or generating
alerts).
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
for validator in self.custom_validators.pre_token:
validator(request)
for param in ('grant_type', 'username', 'password'):
if not getattr(request, param, None):
raise errors.InvalidRequestError(
@ -192,3 +194,6 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
if request.client:
request.client_id = request.client_id or request.client.client_id
self.validate_scopes(request)
for validator in self.custom_validators.post_token:
validator(request)

View file

@ -1,28 +1,25 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains methods related to `Section 4`_ of the OAuth 2 RFC.
.. _`Section 4`: http://tools.ietf.org/html/rfc6749#section-4
.. _`Section 4`: https://tools.ietf.org/html/rfc6749#section-4
"""
from __future__ import absolute_import, unicode_literals
import json
import os
import time
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type
import urllib.parse as urlparse
from oauthlib.common import add_params_to_qs, add_params_to_uri
from oauthlib.signals import scope_changed
from .errors import raise_from_error, MissingTokenError, MissingTokenTypeError
from .errors import MismatchingStateError, MissingCodeError
from .errors import InsecureTransportError
from .errors import (
InsecureTransportError, MismatchingStateError, MissingCodeError,
MissingTokenError, MissingTokenTypeError, raise_from_error,
)
from .tokens import OAuth2Token
from .utils import list_to_scope, scope_to_list, is_secure_transport
from .utils import is_secure_transport, list_to_scope, scope_to_list
def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
@ -34,14 +31,14 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
using the ``application/x-www-form-urlencoded`` format as defined by
[`W3C.REC-html401-19991224`_]:
:param uri:
:param client_id: The client identifier as described in `Section 2.2`_.
:param response_type: To indicate which OAuth 2 grant/flow is required,
"code" and "token".
:param client_id: The client identifier as described in `Section 2.2`_.
:param redirect_uri: The client provided URI to redirect back to after
authorization as described in `Section 3.1.2`_.
:param scope: The scope of the access request as described by
`Section 3.3`_.
:param state: An opaque value used by the client to maintain
state between the request and callback. The authorization
server includes this value when redirecting the user-agent
@ -58,11 +55,11 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
.. _`W3C.REC-html401-19991224`: http://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224
.. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
.. _`W3C.REC-html401-19991224`: https://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224
.. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
.. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
"""
if not is_secure_transport(uri):
raise InsecureTransportError()
@ -79,12 +76,12 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
for k in kwargs:
if kwargs[k]:
params.append((unicode_type(k), kwargs[k]))
params.append((str(k), kwargs[k]))
return add_params_to_uri(uri, params)
def prepare_token_request(grant_type, body='', **kwargs):
def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs):
"""Prepare the access token request.
The client makes a request to the token endpoint by adding the
@ -92,15 +89,39 @@ def prepare_token_request(grant_type, body='', **kwargs):
format in the HTTP request entity-body:
:param grant_type: To indicate grant type being used, i.e. "password",
"authorization_code" or "client_credentials".
:param body: Existing request body to embed parameters in.
:param code: If using authorization code grant, pass the previously
obtained authorization code as the ``code`` argument.
"authorization_code" or "client_credentials".
:param body: Existing request body (URL encoded string) to embed parameters
into. This may contain extra parameters. Default ''.
:param include_client_id: `True` (default) 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`_.
:type include_client_id: Boolean
:param client_id: Unicode client identifier. Will only appear if
`include_client_id` is True. *
:param client_secret: Unicode client secret. Will only appear if set to a
value that is not `None`. Invoking this function with
an empty string will send an empty `client_secret`
value to the server. *
:param code: If using authorization_code grant, pass the previously
obtained authorization code as the ``code`` argument. *
:param redirect_uri: If the "redirect_uri" parameter was included in the
authorization request as described in
`Section 4.1.1`_, and their values MUST be identical.
`Section 4.1.1`_, and their values MUST be identical. *
:param kwargs: Extra arguments to embed in the request body.
Parameters marked with a `*` above are not explicit arguments in the
function signature, but are specially documented arguments for items
appearing in the generic `**kwargs` keyworded input.
An example of an authorization code token request body:
.. code-block:: http
@ -108,16 +129,29 @@ def prepare_token_request(grant_type, body='', **kwargs):
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
.. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1
.. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
"""
params = [('grant_type', grant_type)]
if 'scope' in kwargs:
kwargs['scope'] = list_to_scope(kwargs['scope'])
# pull the `client_id` out of the kwargs.
client_id = kwargs.pop('client_id', None)
if include_client_id:
if client_id is not None:
params.append(('client_id', client_id))
# 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)
if client_secret is not None:
params.append(('client_secret', client_secret))
# this handles: `code`, `redirect_uri`, and other undocumented params
for k in kwargs:
if kwargs[k]:
params.append((unicode_type(k), kwargs[k]))
params.append((str(k), kwargs[k]))
return add_params_to_qs(body, params)
@ -127,18 +161,22 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token",
"""Prepare a token revocation request.
The client constructs the request by including the following parameters
using the "application/x-www-form-urlencoded" format in the HTTP request
using the ``application/x-www-form-urlencoded`` format in the HTTP request
entity-body:
token REQUIRED. The token that the client wants to get revoked.
:param token: REQUIRED. The token that the client wants to get revoked.
token_type_hint OPTIONAL. A hint about the type of the token submitted
for revocation. Clients MAY pass this parameter in order to help the
authorization server to optimize the token lookup. If the server is unable
to locate the token using the given hint, it MUST extend its search across
all of its supported token types. An authorization server MAY ignore this
parameter, particularly if it is able to detect the token type
automatically. This specification defines two such values:
:param token_type_hint: OPTIONAL. A hint about the type of the token
submitted for revocation. Clients MAY pass this
parameter in order to help the authorization server
to optimize the token lookup. If the server is
unable to locate the token using the given hint, it
MUST extend its search across all of its supported
token types. An authorization server MAY ignore
this parameter, particularly if it is able to detect
the token type automatically.
This specification defines two values for `token_type_hint`:
* access_token: An access token as defined in [RFC6749],
`Section 1.4`_
@ -150,9 +188,9 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token",
specification MAY define other values for this parameter using the
registry defined in `Section 4.1.2`_.
.. _`Section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
.. _`Section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
.. _`Section 4.1.2`: http://tools.ietf.org/html/rfc7009#section-4.1.2
.. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4
.. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5
.. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2
"""
if not is_secure_transport(url):
@ -165,7 +203,7 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token",
for k in kwargs:
if kwargs[k]:
params.append((unicode_type(k), kwargs[k]))
params.append((str(k), kwargs[k]))
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
@ -220,12 +258,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
@ -261,6 +302,10 @@ def parse_implicit_response(uri, state=None, scope=None):
authorization request. The exact value received from the
client.
:param uri:
:param state:
:param scope:
Similar to the authorization code response, but with a full token provided
in the URL fragment:
@ -276,6 +321,10 @@ def parse_implicit_response(uri, state=None, scope=None):
fragment = urlparse.urlparse(uri).fragment
params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
for key in ('expires_in',):
if key in params: # cast things to int
params[key] = int(params[key])
if 'scope' in params:
params['scope'] = scope_to_list(params['scope'])
@ -345,10 +394,10 @@ def parse_token_response(body, scope=None):
"example_parameter":"example_value"
}
.. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
.. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
.. _`RFC4627`: http://tools.ietf.org/html/rfc4627
.. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
.. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
.. _`RFC4627`: https://tools.ietf.org/html/rfc4627
"""
try:
params = json.loads(body)
@ -356,21 +405,21 @@ def parse_token_response(body, scope=None):
# Fall back to URL-encoded string, to support old implementations,
# including (at time of writing) Facebook. See:
# https://github.com/idan/oauthlib/issues/267
# https://github.com/oauthlib/oauthlib/issues/267
params = dict(urlparse.parse_qsl(body))
for key in ('expires_in', 'expires'):
if key in params: # cast a couple things to int
for key in ('expires_in',):
if key in params: # cast things to int
params[key] = int(params[key])
if 'scope' in params:
params['scope'] = scope_to_list(params['scope'])
if 'expires' in params:
params['expires_in'] = params.pop('expires')
if 'expires_in' in params:
params['expires_at'] = time.time() + int(params['expires_in'])
if params['expires_in'] is None:
params.pop('expires_in')
else:
params['expires_at'] = time.time() + int(params['expires_in'])
params = OAuth2Token(params, old_scope=scope)
validate_token_parameters(params)
@ -378,7 +427,7 @@ def parse_token_response(body, scope=None):
def validate_token_parameters(params):
"""Ensures token precence, token type, expiration and scope in params."""
"""Ensures token presence, token type, expiration and scope in params."""
if 'error' in params:
raise_from_error(params.get('error'), params)
@ -392,7 +441,7 @@ def validate_token_parameters(params):
# If the issued access token scope is different from the one requested by
# the client, the authorization server MUST include the "scope" response
# parameter to inform the client of the actual scope granted.
# http://tools.ietf.org/html/rfc6749#section-3.3
# https://tools.ietf.org/html/rfc6749#section-3.3
if params.scope_changed:
message = 'Scope has changed from "{old}" to "{new}".'.format(
old=params.old_scope, new=params.scope,

View file

@ -1,16 +1,13 @@
# -*- coding: utf-8 -*-
"""
oauthlib.oauth2.rfc6749.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
oauthlib.oauth2.rfc6749.request_validator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import unicode_literals, absolute_import
import logging
log = logging.getLogger(__name__)
class RequestValidator(object):
class RequestValidator:
def client_authentication_required(self, request, *args, **kwargs):
"""Determine if client authentication is required for current request.
@ -26,7 +23,8 @@ class RequestValidator(object):
client credentials or whenever Client provided client authentication, see
`Section 6`_
:param request: oauthlib.common.Request
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -34,9 +32,9 @@ class RequestValidator(object):
- Resource Owner Password Credentials Grant
- Refresh Token Grant
.. _`Section 4.3.2`: http://tools.ietf.org/html/rfc6749#section-4.3.2
.. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3
.. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6
.. _`Section 4.3.2`: https://tools.ietf.org/html/rfc6749#section-4.3.2
.. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3
.. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
"""
return True
@ -50,8 +48,20 @@ class RequestValidator(object):
Headers may be accesses through request.headers and parameters found in
both body and query can be obtained by direct attribute access, i.e.
request.client_id for client_id in the URL query.
The authentication process is required to contain the identification of
the client (i.e. search the database based on the client_id). In case the
client doesn't exist based on the received client_id, this method has to
return False and the HTTP response created by the library will contain
'invalid_client' message.
:param request: oauthlib.common.Request
After the client identification succeeds, this method needs to set the
client on the request, i.e. request.client = client. A client object's
class must contain the 'client_id' attribute and the 'client_id' must have
a value.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -60,7 +70,7 @@ class RequestValidator(object):
- Client Credentials Grant
- Refresh Token Grant
.. _`HTTP Basic Authentication Scheme`: http://tools.ietf.org/html/rfc1945#section-11.1
.. _`HTTP Basic Authentication Scheme`: https://tools.ietf.org/html/rfc1945#section-11.1
"""
raise NotImplementedError('Subclasses must implement this method.')
@ -74,7 +84,9 @@ class RequestValidator(object):
to set request.client to the client object associated with the
given client_id.
:param request: oauthlib.common.Request
:param client_id: Unicode client identifier.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -82,7 +94,7 @@ class RequestValidator(object):
"""
raise NotImplementedError('Subclasses must implement this method.')
def confirm_redirect_uri(self, client_id, code, redirect_uri, client,
def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request,
*args, **kwargs):
"""Ensure that the authorization process represented by this authorization
code began with this 'redirect_uri'.
@ -93,11 +105,12 @@ class RequestValidator(object):
the client's allowed redirect URIs, but against the URI used when the
code was saved.
:param client_id: Unicode client identifier
:param client_id: Unicode client identifier.
:param code: Unicode authorization_code.
:param redirect_uri: Unicode absolute URI
:param client: Client object set by you, see authenticate_client.
:param request: The HTTP Request (oauthlib.common.Request)
:param redirect_uri: Unicode absolute URI.
:param client: Client object set by you, see ``.authenticate_client``.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -108,8 +121,9 @@ class RequestValidator(object):
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
"""Get the default redirect URI for the client.
:param client_id: Unicode client identifier
:param request: The HTTP Request (oauthlib.common.Request)
:param client_id: Unicode client identifier.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: The default redirect URI for the client
Method is used by:
@ -121,8 +135,9 @@ class RequestValidator(object):
def get_default_scopes(self, client_id, request, *args, **kwargs):
"""Get the default scopes for the client.
:param client_id: Unicode client identifier
:param request: The HTTP Request (oauthlib.common.Request)
:param client_id: Unicode client identifier.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: List of default scopes
Method is used by all core grant types:
@ -136,8 +151,9 @@ class RequestValidator(object):
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
"""Get the list of scopes associated with the refresh token.
:param refresh_token: Unicode refresh token
:param request: The HTTP Request (oauthlib.common.Request)
:param refresh_token: Unicode refresh token.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: List of scopes.
Method is used by:
@ -156,9 +172,10 @@ class RequestValidator(object):
used in situations where returning all valid scopes from the
get_original_scopes is not practical.
:param request_scopes: A list of scopes that were requested by client
:param refresh_token: Unicode refresh_token
:param request: The HTTP Request (oauthlib.common.Request)
:param request_scopes: A list of scopes that were requested by client.
:param refresh_token: Unicode refresh_token.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -166,12 +183,54 @@ class RequestValidator(object):
"""
return False
def introspect_token(self, token, token_type_hint, request, *args, **kwargs):
"""Introspect an access or refresh token.
Called once the introspect request is validated. This method should
verify the *token* and either return a dictionary with the list of
claims associated, or `None` in case the token is unknown.
Below the list of registered claims you should be interested in:
- scope : space-separated list of scopes
- client_id : client identifier
- username : human-readable identifier for the resource owner
- token_type : type of the token
- exp : integer timestamp indicating when this token will expire
- iat : integer timestamp indicating when this token was issued
- nbf : integer timestamp indicating when it can be "not-before" used
- sub : subject of the token - identifier of the resource owner
- aud : list of string identifiers representing the intended audience
- iss : string representing issuer of this token
- jti : string identifier for the token
Note that most of them are coming directly from JWT RFC. More details
can be found in `Introspect Claims`_ or `_JWT Claims`_.
The implementation can use *token_type_hint* to improve lookup
efficency, but must fallback to other types to be compliant with RFC.
The dict of claims is added to request.token after this method.
:param token: The token string.
:param token_type_hint: access_token or refresh_token.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
Method is used by:
- Introspect Endpoint (all grants are compatible)
.. _`Introspect Claims`: https://tools.ietf.org/html/rfc7662#section-2.2
.. _`JWT Claims`: https://tools.ietf.org/html/rfc7519#section-4
"""
raise NotImplementedError('Subclasses must implement this method.')
def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
"""Invalidate an authorization code after use.
:param client_id: Unicode client identifier
:param client_id: Unicode client identifier.
:param code: The authorization code grant (request.code).
:param request: The HTTP Request (oauthlib.common.Request)
:param request: OAuthlib request.
:type request: oauthlib.common.Request
Method is used by:
- Authorization Code Grant
@ -183,7 +242,8 @@ class RequestValidator(object):
:param token: The token string.
:param token_type_hint: access_token or refresh_token.
:param request: The HTTP Request (oauthlib.common.Request)
:param request: OAuthlib request.
:type request: oauthlib.common.Request
Method is used by:
- Revocation Endpoint
@ -197,7 +257,8 @@ class RequestValidator(object):
or replaced with a new one (rotated). Return True to rotate and
and False for keeping original.
:param request: oauthlib.common.Request
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -209,30 +270,49 @@ 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 'code' argument is actually a dictionary, containing at least a
'code' key with the actual authorization code:
To support PKCE, you MUST associate the code with:
- Code Challenge (``request.code_challenge``) and
- Code Challenge Method (``request.code_challenge_method``)
{'code': 'sdf345jsdf0934f'}
To support OIDC, you MUST associate the code with:
- nonce, if present (``code["nonce"]``)
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'.
The ``code`` argument is actually a dictionary, containing at least a
``code`` key with the actual authorization code:
:param client_id: Unicode client identifier
``{'code': 'sdf345jsdf0934f'}``
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``.
:param client_id: Unicode client identifier.
:param code: A dict of the authorization code grant and, optionally, state.
:param request: The HTTP Request (oauthlib.common.Request)
:param request: OAuthlib request.
:type request: oauthlib.common.Request
Method is used by:
- Authorization Code Grant
"""
raise NotImplementedError('Subclasses must implement this method.')
def save_token(self, token, request, *args, **kwargs):
"""Persist the token with a token type specific method.
Currently, only save_bearer_token is supported.
:param token: A (Bearer) token dict.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
return self.save_bearer_token(token, request, *args, **kwargs)
def save_bearer_token(self, token, request, *args, **kwargs):
"""Persist the Bearer token.
@ -242,6 +322,7 @@ class RequestValidator(object):
- authorized scopes (request.scopes)
- an expiration time
- a refresh token, if issued
- a claims document, if present in request.claims
The Bearer token dict may hold a number of items::
@ -251,15 +332,27 @@ class RequestValidator(object):
'expires_in': 3600,
'scope': 'string of space separated authorized scopes',
'refresh_token': '23sdf876234', # if issued
'state': 'given_by_client', # if supplied by client
'state': 'given_by_client', # if supplied by client (implicit ONLY)
}
Note that while "scope" is a string-separated list of authorized scopes,
the original list is still available in request.scopes
the original list is still available in request.scopes.
:param client_id: Unicode client identifier
:param token: A Bearer token dict
:param request: The HTTP Request (oauthlib.common.Request)
The token dict is passed as a reference so any changes made to the dictionary
will go back to the user. If additional information must return to the client
user, and it is only possible to get this information after writing the token
to storage, it should be added to the token dictionary. If the token
dictionary must be modified but the changes should not go back to the user,
a copy of the dictionary must be made before making the changes.
Also note that if an Authorization Code grant request included a valid claims
parameter (for OpenID Connect) then the request.claims property will contain
the claims dict, which should be saved for later use when generating the
id_token and/or UserInfo response content.
:param token: A Bearer token dict.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: The default redirect URI for the client
Method is used by all core grant types issuing Bearer tokens:
@ -275,7 +368,8 @@ class RequestValidator(object):
:param token: A string of random characters.
:param scopes: A list of scopes associated with the protected resource.
:param request: The HTTP Request (oauthlib.common.Request)
:param request: OAuthlib request.
:type request: oauthlib.common.Request
A key to OAuth 2 security and restricting impact of leaked tokens is
the short expiration time of tokens, *always ensure the token has not
@ -309,7 +403,8 @@ class RequestValidator(object):
:param token: Unicode Bearer token
:param scopes: List of scopes (defined by you)
:param request: The HTTP Request (oauthlib.common.Request)
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is indirectly used by all core Bearer token issuing grant types:
@ -327,7 +422,9 @@ class RequestValidator(object):
to set request.client to the client object associated with the
given client_id.
:param request: oauthlib.common.Request
:param client_id: Unicode client identifier.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -344,16 +441,24 @@ class RequestValidator(object):
with the code in 'save_authorization_code':
- request.user
- request.state (if given)
- request.scopes
- request.claims (if given)
OBS! The request.user attribute should be set to the resource owner
associated with this authorization code. Similarly request.scopes
must also be set.
:param client_id: Unicode client identifier
:param code: Unicode authorization code
:param client: Client object set by you, see authenticate_client.
:param request: The HTTP Request (oauthlib.common.Request)
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``.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -364,10 +469,11 @@ class RequestValidator(object):
def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
"""Ensure client is authorized to use the grant_type requested.
:param client_id: Unicode client identifier
:param client_id: Unicode client identifier.
:param grant_type: Unicode grant type, i.e. authorization_code, password.
:param client: Client object set by you, see authenticate_client.
:param request: The HTTP Request (oauthlib.common.Request)
:param client: Client object set by you, see ``.authenticate_client``.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -384,9 +490,10 @@ class RequestValidator(object):
All clients should register the absolute URIs of all URIs they intend
to redirect to. The registration is outside of the scope of oauthlib.
:param client_id: Unicode client identifier
:param redirect_uri: Unicode absolute URI
:param request: The HTTP Request (oauthlib.common.Request)
:param client_id: Unicode client identifier.
:param redirect_uri: Unicode absolute URI.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -401,9 +508,10 @@ class RequestValidator(object):
OBS! The request.user attribute should be set to the resource owner
associated with this refresh token.
:param refresh_token: Unicode refresh token
:param client: Client object set by you, see authenticate_client.
:param request: The HTTP Request (oauthlib.common.Request)
:param refresh_token: Unicode refresh token.
:param client: Client object set by you, see ``.authenticate_client``.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -416,10 +524,11 @@ class RequestValidator(object):
def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs):
"""Ensure client is authorized to use the response_type requested.
:param client_id: Unicode client identifier
:param client_id: Unicode client identifier.
:param response_type: Unicode response type, i.e. code, token.
:param client: Client object set by you, see authenticate_client.
:param request: The HTTP Request (oauthlib.common.Request)
:param client: Client object set by you, see ``.authenticate_client``.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@ -431,10 +540,11 @@ class RequestValidator(object):
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
"""Ensure the client is authorized access to requested scopes.
:param client_id: Unicode client identifier
:param scopes: List of scopes (defined by you)
:param client: Client object set by you, see authenticate_client.
:param request: The HTTP Request (oauthlib.common.Request)
:param client_id: Unicode client identifier.
:param scopes: List of scopes (defined by you).
:param client: Client object set by you, see ``.authenticate_client``.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by all core grant types:
@ -453,13 +563,89 @@ class RequestValidator(object):
not set you will be unable to associate a token with a user in the
persistance method used (commonly, save_bearer_token).
:param username: Unicode username
:param password: Unicode password
:param client: Client object set by you, see authenticate_client.
:param request: The HTTP Request (oauthlib.common.Request)
:param username: Unicode username.
:param password: Unicode password.
:param client: Client object set by you, see ``.authenticate_client``.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
- Resource Owner Password Credentials Grant
"""
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.')

View file

@ -4,21 +4,17 @@ oauthlib.oauth2.rfc6749.tokens
This module contains methods for adding two types of access tokens to requests.
- Bearer http://tools.ietf.org/html/rfc6750
- MAC http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
- Bearer https://tools.ietf.org/html/rfc6750
- MAC https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
"""
from __future__ import absolute_import, unicode_literals
from binascii import b2a_base64
import hashlib
import hmac
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
import warnings
from binascii import b2a_base64
from urllib.parse import urlparse
from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type
from oauthlib import common
from oauthlib.common import add_params_to_qs, add_params_to_uri
from . import utils
@ -26,7 +22,7 @@ from . import utils
class OAuth2Token(dict):
def __init__(self, params, old_scope=None):
super(OAuth2Token, self).__init__(params)
super().__init__(params)
self._new_scope = None
if 'scope' in params and params['scope']:
self._new_scope = set(utils.scope_to_list(params['scope']))
@ -92,13 +88,17 @@ def prepare_mac_header(token, uri, key, http_method,
nonce="1336363200:dj83hs9s",
mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
.. _`MAC Access Authentication`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
.. _`extension algorithms`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
.. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
.. _`extension algorithms`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
:param token:
:param uri: Request URI.
:param headers: Request headers as a dictionary.
:param http_method: HTTP Request method.
:param key: MAC given provided by token endpoint.
:param http_method: HTTP Request method.
:param nonce:
:param headers: Request headers as a dictionary.
:param body:
:param ext:
:param hash_algorithm: HMAC algorithm provided by token endpoint.
:param issue_time: Time when the MAC credentials were issued (datetime).
:param draft: MAC authentication specification version.
@ -115,7 +115,7 @@ def prepare_mac_header(token, uri, key, http_method,
raise ValueError('unknown hash algorithm')
if draft == 0:
nonce = nonce or '{0}:{1}'.format(utils.generate_age(issue_time),
nonce = nonce or '{}:{}'.format(utils.generate_age(issue_time),
common.generate_nonce())
else:
ts = common.generate_timestamp()
@ -152,7 +152,7 @@ def prepare_mac_header(token, uri, key, http_method,
base_string = '\n'.join(base) + '\n'
# hmac struggles with unicode strings - http://bugs.python.org/issue5285
if isinstance(key, unicode_type):
if isinstance(key, str):
key = key.encode('utf-8')
sign = hmac.new(key, base_string.encode('utf-8'), h)
sign = b2a_base64(sign.digest())[:-1].decode('utf-8')
@ -179,7 +179,10 @@ def prepare_bearer_uri(token, uri):
http://www.example.com/path?access_token=h480djs93hd8
.. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
.. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
:param token:
:param uri:
"""
return add_params_to_uri(uri, [(('access_token', token))])
@ -190,7 +193,10 @@ def prepare_bearer_headers(token, headers=None):
Authorization: Bearer h480djs93hd8
.. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
.. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
:param token:
:param headers:
"""
headers = headers or {}
headers['Authorization'] = 'Bearer %s' % token
@ -202,16 +208,27 @@ def prepare_bearer_body(token, body=''):
access_token=h480djs93hd8
.. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
.. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
:param token:
:param body:
"""
return add_params_to_qs(body, [(('access_token', token))])
def random_token_generator(request, refresh_token=False):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param refresh_token:
"""
return common.generate_token()
def signed_token_generator(private_pem, **kwargs):
"""
:param private_pem:
"""
def signed_token_generator(request):
request.claims = kwargs
return common.generate_signed_token(private_pem, request)
@ -219,15 +236,43 @@ def signed_token_generator(private_pem, **kwargs):
return signed_token_generator
class TokenBase(object):
def get_token_from_header(request):
"""
Helper function to extract a token from the request header.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:return: Return the token or None if the Authorization header is malformed.
"""
token = None
if 'Authorization' in request.headers:
split_header = request.headers.get('Authorization').split()
if len(split_header) == 2 and split_header[0].lower() == 'bearer':
token = split_header[1]
else:
token = request.access_token
return token
class TokenBase:
def __call__(self, request, refresh_token=False):
raise NotImplementedError('Subclasses must implement this method.')
def validate_request(self, request):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
raise NotImplementedError('Subclasses must implement this method.')
def estimate_type(self, request):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
raise NotImplementedError('Subclasses must implement this method.')
@ -246,8 +291,18 @@ class BearerToken(TokenBase):
)
self.expires_in = expires_in or 3600
def create_token(self, request, refresh_token=False):
"""Create a BearerToken, by default without refresh token."""
def create_token(self, request, refresh_token=False, **kwargs):
"""
Create a BearerToken, by default without refresh token.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:param refresh_token:
"""
if "save_token" in kwargs:
warnings.warn("`save_token` has been deprecated, it was not called internally."
"If you do, call `request_validator.save_token()` instead.",
DeprecationWarning)
if callable(self.expires_in):
expires_in = self.expires_in(request)
@ -262,12 +317,12 @@ class BearerToken(TokenBase):
'token_type': 'Bearer',
}
# If provided, include - this is optional in some cases https://tools.ietf.org/html/rfc6749#section-3.3 but
# there is currently no mechanism to coordinate issuing a token for only a subset of the requested scopes so
# all tokens issued are for the entire set of requested scopes.
if request.scopes is not None:
token['scope'] = ' '.join(request.scopes)
if request.state is not None:
token['state'] = request.state
if refresh_token:
if (request.refresh_token and
not self.request_validator.rotate_refresh_token(request)):
@ -276,21 +331,23 @@ class BearerToken(TokenBase):
token['refresh_token'] = self.refresh_token_generator(request)
token.update(request.extra_credentials or {})
token = OAuth2Token(token)
self.request_validator.save_bearer_token(token, request)
return token
return OAuth2Token(token)
def validate_request(self, request):
token = None
if 'Authorization' in request.headers:
token = request.headers.get('Authorization')[7:]
else:
token = request.access_token
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
token = get_token_from_header(request)
return self.request_validator.validate_bearer_token(
token, request.scopes, request)
def estimate_type(self, request):
if request.headers.get('Authorization', '').startswith('Bearer'):
"""
:param request: OAuthlib request.
:type request: oauthlib.common.Request
"""
if request.headers.get('Authorization', '').split(' ')[0].lower() == 'bearer':
return 9
elif request.access_token is not None:
return 5

View file

@ -1,31 +1,22 @@
# -*- coding: utf-8 -*-
"""
oauthlib.utils
~~~~~~~~~~~~~~
This module contains utility methods used by various parts of the OAuth 2 spec.
"""
from __future__ import absolute_import, unicode_literals
import os
import datetime
try:
from urllib import quote
except ImportError:
from urllib.parse import quote
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from oauthlib.common import unicode_type, urldecode
import os
from urllib.parse import quote, urlparse
from oauthlib.common import urldecode
def list_to_scope(scope):
"""Convert a list of scopes to a space separated string."""
if isinstance(scope, unicode_type) or scope is None:
if isinstance(scope, str) or scope is None:
return scope
elif isinstance(scope, (set, tuple, list)):
return " ".join([unicode_type(s) for s in scope])
return " ".join([str(s) for s in scope])
else:
raise ValueError("Invalid scope (%s), must be string, tuple, set, or list." % scope)
@ -33,7 +24,7 @@ def list_to_scope(scope):
def scope_to_list(scope):
"""Convert a space separated string to a list of scopes."""
if isinstance(scope, (tuple, list, set)):
return [unicode_type(s) for s in scope]
return [str(s) for s in scope]
elif scope is None:
return None
else:
@ -72,7 +63,7 @@ def escape(u):
TODO: verify whether this can in fact be used for OAuth 2
"""
if not isinstance(u, unicode_type):
if not isinstance(u, str):
raise ValueError('Only unicode objects are escapable.')
return quote(u.encode('utf-8'), safe=b'~')
@ -82,7 +73,7 @@ def generate_age(issue_time):
td = datetime.datetime.now() - issue_time
age = (td.microseconds + (td.seconds + td.days * 24 * 3600)
* 10 ** 6) / 10 ** 6
return unicode_type(age)
return str(age)
def is_secure_transport(uri):

View file

@ -0,0 +1,7 @@
"""
oauthlib.openid
~~~~~~~~~~~~~~
"""
from .connect.core.endpoints import Server, UserInfoEndpoint
from .connect.core.request_validator import RequestValidator

View file

View file

@ -0,0 +1,9 @@
"""
oauthlib.oopenid.core
~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various logic needed
for consuming and providing OpenID Connect
"""
from .pre_configured import Server
from .userinfo import UserInfoEndpoint

View file

@ -0,0 +1,97 @@
"""
oauthlib.openid.connect.core.endpoints.pre_configured
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of various endpoints needed
for providing OpenID Connect servers.
"""
from oauthlib.oauth2.rfc6749.endpoints import (
AuthorizationEndpoint, IntrospectEndpoint, ResourceEndpoint,
RevocationEndpoint, TokenEndpoint,
)
from oauthlib.oauth2.rfc6749.grant_types import (
AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
ClientCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant,
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
)
from oauthlib.oauth2.rfc6749.tokens import BearerToken
from ..grant_types import AuthorizationCodeGrant, HybridGrant, ImplicitGrant
from ..grant_types.dispatchers import (
AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher,
ImplicitTokenGrantDispatcher,
)
from ..tokens import JWTToken
from .userinfo import UserInfoEndpoint
class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
ResourceEndpoint, RevocationEndpoint, UserInfoEndpoint):
"""An all-in-one endpoint featuring all four major grant types."""
def __init__(self, request_validator, token_expires_in=None,
token_generator=None, refresh_token_generator=None,
*args, **kwargs):
"""Construct a new all-grants-in-one server.
:param request_validator: An implementation of
oauthlib.oauth2.RequestValidator.
:param token_expires_in: An int or a function to generate a token
expiration offset (in seconds) given a
oauthlib.common.Request object.
:param token_generator: A function to generate a token from a request.
:param refresh_token_generator: A function to generate a token from a
request for the refresh token.
:param kwargs: Extra parameters to pass to authorization-,
token-, resource-, and revocation-endpoint constructors.
"""
self.auth_grant = OAuth2AuthorizationCodeGrant(request_validator)
self.implicit_grant = OAuth2ImplicitGrant(request_validator)
self.password_grant = ResourceOwnerPasswordCredentialsGrant(
request_validator)
self.credentials_grant = ClientCredentialsGrant(request_validator)
self.refresh_grant = RefreshTokenGrant(request_validator)
self.openid_connect_auth = AuthorizationCodeGrant(request_validator)
self.openid_connect_implicit = ImplicitGrant(request_validator)
self.openid_connect_hybrid = HybridGrant(request_validator)
self.bearer = BearerToken(request_validator, token_generator,
token_expires_in, refresh_token_generator)
self.jwt = JWTToken(request_validator, token_generator,
token_expires_in, refresh_token_generator)
self.auth_grant_choice = AuthorizationCodeGrantDispatcher(default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth)
self.implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit)
# See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations
# internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination
AuthorizationEndpoint.__init__(self, default_response_type='code',
response_types={
'code': self.auth_grant_choice,
'token': self.implicit_grant_choice,
'id_token': self.openid_connect_implicit,
'id_token token': self.openid_connect_implicit,
'code token': self.openid_connect_hybrid,
'code id_token': self.openid_connect_hybrid,
'code id_token token': self.openid_connect_hybrid,
'none': self.auth_grant
},
default_token_type=self.bearer)
self.token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth)
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
grant_types={
'authorization_code': self.token_grant_choice,
'password': self.password_grant,
'client_credentials': self.credentials_grant,
'refresh_token': self.refresh_grant,
},
default_token_type=self.bearer)
ResourceEndpoint.__init__(self, default_token='Bearer',
token_types={'Bearer': self.bearer, 'JWT': self.jwt})
RevocationEndpoint.__init__(self, request_validator)
IntrospectEndpoint.__init__(self, request_validator)
UserInfoEndpoint.__init__(self, request_validator)

View file

@ -0,0 +1,99 @@
"""
oauthlib.openid.connect.core.endpoints.userinfo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module is an implementation of userinfo endpoint.
"""
import json
import logging
from oauthlib.common import Request
from oauthlib.oauth2.rfc6749 import errors
from oauthlib.oauth2.rfc6749.endpoints.base import (
BaseEndpoint, catch_errors_and_unavailability,
)
from oauthlib.oauth2.rfc6749.tokens import BearerToken
log = logging.getLogger(__name__)
class UserInfoEndpoint(BaseEndpoint):
"""Authorizes access to userinfo resource.
"""
def __init__(self, request_validator):
self.bearer = BearerToken(request_validator, None, None, None)
self.request_validator = request_validator
BaseEndpoint.__init__(self)
@catch_errors_and_unavailability
def create_userinfo_response(self, uri, http_method='GET', body=None, headers=None):
"""Validate BearerToken and return userinfo from RequestValidator
The UserInfo Endpoint MUST return a
content-type header to indicate which format is being returned. The
content-type of the HTTP response MUST be application/json if the
response body is a text JSON object; the response body SHOULD be encoded
using UTF-8.
"""
request = Request(uri, http_method, body, headers)
request.scopes = ["openid"]
self.validate_userinfo_request(request)
claims = self.request_validator.get_userinfo_claims(request)
if claims is None:
log.error('Userinfo MUST have claims for %r.', request)
raise errors.ServerError(status_code=500)
if isinstance(claims, dict):
resp_headers = {
'Content-Type': 'application/json'
}
if "sub" not in claims:
log.error('Userinfo MUST have "sub" for %r.', request)
raise errors.ServerError(status_code=500)
body = json.dumps(claims)
elif isinstance(claims, str):
resp_headers = {
'Content-Type': 'application/jwt'
}
body = claims
else:
log.error('Userinfo return unknown response for %r.', request)
raise errors.ServerError(status_code=500)
log.debug('Userinfo access valid for %r.', request)
return resp_headers, body, 200
def validate_userinfo_request(self, request):
"""Ensure the request is valid.
5.3.1. UserInfo Request
The Client sends the UserInfo Request using either HTTP GET or HTTP
POST. The Access Token obtained from an OpenID Connect Authentication
Request MUST be sent as a Bearer Token, per Section 2 of OAuth 2.0
Bearer Token Usage [RFC6750].
It is RECOMMENDED that the request use the HTTP GET method and the
Access Token be sent using the Authorization header field.
The following is a non-normative example of a UserInfo Request:
GET /userinfo HTTP/1.1
Host: server.example.com
Authorization: Bearer SlAV32hkKG
5.3.3. UserInfo Error Response
When an error condition occurs, the UserInfo Endpoint returns an Error
Response as defined in Section 3 of OAuth 2.0 Bearer Token Usage
[RFC6750]. (HTTP errors unrelated to RFC 6750 are returned to the User
Agent using the appropriate HTTP status code.)
The following is a non-normative example of a UserInfo Error Response:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token",
error_description="The Access Token expired"
"""
if not self.bearer.validate_request(request):
raise errors.InvalidTokenError()
if "openid" not in request.scopes:
raise errors.InsufficientScopeError()

View file

@ -0,0 +1,149 @@
"""
oauthlib.oauth2.rfc6749.errors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Error used both by OAuth 2 clients and providers to represent the spec
defined error responses for all four core grant types.
"""
from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error
class FatalOpenIDClientError(FatalClientError):
pass
class OpenIDClientError(OAuth2Error):
pass
class InteractionRequired(OpenIDClientError):
"""
The Authorization Server requires End-User interaction to proceed.
This error MAY be returned when the prompt parameter value in the
Authentication Request is none, but the Authentication Request cannot be
completed without displaying a user interface for End-User interaction.
"""
error = 'interaction_required'
status_code = 401
class LoginRequired(OpenIDClientError):
"""
The Authorization Server requires End-User authentication.
This error MAY be returned when the prompt parameter value in the
Authentication Request is none, but the Authentication Request cannot be
completed without displaying a user interface for End-User authentication.
"""
error = 'login_required'
status_code = 401
class AccountSelectionRequired(OpenIDClientError):
"""
The End-User is REQUIRED to select a session at the Authorization Server.
The End-User MAY be authenticated at the Authorization Server with
different associated accounts, but the End-User did not select a session.
This error MAY be returned when the prompt parameter value in the
Authentication Request is none, but the Authentication Request cannot be
completed without displaying a user interface to prompt for a session to
use.
"""
error = 'account_selection_required'
class ConsentRequired(OpenIDClientError):
"""
The Authorization Server requires End-User consent.
This error MAY be returned when the prompt parameter value in the
Authentication Request is none, but the Authentication Request cannot be
completed without displaying a user interface for End-User consent.
"""
error = 'consent_required'
status_code = 401
class InvalidRequestURI(OpenIDClientError):
"""
The request_uri in the Authorization Request returns an error or
contains invalid data.
"""
error = 'invalid_request_uri'
description = 'The request_uri in the Authorization Request returns an ' \
'error or contains invalid data.'
class InvalidRequestObject(OpenIDClientError):
"""
The request parameter contains an invalid Request Object.
"""
error = 'invalid_request_object'
description = 'The request parameter contains an invalid Request Object.'
class RequestNotSupported(OpenIDClientError):
"""
The OP does not support use of the request parameter.
"""
error = 'request_not_supported'
description = 'The request parameter is not supported.'
class RequestURINotSupported(OpenIDClientError):
"""
The OP does not support use of the request_uri parameter.
"""
error = 'request_uri_not_supported'
description = 'The request_uri parameter is not supported.'
class RegistrationNotSupported(OpenIDClientError):
"""
The OP does not support use of the registration parameter.
"""
error = 'registration_not_supported'
description = 'The registration parameter is not supported.'
class InvalidTokenError(OAuth2Error):
"""
The access token provided is expired, revoked, malformed, or
invalid for other reasons. The resource SHOULD respond with
the HTTP 401 (Unauthorized) status code. The client MAY
request a new access token and retry the protected resource
request.
"""
error = 'invalid_token'
status_code = 401
description = ("The access token provided is expired, revoked, malformed, "
"or invalid for other reasons.")
class InsufficientScopeError(OAuth2Error):
"""
The request requires higher privileges than provided by the
access token. The resource server SHOULD respond with the HTTP
403 (Forbidden) status code and MAY include the "scope"
attribute with the scope necessary to access the protected
resource.
"""
error = 'insufficient_scope'
status_code = 403
description = ("The request requires higher privileges than provided by "
"the access token.")
def raise_from_error(error, params=None):
import inspect
import sys
kwargs = {
'description': params.get('error_description'),
'uri': params.get('error_uri'),
'state': params.get('state')
}
for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
if cls.error == error:
raise cls(**kwargs)

View file

@ -0,0 +1,12 @@
"""
oauthlib.openid.connect.core.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from .authorization_code import AuthorizationCodeGrant
from .base import GrantTypeBase
from .dispatchers import (
AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher,
ImplicitTokenGrantDispatcher,
)
from .hybrid import HybridGrant
from .implicit import ImplicitGrant

View file

@ -0,0 +1,43 @@
"""
oauthlib.openid.connect.core.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
import logging
from oauthlib.oauth2.rfc6749.grant_types.authorization_code import (
AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
)
from .base import GrantTypeBase
log = logging.getLogger(__name__)
class AuthorizationCodeGrant(GrantTypeBase):
def __init__(self, request_validator=None, **kwargs):
self.proxy_target = OAuth2AuthorizationCodeGrant(
request_validator=request_validator, **kwargs)
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().add_id_token(token, token_handler, request, nonce=nonce)

View file

@ -0,0 +1,327 @@
import base64
import hashlib
import logging
import time
from json import loads
from oauthlib.oauth2.rfc6749.errors import (
ConsentRequired, InvalidRequestError, LoginRequired,
)
log = logging.getLogger(__name__)
class GrantTypeBase:
# Just proxy the majority of method calls through to the
# proxy_target grant type handler, which will usually be either
# the standard OAuth2 AuthCode or Implicit grant types.
def __getattr__(self, attr):
return getattr(self.proxy_target, attr)
def __setattr__(self, attr, value):
proxied_attrs = {'refresh_token', 'response_types'}
if attr in proxied_attrs:
setattr(self.proxy_target, attr, value)
else:
super(OpenIDConnectBase, self).__setattr__(attr, value)
def validate_authorization_request(self, request):
"""Validates the OpenID Connect authorization request parameters.
:returns: (list of scopes, dict of request info)
"""
return self.proxy_target.validate_authorization_request(request)
def _inflate_claims(self, request):
# this may be called multiple times in a single request so make sure we only de-serialize the claims once
if request.claims and not isinstance(request.claims, dict):
# specific claims are requested during the Authorization Request and may be requested for inclusion
# in either the id_token or the UserInfo endpoint response
# see http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
try:
request.claims = loads(request.claims)
except Exception as ex:
raise InvalidRequestError(description="Malformed claims parameter",
uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter")
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
# Only add an id token on auth/token step if asked for.
if request.response_type and 'id_token' not in request.response_type:
return 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
# 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
def openid_authorization_validator(self, request):
"""Perform OpenID Connect specific authorization request validation.
nonce
OPTIONAL. String value used to associate a Client session with
an ID Token, and to mitigate replay attacks. The value is
passed through unmodified from the Authentication Request to
the ID Token. Sufficient entropy MUST be present in the nonce
values used to prevent attackers from guessing values
display
OPTIONAL. ASCII string value that specifies how the
Authorization Server displays the authentication and consent
user interface pages to the End-User. The defined values are:
page - The Authorization Server SHOULD display the
authentication and consent UI consistent with a full User
Agent page view. If the display parameter is not specified,
this is the default display mode.
popup - The Authorization Server SHOULD display the
authentication and consent UI consistent with a popup User
Agent window. The popup User Agent window should be of an
appropriate size for a login-focused dialog and should not
obscure the entire window that it is popping up over.
touch - The Authorization Server SHOULD display the
authentication and consent UI consistent with a device that
leverages a touch interface.
wap - The Authorization Server SHOULD display the
authentication and consent UI consistent with a "feature
phone" type display.
The Authorization Server MAY also attempt to detect the
capabilities of the User Agent and present an appropriate
display.
prompt
OPTIONAL. Space delimited, case sensitive list of ASCII string
values that specifies whether the Authorization Server prompts
the End-User for reauthentication and consent. The defined
values are:
none - The Authorization Server MUST NOT display any
authentication or consent user interface pages. An error is
returned if an End-User is not already authenticated or the
Client does not have pre-configured consent for the
requested Claims or does not fulfill other conditions for
processing the request. The error code will typically be
login_required, interaction_required, or another code
defined in Section 3.1.2.6. This can be used as a method to
check for existing authentication and/or consent.
login - The Authorization Server SHOULD prompt the End-User
for reauthentication. If it cannot reauthenticate the
End-User, it MUST return an error, typically
login_required.
consent - The Authorization Server SHOULD prompt the
End-User for consent before returning information to the
Client. If it cannot obtain consent, it MUST return an
error, typically consent_required.
select_account - The Authorization Server SHOULD prompt the
End-User to select a user account. This enables an End-User
who has multiple accounts at the Authorization Server to
select amongst the multiple accounts that they might have
current sessions for. If it cannot obtain an account
selection choice made by the End-User, it MUST return an
error, typically account_selection_required.
The prompt parameter can be used by the Client to make sure
that the End-User is still present for the current session or
to bring attention to the request. If this parameter contains
none with any other value, an error is returned.
max_age
OPTIONAL. Maximum Authentication Age. Specifies the allowable
elapsed time in seconds since the last time the End-User was
actively authenticated by the OP. If the elapsed time is
greater than this value, the OP MUST attempt to actively
re-authenticate the End-User. (The max_age request parameter
corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] max_auth_age
request parameter.) When max_age is used, the ID Token returned
MUST include an auth_time Claim Value.
ui_locales
OPTIONAL. End-User's preferred languages and scripts for the
user interface, represented as a space-separated list of BCP47
[RFC5646] language tag values, ordered by preference. For
instance, the value "fr-CA fr en" represents a preference for
French as spoken in Canada, then French (without a region
designation), followed by English (without a region
designation). An error SHOULD NOT result if some or all of the
requested locales are not supported by the OpenID Provider.
id_token_hint
OPTIONAL. ID Token previously issued by the Authorization
Server being passed as a hint about the End-User's current or
past authenticated session with the Client. If the End-User
identified by the ID Token is logged in or is logged in by the
request, then the Authorization Server returns a positive
response; otherwise, it SHOULD return an error, such as
login_required. When possible, an id_token_hint SHOULD be
present when prompt=none is used and an invalid_request error
MAY be returned if it is not; however, the server SHOULD
respond successfully when possible, even if it is not present.
The Authorization Server need not be listed as an audience of
the ID Token when it is used as an id_token_hint value. If the
ID Token received by the RP from the OP is encrypted, to use it
as an id_token_hint, the Client MUST decrypt the signed ID
Token contained within the encrypted ID Token. The Client MAY
re-encrypt the signed ID token to the Authentication Server
using a key that enables the server to decrypt the ID Token,
and use the re-encrypted ID token as the id_token_hint value.
login_hint
OPTIONAL. Hint to the Authorization Server about the login
identifier the End-User might use to log in (if necessary).
This hint can be used by an RP if it first asks the End-User
for their e-mail address (or other identifier) and then wants
to pass that value as a hint to the discovered authorization
service. It is RECOMMENDED that the hint value match the value
used for discovery. This value MAY also be a phone number in
the format specified for the phone_number Claim. The use of
this parameter is left to the OP's discretion.
acr_values
OPTIONAL. Requested Authentication Context Class Reference
values. Space-separated string that specifies the acr values
that the Authorization Server is being requested to use for
processing this Authentication Request, with the values
appearing in order of preference. The Authentication Context
Class satisfied by the authentication performed is returned as
the acr Claim Value, as specified in Section 2. The acr Claim
is requested as a Voluntary Claim by this parameter.
"""
# 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 {}
prompt = request.prompt if request.prompt else []
if hasattr(prompt, 'split'):
prompt = prompt.strip().split()
prompt = set(prompt)
if 'none' in prompt:
if len(prompt) > 1:
msg = "Prompt none is mutually exclusive with other values."
raise InvalidRequestError(request=request, description=msg)
if not self.request_validator.validate_silent_login(request):
raise LoginRequired(request=request)
if not self.request_validator.validate_silent_authorization(request):
raise ConsentRequired(request=request)
self._inflate_claims(request)
if not self.request_validator.validate_user_match(
request.id_token_hint, request.scopes, request.claims, request):
msg = "Session user does not match client supplied user."
raise LoginRequired(request=request, description=msg)
request_info = {
'display': request.display,
'nonce': request.nonce,
'prompt': prompt,
'ui_locales': request.ui_locales.split() if request.ui_locales else [],
'id_token_hint': request.id_token_hint,
'login_hint': request.login_hint,
'claims': request.claims
}
return request_info
OpenIDConnectBase = GrantTypeBase

View file

@ -0,0 +1,101 @@
import logging
log = logging.getLogger(__name__)
class Dispatcher:
default_grant = None
oidc_grant = None
class AuthorizationCodeGrantDispatcher(Dispatcher):
"""
This is an adapter class that will route simple Authorization Code
requests, those that have `response_type=code` and a scope including
`openid` to either the `default_grant` or the `oidc_grant` based on
the scopes requested.
"""
def __init__(self, default_grant=None, oidc_grant=None):
self.default_grant = default_grant
self.oidc_grant = oidc_grant
def _handler_for_request(self, request):
handler = self.default_grant
if request.scopes and "openid" in request.scopes:
handler = self.oidc_grant
log.debug('Selecting handler for request %r.', handler)
return handler
def create_authorization_response(self, request, token_handler):
"""Read scope and route to the designated handler."""
return self._handler_for_request(request).create_authorization_response(request, token_handler)
def validate_authorization_request(self, request):
"""Read scope and route to the designated handler."""
return self._handler_for_request(request).validate_authorization_request(request)
class ImplicitTokenGrantDispatcher(Dispatcher):
"""
This is an adapter class that will route simple Authorization
requests, those that have `id_token` in `response_type` and a scope
including `openid` to either the `default_grant` or the `oidc_grant`
based on the scopes requested.
"""
def __init__(self, default_grant=None, oidc_grant=None):
self.default_grant = default_grant
self.oidc_grant = oidc_grant
def _handler_for_request(self, request):
handler = self.default_grant
if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type:
handler = self.oidc_grant
log.debug('Selecting handler for request %r.', handler)
return handler
def create_authorization_response(self, request, token_handler):
"""Read scope and route to the designated handler."""
return self._handler_for_request(request).create_authorization_response(request, token_handler)
def validate_authorization_request(self, request):
"""Read scope and route to the designated handler."""
return self._handler_for_request(request).validate_authorization_request(request)
class AuthorizationTokenGrantDispatcher(Dispatcher):
"""
This is an adapter class that will route simple Token requests, those that authorization_code have a scope
including 'openid' to either the default_grant or the oidc_grant based on the scopes requested.
"""
def __init__(self, request_validator, default_grant=None, oidc_grant=None):
self.default_grant = default_grant
self.oidc_grant = oidc_grant
self.request_validator = request_validator
def _handler_for_request(self, request):
handler = self.default_grant
scopes = ()
parameters = dict(request.decoded_body)
client_id = parameters.get('client_id', None)
code = parameters.get('code', None)
redirect_uri = parameters.get('redirect_uri', None)
# If code is not pressent fallback to `default_grant` which will
# raise an error for the missing `code` in `create_token_response` step.
if code:
scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request)
if 'openid' in scopes:
handler = self.oidc_grant
log.debug('Selecting handler for request %r.', handler)
return handler
def create_token_response(self, request, token_handler):
"""Read scope and route to the designated handler."""
handler = self._handler_for_request(request)
return handler.create_token_response(request, token_handler)

View file

@ -0,0 +1,63 @@
"""
oauthlib.openid.connect.core.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
import logging
from oauthlib.oauth2.rfc6749.errors import InvalidRequestError
from oauthlib.oauth2.rfc6749.grant_types.authorization_code import (
AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
)
from ..request_validator import RequestValidator
from .base import GrantTypeBase
log = logging.getLogger(__name__)
class HybridGrant(GrantTypeBase):
def __init__(self, request_validator=None, **kwargs):
self.request_validator = request_validator or RequestValidator()
self.proxy_target = OAuth2AuthorizationCodeGrant(
request_validator=request_validator, **kwargs)
# All hybrid response types should be fragment-encoded.
self.proxy_target.default_response_mode = "fragment"
self.register_response_type('code id_token')
self.register_response_type('code token')
self.register_response_type('code id_token token')
self.custom_validators.post_auth.append(
self.openid_authorization_validator)
# Hybrid flows can return the id_token from the authorization
# endpoint as part of the 'code' response
self.register_code_modifier(self.add_token)
self.register_code_modifier(self.add_id_token)
self.register_token_modifier(self.add_id_token)
def add_id_token(self, token, token_handler, request):
return super().add_id_token(token, token_handler, request, nonce=request.nonce)
def openid_authorization_validator(self, request):
"""Additional validation when following the Authorization Code flow.
"""
request_info = super().openid_authorization_validator(request)
if not request_info: # returns immediately if OAuth2.0
return request_info
# REQUIRED if the Response Type of the request is `code
# id_token` or `code id_token token` and OPTIONAL when the
# Response Type of the request is `code token`. It is a string
# value used to associate a Client session with an ID Token,
# and to mitigate replay attacks. The value is passed through
# unmodified from the Authentication Request to the ID
# Token. Sufficient entropy MUST be present in the `nonce`
# values used to prevent attackers from guessing values. For
# implementation notes, see Section 15.5.2.
if request.response_type in ["code id_token", "code id_token token"]:
if not request.nonce:
raise InvalidRequestError(
request=request,
description='Request is missing mandatory nonce parameter.'
)
return request_info

View file

@ -0,0 +1,51 @@
"""
oauthlib.openid.connect.core.grant_types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
import logging
from oauthlib.oauth2.rfc6749.errors import InvalidRequestError
from oauthlib.oauth2.rfc6749.grant_types.implicit import (
ImplicitGrant as OAuth2ImplicitGrant,
)
from .base import GrantTypeBase
log = logging.getLogger(__name__)
class ImplicitGrant(GrantTypeBase):
def __init__(self, request_validator=None, **kwargs):
self.proxy_target = OAuth2ImplicitGrant(
request_validator=request_validator, **kwargs)
self.register_response_type('id_token')
self.register_response_type('id_token token')
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):
if 'state' not in token and request.state:
token['state'] = request.state
return super().add_id_token(token, token_handler, request, nonce=request.nonce)
def openid_authorization_validator(self, request):
"""Additional validation when following the implicit flow.
"""
request_info = super().openid_authorization_validator(request)
if not request_info: # returns immediately if OAuth2.0
return request_info
# REQUIRED. String value used to associate a Client session with an ID
# Token, and to mitigate replay attacks. The value is passed through
# unmodified from the Authentication Request to the ID Token.
# Sufficient entropy MUST be present in the nonce values used to
# prevent attackers from guessing values. For implementation notes, see
# Section 15.5.2.
if not request.nonce:
raise InvalidRequestError(
request=request,
description='Request is missing mandatory nonce parameter.'
)
return request_info

View file

@ -0,0 +1,308 @@
"""
oauthlib.openid.connect.core.request_validator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
import logging
from oauthlib.oauth2.rfc6749.request_validator import (
RequestValidator as OAuth2RequestValidator,
)
log = logging.getLogger(__name__)
class RequestValidator(OAuth2RequestValidator):
def get_authorization_code_scopes(self, client_id, code, redirect_uri, request):
""" Extracts scopes from saved authorization code.
The scopes returned by this method is used to route token requests
based on scopes passed to Authorization Code requests.
With that the token endpoint knows when to include OpenIDConnect
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 have a
blank value `""` don't forget to check it before using those values
in a select query if a database is used.
:param client_id: Unicode client identifier
:param code: Unicode authorization code grant
:param redirect_uri: Unicode absolute URI
:return: A list of scope
Method is used by:
- Authorization Token Grant Dispatcher
"""
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
If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_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 JWT Bearer token or OpenID Connect ID token (a JWS signed JWT)
Method is used by JWT Bearer and OpenID Connect tokens:
- JWTToken.create_token
"""
raise NotImplementedError('Subclasses must implement this method.')
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.
In addition to the standard OAuth2 request properties, the request may also contain
these OIDC specific properties which are useful to this method:
- nonce, if workflow is implicit or hybrid and it was provided
- claims, if provided to the original Authorization Code request
The token parameter is a dict which may contain an ``access_token`` entry, in which
case the resulting ID Token *should* include a calculated ``at_hash`` claim.
Similarly, when the request parameter has a ``code`` property defined, the ID Token
*should* include a calculated ``c_hash`` claim.
http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_)
.. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
.. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
.. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
: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)
"""
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):
"""Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes.
If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token`
If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response.
OpenID connect core 1.0 describe how to validate an id_token:
- http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
- http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
- http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation
- http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2
:param token: Unicode Bearer token
:param scopes: List of scopes (defined by you)
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is indirectly used by all core OpenID connect JWT token issuing grant types:
- Authorization Code Grant
- Implicit Grant
- Hybrid Grant
"""
raise NotImplementedError('Subclasses must implement this method.')
def validate_id_token(self, token, scopes, request):
"""Ensure the id token is valid and authorized access to scopes.
OpenID connect core 1.0 describe how to validate an id_token:
- http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
- http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
- http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation
- http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2
:param token: Unicode Bearer token
:param scopes: List of scopes (defined by you)
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is indirectly used by all core OpenID connect JWT token issuing grant types:
- Authorization Code Grant
- Implicit Grant
- Hybrid Grant
"""
raise NotImplementedError('Subclasses must implement this method.')
def validate_silent_authorization(self, request):
"""Ensure the logged in user has authorized silent OpenID authorization.
Silent OpenID authorization allows access tokens and id tokens to be
granted to clients without any user prompt or interaction.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
- OpenIDConnectAuthCode
- OpenIDConnectImplicit
- OpenIDConnectHybrid
"""
raise NotImplementedError('Subclasses must implement this method.')
def validate_silent_login(self, request):
"""Ensure session user has authorized silent OpenID login.
If no user is logged in or has not authorized silent login, this
method should return False.
If the user is logged in but associated with multiple accounts and
not selected which one to link to the token then this method should
raise an oauthlib.oauth2.AccountSelectionRequired error.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
- OpenIDConnectAuthCode
- OpenIDConnectImplicit
- OpenIDConnectHybrid
"""
raise NotImplementedError('Subclasses must implement this method.')
def validate_user_match(self, id_token_hint, scopes, claims, request):
"""Ensure client supplied user id hint matches session user.
If the sub claim or id_token_hint is supplied then the session
user must match the given ID.
:param id_token_hint: User identifier string.
:param scopes: List of OAuth 2 scopes and OpenID claims (strings).
:param claims: OpenID Connect claims dict.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
- OpenIDConnectAuthCode
- OpenIDConnectImplicit
- OpenIDConnectHybrid
"""
raise NotImplementedError('Subclasses must implement this method.')
def get_userinfo_claims(self, request):
"""Return the UserInfo claims in JSON or Signed or Encrypted.
The UserInfo Claims MUST be returned as the members of a JSON object
unless a signed or encrypted response was requested during Client
Registration. The Claims defined in Section 5.1 can be returned, as can
additional Claims not specified there.
For privacy reasons, OpenID Providers MAY elect to not return values for
some requested Claims.
If a Claim is not returned, that Claim Name SHOULD be omitted from the
JSON object representing the Claims; it SHOULD NOT be present with a
null or empty string value.
The sub (subject) Claim MUST always be returned in the UserInfo
Response.
Upon receipt of the UserInfo Request, the UserInfo Endpoint MUST return
the JSON Serialization of the UserInfo Response as in Section 13.3 in
the HTTP response body unless a different format was specified during
Registration [OpenID.Registration].
If the UserInfo Response is signed and/or encrypted, then the Claims are
returned in a JWT and the content-type MUST be application/jwt. The
response MAY be encrypted without also being signed. If both signing and
encryption are requested, the response MUST be signed then encrypted,
with the result being a Nested JWT, as defined in [JWT].
If signed, the UserInfo Response SHOULD contain the Claims iss (issuer)
and aud (audience) as members. The iss value SHOULD be the OP's Issuer
Identifier URL. The aud value SHOULD be or include the RP's Client ID
value.
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:rtype: Claims as a dict OR JWT/JWS/JWE as a string
Method is used by:
UserInfoEndpoint
"""

View file

@ -0,0 +1,46 @@
"""
authlib.openid.connect.core.tokens
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains methods for adding JWT tokens to requests.
"""
from oauthlib.oauth2.rfc6749.tokens import TokenBase, random_token_generator, get_token_from_header
class JWTToken(TokenBase):
__slots__ = (
'request_validator', 'token_generator',
'refresh_token_generator', 'expires_in'
)
def __init__(self, request_validator=None, token_generator=None,
expires_in=None, refresh_token_generator=None):
self.request_validator = request_validator
self.token_generator = token_generator or random_token_generator
self.refresh_token_generator = (
refresh_token_generator or self.token_generator
)
self.expires_in = expires_in or 3600
def create_token(self, request, refresh_token=False):
"""Create a JWT Token, using requestvalidator method."""
if callable(self.expires_in):
expires_in = self.expires_in(request)
else:
expires_in = self.expires_in
request.expires_in = expires_in
return self.request_validator.get_jwt_bearer_token(None, None, request)
def validate_request(self, request):
token = get_token_from_header(request)
return self.request_validator.validate_jwt_bearer_token(
token, request.scopes, request)
def estimate_type(self, request):
token = get_token_from_header(request)
if token and token.startswith('ey') and token.count('.') in (2, 4):
return 10
return 0

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
Implements signals based on blinker if available, otherwise
falls silently back to a noop. Shamelessly stolen from flask.signals:
@ -8,12 +7,12 @@ signals_available = False
try:
from blinker import Namespace
signals_available = True
except ImportError:
class Namespace(object):
except ImportError: # noqa
class Namespace:
def signal(self, name, doc=None):
return _FakeSignal(name, doc)
class _FakeSignal(object):
class _FakeSignal:
"""If blinker is unavailable, create a fake class with the same
interface that allows sending of signals but will fail with an
error on anything else. Instead of doing anything on send, it

View file

@ -8,7 +8,6 @@ They should be processed with re.VERBOSE.
Thanks Mark Nottingham for this code - https://gist.github.com/138549
"""
from __future__ import unicode_literals
import re
# basics
@ -66,32 +65,8 @@ dec_octet = r"""(?: %(DIGIT)s |
IPv4address = r"%(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s" % locals(
)
# h16 = 1*4HEXDIG
h16 = r"(?: %(HEXDIG)s ){1,4}" % locals()
# ls32 = ( h16 ":" h16 ) / IPv4address
ls32 = r"(?: (?: %(h16)s : %(h16)s ) | %(IPv4address)s )" % locals()
# IPv6address = 6( h16 ":" ) ls32
# / "::" 5( h16 ":" ) ls32
# / [ h16 ] "::" 4( h16 ":" ) ls32
# / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
# / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
# / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
# / [ *4( h16 ":" ) h16 ] "::" ls32
# / [ *5( h16 ":" ) h16 ] "::" h16
# / [ *6( h16 ":" ) h16 ] "::"
IPv6address = r"""(?: (?: %(h16)s : ){6} %(ls32)s |
:: (?: %(h16)s : ){5} %(ls32)s |
%(h16)s :: (?: %(h16)s : ){4} %(ls32)s |
(?: %(h16)s : ) %(h16)s :: (?: %(h16)s : ){3} %(ls32)s |
(?: %(h16)s : ){2} %(h16)s :: (?: %(h16)s : ){2} %(ls32)s |
(?: %(h16)s : ){3} %(h16)s :: %(h16)s : %(ls32)s |
(?: %(h16)s : ){4} %(h16)s :: %(ls32)s |
(?: %(h16)s : ){5} %(h16)s :: %(h16)s |
(?: %(h16)s : ){6} %(h16)s ::
)
""" % locals()
# IPv6address
IPv6address = r"([A-Fa-f0-9:]+:+)+[A-Fa-f0-9]+"
# IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
IPvFuture = r"v %(HEXDIG)s+ \. (?: %(unreserved)s | %(sub_delims)s | : )+" % locals()