157 lines
6.2 KiB
Python
157 lines
6.2 KiB
Python
""" Common Authentication Handlers used across projects. """
|
|
|
|
import logging
|
|
|
|
import django.utils.timezone
|
|
from oauth2_provider import models as dot_models
|
|
from rest_framework.exceptions import AuthenticationFailed
|
|
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
|
from edx_django_utils.monitoring import set_custom_attribute
|
|
|
|
OAUTH2_TOKEN_ERROR = 'token_error'
|
|
OAUTH2_TOKEN_ERROR_EXPIRED = 'token_expired'
|
|
OAUTH2_TOKEN_ERROR_MALFORMED = 'token_malformed'
|
|
OAUTH2_TOKEN_ERROR_NONEXISTENT = 'token_nonexistent'
|
|
OAUTH2_TOKEN_ERROR_NOT_PROVIDED = 'token_not_provided'
|
|
OAUTH2_USER_NOT_ACTIVE_ERROR = 'user_not_active'
|
|
OAUTH2_USER_DISABLED_ERROR = 'user_is_disabled'
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BearerAuthentication(BaseAuthentication):
|
|
"""
|
|
BearerAuthentication backend using either `django-oauth2-provider` or 'django-oauth-toolkit'
|
|
"""
|
|
|
|
www_authenticate_realm = 'api'
|
|
|
|
# currently, active users are users that confirm their email.
|
|
# a subclass could override `allow_inactive_users` to enable access without email confirmation,
|
|
# like in the case of mobile users.
|
|
allow_inactive_users = False
|
|
|
|
def authenticate(self, request):
|
|
"""
|
|
Returns tuple (user, token) if access token authentication succeeds,
|
|
returns None if the user did not try to authenticate using an access
|
|
token, or raises an AuthenticationFailed (HTTP 401) if authentication
|
|
fails.
|
|
"""
|
|
|
|
set_custom_attribute("BearerAuthentication", "Failed") # default value
|
|
auth = get_authorization_header(request).split()
|
|
|
|
if len(auth) == 1: # lint-amnesty, pylint: disable=no-else-raise
|
|
raise AuthenticationFailed({
|
|
'error_code': OAUTH2_TOKEN_ERROR_NOT_PROVIDED,
|
|
'developer_message': 'Invalid token header. No credentials provided.'})
|
|
elif len(auth) > 2:
|
|
raise AuthenticationFailed({
|
|
'error_code': OAUTH2_TOKEN_ERROR_MALFORMED,
|
|
'developer_message': 'Invalid token header. Token string should not contain spaces.'})
|
|
|
|
if auth and auth[0].lower() == b'bearer':
|
|
access_token = auth[1].decode('utf8')
|
|
else:
|
|
set_custom_attribute("BearerAuthentication", "None")
|
|
return None
|
|
|
|
user, token = self.authenticate_credentials(access_token)
|
|
|
|
set_custom_attribute("BearerAuthentication", "Success")
|
|
|
|
return user, token
|
|
|
|
def authenticate_credentials(self, access_token):
|
|
"""
|
|
Authenticate the request, given the access token.
|
|
|
|
Overrides base class implementation to discard failure if user is
|
|
inactive.
|
|
"""
|
|
|
|
try:
|
|
token = self.get_access_token(access_token)
|
|
except AuthenticationFailed as exc:
|
|
raise AuthenticationFailed({ # lint-amnesty, pylint: disable=raise-missing-from
|
|
'error_code': OAUTH2_TOKEN_ERROR,
|
|
'developer_message': exc.detail
|
|
})
|
|
|
|
if not token: # lint-amnesty, pylint: disable=no-else-raise
|
|
raise AuthenticationFailed({
|
|
'error_code': OAUTH2_TOKEN_ERROR_NONEXISTENT,
|
|
'developer_message': 'The provided access token does not match any valid tokens.'
|
|
})
|
|
elif token.expires < django.utils.timezone.now():
|
|
raise AuthenticationFailed({
|
|
'error_code': OAUTH2_TOKEN_ERROR_EXPIRED,
|
|
'developer_message': 'The provided access token has expired and is no longer valid.',
|
|
})
|
|
else:
|
|
user = token.user
|
|
has_application = dot_models.Application.objects.filter(user_id=user.id)
|
|
if not user.has_usable_password() and not has_application:
|
|
msg = 'User disabled by admin: %s' % user.get_username()
|
|
raise AuthenticationFailed({
|
|
'error_code': OAUTH2_USER_DISABLED_ERROR,
|
|
'developer_message': msg})
|
|
|
|
# Check to make sure the users have activated their account (by confirming their email)
|
|
if not self.allow_inactive_users and not user.is_active: # lint-amnesty, pylint: disable=no-else-raise
|
|
set_custom_attribute("BearerAuthentication_user_active", False)
|
|
msg = 'User inactive or deleted: %s' % user.get_username()
|
|
raise AuthenticationFailed({
|
|
'error_code': OAUTH2_USER_NOT_ACTIVE_ERROR,
|
|
'developer_message': msg})
|
|
else:
|
|
set_custom_attribute("BearerAuthentication_user_active", True)
|
|
|
|
return user, token
|
|
|
|
def get_access_token(self, access_token):
|
|
"""
|
|
Return a valid access token stored by django-oauth-toolkit (DOT), or
|
|
None if no matching token is found.
|
|
"""
|
|
token_query = dot_models.AccessToken.objects.select_related('user')
|
|
return token_query.filter(token=access_token).first()
|
|
|
|
def authenticate_header(self, request):
|
|
"""
|
|
Return a string to be used as the value of the `WWW-Authenticate`
|
|
header in a `401 Unauthenticated` response
|
|
"""
|
|
return 'Bearer realm="%s"' % self.www_authenticate_realm
|
|
|
|
|
|
class BearerAuthenticationAllowInactiveUser(BearerAuthentication):
|
|
"""
|
|
Currently, is_active field on the user is coupled
|
|
with whether or not the user has verified ownership of their claimed email address.
|
|
Once is_active is decoupled from verified_email, we will no longer need this
|
|
class override.
|
|
|
|
This class can be used for an OAuth2-accessible endpoint that allows users to access
|
|
that endpoint without having their email verified. For example, this is used
|
|
for mobile endpoints.
|
|
"""
|
|
|
|
allow_inactive_users = True
|
|
|
|
|
|
class OAuth2Authentication(BearerAuthentication):
|
|
"""
|
|
Creating temperary class cause things outside of edx-platform need OAuth2Authentication.
|
|
This will be removed when repos outside edx-platform import BearerAuthentiction instead.
|
|
"""
|
|
|
|
|
|
class OAuth2AuthenticationAllowInactiveUser(BearerAuthenticationAllowInactiveUser):
|
|
"""
|
|
Creating temperary class cause things outside of edx-platform need OAuth2Authentication.
|
|
This will be removed when repos outside edx-platform import BearerAuthentiction instead.
|
|
"""
|