Authentication with Django and Next.js

If you want to use React in a Django application, often the easiest way to do that is to separate out the frontend and backend into a React-based single page application with a Django-powered API. Why? The day you introduce React to a monolithic Django application is the day you enter the world of Webpack and Django Webpack loader. These are great tools but can require considerable overhead to get set up correctly for local development and can add complication to the deployment process. Going down the path of the single-page application -- using something like Next.js or Create React App -- results in a much simpler setup. You get to leverage the great tooling that both frameworks provide while also keeping most of what makes Django great. And deployment is a breeze: you can deploy the Next.js app with something like Vercel or Netlify and the Django API can be deployed to Heroku or AWS easily. Yes, you have to think in terms of REST and API endpoints instead of just giving your Django templates their context on the server but Django REST Framework makes this really easy. Overall, I'd argue that this setup is a pretty great developer experience (at least for one that relies on a Javascript framework) for a full stack developer or a small team of frontend and backend developers.

Well Except for One Thing

The main hurdle with a setup like this: authentication. When you split out the frontend from the backend, one of the hardest things to part ways with is Django's built-in session authentication. Back when you were just rendering good old-fashioned HTML with Django, authentication was something you barely had to think about: it just worked. Now, with your SPA, Django's session cookie that unassumingly did so much work for you in the past has been put into retirement.

JWT Authentication

So, what do you do instead? As you will see on the authentication section of the Django REST Framework docs, there are many different ways to do authentication in a REST API but only a few that make sense with a SPA. Of those, one of the easiest and most common ways is using JSON web tokens (JWTs). There are a ton of libraries and resources for generating JWTs but best practices for storing them and securing them is surprisingly unclear. The main source of tension is that you have to store the tokens securely while also likely wanting users to have long-lasting sessions rather than logging them out after short periods of time. Achieving both is surprisingly difficult (again, especially if you come from the Django world where this was all done for you). I recently came across this article by Hasura that gives as good and as thorough an overview as I've found of how to securely persist sessions with JWTs. The rest of this post will attempt to translate those guidelines to a Next.js application powered by a Django API.

The link to the repo containing the full application can be found here.

JWT Overview

I'd recommend reading the article in its entirety but here’s a quick overview of some of the key points. Because of the stateless nature of JWTs, if an attacker were to gain access to a user's token, there would be no way for the backend to know its source or that it was stolen. So, our goals are to store the access token in such a way to make it hard to steal. And secondly, in case it does get stolen, the token expiry should be very short so that the window it can be used by an attacker is very narrow. Something like 15 minutes. Obviously, we don't want users to be logged out after 15 minutes so we generate new tokens using a second token: a refresh token. The refresh token has a much longer expiry but also additional precautions. That leads us to how the tokens are stored. To prevent against XSS attacks, the article recommends against using the browser localstorage to store the tokens. Instead, we store the access token as a variable in javascript. And we store the refresh token as a cookie. Storing the refresh token in a cookie is not entirely safe from XSS attacks but safer than storing it in localstorage plus they can be blacklisted from generating new access tokens if necessary. So, in a worst-case scenario, the stolen refresh token could be effectively remotely disabled.

Django Endpoints for Managing Tokens

So how do we translate this into Django API endpoints? There are two libraries that on first glance seem to do this but on closer inspection only get us part of the way there. Django REST Framework Simple JWT provides interfaces with the PYJWT library and provides DRF endpoints for managing tokens however it doesn't provide any support for setting the refresh token as a cookie. There's a discussion on their Github page about why that is. As well, there is the dj-rest-auth library which optionally supports JWT and actually uses the simplejwt library under the hood and supports setting the access token as a cookie however it doesn't provide support for refreshing tokens. So, neither of these libraries implement the approach describe in the Hasura article out-the-box but with some modifications, the simplejwt library can be extended to provide the endpoints we need. Here is an overview of those backend endpoints:

Login

This endpoint calls Django's authentication system using the provided credentials and, if successful, generates an access token and a refresh token and returns the access token and its expiry in the JSON response and sets the refresh token as a HttpOnly cookie in the response.

Refresh

This endpoint checks that the refresh token contained in the cookie is valid and generates a new access token and returns the access token as well as its expiry in the JSON response.

Logout

