Next.js JWT Authentication with NextAuth and Integration with Hasura

If you are a Next.js developer and looking for an Authentication solution, look no further than next-auth, an open-source Authentication library for Next.js. It's built for serverless (can run anywhere too) and supports various services like Sign in with Google, Apple, Facebook, Github or a simple email/password combination among others. You can bring your own database for storing sessions in the db or simply use JWT as we are going to do in this tutorial.

Hasura supports Authentication in the form of JWT / webhooks. The recommendation is to typically use JWT over webhooks for most use cases. With JWT, you get latency free requests since the session information is stored on the client and not on the server. Read more on the Best Practices for using JWT on frontend clients.

Clone next-auth example

In this tutorial, we will look at implementing a custom JWT solution with next-auth, served by Next.js and integrate the same with Hasura and make authenticated GraphQL API calls.

Let's start with the official example app and configure it.

git clone https://github.com/nextauthjs/next-auth-example.git
cd next-auth-example
npm i
npm i next-auth pg

Copy environment variables:

cp .env.local.example .env.local

We will come back and modify the values here later. Let's look at the directory structure. Inside the pages directory under the api routes, you have the logic written for auth in the file [...nextauth.js]. This will let this component handle all the requests coming in to /api/auth/*. Some example include, signin, signout, callback etc.

Deploy Hasura to get a GraphQL API

  1. Click on the following button to deploy GraphQL engine on Hasura Cloud including Postgres add-on or using an existing Postgres database:

Deploy to Hasura Cloud

2. Open the Hasura console by clicking on the button "Launch console".

3. Create table users.

Head to the Data tab and create a new table called users with columns:

  • id (text)
  • name (text)
  • created_at (timestamp now()).

JWT Configuration

Hasura supports authentication via webhook and JWT. For this next-auth example, we will look at creating a custom JWT server to sign and verify tokens.

Generating Secret

We need to generate a secret that can be used to hash the tokens and configure them on Hasura. There are a couple of options available.

  • HS256
  • RS256 / RS512

If you are going to use HS256 algorithm, there is only a secret to be generated that will be used on both the Next.js server and inside Hasura config. On the other hand, if you are going to use RS256, we need to generate a public/private key pair and the private key will be used to sign the token, and the public key will be used to verify the token on the Hasura's end.

JWT with HS256

Head to https://generate-secret.now.sh/32 to generate a random secret that can be used on both the Next.js server and Hasura config. I'm taking a value 69f8fd4d54342b7ee3b0fcdf6def434c for the secret.

Let's configure this on the .env.local file that we copied earlier and substitute this value for the SECRET env.

On the Hasura Cloud projects page, head to the ENV vars page to configure this secret. Note that you need to add an admin secret using HASURA_GRAPHQL_ADMIN_SECRET before configuring the HASURA_GRAPHQL_JWT_SECRET.

The JWT config for this would look like:

{ "type": "HS256", "key": "69f8fd4d54342b7ee3b0fcdf6def434c"}
Add Hasura_GRAPHQL_JWT_SECRET env var

Do replace the secret with your own generated code.

JWT with RS256

Follow the steps to generate a private/public key pair for using the RS256 algorithm.

  • Generate Private Key
openssl genrsa -out private.pem 2048
  • Generate Public Key
openssl rsa -in private.pem -pubout -out public.pem
  • Transform public key into a single line
awk -v ORS='\\n' '1' public.pem | pbcopy

Now paste this value in your clipboard to Hasura Cloud env in the format

{ "type": "RS256", "key": "<insert-your-public-key-here>"}
  • Transform private key into a single line
awk -v ORS='\\n' '1' private.pem | pbcopy

Configure this secret into your .env.local file under the SECRET env. This will be used by the next-auth config to sign the token.

Sign In with GitHub

next-auth supports various data providers to integrate with Sign In services, OAuth providers and email/password combinations. We will use the GitHub provider in this example. Follow the docs for enabling that.

Head to the [...nextauth.js] file and remove the other providers. The configuration for providers will look like this:

providers: [
    Providers.GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
],

Encode JWT

Inside the jwt object, we need to enable the secret that would come from process.env.SECRET and expand on the logic for encode and decode methods.

We will need the jsonwebtoken module for signing the token with the secret.

So let's do that by npm install jsonwebtoken. Here's the Hasura Docs on Custom Claims. All the custom claims for Hasura stays in https://hasura.io/jwt/claims. The values we care are x-hasura-role and x-hasura-user-id because these session variables will be used in this example's permission system.

encode: async ({ secret, token, maxAge }) => {
      const jwtClaims = {
        "sub": token.id.toString() ,
        "name": token.name ,
        "email": token.email,
        "iat": Date.now() / 1000,
        "exp": Math.floor(Date.now() / 1000) + (24*60*60),
        "https://hasura.io/jwt/claims": {
          "x-hasura-allowed-roles": ["user"],
          "x-hasura-default-role": "user",
          "x-hasura-role": "user",
          "x-hasura-user-id": token.id,
        }
      };
      const encodedToken = jwt.sign(jwtClaims, secret, { algorithm: 'HS256'});
      return encodedToken;
},

Decode JWT

decode: async ({ secret, token, maxAge }) => {
      const decodedToken = jwt.verify(token, secret, { algorithms: ['HS256']});
      return decodedToken;
 },

Adding User ID to Token

From NextAuth Docs:

If you want to pass data such as an Access Token or User ID to the browser when using JSON Web Tokens, you can persist the data in the token when the jwt callback is called, then pass the data through to the browser in the session callback.

We need to handle the callbacks from our Sign In service to be able to add user ID to the token. Inside the callbacks object, handle the logic for session:

async session(session, token) { 
      const encodedToken = jwt.sign(token, process.env.SECRET, { algorithm: 'HS256'});
      session.id = token.id;
      session.token = encodedToken;
      return Promise.resolve(session);
    },

and jwt callback

async jwt(token, user, account, profile, isNewUser) { 
      const isUserSignedIn = user ? true : false;
      // make a http call to our graphql api
      // store this in postgres
      
      if(isUserSignedIn) {
        token.id = user.id.toString();
      }
      return Promise.resolve(token);
    }

Note that we need to write the handler to make an http call to GraphQL API to insert the user and sync it with the users table in the database.

Head to server.js in the pages directory to see how the session prop is exported to use session information with Server Side Rendering.

export async function getServerSideProps(context) {
  return {
    props: {
      session: await getSession(context)
    }
  }
}

This set up enables you to access the session information in _app.js where we use the NextAuth <Provider> HOC to render the components.

After logging in, check the token generated on the network tab. There is an API call made to /api/auth/session that would return the token. This token can now be used to make authenticated GraphQL API calls with Hasura via the Authorization header.

Link to Github - https://github.com/praveenweb/next-auth-hasura-example

Check out the YouTube video to follow along the whole integration from scratch.

Do let me know in the comments if you want specific use cases to be covered!

Blog
04 Feb, 2021
Email
Subscribe to stay up-to-date on all things Hasura. One newsletter, once a month.
Loading...
v3-pattern
Accelerate development and data access with radically reduced complexity.