diff options
| author | Tom Christie | 2013-03-18 21:03:05 +0000 |
|---|---|---|
| committer | Tom Christie | 2013-03-18 21:03:05 +0000 |
| commit | 74fb366c595db87bb71baeffcacfb7d2482e3a18 (patch) | |
| tree | 2e28cb52542742f32cdd3fbeb625f7f59cba0a3f /rest_framework/authentication.py | |
| parent | 4c6396108704d38f534a16577de59178b1d0df3b (diff) | |
| parent | 034c4ce4081dd6d15ea47fb8318754321a3faf0c (diff) | |
| download | django-rest-framework-74fb366c595db87bb71baeffcacfb7d2482e3a18.tar.bz2 | |
Merge branch 'master' into resources-routers
Diffstat (limited to 'rest_framework/authentication.py')
| -rw-r--r-- | rest_framework/authentication.py | 276 |
1 files changed, 246 insertions, 30 deletions
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 30c78ebc..b4b73699 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -1,15 +1,30 @@ """ Provides a set of pluggable authentication policies. """ - +from __future__ import unicode_literals from django.contrib.auth import authenticate -from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError -from rest_framework import exceptions +from django.core.exceptions import ImproperlyConfigured +from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware +from rest_framework.compat import oauth, oauth_provider, oauth_provider_store +from rest_framework.compat import oauth2_provider, oauth2_provider_forms, oauth2_provider_backends from rest_framework.authtoken.models import Token import base64 +def get_authorization_header(request): + """ + Return request's 'Authorization:' header, as a bytestring. + + Hide some test client ickyness where the header can be unicode. + """ + auth = request.META.get('HTTP_AUTHORIZATION', b'') + if type(auth) == type(''): + # Work around django test client oddness + auth = auth.encode(HTTP_HEADER_ENCODING) + return auth + + class BaseAuthentication(object): """ All authentication classes should extend BaseAuthentication. @@ -21,40 +36,58 @@ class BaseAuthentication(object): """ raise NotImplementedError(".authenticate() must be overridden.") + def authenticate_header(self, request): + """ + Return a string to be used as the value of the `WWW-Authenticate` + header in a `401 Unauthenticated` response, or `None` if the + authentication scheme should return `403 Permission Denied` responses. + """ + pass + class BasicAuthentication(BaseAuthentication): """ HTTP Basic authentication against username/password. """ + www_authenticate_realm = 'api' def authenticate(self, request): """ Returns a `User` if a correct username and password have been supplied using HTTP Basic authentication. Otherwise returns `None`. """ - if 'HTTP_AUTHORIZATION' in request.META: - auth = request.META['HTTP_AUTHORIZATION'].split() - if len(auth) == 2 and auth[0].lower() == "basic": - try: - auth_parts = base64.b64decode(auth[1]).partition(':') - except TypeError: - return None + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != b'basic': + return None - try: - userid = smart_unicode(auth_parts[0]) - password = smart_unicode(auth_parts[2]) - except DjangoUnicodeDecodeError: - return None + if len(auth) == 1: + msg = 'Invalid basic header. No credentials provided.' + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = 'Invalid basic header. Credentials string should not contain spaces.' + raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(userid, password) + try: + auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':') + except (TypeError, UnicodeDecodeError): + msg = 'Invalid basic header. Credentials not correctly base64 encoded' + raise exceptions.AuthenticationFailed(msg) + + userid, password = auth_parts[0], auth_parts[2] + return self.authenticate_credentials(userid, password) def authenticate_credentials(self, userid, password): """ Authenticate the userid and password against username and password. """ user = authenticate(username=userid, password=password) - if user is not None and user.is_active: - return (user, None) + if user is None or not user.is_active: + raise exceptions.AuthenticationFailed('Invalid username/password') + return (user, None) + + def authenticate_header(self, request): + return 'Basic realm="%s"' % self.www_authenticate_realm class SessionAuthentication(BaseAuthentication): @@ -74,7 +107,7 @@ class SessionAuthentication(BaseAuthentication): # Unauthenticated, CSRF validation not required if not user or not user.is_active: - return + return None # Enforce CSRF validation for session based authentication. class CSRFCheck(CsrfViewMiddleware): @@ -85,7 +118,7 @@ class SessionAuthentication(BaseAuthentication): reason = CSRFCheck().process_view(http_request, None, (), {}) if reason: # CSRF failed, bail with explicit error message - raise exceptions.PermissionDenied('CSRF Failed: %s' % reason) + raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason) # CSRF passed with authenticated user return (user, None) @@ -110,16 +143,199 @@ class TokenAuthentication(BaseAuthentication): """ def authenticate(self, request): - auth = request.META.get('HTTP_AUTHORIZATION', '').split() + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != b'token': + return None + + if len(auth) == 1: + msg = 'Invalid token header. No credentials provided.' + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = 'Invalid token header. Token string should not contain spaces.' + raise exceptions.AuthenticationFailed(msg) + + return self.authenticate_credentials(auth[1]) + + def authenticate_credentials(self, key): + try: + token = self.model.objects.get(key=key) + except self.model.DoesNotExist: + raise exceptions.AuthenticationFailed('Invalid token') + + if not token.user.is_active: + raise exceptions.AuthenticationFailed('User inactive or deleted') + + return (token.user, token) + + def authenticate_header(self, request): + return 'Token' + + +class OAuthAuthentication(BaseAuthentication): + """ + OAuth 1.0a authentication backend using `django-oauth-plus` and `oauth2`. + + Note: The `oauth2` package actually provides oauth1.0a support. Urg. + We import it from the `compat` module as `oauth`. + """ + www_authenticate_realm = 'api' + + def __init__(self, *args, **kwargs): + super(OAuthAuthentication, self).__init__(*args, **kwargs) + + if oauth is None: + raise ImproperlyConfigured( + "The 'oauth2' package could not be imported." + "It is required for use with the 'OAuthAuthentication' class.") + + if oauth_provider is None: + raise ImproperlyConfigured( + "The 'django-oauth-plus' package could not be imported." + "It is required for use with the 'OAuthAuthentication' class.") + + def authenticate(self, request): + """ + Returns two-tuple of (user, token) if authentication succeeds, + or None otherwise. + """ + try: + oauth_request = oauth_provider.utils.get_oauth_request(request) + except oauth.Error as err: + raise exceptions.AuthenticationFailed(err.message) + + oauth_params = oauth_provider.consts.OAUTH_PARAMETERS_NAMES + + found = any(param for param in oauth_params if param in oauth_request) + missing = list(param for param in oauth_params if param not in oauth_request) + + if not found: + # OAuth authentication was not attempted. + return None + + if missing: + # OAuth was attempted but missing parameters. + msg = 'Missing parameters: %s' % (', '.join(missing)) + raise exceptions.AuthenticationFailed(msg) + + if not self.check_nonce(request, oauth_request): + msg = 'Nonce check failed' + raise exceptions.AuthenticationFailed(msg) + + try: + consumer_key = oauth_request.get_parameter('oauth_consumer_key') + consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key) + except oauth_provider_store.InvalidConsumerError as err: + raise exceptions.AuthenticationFailed(err) + + if consumer.status != oauth_provider.consts.ACCEPTED: + msg = 'Invalid consumer key status: %s' % consumer.get_status_display() + raise exceptions.AuthenticationFailed(msg) + + try: + token_param = oauth_request.get_parameter('oauth_token') + token = oauth_provider_store.get_access_token(request, oauth_request, consumer, token_param) + except oauth_provider_store.InvalidTokenError: + msg = 'Invalid access token: %s' % oauth_request.get_parameter('oauth_token') + raise exceptions.AuthenticationFailed(msg) + + try: + self.validate_token(request, consumer, token) + except oauth.Error as err: + raise exceptions.AuthenticationFailed(err.message) + + user = token.user + + if not user.is_active: + msg = 'User inactive or deleted: %s' % user.username + raise exceptions.AuthenticationFailed(msg) + + return (token.user, token) + + def authenticate_header(self, request): + """ + If permission is denied, return a '401 Unauthorized' response, + with an appropraite 'WWW-Authenticate' header. + """ + return 'OAuth realm="%s"' % self.www_authenticate_realm + + def validate_token(self, request, consumer, token): + """ + Check the token and raise an `oauth.Error` exception if invalid. + """ + oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request) + oauth_server.verify_request(oauth_request, consumer, token) + + def check_nonce(self, request, oauth_request): + """ + Checks nonce of request, and return True if valid. + """ + return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) + + +class OAuth2Authentication(BaseAuthentication): + """ + OAuth 2 authentication backend using `django-oauth2-provider` + """ + www_authenticate_realm = 'api' + + def __init__(self, *args, **kwargs): + super(OAuth2Authentication, self).__init__(*args, **kwargs) + + if oauth2_provider is None: + raise ImproperlyConfigured( + "The 'django-oauth2-provider' package could not be imported. " + "It is required for use with the 'OAuth2Authentication' class.") + + def authenticate(self, request): + """ + Returns two-tuple of (user, token) if authentication succeeds, + or None otherwise. + """ + + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != b'bearer': + return None - if len(auth) == 2 and auth[0].lower() == "token": - key = auth[1] - try: - token = self.model.objects.get(key=key) - except self.model.DoesNotExist: - return None + if len(auth) == 1: + msg = 'Invalid bearer header. No credentials provided.' + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = 'Invalid bearer header. Token string should not contain spaces.' + raise exceptions.AuthenticationFailed(msg) - if token.user.is_active: - return (token.user, token) + return self.authenticate_credentials(request, auth[1]) -# TODO: OAuthAuthentication + def authenticate_credentials(self, request, access_token): + """ + Authenticate the request, given the access token. + """ + + # Authenticate the client + oauth2_client_form = oauth2_provider_forms.ClientAuthForm(request.REQUEST) + if not oauth2_client_form.is_valid(): + raise exceptions.AuthenticationFailed('Client could not be validated') + client = oauth2_client_form.cleaned_data.get('client') + + # Retrieve the `OAuth2AccessToken` instance from the access_token + auth_backend = oauth2_provider_backends.AccessTokenBackend() + token = auth_backend.authenticate(access_token, client) + if token is None: + raise exceptions.AuthenticationFailed('Invalid token') + + user = token.user + + if not user.is_active: + msg = 'User inactive or deleted: %s' % user.username + raise exceptions.AuthenticationFailed(msg) + + return (token.user, token) + + def authenticate_header(self, request): + """ + Bearer is the only finalized type currently + + Check details on the `OAuth2Authentication.authenticate` method + """ + return 'Bearer realm="%s"' % self.www_authenticate_realm |