This endpoint removes the refresh token cookie and blacklists the refresh token in the database.

Authentication Context for Next.js

The backend is doing most of the work for managing the JWTs but the frontend client still has three main responsibilities: 1) the flow for logging in and storing the access token in a variable (not in local storage), 2) the flow for silently refreshing the token so that the user's sessions can last a long time, and 3) the logout flow. Because authentication can be considered "global", React context and the useContext hook are a good fit for managing the refresh token flow. You can see the full code here. Everything is done within the context provider and any Next.js page that needs to perform authentication tasks or read authentication status can just import useAuth to access that context.

The Login Page

On the login page (in Next, pages/login.tsx), we call the login function which useAuth exposes, which does the following:

  • calls the Django API with the username and password
  • stores the returned access token in a state variable
  • sets the isAuthenticated variable
  • calls the /me/ API endpoint to get the user's details. This is an optional step. You could include any user information you want in the JWT claims and have one less API call but I prefer to do it this way to keep the token small in size and I find it makes adding or updating fields on the user model easier to manage when they are only interfaced through the endpoint instead of the token claims

Now, any page in Next can just access the isAuthenticated flag available from useAuth in order to block access to unauthenticated users.

The Token Refresh Process

The login API call response includes the access token and access token expiry. We included a getToken helper function as part of the auth context and we use this to retrieve the access token for any API calls to our backend. The getToken function just pulls the token from the state variable if it's available and not expired. If it's expired, the function fetches a new one from the API using the refresh endpoint. We need to make sure to set credentials: "include" in the fetch call in order for the refresh token cookie that was set when we make the login API call to be sent. Another approach which is implied in the Hasura article is to use setTimeout to fetch the token in the background on a timer. I personally find the on-demand method a little less messy in React but using a timer can be a slicker option if you can make it work.

The app follows a similar workflow when opened in a new browser tab after the user has already logged in but closed their tab. Since there’s no access token, the useAuth context initializes by fetching a new token using the refresh endpoint. Any pages that require authentication can then wait for that API call to succeed and the isAuthenticated flag to be set to true before rendering the page.

Deployment Considerations

As I mentioned at the beginning, one of the selling points of this setup is that it makes deploying the app easy. The example app is set up to be deployed to Vercel and Heroku for the frontend and backend respectively with only modifications to environmental variables required. The Github repo lists all of them but some additional info for auth-related ones are below.

Whether the Next.js and Django API are deployed to different subdomains on the same second level domain (so something like Next.js -> www.example.com, Django -> api.example.com) or completely separate domains (www.example.com and www.exampleapi.com) will affect how the refresh token cookie settings should be configured. This is because the former configuration results in requests that are considered same-site which allows us to set the SameSite attribute in the cookie to Lax. Otherwise, we need to set the SameSite to None. Setting SameSite to None also necessitates setting the Secure cookie attribute.

Backend Deployment Steps

The steps for deploying the Django API to Heroku are straightforward and based off of the example tutorial on Heroku’s website:

  1. Create a new app on Heroku
  2. Add Heroku Postgres as an add-on
  3. Connect the app to your Github repo
  4. Update the config variables (see below)
  5. On the Deploy tab in Heroku, trigger a deploy manually from Github (or switch on automatic deploys if you want)

Backend Deployment Settings

  • JWT_COOKIE_NAME: The name of the refresh cookie. Defaults to refresh_token.
  • JWT_COOKIE_SECURE: Whether or not the cookie should be set as secure. This needs to be set to true if SameSite is set to None.
  • JWT_COOKIE_SAMESITE: This should be set to true if the the two apps are considered same-site (if they share the same 2nd and 1st level domain). (Ref)

Frontend Deployment Steps

The steps for deploying the Next.js app to Vercel:

  1. On the Vercel dashboard, click "Import Project"
  2. Enter the URL of your Github repo
  3. Select the www subdirectory.
  4. Add the NEXT_PUBLIC_API_HOST env var with the value set to the URL the Django API gets deployed to
  5. Complete the build

Conclusion

If you enjoy building UIs in React but also see how helpful Django can be for building APIs that your UIs can consume, you can get the best of both worlds once you get over the hurdle of having a solid authentication system. Hopefully this guide can help with that.

Get in touch if this was helpful or if you have any tips or questions.