181 lines
6.2 KiB
Python
181 lines
6.2 KiB
Python
![]() |
from typing import Optional, Set, Tuple, TypeVar
|
||
|
|
||
|
from django.contrib.auth import get_user_model
|
||
|
from django.contrib.auth.models import AbstractBaseUser
|
||
|
from django.utils.translation import gettext_lazy as _
|
||
|
from rest_framework import HTTP_HEADER_ENCODING, authentication
|
||
|
from rest_framework.request import Request
|
||
|
|
||
|
from .exceptions import AuthenticationFailed, InvalidToken, TokenError
|
||
|
from .models import TokenUser
|
||
|
from .settings import api_settings
|
||
|
from .tokens import Token
|
||
|
from .utils import get_md5_hash_password
|
||
|
|
||
|
AUTH_HEADER_TYPES = api_settings.AUTH_HEADER_TYPES
|
||
|
|
||
|
if not isinstance(api_settings.AUTH_HEADER_TYPES, (list, tuple)):
|
||
|
AUTH_HEADER_TYPES = (AUTH_HEADER_TYPES,)
|
||
|
|
||
|
AUTH_HEADER_TYPE_BYTES: Set[bytes] = {
|
||
|
h.encode(HTTP_HEADER_ENCODING) for h in AUTH_HEADER_TYPES
|
||
|
}
|
||
|
|
||
|
AuthUser = TypeVar("AuthUser", AbstractBaseUser, TokenUser)
|
||
|
|
||
|
|
||
|
class JWTAuthentication(authentication.BaseAuthentication):
|
||
|
"""
|
||
|
An authentication plugin that authenticates requests through a JSON web
|
||
|
token provided in a request header.
|
||
|
"""
|
||
|
|
||
|
www_authenticate_realm = "api"
|
||
|
media_type = "application/json"
|
||
|
|
||
|
def __init__(self, *args, **kwargs) -> None:
|
||
|
super().__init__(*args, **kwargs)
|
||
|
self.user_model = get_user_model()
|
||
|
|
||
|
def authenticate(self, request: Request) -> Optional[Tuple[AuthUser, Token]]:
|
||
|
header = self.get_header(request)
|
||
|
if header is None:
|
||
|
return None
|
||
|
|
||
|
raw_token = self.get_raw_token(header)
|
||
|
if raw_token is None:
|
||
|
return None
|
||
|
|
||
|
validated_token = self.get_validated_token(raw_token)
|
||
|
|
||
|
return self.get_user(validated_token), validated_token
|
||
|
|
||
|
def authenticate_header(self, request: Request) -> str:
|
||
|
return '{} realm="{}"'.format(
|
||
|
AUTH_HEADER_TYPES[0],
|
||
|
self.www_authenticate_realm,
|
||
|
)
|
||
|
|
||
|
def get_header(self, request: Request) -> bytes:
|
||
|
"""
|
||
|
Extracts the header containing the JSON web token from the given
|
||
|
request.
|
||
|
"""
|
||
|
header = request.META.get(api_settings.AUTH_HEADER_NAME)
|
||
|
|
||
|
if isinstance(header, str):
|
||
|
# Work around django test client oddness
|
||
|
header = header.encode(HTTP_HEADER_ENCODING)
|
||
|
|
||
|
return header
|
||
|
|
||
|
def get_raw_token(self, header: bytes) -> Optional[bytes]:
|
||
|
"""
|
||
|
Extracts an unvalidated JSON web token from the given "Authorization"
|
||
|
header value.
|
||
|
"""
|
||
|
parts = header.split()
|
||
|
|
||
|
if len(parts) == 0:
|
||
|
# Empty AUTHORIZATION header sent
|
||
|
return None
|
||
|
|
||
|
if parts[0] not in AUTH_HEADER_TYPE_BYTES:
|
||
|
# Assume the header does not contain a JSON web token
|
||
|
return None
|
||
|
|
||
|
if len(parts) != 2:
|
||
|
raise AuthenticationFailed(
|
||
|
_("Authorization header must contain two space-delimited values"),
|
||
|
code="bad_authorization_header",
|
||
|
)
|
||
|
|
||
|
return parts[1]
|
||
|
|
||
|
def get_validated_token(self, raw_token: bytes) -> Token:
|
||
|
"""
|
||
|
Validates an encoded JSON web token and returns a validated token
|
||
|
wrapper object.
|
||
|
"""
|
||
|
messages = []
|
||
|
for AuthToken in api_settings.AUTH_TOKEN_CLASSES:
|
||
|
try:
|
||
|
return AuthToken(raw_token)
|
||
|
except TokenError as e:
|
||
|
messages.append(
|
||
|
{
|
||
|
"token_class": AuthToken.__name__,
|
||
|
"token_type": AuthToken.token_type,
|
||
|
"message": e.args[0],
|
||
|
}
|
||
|
)
|
||
|
|
||
|
raise InvalidToken(
|
||
|
{
|
||
|
"detail": _("Given token not valid for any token type"),
|
||
|
"messages": messages,
|
||
|
}
|
||
|
)
|
||
|
|
||
|
def get_user(self, validated_token: Token) -> AuthUser:
|
||
|
"""
|
||
|
Attempts to find and return a user using the given validated token.
|
||
|
"""
|
||
|
try:
|
||
|
user_id = validated_token[api_settings.USER_ID_CLAIM]
|
||
|
except KeyError:
|
||
|
raise InvalidToken(_("Token contained no recognizable user identification"))
|
||
|
|
||
|
try:
|
||
|
user = self.user_model.objects.get(**{api_settings.USER_ID_FIELD: user_id})
|
||
|
except self.user_model.DoesNotExist:
|
||
|
raise AuthenticationFailed(_("User not found"), code="user_not_found")
|
||
|
|
||
|
if api_settings.CHECK_USER_IS_ACTIVE and not user.is_active:
|
||
|
raise AuthenticationFailed(_("User is inactive"), code="user_inactive")
|
||
|
|
||
|
if api_settings.CHECK_REVOKE_TOKEN:
|
||
|
if validated_token.get(
|
||
|
api_settings.REVOKE_TOKEN_CLAIM
|
||
|
) != get_md5_hash_password(user.password):
|
||
|
raise AuthenticationFailed(
|
||
|
_("The user's password has been changed."), code="password_changed"
|
||
|
)
|
||
|
|
||
|
return user
|
||
|
|
||
|
|
||
|
class JWTStatelessUserAuthentication(JWTAuthentication):
|
||
|
"""
|
||
|
An authentication plugin that authenticates requests through a JSON web
|
||
|
token provided in a request header without performing a database lookup to obtain a user instance.
|
||
|
"""
|
||
|
|
||
|
def get_user(self, validated_token: Token) -> AuthUser:
|
||
|
"""
|
||
|
Returns a stateless user object which is backed by the given validated
|
||
|
token.
|
||
|
"""
|
||
|
if api_settings.USER_ID_CLAIM not in validated_token:
|
||
|
# The TokenUser class assumes tokens will have a recognizable user
|
||
|
# identifier claim.
|
||
|
raise InvalidToken(_("Token contained no recognizable user identification"))
|
||
|
|
||
|
return api_settings.TOKEN_USER_CLASS(validated_token)
|
||
|
|
||
|
|
||
|
JWTTokenUserAuthentication = JWTStatelessUserAuthentication
|
||
|
|
||
|
|
||
|
def default_user_authentication_rule(user: AuthUser) -> bool:
|
||
|
# Prior to Django 1.10, inactive users could be authenticated with the
|
||
|
# default `ModelBackend`. As of Django 1.10, the `ModelBackend`
|
||
|
# prevents inactive users from authenticating. App designers can still
|
||
|
# allow inactive users to authenticate by opting for the new
|
||
|
# `AllowAllUsersModelBackend`. However, we explicitly prevent inactive
|
||
|
# users from authenticating to enforce a reasonable policy and provide
|
||
|
# sensible backwards compatibility with older Django versions.
|
||
|
return user is not None and (
|
||
|
not api_settings.CHECK_USER_IS_ACTIVE or user.is_active
|
||
|
)
|