Skip to content
593 changes: 592 additions & 1 deletion README.rst

Large diffs are not rendered by default.

36 changes: 31 additions & 5 deletions rest_framework_simplejwt/authentication.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import HTTP_HEADER_ENCODING, authentication
from rest_framework import HTTP_HEADER_ENCODING, authentication, exceptions
from rest_framework.authentication import CSRFCheck

from .exceptions import AuthenticationFailed, InvalidToken, TokenError
from .settings import api_settings
Expand All @@ -16,6 +17,21 @@
)


def enforce_csrf(request):
"""
Enforce CSRF validation.
"""
def dummy_get_response(request): # pragma: no cover
return None

check = CSRFCheck(dummy_get_response) # populates request.META['CSRF_COOKIE'], which is used in process_view()
check.process_request(request)
reason = check.process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)


class JWTAuthentication(authentication.BaseAuthentication):
"""
An authentication plugin that authenticates requests through a JSON web
Expand All @@ -26,15 +42,25 @@ class JWTAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
header = self.get_header(request)
if header is None:
return None

raw_token = self.get_raw_token(header)
if not api_settings.AUTH_COOKIE:
return None
else:
raw_token = request.COOKIES.get(api_settings.AUTH_COOKIE) or None
else:
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
user = self.get_user(validated_token)
if not user or not user.is_active:
return None

if api_settings.AUTH_COOKIE:
enforce_csrf(request)

return user, validated_token

def authenticate_header(self, request):
return '{0} realm="{1}"'.format(
Expand Down
2 changes: 1 addition & 1 deletion rest_framework_simplejwt/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def encode(self, payload):
jwt_payload['iss'] = self.issuer

token = jwt.encode(jwt_payload, self.signing_key, algorithm=self.algorithm)
return token.decode('utf-8')
return token

def decode(self, token, verify=True):
"""
Expand Down
12 changes: 12 additions & 0 deletions rest_framework_simplejwt/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),

# Cookie name. Enables cookies if value is set.
'AUTH_COOKIE': None,
# A string like "example.com", or None for standard domain cookie.
'AUTH_COOKIE_DOMAIN': None,
# Whether the auth cookies should be secure (https:// only).
'AUTH_COOKIE_SECURE': False,
# The path of the auth cookie.
'AUTH_COOKIE_PATH': '/',
# Whether to set the flag restricting cookie leaks on cross-site requests.
# This can be 'Lax', 'Strict', or None to disable the flag.
'AUTH_COOKIE_SAMESITE': 'Lax',
}

