Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow httpOnly cookie storage #71

Open
jaycle opened this issue Jan 26, 2019 · 65 comments
Open

Allow httpOnly cookie storage #71

jaycle opened this issue Jan 26, 2019 · 65 comments

Comments

@jaycle
Copy link

jaycle commented Jan 26, 2019

Similar to #23 but with a different motivation.

To protect against XSS, I would like the option to store the JWT in an HttpOnly cookie. django-rest-framework-jwt has this feature as an optional setting but that project I believe is abandoned and also has a vulnerability due to preventing the usage of django's CSRF token (see: jpadilla/django-rest-framework-jwt#434). Combining an HttpOnly cookie with CSRF token would be a pretty rock solid solution.

References:
https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage
https://stackoverflow.com/questions/44133536/is-it-safe-to-store-a-jwt-in-localstorage-with-reactjs

@derek-adair
Copy link

This is super important for me going forward... and it should be for everyone who uses this project. It SEEMS to be rather trivial to set the cookie.

Unsure what other side effects this may have.

@fergyfresh
Copy link
Contributor

@derek-adair I pick the cookie out in nginx and put it in the authorization header. This is a workaround, but it works for me.

@derek-adair
Copy link

I'll hopefully be submitting a PR for this in the near future!

@davesque
Copy link
Member

davesque commented Apr 9, 2019

Cool, @derek-adair . Looking forward to seeing that!

@mrodal
Copy link

mrodal commented May 27, 2019

I could give it a try too, but I'm not sure how the refresh tokens come into play with cookies.. I think they don't make sense with cookies,

  • If both are cookied, then both would be included in each request.. which takes away little secrecy the refresh token had.
  • If only one of the tokens is cookied, the other will be kept in the client, which makes it vulnerable to XSS attacks. In this case, it would make more sense to leave the access token in the client, since its lifespan is shorter

So.. I guess that in order to touch the less amount of code possible, the best solution would be to cookie both tokens, but yeah, the refresh tokens doesn't add any security here

@derek-adair
Copy link

derek-adair commented May 29, 2019

@mrodal - yes it does, when its an httpOnly cookie. This may only add an extra step, but exposing your tokens to native, insecure browser api's is not good.

https://blog.codinghorror.com/protecting-your-cookies-httponly/

@derek-adair
Copy link

derek-adair commented May 29, 2019

I'm pretty sure the scenario described is the way to go. If anyone else has a more thorough understanding of how to secure these tokens i'd love to learn about it.

While modern browsers support and prevent reading/writing via httpOnly. there are a fair amount of legacy browsers that allow for reading/writing of these headers. So this will really only slow down script kiddies, but its a pretty low effort feature that will add security.

It's my understanding that JWT is inherently insecure and requires certain steps to harden that are likely beyond the scope of this project. I'm certainly no expert on this subject so I welcome any corrections or direction on how to implement this project, or JWT in general... securely.

@mrodal
Copy link

mrodal commented May 29, 2019

what do you mean by "it does"? What I meant is that as far as i know, refresh tokens provide an extra security layer because they are less exposed (they are only used when the access token expires)

But if they are stored in cookies, both travel in each request, so I don't see how refresh tokens make it more secure

@davesque
Copy link
Member

@mrodal makes a good point. Refresh tokens are supposed to be sent over the wire less frequently (only when obtaining a new access token or access/refresh pair). It's a bit awkward in the context of web browsers, but JWTs can potentially be used by non-browser clients as well. The access/refresh dichotomy makes more sense in that broader scope.

@davesque
Copy link
Member

IIRC, another option would be that the refresh token cookie is only set for the refresh view's path. Then, it would only be sent over the wire for refresh requests in particular.

@derek-adair
Copy link

@mrodal it's my understanding that httpOnly + same site cookie is more secure than storing it in memory or localStorage (XSS vulnerability).

If the refresh token is not persisted in a cookie where would you suggest storing it?

Honestly this shit is soooo obtuse... this is the healthiest dialog i've participated around this subject. I'm almost convinced to NOT implement JWT for web browsers. However, i'm not sure the alternative for a single page app.

