question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Single token refresh does not behave as described

See original GitHub issue

As discussed here https://github.com/flavors/django-graphql-jwt/issues/2#issuecomment-424451507, if RefreshToken is called after JWT_EXPIRATION_DELTA, an error is returned.

This makes JWT_REFRESH_EXPIRATION_DELTA redundant, since after JWT_EXPIRATION_DELTA, the token cannot be refreshed.

This behaviour is in conflict with the docs that seem to suggest (in the diagrams) that refreshing a token can still be done after JWT_EXPIRATION_DELTA. This makes a lot more sense than the current behaviour.

Possible cause

This is likely due to the get_payload function not making a distinction between tokens that have JWT_EXPIRATION_DELTA and those that have reached JWT_REFRESH_EXPIRATION_DELTA.

This seems like the bug, since it makes single token refresh pretty useless unless the client sends a refresh token every 5 minutes (or whatever JWT_EXPIRATION_DELTA is set to) regardless. That means that clients who are not active for more than 5 minutes will have to log in again, which isn’t how JWT usually works.

Instead, JWT_REFRESH_EXPIRED_HANDLER is called after get_payload.

Therefore the “Refresh has expired” exception can never be raised since “Signature has expired” will be raised first (as the refresh delta is usually longer than the expiration delta).

Possible fix

get_payload should not raise any errors until the JWT_REFRESH_EXPIRED_HANDLER has been called.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:5 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
jckwcommented, Apr 19, 2019

In case this is an issue, and other people come across it with the same problem: here’s a patch.

"""
A kind of monkey patch for 'django-graphql-jwt' until https://github.com/flavors/django-graphql-jwt/issues/91 is resolved.
"""
from django.utils.translation import ugettext as _
import jwt
import graphene
from graphene.types.generic import GenericScalar
from graphql_jwt.settings import jwt_settings
from graphql_jwt import exceptions
from graphql_jwt.utils import get_user_by_payload
from graphql_jwt.mixins import RefreshTokenMixin, JSONWebTokenMixin
from graphql_jwt.decorators import setup_jwt_cookie


def jwt_decode(token, context=None, refresh=False):
    return jwt.decode(
        token,
        jwt_settings.JWT_SECRET_KEY,
        jwt_settings.JWT_VERIFY,
        options={
            'verify_exp': False if refresh else jwt_settings.JWT_VERIFY_EXPIRATION,
        },
        leeway=jwt_settings.JWT_LEEWAY,
        audience=jwt_settings.JWT_AUDIENCE,
        issuer=jwt_settings.JWT_ISSUER,
        algorithms=[jwt_settings.JWT_ALGORITHM])


def get_payload(token, context=None, refresh=False):
    try:
        payload = jwt_decode(token, context, refresh)
    except jwt.ExpiredSignature:
        raise exceptions.JSONWebTokenExpired()
    except jwt.DecodeError:
        raise exceptions.JSONWebTokenError(_('Error decoding signature'))
    except jwt.InvalidTokenError:
        raise exceptions.JSONWebTokenError(_('Invalid token'))
    return payload


class KeepAliveRefreshMixin(object):

    class Fields:
        token = graphene.String(required=True)

    @classmethod
    @setup_jwt_cookie
    def refresh(cls, root, info, token, **kwargs):
        context = info.context
        payload = get_payload(token, context, refresh=True)
        user = get_user_by_payload(payload)
        orig_iat = payload.get('origIat')

        if not orig_iat:
            raise exceptions.JSONWebTokenError(_('origIat field is required'))

        if jwt_settings.JWT_REFRESH_EXPIRED_HANDLER(orig_iat, context):
            raise exceptions.JSONWebTokenError(_('Refresh has expired'))

        payload = jwt_settings.JWT_PAYLOAD_HANDLER(user, context)
        payload['origIat'] = orig_iat

        token = jwt_settings.JWT_ENCODE_HANDLER(payload, context)
        return cls(token=token, payload=payload)


class RefreshMixin((RefreshTokenMixin
                    if jwt_settings.JWT_LONG_RUNNING_REFRESH_TOKEN
                    else KeepAliveRefreshMixin),
                   JSONWebTokenMixin):

    payload = GenericScalar()


class Refresh(RefreshMixin, graphene.Mutation):

    class Arguments(RefreshMixin.Fields):
        """Refresh Arguments"""

    @classmethod
    def mutate(cls, *arg, **kwargs):
        return cls.refresh(*arg, **kwargs)
0reactions
themotucommented, Jul 12, 2019

I’ve begun work on implementing this without database additions on a fork. One change needs to be made where refresh time limit needs to be part of the token. Then it will work as described. PR #116

Read more comments on GitHub >

github_iconTop Results From Across the Web

What Are Refresh Tokens and How to Use Them Securely
This post will explore the concept of refresh tokens as defined by OAuth 2.0. We will learn how they compare to other token...
Read more >
A Critical Analysis of Refresh Token Rotation in Single-page ...
When a client uses a refresh token, it always receives a new refresh token for next time. As a result, refresh tokens are...
Read more >
Refresh tokens: a refresher - Stytch
When a hacker possesses an active refresh token, they have free reign to request access tokens until the refresh token expires or is...
Read more >
An in-depth look at refresh tokens in the browser
During the attack window, the attacker can steal and abuse access tokens. The security token service has no way to detect this behavior...
Read more >
Authentication and Authorization: Refresh Tokens - OCLC
A Refresh token is a string that represents an authorization that was granted to a client to use a particular set of web...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found