Source code for oauth2app.authenticate

#-*- coding: utf-8 -*-


"""OAuth 2.0 Authentication"""


from hashlib import sha256
from urlparse import parse_qsl
try: import simplejson as json
except ImportError: import json
from django.conf import settings
from django.http import HttpResponse
from .exceptions import OAuth2Exception
from .models import AccessToken, AccessRange, TimestampGenerator
from .consts import REALM, AUTHENTICATION_METHOD, MAC, BEARER

[docs]class AuthenticationException(OAuth2Exception): """Authentication exception base class.""" pass
[docs]class InvalidRequest(AuthenticationException): """The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, uses more than one method for including an access token, or is otherwise malformed.""" error = 'invalid_request'
[docs]class InvalidToken(AuthenticationException): """The access token provided is expired, revoked, malformed, or invalid for other reasons.""" error = 'invalid_token'
[docs]class InsufficientScope(AuthenticationException): """The request requires higher privileges than provided by the access token.""" error = 'insufficient_scope'
[docs]class UnvalidatedRequest(OAuth2Exception): """The method requested requires a validated request to continue.""" pass
[docs]class Authenticator(object): """Django HttpRequest authenticator. Checks a request for valid credentials and scope. **Kwargs:** * *scope:* An iterable of oauth2app.models.AccessRange objects representing the scope the authenticator will authenticate. *Default None* * *authentication_method:* Accepted authentication methods. Possible values are: oauth2app.consts.MAC, oauth2app.consts.BEARER, oauth2app.consts.MAC | oauth2app.consts.BEARER, *Default oauth2app.consts.BEARER* """ valid = False access_token = None auth_type = None auth_value = None error = None attempted_validation = False def __init__( self, scope=None, authentication_method=AUTHENTICATION_METHOD): if authentication_method not in [BEARER, MAC, BEARER | MAC]: raise OAuth2Exception("Possible values for authentication_method" " are oauth2app.consts.MAC, oauth2app.consts.BEARER, " "oauth2app.consts.MAC | oauth2app.consts.BEARER") self.authentication_method = authentication_method if scope is None: self.authorized_scope = None elif isinstance(scope, AccessRange): self.authorized_scope = set([scope.key]) else: self.authorized_scope = set([x.key for x in scope])
[docs] def validate(self, request): """Validate the request. Raises an AuthenticationException if the request fails authentication. **Args:** * *request:* Django HttpRequest object. *Returns None*""" self.request = request self.bearer_token = request.REQUEST.get('bearer_token') if "HTTP_AUTHORIZATION" in self.request.META: auth = self.request.META["HTTP_AUTHORIZATION"].split() self.auth_type = auth[0].lower() self.auth_value = " ".join(auth[1:]).strip() self.request_hostname = self.request.META.get("REMOTE_HOST") self.request_port = self.request.META.get("SERVER_PORT") try: self._validate() except AuthenticationException as e: self.error = e raise e self.valid = True
def _validate(self): """Validate the request.""" # Check for Bearer or Mac authorization if self.auth_type in ["bearer", "mac"]: self.attempted_validation = True if self.auth_type == "bearer": self._validate_bearer(self.auth_value) elif self.auth_type == "mac": self._validate_mac(self.auth_value) self.valid = True # Check for posted/paramaterized bearer token. elif self.bearer_token is not None: self.attempted_validation = True self._validate_bearer(self.bearer_token) self.valid = True return else: raise InvalidRequest("Request authentication failed, no " "authentication credentials provided.") if self.authorized_scope is not None: token_scope = set([x.key for x in self.access_token.scope.all()]) new_scope = self.authorized_scope - token_scope if len(new_scope) > 0: raise InsufficientScope(("Access token has insufficient " "scope: %s") % ','.join(self.authorized_scope)) now = TimestampGenerator()() if self.access_token.expire < now: raise InvalidToken("Token is expired") def _validate_bearer(self, token): """Validate Bearer token.""" if self.authentication_method & BEARER == 0: raise InvalidToken("Bearer authentication is not supported.") try: self.access_token = AccessToken.objects.get(token=token) except AccessToken.DoesNotExist: raise InvalidToken("Token doesn't exist") def _validate_mac(self, mac_header): """Validate MAC authentication. Not implemented.""" if self.authentication_method & MAC == 0: raise InvalidToken("MAC authentication is not supported.") mac_header = parse_qsl(mac_header.replace(",","&").replace('"', '')) mac_header = dict([(x[0].strip(), x[1].strip()) for x in mac_header]) for parameter in ["id", "nonce", "mac"]: if "parameter" not in mac_header: raise InvalidToken("MAC Authorization header does not contain" " required parameter '%s'" % parameter) if "bodyhash" in mac_header: bodyhash = mac_header["bodyhash"] else: bodyhash = "" if "ext" in mac_header: ext = mac_header["ext"] else: ext = "" if self.request_hostname is None: raise InvalidRequest("Request does not contain a hostname.") if self.request_port is None: raise InvalidRequest("Request does not contain a port.") nonce_timestamp, nonce_string = mac_header["nonce"].split(":") mac = sha256("\n".join([ mac_header["nonce"], # The nonce value generated for the request self.request.method.upper(), # The HTTP request method "XXX", # The HTTP request-URI self.request_hostname, # The hostname included in the HTTP request self.request_port, # The port as included in the HTTP request bodyhash, ext])).hexdigest() raise NotImplementedError() # Todo: # 1. Recalculate the request body hash (if included in the request) as # described in Section 3.2 and request MAC as described in # Section 3.3 and compare the request MAC to the value received # from the client via the "mac" attribute. # 2. Ensure that the combination of nonce and MAC key identifier # received from the client has not been used before in a previous # request (the server MAY reject requests with stale timestamps; # the determination of staleness is left up to the server to # define). # 3. Verify the scope and validity of the MAC credentials. def _get_user(self): """The user associated with the valid access token. *django.auth.User object*""" if not self.valid: raise UnvalidatedRequest("This request is invalid or has not " "been validated.") return self.access_token.user user = property(_get_user) def _get_scope(self): """The client scope associated with the valid access token. *QuerySet of AccessRange objects.*""" if not self.valid: raise UnvalidatedRequest("This request is invalid or has not " "been validated.") return self.access_token.scope.all() scope = property(_get_scope) def _get_client(self): """The client associated with the valid access token. *oauth2app.models.Client object*""" if not self.valid: raise UnvalidatedRequest("This request is invalid or has not " "been validated.") return self.access_token.client client = property(_get_client)
[docs] def error_response(self, content='', mimetype=None, content_type=settings.DEFAULT_CONTENT_TYPE): """Error response generator. Returns a Django HttpResponse with status 401 and the approproate headers set. See Django documentation for details. https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpResponse.__init__ **Kwargs:** * *content:* See Django docs. *Default ''* * *mimetype:* See Django docs. *Default None* * *content_type:* See Django docs. *Default DEFAULT_CONTENT_TYPE* """ response = HttpResponse( content=content, mimetype=mimetype, content_type=content_type) if not self.attempted_validation: response['WWW-Authenticate'] = 'Bearer realm="%s"' % REALM response.status_code = 401 return response else: if self.error is not None: error = getattr(self.error, "error", "invalid_request") error_description = self.error.message else: error = "invalid_request" error_description = "Invalid Request." header = [ 'Bearer realm="%s"' % REALM, 'error="%s"' % error, 'error_description="%s"' % error_description] if isinstance(self.error, InsufficientScope): header.append('scope=%s' % ' '.join(self.authorized_scope)) response.status_code = 403 elif isinstance(self.error, InvalidToken): response.status_code = 401 elif isinstance(self.error, InvalidRequest): response.status_code = 400 else: response.status_code = 401 response['WWW-Authenticate'] = ', '.join(header) return response
[docs]class JSONAuthenticator(Authenticator): """Wraps Authenticator, adds support for a callback parameter and JSON related. convenience methods. **Args:** * *request:* Django HttpRequest object. **Kwargs:** * *scope:* A iterable of oauth2app.models.AccessRange objects. """ callback = None def __init__(self, scope=None): Authenticator.__init__(self, scope=scope)
[docs] def validate(self, request): self.callback = request.REQUEST.get('callback') return Authenticator.validate(self, request)
[docs] def response(self, data): """Returns a HttpResponse object of JSON serialized data. **Args:** * *data:* Object to be JSON serialized and returned. """ json_data = json.dumps(data) if self.callback is not None: json_data = "%s(%s);" % (self.callback, json_data) response = HttpResponse( content=json_data, content_type='application/json') return response
[docs] def error_response(self): """Returns a HttpResponse object of JSON error data.""" if self.error is not None: content = json.dumps({ "error":getattr(self.error, "error", "invalid_request"), "error_description":self.error.message}) else: content = ({ "error":"invalid_request", "error_description":"Invalid Request."}) if self.callback is not None: content = "%s(%s);" % (self.callback, content) response = Authenticator.error_response( self, content=content, content_type='application/json') if self.callback is not None: response.status_code = 200 return response