IIRC, another option would be that the refresh token cookie is only set for the refresh view's path. Then, it would only be sent over the wire for refresh requests in particular.

Could you elaborate? Didn't know it was possible to do this.

@derek-adair
Copy link

Does django mitigate XSS?

As someone who possesses JUST enough knowledge about security to fuck it up... i'm quite confused as to how to implement ANY jwt setup securely (in a web browser).

@mrodal
Copy link

mrodal commented May 31, 2019

If the refresh token is not persisted in a cookie where would you suggest storing it?

I would actually not use it at all when using cookies.. Since it requires extra logic in the front-end and, as I said before, I don't see the benefit from using it. This is what Im doing in a mobile app that's also going to be a spa in the future.

Take into account that storing it in the cookie makes you possibly vulnerable to CSRF attacks if you don't use CSRF tokens, since the auth token is automatically added to each request...

Does django mitigate XSS?

If you are using django templates, you are protected by default when printing data from the server side. But you could still be vulnerable if you generate content on the front-end, although last time I checked, browsers also help quite a lot to prevent these attacks.

@derek-adair
Copy link

derek-adair commented May 31, 2019

I would actually not use it at all when using cookies

So just make them re-auth when it expires and/or store the actual token for longer?

Since it requires extra logic in the front-end

Now that you mention it... man i wish i would have came to that conclusion before I implemented a replay request thing in my redux app.... ha!

This is what Im doing in a mobile app that's also going to be a spa in the future.

Right, it's clear this is where the miscommunication.

Take into account that storing it in the cookie makes you possibly vulnerable to CSRF attacks if you don't use CSRF tokens, since the auth token is automatically added to each request...

What about if you are using CSRF tokens? Should be good is my understanding.

@derek-adair
Copy link

derek-adair commented May 31, 2019

Jeeeeez. So the refresh token seems to be useless in a browser... however....

httpOnly cookie still seems essential for the access-token.

@mrodal
Copy link

mrodal commented May 31, 2019

So just make them re-auth when it expires and/or store the actual token for longer?

Make the expiration time longer.. assuming https is being used, the risk of it being captured in the middle is negligible, and since its an httpOnly cookie, from js it cant be accessed either.. the only possibility I see is an attacker gaining physical access to the device and copying the cookies, but that I believe is always present anyways

What about if you are using CSRF tokens? Should be good is my understanding

Yeah, if you are using both CSRF and httpOnly cookies for the access token you are pretty much covered

Please someone correct me if I'm missing something

@davesque
Copy link
Member

davesque commented May 31, 2019

@derek-adair This is what I was talking about with the cookie path. If the refresh token is set for the refresh view path, the browser will only include a Cookie header with the refresh token when making requests to the refresh view. So the refresh token is still used more securely and isn't sent over the wire as often as an access token.

What @mrodal is saying about CSRF also applies. Since cookies are automatically sent with any request that matches them, they don't care about what initiated that request or from where it originated. So you can't really tell if the user triggered the request intentionally. You need to include the CSRF token with the request so that Django can verify the request came from an "authorized" source e.g. a redux app hosted on a trusted server. All this stuff is bothersome but necessary. Since JWTs are still relatively new tech, they require that you think more about the nuts and bolts of auth system and why they work the way they do.

As XSS goes, Django does provide out-of-the-box protection against it, but it's not perfect. There are still certain cases in which it can be disabled whether intentionally or unintentionally. You still need to think about how it could happen and verify that the possibility doesn't apply to you.

NiyazNz added a commit to NiyazNz/django-rest-framework-simplejwt that referenced this issue Sep 7, 2019
@yoerivdm
Copy link

An HttpOnly Cookie access token combined with a Refresh on a certain url (refresh view) looks nice to me.

