Link Search Menu Expand Document

Next Auth implementation with JWT API

David Inga, March 2021

Some days ago we decided to implement Next-Auth on the Marxan project. Why? For many reasons:

  • Ability to add whatever service (called provider) such as Google, Twitter, Facebook, and others like Battle.net, Spotify (why not?)
  • Better workflow working with JWT
  • Increase of security
  • Standardarization of authentication workflow
  • Less code to maintain
  • Sync of expiration session
  • It provides a login page if you want (we usually don’t)

How to use it using on Vizzuality projects

You can mostfly follow the documentation. And, use Credentials provider which seems to be the one we need. And it works fine but we found two main issues.

1. No possibility to extend the session object.

The standard JWT object will have the next format:

{
  "user": {
    "name": "Juan Palomo",
    "email": "yomeloguiso@yomelo.como",
    "image": "route-to-image.png",
  },
  "acess_token": "the-token-generated-from-the-api",
  "expires": "expiration-date-given-from-authentication-endpoint"
}

The problem, when we try to create the session it will only response a object with the above format. Whatever additional parameter will be ignored. Our response have the next structure:

{
  "email": "yomeloguiso@yomelo.como",
  "displayName": "Juan Palomo",
  "fname": "",
  "lname": "",
}

So, displayName, fname, lname and access_token are ignored in the session creation.

// session object created by next-auth
{
    "user": {
        "name": null,
        "email": "yomeloguiso@yomelo.como",
        "image": null
    },
    "expires": "2021-04-07T09:39:33.591Z"
}

You will say, in the documentation seems there is a way to extend the session. It’s true but the paramaters you received are the session object (limited) and the decoded json web token (also limited).

Conclusion, you cannot use a complete profile for session creation, at least with Credentials provider. We can live with that, because you can request the profile to the API. But it opens the second issue.

2. We cannot access to original encoded token from the API.

The session that you receives in the application is omitting (for any reason) the access_token attribute. So, in your application there is not a way to get the token you need for HTTP request. You need to include Authorization: "Bearer token" in your HTTP request using the API and it’s not possible using this method.

I also, tried the next:

// _app.js
import { getToken } from 'next-auth/jwt';
export const getServerSideProps = async (context) => {
  const token = await getToken(context);
  return { props: { token };
};

But it also requires to add the option signingKey: process.env.JWT_SIGNING_PRIVATE_KEY, in [...nextauth].js file.

The problem is, the JWT_SIGNING_PRIVATE_KEY is used to encode and decode the token in the client. But it seems you cannot use a random key because you will have the next error:

JWSVerificationFailed: signature verification failed

Solution

Creates a custom session object, to do that you have to include this in [...nextauth].js file:

  ...
  callbacks: {
    // Assigning encoded token from API to token created in the session
    async jwt(token, user) {
      if (user) {
        const { accessToken, ...rest } = user;
        token.accessToken = accessToken;
        token.user = rest;
      }
      return token;
    },

    // Extending session object
    async session(session, token) {
      session.user = {
        ...session.user,
        ...token.user,
      };
      session.accessToken = token.accessToken;
      return session;
    },
  },
  ...

And this is how my Credentials looks like:

// Configure one or more authentication providers
  providers: [
    Providers.Credentials({
      // The name to display on the sign in form (e.g. 'Sign in with...')
      name: 'Sign in with Marxan',
      // The credentials is used to generate a suitable form on the sign in page.
      // You can specify whatever fields you are expecting to be submitted.
      // e.g. domain, username, password, 2FA token, etc.
      credentials: {
        username: { label: 'Email', type: 'email', placeholder: 'username@domain.com' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        const { username, password } = credentials;

        // Request to sign in
        const signInRequest = await AUTHENTICATION.request({
          url: '/sign-in',
          method: 'POST',
          data: { username, password },
          headers: { 'Content-Type': 'application/json' },
        });

        const { data } = signInRequest;
        const { accessToken } = data;

        // After sign-in, request data user to create session with a complete profile
        const userRequest = await USERS.request({
          url: '/me',
          method: 'GET',
          headers: {
            Authorization: `Bearer ${accessToken}`,
            'Content-Type': 'application/json',
          },
        });

        const { data: userData } = userRequest;

        if (userRequest.statusText === 'OK') {
          const user = { ...userData, ...userData, accessToken };
          return user;
        }

        throw new Error(data);
      },
    }),
  ],