How to secure JWT tokens with a DRF backend supporting both mobile and SPA clients?

I am developing an application that uses the Django REST Framework to provide a REST API.

I intend to protect it using token authentication, with the help of simple_jwt.

Example API overview:

  • /auth/login/: Supports POST, requires valid username and password, returns a JWT access and refresh
  • /auth/refresh/: Supports POST, requires a valid refresh token, returns new access and refresh tokens
  • /protected/endpoint/: Requires a valid access token

From reading on CSRF I have gathered:

  • CSRF is only needed if cookies are involved, so only use for the SPA client
  • To persist the JWT token:
    • SPA: Persist in an httpOnly cookie with secure=true and same-origin set
    • App: Persist in however the app persistent state works

So far so good, however, this would mean that my REST API needs to both require CSRF tokens for requests coming from the SPA and not require/ignore CSRF tokens for requests coming from mobile app clients.

Since this is logically impossible I was first thinking of implementing 2 separate APIs. I.e.:

  • /spa/auth/..., /spa/protected/...: For SPA clients, where all endpoints require a CSRF token
  • /mobile/auth/..., /mobile/protected/...: For mobile clients, where all endpoints are CSRF exempt.

But doing this just means that my CSRF protection is useless, since a malicious actor can just target my mobile API instead of my SPA API.

I also read about a bit about CORS but I am not sure how it fits into all of this.

Questions:

  • Am I correct CSFR is only needed when cookies are involved?
  • If the answer to the previous question is "yes", can I avoid storing the JWT token in a cookie altogether for SPA, but still somehow securely persist SPA login across browser sessions and page refreshes?
  • If the answer to the previous question is "no", then how can I support both types of clients while also preventing CSRF attacks?
  • If I cannot get around CSRF tokens for SPA, how should I store the CSRF cookie (do I even need it to be in a cookie?)? I assume it will need at least same-origin to be set, to avoid leaking it to other domains, but does it also need to be httpOnly and secure=true? I've read conflicting opinions on that.
  • How should I implement CSRF with DRF? The documentation for SessionAuthentication mentions CSRF but it is vague and even states that the default DRF CSRF implementation is not suitable for login views.
  • Where does CORS fit into all of this? Can I use it to fix the problems I have?

EDIT:

I've thought about this more - what if I subclass the CSRF middleware and allow it to be bypassed if there is an Authorization header present? Then the request can be granted or denied by the authentication middleware directly (effectively I am assuming that if there is an Authorization header I am dealing with a mobile client, which does not need CSRF protection).

Is that a bad idea?

In case it isn't, then how should the LoginView look like? It should still be protected against CSRF but neither the mobile, nor the SPA clients will have a token to set as the Authorizaton header...

I wasn't aiming for a self-answer, but after more research and a lack of useful responses from people here I have settled on the following approach:

  • I shall set a strict CORS policy to only allow requests from the same domain
  • I shall set the access token validity to a small period (probably 60s). Refresh token validity shall be configurable from a few hours to a few days
  • I shall require a custom authentication header to be present for all requests. For example something in the form of X-MyApp-Auth: Bearer <token>
  • For login CSRF prevention I shall require the authentication header to contain a base64 encoded <userid>:<password> pair (HTTP Basic Auth) instead of a JWT token. On successful authentication, I shall return a JWT access and refresh tokens in the response body.
  • For access token refreshing I shall have the same workflow but require a valid refresh token to be present in the authentication header

Additionally:

  • I shall check for an additional custom header to determine the client type (for example X-MyApp-ClientType: SPA/mobile), failing that I shall check if the Origin header is present in the request and if it is I shall consider the client to be a SPA.
  • If the client is a mobile app, the workflow specified above applies
  • If the client is an SPA:
    • On successful login, I shall return the access token in the response body but set the refresh token as an HttpOnly cookie (with same-origin set and secure=true set unless the server is in debug mode)
    • On refresh endpoint request I will require the authentication header to be present in the request but disregard its value and take the refresh token from the HttpOnly cookie.

Token storage:

  • SPA: Access token in SPA memory only. Refresh token in HttpOnly cookie. On page refresh the access token is lost and a new one must be requested from the server.
  • Mobile app: Access and refresh tokens securely stored in the app persistent storage according to platform reccomendations.

Rationale

  • Most (if not all) modern browsers implement 'Same-origin' policy which requires a successful preflight check before allowing a custom header to be sent via an 'unsafe' (as defined by RFC 9110) cross-origin HTTP request. Thus, if CORS is set up correctly it can be guaranteed no CSRF attack has happened if custom headers are present in the request
  • The HttpOnly cookie is for additional security in SPA clients to prevent any chance of accidentally leaking a long-lived refresh token via JS
Back to Top