As for SPA-clients, you can protect against CSRF by setting the x-requested-with header and checking that one on the server (see https://markitzeroday.com/x-requested-with/cors/2017/06/29/csrf-mitigation-for-ajax-requests.html).

@davesque How would you cover both web and mobile scenario's? I have the same django rest backend for web and mobile. As for mobile, it's ok to send both tokens and store them on the device (secure storage it is). When handling a web-request I want to use the cookie tokens?

@Andrew-Chen-Wang
Copy link
Member

Andrew-Chen-Wang commented Feb 22, 2020

I am very... confused as to the purpose of this? Why would you use JWT authentication for web when you have session cookies already? So assuming this is for the web application / the website, here's my opinion:

If anything, you're going to be implementing a lot more JS which means a new point of attack (as previously mentioned in the XSS and CSRF token misplacements). Seems more like a headache to me; providing this would be a disservice.

DRF should really only be used for mobile application and occasionally be used with AJAX on an insecure endpoint. Sure, you can tack on some IP rate limit, but, in reality, you should be using a user rate limit and sending 403 forbidden back to unauthenticated users. But with a user rate limit, you're sending refresh tokens via http which can easily be eavesdropped on... so again:

What's the point of this? Feels more like a security vulnerability to users than a practical feature.

Also a reminder: I could also be interpreting this issue incorrectly :) lemme know if I did.

@yoerivdm
Copy link

@Andrew-Chen-Wang I just think we need both :-)

I have one API for both web and mobile, would like to use cookies for the websession part and jwt for API auth. If we can't differentiate the method (cookie vs jwt) then the jwt-token should be stored in a httponly cookie, not readable by js. In this case a few more measures need to be taken to further secure the token.

https://blog.logrocket.com/jwt-authentication-best-practices/

@belhassen07
Copy link

Shouldn't we store the refresh token rather than the access token inside the http only cookie. JWT access tokens are best stored in memory with a countdown to refresh using the refresh endpoint. If you store the access token in the http cookie, like this patch/plugin suggests, where do I store the refresh token.

@juhanakristian
Copy link

I liked @davesque's idea of storing both in HttpOnly cookies but setting the refresh token cookie for the refresh path only. This way there's no need to store tokens in localStorage but the refresh token isn't sent with every request.

@belhassen07
Copy link

What's the progress on this issue?

@schlunsen
Copy link

I've made a fork using a pull request allowing httponly and merged it with the current master.
You can find it here: https://github.com/schlunsen/django-rest-framework-simplejwt

@derek-adair
Copy link

DRF should really only be used for mobile application

Definitely false.

@derek-adair
Copy link

derek-adair commented May 5, 2020

@Andrew-Chen-Wang This is a really heated and opinionated subject to the point where some have sworn JWT is insecure by design.

You can read up on why you would want to store your tokens here. Honsetly not sure of the quality of that article I just really am sick to death of this subject.

EDIT:

If anything, you're going to be implementing a lot more JS

You set and read cookies. It's really not any different than any other storage method. The ideal solution would be transparent to the client and have no effect if you do not want to use secure cookies.

@Andrew-Chen-Wang
Copy link
Member

