jazzband / djangorestframework-simplejwt

A JSON Web Token authentication plugin for the Django REST Framework.
https://django-rest-framework-simplejwt.readthedocs.io/
MIT License
3.98k stars 659 forks source link

Claims not being updated after token refresh #705

Open tomcajot opened 1 year ago

tomcajot commented 1 year ago

I'm currently facing an issue where, after I refresh my token, the claims aren't being updated and the value is outdated. Until I create a new token by signin a user in. I simply implemented a custom TokenObtainPairSerializer like the docs recommend. Is there a specific setting I should change in order to have the claims be regenerated when the token gets updated? Please tell me if there is any piece of code you'd like to check. Thanks.

doublo7 commented 1 year ago

I accidentally dived into the source code and found something that might be relevant to your problem.

The issue I encountered is after I set "rotate_refresh_token = True", every time when my fresh token updated with a new "iat", the corresponding access token still carry the "iat" value from the previous refresh token.

After reviewing the code, I found the issue is from tokens.py, in the class RefreshToken(BlacklistMixin, Token):


@property
    def access_token(self):
        """
        Returns an access token created from this refresh token.  Copies all
        claims present in this refresh token to the new access token except
        those claims listed in the `no_copy_claims` attribute.
        """
        access = self.access_token_class()

        # Use instantiation time of refresh token as relative timestamp for
        # access token "exp" claim.  This ensures that both a refresh and
        # access token expire relative to the same time if they are created as
        # a pair.
        access.set_exp(from_time=self.current_time)

        no_copy = self.no_copy_claims
        for claim, value in self.payload.items():
            if claim in no_copy:
                continue
            access[claim] = value

        return access

When getting the Access token after refreshing, it copy some claims from the current Refresh token.

and in the serializers.py, in the class TokenRefreshSerializer(serializers.Serializer):


def validate(self, attrs):
        refresh = self.token_class(attrs["refresh"])

        data = {"access": str(refresh.access_token)}

        if api_settings.ROTATE_REFRESH_TOKENS:
            if api_settings.BLACKLIST_AFTER_ROTATION:
                try:
                    # Attempt to blacklist the given refresh token
                    refresh.blacklist()
                except AttributeError:
                    # If blacklist app not installed, `blacklist` method will
                    # not be present
                    pass

            refresh.set_jti()
            refresh.set_exp()
            refresh.set_iat()

            data["refresh"] = str(refresh)

        return data

The access token remains some "copied" claims with the "outdated" refresh token, and doesn't renew with the "new" refresh token.

I guess there should be a possibility that the no_copy_claims could be configured. Hope this helps.

blueshack112 commented 1 week ago

Would be great if we can get some info on this by the devs. Faced the same issue recently

blueshack112 commented 1 week ago

I solved it by a custom refresh token view serializer that uses a RefreshToken subclass. The access_token function of this subclass finds the uid, gets the user from db, and recreates a new refresh token and returns the access token off to that. Wondered if there was a better way than overriding classes this drastically. I don't like that I'm creating a new refresh token out and only using it's access token. Not an expert on JWT but I would guess that there is some configuration where access and refresh are linked and this code of mine will destroy the authentication in that configuration :D

Something like "FORCE_REEVALUATE_ACCESS_ON_REFRESH" which can be enabled and clearly known to be more expensive since it makes more db calls I guess.

This is my code by the way:

class MyRefreshToken(RefreshToken):
    @property
    def access_token(self) -> typing.Optional[AccessToken]:
        """
        Returns an access token created from this refresh token.  Copies all
        claims present in this refresh token to the new access token except
        those claims listed in the `no_copy_claims` attribute.

        # TODO: Surely there is a better way to do this by avoiding overriding of classes. Couldn't find it for now
        """
        uid = self.payload.get("uid")
        if uid:
            user: User = User.objects.get(pk=uid)
            refresh = MyTokenObtainPairSerializer.get_token(user)
            return refresh.access_token

        return super().access_token

class MyTokenRefreshSerializer(TokenRefreshSerializer):
    token_class = MyRefreshToken