Files
edx-platform/openedx/core/lib/api/authentication.py
Manjinder Singh cce8cdd661 Replacing OAuth2Auth (#23067)
- Removing deprecated auth classes
- Replacing OAuth2AuthenticationAllowInactiveUserDeprecated with OAuth2AuthenticationAllowInactiveUser
- the old class was a child class of a deprecated class that we are removing and replacing.
2020-02-11 10:38:13 -05:00

168 lines
6.5 KiB
Python

""" Common Authentication Handlers used across projects. """
import logging
import django.utils.timezone
from oauth2_provider import models as dot_models
from provider.oauth2 import models as dop_models
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from edx_django_utils.monitoring import set_custom_metric
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'
logger = logging.getLogger(__name__)
class OAuth2Authentication(BaseAuthentication):
"""
OAuth 2 authentication 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_metric("OAuth2Authentication", "Failed") # default value
auth = get_authorization_header(request).split()
if len(auth) == 1:
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')
set_custom_metric('OAuth2Authentication_token_location', 'bearer-in-header')
elif 'access_token' in request.POST:
access_token = request.POST['access_token']
set_custom_metric('OAuth2Authentication_token_location', 'post-token')
else:
set_custom_metric("OAuth2Authentication", "None")
return None
set_custom_metric("OAuth2Authentication_token_parts", len(access_token.split('.')))
user, token = self.authenticate_credentials(access_token)
set_custom_metric("OAuth2Authentication", "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({
u'error_code': OAUTH2_TOKEN_ERROR,
u'developer_message': exc.detail
})
if not token:
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
# 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:
set_custom_metric("OAuth2Authentication_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_metric("OAuth2Authentication_user_active", True)
return user, token
def get_access_token(self, access_token):
"""
Return a valid access token that exists in one of our OAuth2 libraries,
or None if no matching token is found.
"""
dot_token_return = self._get_dot_token(access_token)
if dot_token_return is not None:
set_custom_metric('OAuth2Authentication_token_type', 'dot')
return dot_token_return
dop_token_return = self._get_dop_token(access_token)
if dop_token_return is not None:
set_custom_metric('OAuth2Authentication_token_type', 'dop')
return dop_token_return
set_custom_metric('OAuth2Authentication_token_type', 'None')
return None
def _get_dop_token(self, access_token):
"""
Return a valid access token stored by django-oauth2-provider (DOP), or
None if no matching token is found.
"""
token_query = dop_models.AccessToken.objects.select_related('user')
return token_query.filter(token=access_token).first()
def _get_dot_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 OAuth2AuthenticationAllowInactiveUser(OAuth2Authentication):
"""
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