aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/authentication.py
diff options
context:
space:
mode:
authorTom Christie2013-03-18 21:03:05 +0000
committerTom Christie2013-03-18 21:03:05 +0000
commit74fb366c595db87bb71baeffcacfb7d2482e3a18 (patch)
tree2e28cb52542742f32cdd3fbeb625f7f59cba0a3f /rest_framework/authentication.py
parent4c6396108704d38f534a16577de59178b1d0df3b (diff)
parent034c4ce4081dd6d15ea47fb8318754321a3faf0c (diff)
downloaddjango-rest-framework-74fb366c595db87bb71baeffcacfb7d2482e3a18.tar.bz2
Merge branch 'master' into resources-routers
Diffstat (limited to 'rest_framework/authentication.py')
-rw-r--r--rest_framework/authentication.py276
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