@trentmurray I think an issue with this strategy is localhost testing. I think in production, that should work (if you're setting the session cookie location to be .domain.com), but I worry about local debugging. Additionally, I wouldn't know how that'd do with unittesting with Django's Client class.

@leogout
Copy link

leogout commented Dec 1, 2020

Hi, I have been reading through the whole issue without being able to answer this question :
Is it possible to use an HttpOnly cookie to pass an access token to the backend with SimpleJWT ?
Not a refresh token, as it seems to be the issue here, but an access token.

I do not blame anyone, the answer is certainly here, but I am not able find it.

EDIT: All right I think I found the answer. For other lost people like me : NO, it is not currently possible. Although @ahmedosama5200 shared a very useful piece of code.

EDIT 2 : Ok that piece of code works great and now I am wondering why not using this method ? I can probably make a PR if you do not have time !

EDIT 3 : Well actually it does not work as expected in all cases and things are too complicated for me to understand.

@Andrew-Chen-Wang
Copy link
Member

Andrew-Chen-Wang commented Dec 1, 2020

@leogout I think I've come to the conclusion of what people want: currently Django's session middleware is implementing stateful sessions. What people want here is stateless sessions, meaning it's backed by nothing. For both, we encrypt and hit the database to make sure the user is there (ref: https://code.djangoproject.com/ticket/23011). But the statefulness of sessions middleware is we back it by a cache/database.

If we want stateless sessions, then the only move to do is have a single token. Not necessarily a refresh or access; it's just the amount of time before a generated token is invalid.

So to answer your question directly, yes but why bother?

@leogout
Copy link

leogout commented Dec 1, 2020

@Andrew-Chen-Wang My thought exactly. I just went with a simple LocalStorage + Authorization header solution.
Allowing credentials through CORS + joining the cookie is the same amount of work for a JWT or for a stateful session.
Except stateful cookie based sessions are less error prone and last longer than a JWT...

@Andrew-Chen-Wang
Copy link
Member

Hm, local storage is still not safe. Lemme elaborate throughout my learning process here:

To me, stateless authentication is still not safe in the hands of the developers since we're all so bad at following rules and standards and protocols etc. Luckily, the default in this repository is the secret key for your Django application, but if you use a weak algorithm, then you secret key can be leaked.

I've said it before but giving too much flexibility to the developer when it comes to security is not good. For example, since you mentioned localStorage, you're at risk for losing all user's confidence in a XSS scripting attack. httpOnly cookies avoid XSS for authorization details. Even if you've lost to a XSS attack with the cookies, at least they can't deal damage to ALL users' confidentiality/details, only some certain details. Twitter gets a XSS attack a bunch, but people's usernames aren't changed.

I think the localStorage option has been the default for every tutorial. I want to merge one of the two PRs for httpOnly cookies soon just to experiment and watch out for CVEs.

@navaneeth-dev
Copy link

I'm really confused. Should I use session or jwt for react app?
Also i might make a mobile app in the future, so what should i use then?

@motaz-hejaze
Copy link

motaz-hejaze commented Dec 6, 2020 via email

@Andrew-Chen-Wang
Copy link
Member

@RizeXor You should use JWT for mobile apps for security reasons (not secure connections, no csrf token, etc.). You don't have cookies to set in transit for mobile apps, so you use JWT.

When it comes to a ReactJS app, the current implementation method would be JWT. There's already a repository set up for you that you can download as a template so that you don't need to set it up yourself. Ref #263

@LoranKloeze
Copy link

LoranKloeze commented Jan 19, 2021

To implement this for now as a workaround, you can do the following.

Add this to views.py:

# views.py
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from rest_framework_simplejwt.exceptions import InvalidToken

class CookieTokenRefreshSerializer(TokenRefreshSerializer):
    refresh = None
    def validate(self, attrs):
        attrs['refresh'] = self.context['request'].COOKIES.get('refresh_token')
        if attrs['refresh']:
            return super().validate(attrs)
        else:
            raise InvalidToken('No valid token found in cookie \'refresh_token\'')

class CookieTokenObtainPairView(TokenObtainPairView):
  def finalize_response(self, request, response, *args, **kwargs):
    if response.data.get('refresh'):
        cookie_max_age = 3600 * 24 * 14 # 14 days
        response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
        del response.data['refresh']
    return super().finalize_response(request, response, *args, **kwargs)

class CookieTokenRefreshView(TokenRefreshView):
    def finalize_response(self, request, response, *args, **kwargs):
        if response.data.get('refresh'):
            cookie_max_age = 3600 * 24 * 14 # 14 days
            response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
            del response.data['refresh']
        return super().finalize_response(request, response, *args, **kwargs)
    serializer_class = CookieTokenRefreshSerializer

Change the urls in url.py to use those views for token obtaining and refreshing:

# url.py
from .views import CookieTokenRefreshView, CookieTokenObtainPairView # Import the above views
# [...]
urlpatterns = [
    path('auth/token/', CookieTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('auth/token/refresh/', CookieTokenRefreshView.as_view(), name='token_refresh'),
    # [...]
]

Check your CORS settings if it doesn't work as expected: maybe you have to set sameSite and secure in set_cookie

Workflow - obtain token pair using credentials

  1. POST /auth/token with valid credentials
  2. In the response body you'll notice that only the 'access' key is set
  3. The 'refresh' key has been moved to the httpOnly cookie named 'refresh_token'

Workflow - obtain access (and optional refresh) token using refresh token

  1. POST /auth/token/refresh with the cookie set from the previous workflow, the body can be empty
  2. In the response body you'll notice that only the 'access' key is set
  3. If you have set ROTATE_REFRESH_TOKENS, the httpOnly cookie 'refresh_token' contains a new refresh token

@Andrew-Chen-Wang
Copy link
Member

Andrew-Chen-Wang commented Jan 19, 2021

Please ref #360 and a specific comment made by stunaz ;) as I'm planning on shutting these ideas, issues, and PRs down.

@mehdithreem
Copy link

mehdithreem commented Feb 19, 2021

To implement this for now as a workaround, you can do the following.

Add this to views.py:

# views.py
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from rest_framework_simplejwt.exceptions import InvalidToken

class CookieTokenRefreshSerializer(TokenRefreshSerializer):
    refresh = None
    def validate(self, attrs):
        attrs['refresh'] = self.context['request'].COOKIES.get('refresh_token')
        if attrs['refresh']:
            return super().validate(attrs)
        else:
            raise InvalidToken('No valid token found in cookie \'refresh_token\'')

class CookieTokenObtainPairView(TokenObtainPairView):
  def finalize_response(self, request, response, *args, **kwargs):
    if response.data.get('refresh'):
        cookie_max_age = 3600 * 24 * 14 # 14 days
        response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
        del response.data['refresh']
    return super().finalize_response(request, response, *args, **kwargs)

class CookieTokenRefreshView(TokenRefreshView):
    def finalize_response(self, request, response, *args, **kwargs):
        if response.data.get('refresh'):
            cookie_max_age = 3600 * 24 * 14 # 14 days
            response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
            del response.data['refresh']
        return super().finalize_response(request, response, *args, **kwargs)
    serializer_class = CookieTokenRefreshSerializer

I think you should override CookieTokenRefreshView's serializer with CookieTokenRefreshSerializer.

@iamr0b0tx
Copy link

To implement this for now as a workaround, you can do the following.

Add this to views.py:

# views.py
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from rest_framework_simplejwt.exceptions import InvalidToken

class CookieTokenRefreshSerializer(TokenRefreshSerializer):
    refresh = None
    def validate(self, attrs):
        attrs['refresh'] = self.context['request'].COOKIES.get('refresh_token')
        if attrs['refresh']:
            return super().validate(attrs)
        else:
            raise InvalidToken('No valid token found in cookie \'refresh_token\'')

class CookieTokenObtainPairView(TokenObtainPairView):
  def finalize_response(self, request, response, *args, **kwargs):
    if response.data.get('refresh'):
        cookie_max_age = 3600 * 24 * 14 # 14 days
        response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
        del response.data['refresh']
    return super().finalize_response(request, response, *args, **kwargs)

class CookieTokenRefreshView(TokenRefreshView):
    def finalize_response(self, request, response, *args, **kwargs):
        if response.data.get('refresh'):
            cookie_max_age = 3600 * 24 * 14 # 14 days
            response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
            del response.data['refresh']
        return super().finalize_response(request, response, *args, **kwargs)
    serializer_class = CookieTokenRefreshSerializer

Change the urls in url.py to use those views for token obtaining and refreshing:

# url.py
from .views import CookieTokenRefreshView, CookieTokenObtainPairView # Import the above views
# [...]
urlpatterns = [
    path('auth/token/', CookieTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('auth/token/refresh/', CookieTokenRefreshView.as_view(), name='token_refresh'),
    # [...]
]

Check your CORS settings if it doesn't work as expected: maybe you have to set sameSite and secure in set_cookie

Workflow - obtain token pair using credentials

  1. POST /auth/token with valid credentials
  2. In the response body you'll notice that only the 'access' key is set
  3. The 'refresh' key has been moved to the httpOnly cookie named 'refresh_token'

Workflow - obtain access (and optional refresh) token using refresh token

  1. POST /auth/token/refresh with the cookie set from the previous workflow, the body can be empty
  2. In the response body you'll notice that only the 'access' key is set
  3. If you have set ROTATE_REFRESH_TOKENS, the httpOnly cookie 'refresh_token' contains a new refresh token

This looks well and good but is the CSRF token not necessary for methods like POST, PUT?
here is an implementation of JWT + CSRF token + Cookies (HttpOnly) LocalStorage vs Cookies: All You Need To Know About Storing JWT Tokens Securely in The Front-End

@hsb-tonmoy
Copy link

To implement this for now as a workaround, you can do the following.
Add this to views.py:

# views.py
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from rest_framework_simplejwt.exceptions import InvalidToken

class CookieTokenRefreshSerializer(TokenRefreshSerializer):
    refresh = None
    def validate(self, attrs):
        attrs['refresh'] = self.context['request'].COOKIES.get('refresh_token')
        if attrs['refresh']:
            return super().validate(attrs)
        else:
            raise InvalidToken('No valid token found in cookie \'refresh_token\'')

class CookieTokenObtainPairView(TokenObtainPairView):
  def finalize_response(self, request, response, *args, **kwargs):
    if response.data.get('refresh'):
        cookie_max_age = 3600 * 24 * 14 # 14 days
        response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
        del response.data['refresh']
    return super().finalize_response(request, response, *args, **kwargs)

class CookieTokenRefreshView(TokenRefreshView):
    def finalize_response(self, request, response, *args, **kwargs):
        if response.data.get('refresh'):
            cookie_max_age = 3600 * 24 * 14 # 14 days
            response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
            del response.data['refresh']
        return super().finalize_response(request, response, *args, **kwargs)
    serializer_class = CookieTokenRefreshSerializer

Change the urls in url.py to use those views for token obtaining and refreshing:

# url.py
from .views import CookieTokenRefreshView, CookieTokenObtainPairView # Import the above views
# [...]
urlpatterns = [
    path('auth/token/', CookieTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('auth/token/refresh/', CookieTokenRefreshView.as_view(), name='token_refresh'),
    # [...]
]

Check your CORS settings if it doesn't work as expected: maybe you have to set sameSite and secure in set_cookie
Workflow - obtain token pair using credentials

  1. POST /auth/token with valid credentials
  2. In the response body you'll notice that only the 'access' key is set
  3. The 'refresh' key has been moved to the httpOnly cookie named 'refresh_token'

Workflow - obtain access (and optional refresh) token using refresh token

  1. POST /auth/token/refresh with the cookie set from the previous workflow, the body can be empty
  2. In the response body you'll notice that only the 'access' key is set
  3. If you have set ROTATE_REFRESH_TOKENS, the httpOnly cookie 'refresh_token' contains a new refresh token

This looks well and good but is the CSRF token not necessary for methods like POST, PUT?
here is an implementation of JWT + CSRF token + Cookies (HttpOnly) LocalStorage vs Cookies: All You Need To Know About Storing JWT Tokens Securely in The Front-End

Hey, I was wondering if you could share how you modified these views and the frontend to implement CSRF as well.

@abetkin
Copy link

abetkin commented May 12, 2022

@davesque Besides the refresh-token via cookie being more secure there is another important point. Jwt usecase is as follows: an authorization service + regular services. The former issues tokens, the latter use access tokens. What if there is multiple regular services and they belong to the same organization? And that organization doesn't want to ask the user to log-in multiple times?

So you have to pass the refresh token to every client (for every "regular service" that you have). And require the user to log-in multiple times. Which could be solved otherwise by setting a cookie for the auth service.

@michaelurban
Copy link

This issue has been open for going on four years.

  1. Why is no one assigned?
  2. Who is the point of contact for this issue on the project?
  3. Why hasn’t this been implemented?
  4. If it was implemented would it be merged?

This is a key feature for any server-side JWT library. I can’t imagine using JWT in the browser using anything but httpOnly cookies.

@mohit56779
Copy link

any update on this ?

@zhenfeng-cao
Copy link

any update on this? thanks.

@famdude
Copy link

famdude commented Jan 12, 2023

Are you kidding me?! After about four years, this critical feature has not been added?!
So what's the point of this whole JWT shit?!
@Andrew-Chen-Wang

@Andrew-Chen-Wang
Copy link
Member

To implement this for now as a workaround, you can do the following.

Add this to views.py:

# views.py
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from rest_framework_simplejwt.exceptions import InvalidToken

class CookieTokenRefreshSerializer(TokenRefreshSerializer):
    refresh = None
    def validate(self, attrs):
        attrs['refresh'] = self.context['request'].COOKIES.get('refresh_token')
        if attrs['refresh']:
            return super().validate(attrs)
        else:
            raise InvalidToken('No valid token found in cookie \'refresh_token\'')

class CookieTokenObtainPairView(TokenObtainPairView):
  def finalize_response(self, request, response, *args, **kwargs):
    if response.data.get('refresh'):
        cookie_max_age = 3600 * 24 * 14 # 14 days
        response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
        del response.data['refresh']
    return super().finalize_response(request, response, *args, **kwargs)

class CookieTokenRefreshView(TokenRefreshView):
    def finalize_response(self, request, response, *args, **kwargs):
        if response.data.get('refresh'):
            cookie_max_age = 3600 * 24 * 14 # 14 days
            response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
            del response.data['refresh']
        return super().finalize_response(request, response, *args, **kwargs)
    serializer_class = CookieTokenRefreshSerializer

Change the urls in url.py to use those views for token obtaining and refreshing:

# url.py
from .views import CookieTokenRefreshView, CookieTokenObtainPairView # Import the above views
# [...]
urlpatterns = [
    path('auth/token/', CookieTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('auth/token/refresh/', CookieTokenRefreshView.as_view(), name='token_refresh'),
    # [...]
]

Check your CORS settings if it doesn't work as expected: maybe you have to set sameSite and secure in set_cookie

Workflow - obtain token pair using credentials

  1. POST /auth/token with valid credentials
  2. In the response body you'll notice that only the 'access' key is set
  3. The 'refresh' key has been moved to the httpOnly cookie named 'refresh_token'

Workflow - obtain access (and optional refresh) token using refresh token

  1. POST /auth/token/refresh with the cookie set from the previous workflow, the body can be empty
  2. In the response body you'll notice that only the 'access' key is set
  3. If you have set ROTATE_REFRESH_TOKENS, the httpOnly cookie 'refresh_token' contains a new refresh token

Refer to this

@michaelurban
Copy link

michaelurban commented Jan 12, 2023

I commented about this last August, waited a month and then implemented my own JWT app. After reviewing drf-sjwt's code I came to the conclusion that it shouldn't be used in production. Even when I was looking for code to copy and paste as a starting point for a fork I couldn't find anything.

It's been a few months so I can't point to all the questionable code, but, look for yourself. Here's a "for example" tho: drf-sjwt's config code uses a non-public DRF code that DRF explicitly flags as internal: Here's the note from DRF and here's where it's used in drf-sjwt. It looked like it was working but I was able to break it during testing.

My advice, roll you own. It took me about three months to create/test configurable auth/RBAC/ABAC+registration apps based on simplejwt. It wasn't bad.

@moe-salek
Copy link

It's been years. Any update?

@michaelurban
Copy link

michaelurban commented Aug 29, 2023

@MohammadSalek After reading the source and taking into account issues like this one, I came to the conclusion that djangorestframework-simplejwt should not be used in production and built my own JWT auth library. Even something as fundamental as the way drf-simplejwt handles settings disregards the explicit warnings of the DRF project.

See my comment above for more details.

@takuonline
Copy link

any update on this? thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests