Skip to content

Commit

Permalink
Merge pull request #449 from bjmc/improve_validator_registration
Browse files Browse the repository at this point in the history
Improve validator registration
  • Loading branch information
thedrow committed Dec 23, 2016
2 parents f0bbc52 + cf41425 commit d2c7be6
Show file tree
Hide file tree
Showing 19 changed files with 360 additions and 199 deletions.
3 changes: 2 additions & 1 deletion AUTHORS
Expand Up @@ -24,4 +24,5 @@ Josh Turmel
David Baumgold
Juan Fabio García Solero
Omer Katz
Joel Stevenson
Joel Stevenson
Brendan McCollam
1 change: 1 addition & 0 deletions docs/oauth2/grants/authcode.rst
Expand Up @@ -3,3 +3,4 @@ Authorization Code Grant

.. autoclass:: oauthlib.oauth2.AuthorizationCodeGrant
:members:
:inherited-members:
1 change: 1 addition & 0 deletions docs/oauth2/grants/credentials.rst
Expand Up @@ -3,3 +3,4 @@ Client Credentials Grant

.. autoclass:: oauthlib.oauth2.ClientCredentialsGrant
:members:
:inherited-members:
5 changes: 5 additions & 0 deletions docs/oauth2/grants/custom_validators.rst
@@ -0,0 +1,5 @@
Custom Validators
-----------------

.. autoclass:: oauthlib.oauth2.rfc6749.grant_types.base.ValidatorsContainer
:members:
7 changes: 7 additions & 0 deletions docs/oauth2/grants/grants.rst
Expand Up @@ -9,6 +9,7 @@ Grant types
implicit
password
credentials
custom_validators
jwt

Grant types are what make OAuth 2 so flexible. The Authorization Code grant is
Expand All @@ -24,6 +25,12 @@ resources in various ways with different security credentials.
Naturally, OAuth 2 allows for extension grant types to be defined and OAuthLib
attempts to cater for easy inclusion of this as much as possible.

OAuthlib also offers hooks for registering your own custom validations for use
with the existing grant type handlers
(:py:class:`oauthlib.oauth2.rfc6749.grant_types.base.ValidatorsContainer`).
In some situations, this may be more convenient than subclassing or writing
your own extension grant type.

Certain grant types allow the issuing of refresh tokens which will allow a
client to request new tokens for as long as you as provider allow them too. In
general, OAuth 2 tokens should expire quickly and rather than annoying the user
Expand Down
1 change: 1 addition & 0 deletions docs/oauth2/grants/implicit.rst
Expand Up @@ -3,3 +3,4 @@ Implicit Grant

.. autoclass:: oauthlib.oauth2.ImplicitGrant
:members:
:inherited-members:
1 change: 1 addition & 0 deletions docs/oauth2/grants/password.rst
Expand Up @@ -3,3 +3,4 @@ Resource Owner Password Credentials Grant

.. autoclass:: oauthlib.oauth2.ResourceOwnerPasswordCredentialsGrant
:members:
:inherited-members:
43 changes: 12 additions & 31 deletions oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
Expand Up @@ -13,7 +13,6 @@

from .base import GrantTypeBase
from .. import errors
from ..request_validator import RequestValidator

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -96,31 +95,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
"""

default_response_mode = 'query'

def __init__(self, request_validator=None, refresh_token=True):
self.request_validator = request_validator or RequestValidator()
self.refresh_token = refresh_token

self._authorization_validators = []
self._token_validators = []
self._code_modifiers = []
self._token_modifiers = []
self.response_types = ['code']

def register_response_type(self, response_type):
self.response_types.append(response_type)

def register_authorization_validator(self, validator):
self._authorization_validators.append(validator)

def register_token_validator(self, validator):
self._token_validators.append(validator)

def register_code_modifier(self, modifier):
self._code_modifiers.append(modifier)

def register_token_modifier(self, modifier):
self._token_modifiers.append(modifier)
response_types = ['code']

def create_authorization_code(self, request):
"""Generates an authorization grant represented as a dictionary."""
Expand Down Expand Up @@ -347,6 +322,10 @@ def validate_authorization_request(self, request):
# 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)
Expand All @@ -367,15 +346,15 @@ def validate_authorization_request(self, request):
# http://tools.ietf.org/html/rfc6749#section-3.3
self.validate_scopes(request)

request_info = {
request_info.update({
'client_id': request.client_id,
'redirect_uri': request.redirect_uri,
'response_type': request.response_type,
'state': request.state,
'request': request
}
})

for validator in self._authorization_validators:
for validator in self.custom_validators.post_auth:
request_info.update(validator(request))

return request.scopes, request_info
Expand All @@ -385,6 +364,9 @@ def validate_token_request(self, request):
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)
Expand Down Expand Up @@ -441,6 +423,5 @@ def validate_token_request(self, request):
request.redirect_uri, request.client_id, request.client)
raise errors.AccessDeniedError(request=request)

for validator in self._token_validators:
for validator in self.custom_validators.post_token:
validator(request)

98 changes: 98 additions & 0 deletions oauthlib/oauth2/rfc6749/grant_types/base.py
Expand Up @@ -4,19 +4,117 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import unicode_literals, absolute_import
from itertools import chain

import logging

from oauthlib.common import add_params_to_uri
from oauthlib.oauth2.rfc6749 import errors, utils
from ..request_validator import RequestValidator

log = logging.getLogger(__name__)

class ValidatorsContainer(object):
"""
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(object):
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):
raise NotImplementedError('Subclasses must implement this method.')
Expand Down
13 changes: 6 additions & 7 deletions oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
Expand Up @@ -50,13 +50,6 @@ class ClientCredentialsGrant(GrantTypeBase):
.. _`Client Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.4
"""

def __init__(self, request_validator=None):
self.request_validator = request_validator or RequestValidator()
self._token_modifiers = []

def register_token_modifier(self, modifier):
self._token_modifiers.append(modifier)

def create_token_response(self, request, token_handler):
"""Return token or error in JSON format.
Expand Down Expand Up @@ -92,6 +85,9 @@ def create_token_response(self, request, token_handler):
return headers, json.dumps(token), 200

def validate_token_request(self, 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)
Expand Down Expand Up @@ -119,3 +115,6 @@ def validate_token_request(self, request):
log.debug('Authorizing access to user %r.', request.user)
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)
45 changes: 27 additions & 18 deletions oauthlib/oauth2/rfc6749/grant_types/implicit.py
Expand Up @@ -117,20 +117,8 @@ class ImplicitGrant(GrantTypeBase):
.. _`Section 10.16`: http://tools.ietf.org/html/rfc6749#section-10.16
"""

def __init__(self, request_validator=None):
self.request_validator = request_validator or RequestValidator()
self._authorization_validators = []
self._token_modifiers = []
self.response_types = ['token']

def register_response_type(self, response_type):
self.response_types.append(response_type)

def register_authorization_validator(self, validator):
self._authorization_validators.append(validator)

def register_token_modifier(self, modifier):
self._token_modifiers.append(modifier)
response_types = ['token']
grant_allows_refresh_token = False

def create_authorization_response(self, request, token_handler):
"""Create an authorization response.
Expand Down Expand Up @@ -328,6 +316,10 @@ def validate_token_request(self, 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
Expand Down Expand Up @@ -359,15 +351,32 @@ def validate_token_request(self, request):
# http://tools.ietf.org/html/rfc6749#section-3.3
self.validate_scopes(request)

request_info = {
request_info.update({
'client_id': request.client_id,
'redirect_uri': request.redirect_uri,
'response_type': request.response_type,
'state': request.state,
'request': request,
}
})

for validator in self._authorization_validators:
request_info.update(validator(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

0 comments on commit d2c7be6

Please sign in to comment.