Reputation: 366
I've been using djangorestframework-simplejwt
for a while and now I want to store the JWT in the cookies (instead of localstorage or front-end states) so that every request that the client makes, contains the token.
So did some research on it and the most relevant result I found was this stackoverflow question, in which the author is using djangorestframework-jwt
package which has a pre-configured setting for cookies called JWT_AUTH_COOKIE
. So figured switching to that package but then ended up finding out that the package is pretty much dead.
Although there is a fork for the djangorestframework-jwt
that is recommended to use instead, I was wondering is there anyway to set the JWTs in HttpOnly cookies with the djagnorestframework_simplejwt
itself?
Upvotes: 12
Views: 18618
Reputation: 101
I've searched everywhere and here is what I found. I will try to explain the whole process from setting the cookie as well as retreiving the cookie. I know I am late, but like me, there must be other people trying to find an answer. Please correct me if I have done something wrong.
SETTING THE COOKIE
In the django documentations under project configurations, you will find a that they use TokenObtainPairView.as_view()
to obtain the token. We will modify the TokenObtainPairView
in a separate views file, call it MyTokenObtainPairView
and import it in. See code below.
# urls.py
from django.urls import path
from .views import MyTokenObtainPairView
urlpatterns = [
path("token/", MyTokenObtainPairView.as_view(), name="token_obtain_pair"),
]
# views.py
from rest_framework_simplejwt.views import TokenObtainPairView
class MyTokenObtainPairView(TokenObtainPairView):
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
token = response.data["access"]
response.set_cookie("pick_a_name_you_like_for_the_cookie", token, httponly=True)
return response
You can test this in postman. Assuming you did this from the root and use localhost, the endpoint should be something like: http://localhost:8000/token/
. You should run a POST request with login credentials (usually username and password). Now, if you use postman, you should see that there is a cookie called pick_a_name_you_like_for_the_cookie
-> Read more (Documentation) - This is more for customizing the token claims.
RETREIVING TOKEN
By default the simplejwt will look in the header for the access token. Therefore, you will not be able to retreive user by using user = request.user
or add permissions such as permission_classes = [IsAuthenticated]
or @permission_classes([IsAuthenticated])
. See docs.
This is because the default authentication classes are set as:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
)
}
We will need to copy and modify the JWTAuthentication class. Create a file in the root directory called whatever you want. In this case custom_auth.py
. Copy everything from rest_framework_simplejwt/authentication.py
. You will find this file by entering the virtual environment or going directly to where pip installed rest_framework_simplejwt.
Now, assuming you've copied everything, change the following codes in custom_auth.py
from:
from .exceptions import AuthenticationFailed, InvalidToken, TokenError
from .settings import api_settings
to
from rest_framework_simplejwt.exceptions import AuthenticationFailed, InvalidToken, TokenError
from rest_framework_simplejwt.settings import api_settings
Now inside the class called JWTAuthentication
you will need to change the authentication function to something like:
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):
super().__init__(*args, **kwargs)
self.user_model = get_user_model()
def authenticate(self, request):
cookie = request.COOKIES.get("pick_a_name_you_like_for_the_cookie")
raw_token = cookie.encode(HTTP_HEADER_ENCODING)
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token
...
You should add some validations etc. as to what should be done if there is no cookie etc. Probably something like:
if cookie is None:
return None
Customize the errorhandling as you would like. Finally go back to settings.py and replace:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
)
}
with:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"custom_auth.JWTAuthentication",
)
}
Please let me know if I leave any vulnerabilities or if I could've done something different! :)
Upvotes: 6
Reputation: 2235
With httponly cookie flag and CSRF protection follow this code.
Both side very useful in mobile app and webapp..
urls.py:
...
path('login/',LoginView.as_view(),name = "login"),
...
view.py:
from rest_framework_simplejwt.tokens import RefreshToken
from django.middleware import csrf
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
return {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
class LoginView(APIView):
def post(self, request, format=None):
data = request.data
response = Response()
username = data.get('username', None)
password = data.get('password', None)
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
data = get_tokens_for_user(user)
response.set_cookie(
key = settings.SIMPLE_JWT['AUTH_COOKIE'],
value = data["access"],
expires = settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
secure = settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'],
httponly = settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'],
samesite = settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE']
)
csrf.get_token(request)
email_template = render_to_string('login_success.html',{"username":user.username})
login = EmailMultiAlternatives(
"Successfully Login",
"Successfully Login",
settings.EMAIL_HOST_USER,
[user.email],
)
login.attach_alternative(email_template, 'text/html')
login.send()
response.data = {"Success" : "Login successfully","data":data}
return response
else:
return Response({"No active" : "This account is not active!!"},status=status.HTTP_404_NOT_FOUND)
else:
return Response({"Invalid" : "Invalid username or password!!"},status=status.HTTP_404_NOT_FOUND)
authenticate.py:
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.conf import settings
from rest_framework.authentication import CSRFCheck
from rest_framework import exceptions
def enforce_csrf(request):
"""
Enforce CSRF validation.
"""
check = CSRFCheck()
# 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 CustomAuthentication(JWTAuthentication):
def authenticate(self, request):
header = self.get_header(request)
if header is None:
raw_token = request.COOKIES.get(settings.SIMPLE_JWT['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)
enforce_csrf(request)
return self.get_user(validated_token), validated_token
settings.py:
....
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'authentication.authenticate.CustomAuthentication',
),
}
SIMPLE_JWT = {
.....
'AUTH_COOKIE': 'access_token', # Cookie name. Enables cookies if value is set.
'AUTH_COOKIE_DOMAIN': None, # A string like "example.com", or None for standard domain cookie.
'AUTH_COOKIE_SECURE': False, # Whether the auth cookies should be secure (https:// only).
'AUTH_COOKIE_HTTP_ONLY' : True, # Http only cookie flag.It's not fetch by javascript.
'AUTH_COOKIE_PATH': '/', # The path of the auth cookie.
'AUTH_COOKIE_SAMESITE': 'Lax', # Whether to set the flag restricting cookie leaks on cross-site requests.
# This can be 'Lax', 'Strict', or None to disable the flag.
}
By using middleware.py:
How to authenticate by using middleware
Must :
withCredentials is True from both side..
Any doubt please comment..
Upvotes: 17
Reputation: 2451
You can do the following to store refresh token in the httpOnly cookie:
Add this to views.py:
# views.py
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from rest_framework_simplejwt.exceptions import InvalidToken
class CookieTokenRefreshSerializer(TokenRefreshSerializer):
refresh = None
def validate(self, attrs):
attrs['refresh'] = self.context['request'].COOKIES.get('refresh_token')
if attrs['refresh']:
return super().validate(attrs)
else:
raise InvalidToken('No valid token found in cookie \'refresh_token\'')
class CookieTokenObtainPairView(TokenObtainPairView):
def finalize_response(self, request, response, *args, **kwargs):
if response.data.get('refresh'):
cookie_max_age = 3600 * 24 * 14 # 14 days
response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
del response.data['refresh']
return super().finalize_response(request, response, *args, **kwargs)
class CookieTokenRefreshView(TokenRefreshView):
def finalize_response(self, request, response, *args, **kwargs):
if response.data.get('refresh'):
cookie_max_age = 3600 * 24 * 14 # 14 days
response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
del response.data['refresh']
return super().finalize_response(request, response, *args, **kwargs)
serializer_class = CookieTokenRefreshSerializer
Change the urls in url.py to use those views for token obtaining and refreshing:
# url.py
from .views import CookieTokenRefreshView, CookieTokenObtainPairView # Import the above views
# [...]
urlpatterns = [
path('auth/token/', CookieTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('auth/token/refresh/', CookieTokenRefreshView.as_view(), name='token_refresh'),
# [...]
]
Check your CORS settings if it doesn't work as expected: maybe you have to set sameSite and secure in set_cookie
Workflow - obtain token pair using credentials
Workflow - obtain access (and optional refresh) token using refresh token
POST /auth/token/refresh with the cookie set from the previous workflow, the body can be empty
In the response body you'll notice that only the 'access' key is set
If you have set ROTATE_REFRESH_TOKENS, the httpOnly cookie 'refresh_token' contains a new refresh token
Ref: https://github.com/jazzband/djangorestframework-simplejwt/issues/71#issuecomment-762927394
Upvotes: 9