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
- Click on the following button to deploy GraphQL engine on Hasura Cloud including Postgres add-on or using an existing Postgres database:
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"}
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!