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

2

2

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.

Ruffianism answered 14/2, 2021 at 17:9 Comment(0)
H
2

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.

Hostetler answered 14/2, 2021 at 20:11 Comment(4)
Please seemy edit, I'm stuck in the view.Ruffianism
But there's the email left out from the token claim.Ruffianism
I updated the code with the email. You'll see a pattern: first, define the data in the serializer, fetch the data with serializer.validated_data.get() and then use this fetched data as you like.Hostetler
@anowlinorbit, is it possible to send a picture back along with the acces_token + refresh token? I am using DRF + SIMPLE JWT.Cianca
W
0
class MyObtainTokenPairView(TokenObtainPairView):
    permission_classes = (permissions.AllowAny,)
    serializer_class = MyTokenObtainPairSerializer
    
    def post(self, request, *args, **kwargs):
        response = super().post(request, *args, **kwargs)

        token = response.data["access"] # NEW LINE

        response.set_cookie('token', token, httponly=True)
        return response

You can solve it by setting the token as the "access" from the response data. Btw, did you find any better solution for this?

Whiffet answered 6/11, 2022 at 21:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.