'Next.js Authentication with JWT

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.



Solution 1:[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:[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.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Yilmaz