Next.js Authentication with JWT

14,061

Solution 1

My answer is purely based on my experiences and things I read. Feel free to correct it if I happened to be wrong.

So, my way is to store your token in HttpOnly cookie, and always use that cookie to authorize your requests to the Node API via Authorization header. I happen to also use Node.js API in my own project, so I know what's going on.

Following is an example of how I usually handle authentication with Next.js and Node.js API.

In order to ease up authentication problems, I'm using Next.js's built in getServerSideProps function in a page to build a new reusable higher order component that will take care of authentication. In this case, I will name it isLoggedIn.

// isLoggedIn.jsx

export default (GetServerSidePropsFunction) => async (ctx) => {
  // 1. Check if there is a token in cookies. Let's assume that your JWT is stored in 'jwt'.
  const token = ctx.req.cookies?.jwt || null;

  // 2. Perform an authorized HTTP GET request to the private API to check if the user is genuine.
  const { data } = await authenticate(...); // your code here...

  // 3. If there is no user, or the user is not authenticated, then redirect to homepage.
  if (!data) {
    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    };
  }

  // 4. Return your usual 'GetServerSideProps' function.
  return await GetServerSidePropsFunction(ctx);
};

getServerSideProps will block rendering until the function has been resolved, so make sure your authentication is fast and does not waste much time.

You can use the higher order component like this. Let's call this one profile.jsx, for one's profile page.

// profile.jsx

export default isLoggedIn(async (ctx) => {
  // In this component, do anything with the authorized user. Maybe getting his data?
  const token = ctx.req.cookies.jwt;
  const { data } = await getUserData(...); // don't forget to pass his token in 'Authorization' header.

  return {
    props: {
      data,
    },
  },
});

This should be secure, as it is almost impossible to manipulate anything that's on server-side, unless one manages to find a way to breach into your back-end.

If you want to make a POST request, then I usually do it like this.

// profile.jsx

const handleEditProfile = async (e) => {
  const apiResponse = await axios.post(API_URL, data, { withCredentials: true });
  
  // do anything...
};

In a POST request, the HttpOnly cookie will also be sent to the server, because of the withCredentials parameter being set to true.

There is also an alternative way of using Next.js's serverless API to send the data to the server. Instead of making a POST request to the API, you'll make a POST request to the 'proxy' Next.js's serverless API, where it will perform another POST request to your API.

Solution 2

there is no standard approach. You should be worried about security. I read this blog post: https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/

This is a long but an awesome blog post. everyhing I post here will be quoted from there:

If a JWT is stolen, then the thief can keep using the JWT. An API that accepts JWTs does an independent verification without depending on the JWT source so the API server has no way of knowing if this was a stolen token! This is why JWTs have an expiry value. And these values are kept short. Common practice is to keep it around 15 minutes.

When server sends you the token, you have to store the JWT on the client persistently.

Doing so you make your app vulnerable to CSRF & XSS attacks, by malicious forms or scripts to use or steal your token. We need to save our JWT token somewhere so that we can forward it to our API as a header. You might be tempted to persist it in localstorage; don’t do it! This is prone to XSS attacks.

What about saving it in a cookie?

Creating cookies on the client to save the JWT will also be prone to XSS. If it can be read on the client from Javascript outside of your app - it can be stolen. You might think an HttpOnly cookie (created by the server instead of the client) will help, but cookies are vulnerable to CSRF attacks. It is important to note that HttpOnly and sensible CORS policies cannot prevent CSRF form-submit attacks and using cookies requires a proper CSRF mitigation strategy.

Note that a SameSite cookie will make Cookie based approaches safe from CSRF attacks. It might not be a solution if your Auth and API servers are hosted on different domains, but it should work really well otherwise!

Where do we save it then?

The OWASP JWT Cheatsheet and OWASP ASVS (Application Security Verification Standard) prescribe guidelines for handling and storing tokens.

The sections that are relevant to this are the Token Storage on Client Side and Token Sidejacking issues in the JWT Cheatsheet, and chapters 3 (Session Management) and 8 (Data Protection) of ASVS.

