Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DO NOT MERGE: Temporarily persisting DIFF #610

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
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