Search code examples
djangocookiesdjango-rest-frameworkdjango-rest-framework-simplejwtcookie-httponly

Django REST: How do i return SimpleJWT access and refresh tokens as HttpOnly cookies with custom claims?


I want to send the SimpleJWT access and refresh tokens through HttpOnly cookie. I have customized the claim. I have defined a post() method in the MyObtainTokenPairView(TokenObtainPairView) in which I am setting the cookie. This is my code:

from .models import CustomUser

class MyObtainTokenPairView(TokenObtainPairView):
    permission_classes = (permissions.AllowAny,)
    serializer_class = MyTokenObtainPairSerializer
    
    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class()
        response = Response()
        tokens = serializer.get_token(CustomUser)
        access = tokens.access
        response.set_cookie('token', access, httponly=True)
        return response   

It's returning this error:

AttributeError: 'RefreshToken' object has no attribute 'access'

The serializer:

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
    
    @classmethod
    def get_token(cls, user):
        print(type(user))
        token = super().get_token(user)
        token['email'] = user.email
        return token

But it's just not working. I think I should not define a post() method here like this. I think if I can only return the value of the get_token() function in the serializer, I could set it as HttpOnly cookie. But, I don't know how to do that.

How do I set the access and refresh tokens in the HttpOnly cookie?

EDIT: I made these changes following anowlinorbit's answer:

I changed my serializer to this:

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
    
    def validate(self, attrs):
        attrs = super().validate(attrs)
        token = self.get_token(self.user)
        token["email"] = self.user.email
        return token

Since this token contains the refresh token by default therefore, I decided that returning only this token would provide both access and refresh token. If I add anything like token["access"] = str(token.access_token) it would just add the access token string inside the refresh token string, which it already contains.

But again in the view, I could not find how to get the refresh token. I could not get it using serializer.validated_data.get('refresh', None) since now I am returning the token from serializer which contains everything.

I changed my view to this:

class MyObtainTokenPairView(TokenObtainPairView):
    permission_classes = (permissions.AllowAny,)
    serializer_class = MyTokenObtainPairSerializer
    
    def post(self, request, *args, **kwargs):
        response = super().post(request, *args, **kwargs)
        response.set_cookie('token', token, httponly=True)
        return response

Now it's saying:

NameError: name 'token' is not defined

What's wrong here? In the view I want to get the token returned from serializer, then get the acces token using token.access_token and set both refresh and access as cookies.


Solution

  • I would leave .get_token() alone and instead focus on .validate(). In your MyTokenObtainPairSerializer I would remove your changes to .get_token() and add the following

    def validate(self, attrs):
        data = super().validate(attrs)
        refresh = self.get_token(self.user)
        data["refresh"] = str(refresh)   # comment out if you don't want this
        data["access"] = str(refresh.access_token)
        data["email"] = self.user.email
    
        """ Add extra responses here should you wish
        data["userid"] = self.user.id
        data["my_favourite_bird"] = "Jack Snipe"
        """
        return data
    

    It is by using the .validate() method with which you can choose which data you wish to return from the serializer object's validated_data attribute. N.B. I have also included the refresh token in the data which the serializer returns. Having both a refresh and access token is important. If a user doesn't have the refresh token they will have to login again when the access token expires. The refresh token allows them to get a new access token without having to login again.

    If for whatever reason you don't want the refresh token, remove it from your validate() serializer method and adjust the view accordingly.

    In this post method, we validate the serializer and access its validated data.

    def post(self, request, *args, **kwargs):
        # you need to instantiate the serializer with the request data
        serializer = self.serializer(data=request.data)
        # you must call .is_valid() before accessing validated_data
        serializer.is_valid(raise_exception=True)  
    
        # get access and refresh tokens to do what you like with
        access = serializer.validated_data.get("access", None)
        refresh = serializer.validated_data.get("refresh", None)
        email = serializer.validated_data.get("email", None)
    
        # build your response and set cookie
        if access is not None:
            response = Response({"access": access, "refresh": refresh, "email": email}, status=200)
            response.set_cookie('token', access, httponly=True)
            response.set_cookie('refresh', refresh, httponly=True)
            response.set_cookie('email', email, httponly=True)
            return response
    
        return Response({"Error": "Something went wrong", status=400)
    

    If you didn't want the refresh token, you would remove the line beginning refresh = and remove the line where you add the refresh cookie.