22
votes

After using djangorestframework-jwt in an unsafe way for over year I've finally decided that I would like to get it working in a safer fashion.

I've read everywhere that is not good to save a JWT token in the local client (for example, local storage) and that the best solution is to use HttpOnly cookies instead.

I understood that an HttpOnly cookie is a cookie indeed, that can be saved but not read by the browser. So I thought it could be used like the following:

  • get_token: the client requests an authorization token to the server by sending user and password: if user and password are valid the server responds with an httpOnly cookie that can be stored but not read by the client.
  • Every request the client does from now on are authorized because inside the HttpOnly cookie there is a valid authorization token.
  • refresh_token: once the client needs to refresh the token, it only needs to request a refresh_token: if the sent cookie contains a valid token, the server will respond with an updated HttpOnly cookie with the new token.

I'm now trying to use djangorestframework-jwt by using HttpOnly cookie and the JWT_AUTH_COOKIE configuration seems to be the most fitting one:

You can set JWT_AUTH_COOKIE a string if you want to use http cookies in addition to the Authorization header as a valid transport for the token. The string you set here will be used as the cookie name that will be set in the response headers when requesting a token. The token validation procedure will also look into this cookie, if set. The 'Authorization' header takes precedence if both the header and the cookie are present in the request.

Default is None and no cookie is set when creating tokens nor accepted when validating them.

After giving a string value to JWT_AUTH_COOKIE I received an httpOnly cookie as expected.

The problem:

When I call refreshToken I get the following response:

{"token":["This field is required."]}

True, I'm not sending any token in the request's HEADER and that is what I want since the client isn't supposed to keep it saved anywhere.

And that is where I'm getting confused:

If i'm not wrong from now on every request the client does to the server, the cookie should be added to the request.

Shouldn't the server check the cookie after it sees that no token has been passed in the Header? How is it supposed to work if not like this?

Also posted a Github issue here if anyone wants to contribute for improvements: https://github.com/jpadilla/django-rest-framework-jwt/issues/482

2
This sounds like a frontend issue. If you're using axios you would set axios.defaults.withCredentials = true and after receiving the cookie you would have to set the headers axios.defaults.headers.common['Authorization'] = 'JWT <token>'bdoubleu
Any update on this?Tarun Lalwani
@Francesco Meli - did this ever work out for you? djangrestframework-jwt isn't behaving the way it's supposed to. I'm using TokenAuthentication as a httponly cookie, but I'm having trouble dealing with a revoked token client-side because Django returns a 401 if you send the expired token along with the request.zerohedge
@zerohedge unfortunately I haven't had time to test it yet. I'll need to test it real soon though. Please, if you find a real world working solution let us all know!Francesco Meli
Is there any way we can connect? Perhaps we can team on finding a solution to this together. I’ve been working on this for days without avail.zerohedge

2 Answers

18
votes

The issue that you observe is correct as the refresh token api has not been implemented with the cookies.

This could be a bug in the code itself. But nothing restrict you from fixing this issue.

You can patch the view to take care of cookie based auth as well. Add below code to the top of your urls.py and it will take care of the same

from rest_framework_jwt.settings import api_settings

if api_settings.JWT_AUTH_COOKIE:
    from rest_framework_jwt.authentication import JSONWebTokenAuthentication
    from rest_framework_jwt.serializers import RefreshJSONWebTokenSerializer
    from rest_framework_jwt.views import RefreshJSONWebToken

    RefreshJSONWebTokenSerializer._declared_fields.pop('token')

    class RefreshJSONWebTokenSerializerCookieBased(RefreshJSONWebTokenSerializer):
        def validate(self, attrs):
            if 'token' not in attrs:
                if api_settings.JWT_AUTH_COOKIE:
                    attrs['token'] = JSONWebTokenAuthentication().get_jwt_value(self.context['request'])
            return super(RefreshJSONWebTokenSerializerCookieBased, self).validate(attrs)

    RefreshJSONWebToken.serializer_class = RefreshJSONWebTokenSerializerCookieBased

Refresh working with cookies

5
votes

I've added this middleware to my Django (3.1):

class YankTokenRefreshFromHeaderIntoTheBody(MiddlewareMixin):
    """
    for Django Rest Framework JWT's POST "/token-refresh" endpoint --- check for a 'token' in the request.COOKIES
    and if, add it to the body payload.
    """

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        return response

    def process_view(self, request, view_func, *view_args, **view_kwargs):
        if request.path == '/v1/token-refresh' and 'token' in request.COOKIES:
            data = json.loads(request.body)
            data['token'] = request.COOKIES['token']
            request._body = json.dumps(data).encode('utf-8')
        return None

Then I added it here in my settings:

MIDDLEWARE = [
    'myproj.utils.middleware.YankTokenRefreshFromHeaderIntoTheBody',
    ...
    ...
]

And that's it. Django REST framework JWT's token-refresh endpoint will now work as it will find the 'token' key/value in there.

Few things to note:

  1. I chose 'token' as the name of the cookie holding tte JWT token. Yours may vary of course.
  2. I changed the endpoint's name to /v1/token-refresh -- You'd need to change that too if you are using the original named endpoint.