IMPORT_STRINGS = (
Expand Down
141 changes: 136 additions & 5 deletions rest_framework_simplejwt/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from datetime import datetime

from django.middleware import csrf
from django.utils.translation import gettext_lazy as _
from rest_framework import generics, status
from rest_framework.exceptions import NotAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.views import APIView

from rest_framework_simplejwt.settings import api_settings
from rest_framework_simplejwt.tokens import RefreshToken
from . import serializers
from .authentication import AUTH_HEADER_TYPES
from .exceptions import InvalidToken, TokenError
Expand Down Expand Up @@ -28,10 +37,69 @@ def post(self, request, *args, **kwargs):
except TokenError as e:
raise InvalidToken(e.args[0])

return Response(serializer.validated_data, status=status.HTTP_200_OK)
response = Response(serializer.validated_data, status=status.HTTP_200_OK)

if api_settings.AUTH_COOKIE:
csrf.get_token(self.request)
response = self.set_auth_cookies(response, serializer.validated_data)

return response

def set_auth_cookies(self, response, data):
return response


class TokenObtainPairView(TokenViewBase):
class TokenRefreshViewBase(TokenViewBase):
def extract_token_from_cookie(self, request):
return request

def post(self, request, *args, **kwargs):
if api_settings.AUTH_COOKIE:
request = self.extract_token_from_cookie(request)
return super().post(request, *args, **kwargs)


class TokenCookieViewMixin:
token_refresh_view_name = 'token_refresh'

def extract_token_from_cookie(self, request):
"""Extracts token from cookie and sets it in request.data as it would be sent by the user"""
if not request.data:
token = request.COOKIES.get('{}_refresh'.format(api_settings.AUTH_COOKIE))
if not token:
raise NotAuthenticated(detail=_('Refresh cookie not set. Try to authenticate first.'))
else:
request.data['refresh'] = token
return request

def set_auth_cookies(self, response, data):
expires = self.get_refresh_token_expiration()
response.set_cookie(
api_settings.AUTH_COOKIE, data['access'],
expires=expires,
domain=api_settings.AUTH_COOKIE_DOMAIN,
path=api_settings.AUTH_COOKIE_PATH,
secure=api_settings.AUTH_COOKIE_SECURE or None,
httponly=True,
samesite=api_settings.AUTH_COOKIE_SAMESITE,
)
if 'refresh' in data:
response.set_cookie(
'{}_refresh'.format(api_settings.AUTH_COOKIE), data['refresh'],
expires=expires,
domain=None,
path=reverse(self.token_refresh_view_name),
secure=api_settings.AUTH_COOKIE_SECURE or None,
httponly=True,
samesite=api_settings.AUTH_COOKIE_SAMESITE,
)
return response

def get_refresh_token_expiration(self):
return datetime.now() + api_settings.REFRESH_TOKEN_LIFETIME


class TokenObtainPairView(TokenCookieViewMixin, TokenViewBase):
"""
Takes a set of user credentials and returns an access and refresh JSON web
token pair to prove the authentication of those credentials.
Expand All @@ -42,18 +110,48 @@ class TokenObtainPairView(TokenViewBase):
token_obtain_pair = TokenObtainPairView.as_view()


class TokenRefreshView(TokenViewBase):
class TokenRefreshView(TokenCookieViewMixin, TokenRefreshViewBase):
"""
Takes a refresh type JSON web token and returns an access type JSON web
token if the refresh token is valid.
"""
serializer_class = serializers.TokenRefreshSerializer

def get_refresh_token_expiration(self):
if api_settings.ROTATE_REFRESH_TOKENS:
return super().get_refresh_token_expiration()
token = RefreshToken(self.request.data['refresh'])
return datetime.fromtimestamp(token.payload['exp'])


token_refresh = TokenRefreshView.as_view()


class TokenObtainSlidingView(TokenViewBase):
class SlidingTokenCookieViewMixin:
def extract_token_from_cookie(self, request):
"""Extracts token from cookie and sets it in request.data as it would be sent by the user"""
if not request.data:
token = request.COOKIES.get(api_settings.AUTH_COOKIE)
if not token:
raise NotAuthenticated(detail=_('Refresh cookie not set. Try to authenticate first.'))
else:
request.data['token'] = token
return request

def set_auth_cookies(self, response, data):
response.set_cookie(
api_settings.AUTH_COOKIE, data['token'],
expires=datetime.now() + api_settings.REFRESH_TOKEN_LIFETIME,
domain=api_settings.AUTH_COOKIE_DOMAIN,
path=api_settings.AUTH_COOKIE_PATH,
secure=api_settings.AUTH_COOKIE_SECURE or None,
httponly=True,
samesite=api_settings.AUTH_COOKIE_SAMESITE,
)
return response


class TokenObtainSlidingView(SlidingTokenCookieViewMixin, TokenViewBase):
"""
Takes a set of user credentials and returns a sliding JSON web token to
prove the authentication of those credentials.
Expand All @@ -64,7 +162,7 @@ class TokenObtainSlidingView(TokenViewBase):
token_obtain_sliding = TokenObtainSlidingView.as_view()


class TokenRefreshSlidingView(TokenViewBase):
class TokenRefreshSlidingView(SlidingTokenCookieViewMixin, TokenRefreshViewBase):
"""
Takes a sliding JSON web token and returns a new, refreshed version if the
token's refresh period has not expired.
Expand All @@ -84,3 +182,36 @@ class TokenVerifyView(TokenViewBase):


token_verify = TokenVerifyView.as_view()


class TokenCookieDeleteView(APIView):
"""
Deletes httpOnly auth cookies.
Used as logout view while using AUTH_COOKIE
"""
token_refresh_view_name = 'token_refresh'
authentication_classes = ()
permission_classes = ()

def post(self, request):
response = Response({})

if api_settings.AUTH_COOKIE:
self.delete_auth_cookies(response)

return response

def delete_auth_cookies(self, response):
response.delete_cookie(
api_settings.AUTH_COOKIE,
domain=api_settings.AUTH_COOKIE_DOMAIN,
path=api_settings.AUTH_COOKIE_PATH
)
response.delete_cookie(
'{}_refresh'.format(api_settings.AUTH_COOKIE),
domain=None,
path=reverse(self.token_refresh_view_name),
)


token_delete = TokenCookieDeleteView.as_view()
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@


setup(
name='djangorestframework_simplejwt',
version='4.4.0',
name='jyve-djangorestframework_simplejwt',
version='4.4.0a2',
url='https://github.com/SimpleJWT/django-rest-framework-simplejwt',
license='MIT',
description='A minimal JSON Web Token authentication plugin for Django REST Framework',
Expand Down
Loading