Search code examples
djangographqljwtgraphene-pythongraphene-django

How do I revoke JWT everytime a new token generated using django-graphql-jwt?


I am using django-graphql-jwt (https://django-graphql-jwt.domake.io/en/latest/index.html) to handle authentication for my Django Python Graphene application. Currently, everytime a new JWT generated, the previous JWT is still active as long as it does not pass its expiry time.

I want to revoke/prevent access to previously generated JWT (even if the JWT is not expired yet) whenever I generate a new JWT.

What I am thinking is utilizing the origIat inside the JWT payload and comparing it with something like a last_login attribute from the User model. I noticed though, that User.last_login is not updated whenever I am authenticating using JWT.

Still finding how to do this problem properly and wondering if there is any of you already solving this problem before.

Thanks!


Solution

  • My current solution:

    1. Add last_jwt_iat field to User model
    class User(AbstractUser):
        id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
        email = models.EmailField(unique=True, null=False, blank=False)
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
    
        last_jwt_iat = models.DateTimeField(null=True, blank=True)
    
        def __str__(self):
            return f'{self.email}'
    
    1. Subclassing graphql_jwt.relay.JSONWebTokenMutation, and update User.last_jwt_iat whenever new token is requested
    class ObtainJSONWebToken(graphql_jwt.relay.JSONWebTokenMutation):
    
        @classmethod
        def resolve(cls, root, info, **kwargs):
            # Update User.last_jwt_iat by generating current UTC timestamp
            # Probably better if we can extract the origIat from GraphQL payload
            user = info.context.user
            last_jwt_iat = datetime.datetime.utcnow()
            user.last_jwt_iat = last_jwt_iat
            user.save()
            return cls()
    
    1. Use Graphene middleware to check if User.last_jwt_iat > current JWT's iat
    def _authenticate(request):
        """
        Check if user is anonymous and JWT authorization header exist
        :param request: HttpRequest instance
        :return: Boolean
        """
        is_anonymous = not hasattr(request, 'user') or request.user.is_anonymous
        return is_anonymous and get_http_authorization(request) is not None
    
    
    class AuthCheckIat(object):
        def resolve(self, next, root, info, **args):
            context = info.context
            # Check if User.last_jwt_iat > token iat, raise error, stop evaluation
            if _authenticate(context):
                jwt_decoded = jwt_decode(get_http_authorization(context))
                username = jwt_decoded['username']
                user = User.objects.get(username=username)
                iat = datetime.datetime.fromtimestamp(jwt_decoded['origIat'], tz=datetime.timezone.utc)
                if user.last_jwt_iat > iat:
                    raise PermissionDenied(
                        'User last JWT issued time is greater than supplied JWT issued time. Please use newer token. ')
            return next(root, info, **args)