From the Cheatsheet, "Issue: Token Storage on the Client Side":

  • Automatically sent by the browser (Cookie storage).
  • Retrieved even if the browser is restarted (Use of browser localStorage container).
  • Retrieved in case of XSS issue (Cookie accessible to JavaScript code or Token stored in browser local/session storage).

"How to Prevent:"

  • Store the token using the browser sessionStorage container.
  • Add it as a Bearer HTTP Authentication header with JavaScript when calling services.
  • Add fingerprint information to the token.

By storing the token in browser sessionStorage container it exposes the token to being stolen through a XSS attack. However, fingerprints added to the token prevent reuse of the stolen token by the attacker on their machine. To close a maximum of exploitation surfaces for an attacker, add a browser Content Security Policy to harden the execution context.

"FingerPrint"

Where a fingerprint is the implementation of the following guidelines from the Token Sidejacking issue: This attack occurs when a token has been intercepted/stolen by an attacker and they use it to gain access to the system using targeted user identity.

"How to Prevent":

A way to prevent it is to add a "user context" in the token. A user context will be composed of the following information:

  • A random string will be generated during the authentication phase. It will be sent to the client as a hardened cookie (flags: HttpOnly + Secure + SameSite + cookie prefixes).

  • A SHA256 hash of the random string will be stored in the token (instead of the raw value) in order to prevent any XSS issues allowing the attacker to read the random string value and setting the expected cookie.

Share:
14,061

Related videos on Youtube

user8463989
Author by

user8463989

Updated on May 12, 2022

Comments

  • user8463989
    user8463989 almost 2 years

    I am moving a project from React to Next.js and was wondering if the same authentication process is okay. Basically, the user enters their username and password and this is checked against database credentials via an API (Node.js/Express). So, I am not using Next.js internal api functionality, but a totally decoupled API from my Next.js project.

    If the login credentials are correct, a JWT token is sent back to the client. I wanted to store that in local storage and then redirect the user. Any future HTTP requests will send the token in the header and check it is valid via the API. Is this okay to do? I ask because I see a lot of Next.js auth using cookies or sessions and don't know if that is the 'standard' approach which I should rather adopt.

    • Mohammad Moallemi
      Mohammad Moallemi about 3 years
      I've had production experience with Next.js/Django JWT and I got to say If you use JWT you can't have authenticated server-side rendered pages, and you must make protected routes CSR only and you can't decide whether your user should be redirected in the Node.js server, it will be kind of a conditional render which you got to decide in a useEffect hook so it only renders on the client and not the server.
    • Eric Burel
      Eric Burel about 3 years
      You should store the token in a HttpOnly cookie, because this way it is passed along with all requests. Using localStorage and Authorization header, the problem is that you have no way to set the Authorization header when the user access a page (+ it is a bit less secure because malicious JS code may access it in some scenarios). I see this localStorage patterns very often (and used it a lot in the past) but it's not the best approach. This pattern is meant for REST API calls from the browser but not to secure web app pages, cookies are better for that.
    • user8463989
      user8463989 about 3 years
      Thanks for your responses. You have mentioned the pattern meant for REST API calls which is what I am having to do because the api isn't in my next.js application. It is an external API that I am having to send the token in the headers to because the jwt token is created on the node.js server. That being said, are you suggesting that when the jwt token is sent from the server to the client, I store the token in a http only cookie and then when making API calls, I send the token from the cookie in the header as opposed to getting the token from local storage?
  • Jason McFarlane
    Jason McFarlane about 3 years
    do you have a repo with an example of this working?
  • Nicholas
    Nicholas about 3 years
    Hi there! Sorry for the late reply, have not checked notifications for a while. I do have a repository that shows an example usage of this concept. Check out github.com/lauslim12/Asuna
  • Joshua Folorunsho
    Joshua Folorunsho about 2 years
    @Nicholas, I am using this approach on my project, I am having a challenge though. What if I want to use getStaticProps on a page?
  • Nicholas
    Nicholas about 2 years
    If you're using getStaticProps, you can create a loader that will be shown to the user when the page is loading (you can fetch the API in a useEffect function for example). Anyways, the thing to keep in mind is: 'the layout/UI itself is not private, the data/contents of the API itself is private' — you should strive to protect your API as always, but the layout itself does not need to be protected (at the worst case scenario it'll show a blank page if you protect your API properly).