Handling Session Updates for Authenticated Users With NextAuth and JWT
Tags: nextjs, nextauth, jwt
Published: 2023-02-04 05:08:00

NextAuth is hands down the best authentication solution for Nextjs.

I've been using it for over a year now and one of the challenges I've faced is updating the session after a successful login.

While it's straightforward enough to do when using the database-persisted approach, it's a different story with the JWT-persisted session flow.

Here are two scenarios where it's necessary to update the session:

1. Authenticated users update their personal and public info (e.g.: first name)
2. Authenticated users have additional data that they pick up throughout their onboarding process (e.g.: selecting a company to manage on a dashboard)

We can all agree that those are pretty common scenarios, right? So today, at work, I needed to handle the second scenario while using the JWT-persisted session and since I've kinda figured it out, I thought I'd document it.

The first solution that came to mind, as many have also suggested, was to use a global state management library, such as Redux, or Zustand, to create a client-side-only session.

The issue with this approach is that it would need to be persisted locally (local storage, session storage...) and because of the server-side nature of Nextjs, I would need to check for its existence on each page (this one could most likely be easily handled using the new Nextjs 13 features, which I'm not able to use in this particular app).

Then I stumbled upon [this solution](https://stackoverflow.com/a/73830201), which got me going. It involved setting up a wrapper around the NextAuth route handler and exploiting the Next API Request object.

I liked it, but I figured I could improve on it:

## Setup and Context

No matter which provider you're using (credentials, email, oAuth...), you should be able to follow along. Just make sure your setup currently works perfectly and successfully authenticates users.

And since this tutorial is around the JWT-persisted session, you should have the `jwt` and `session` callbacks ready.

Here's what my `jwt` callback originally looked like (no worries if yours is slightly different, I have my own needs):
```ts
async jwt({ token, user, account, }) {
  // This will only be true on the first login
  if (account && user) {
    return {
      ...token,
      user: {
        ...user, // make sure you don't store passwords in there
        account,
      },
    }
  }

  return token
},
```

And here's how my `session` callback originally started:
```ts
async session({ session, token, }) {
  session.user = token.user

  return session
},
```

## Custom Route Handler

I started by creating the wrapper in `/pages/api/auth/[...nextauth].ts` (I'm using TypeScript, so mind the types if you're using JS):
```ts
import type { NextApiRequest, NextApiResponse, } from "next"

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  return NextAuth(req, res, createOptions(req))
}

export default handler
```

The only unknown here is the `createOptions` function, which is pretty much a function that returns the NextAuth options. Here's what it looks like without the `providers` property:
```ts
import { AuthOptions, } from "next-auth"

const createOptions = (req: NextApiRequest): AuthOptions => ({
  // ... other options, such as providers, debug, etc..

  callbacks: {
    async jwt({ token, user, account, }) {
      // This will only be true on the first login
      if (account && user) {
        return {
          ...token,
          user: {
            ...user,
            account,
          },
        }
      }

      return token
    },

    async session({ session, token, }) {
      session.user = token.user

      return session
    },
  },
})
```

## Exploiting the Next API Request Object

With this in place, we now have access to the `req` object so we can potentially pass in some (public and non sensitive) data to the `jwt` callback and ultimately the `session` callback. To do so, once it's time to update the session, all we have to do is send a `GET` request to the `/api/auth/session` endpoint that contains our data as query params:

```ts
// Suppose this is being called right after the user selects the company they wish to manage

await fetch(`/api/auth/session?companyId=${company.id}`)

// Then you can potentially navigate away

```
> Tip: If you try to read the response data from this request upon a successful call, it will return the updated token as well. Do what you want with this info.

Now to handle this request from your callbacks, it will first go through the `jwt` one:
```ts
async jwt({ token, user, account, }) {
  // If the specific query param(s) exist(s), we know it's
  // an update. So we add it to the token
  // This is the best place to make an API request so you
  // can complete the data
  if (req.query?.companyId) {
    token.selectedCompanyId = req.query.companyId as string
  }

  // This will only be true on the first login
  if (account && user) {
    return {
      ...token,
      user: {
        ...user,
        account,
      },
    }
  }

  return token
},
```
Now the token will contain our additional data, which will be relayed to the `session` callback where we can make use of it:

```ts
async session({ session, token, }) {
  session.user = token.user
  session.selectedCompanyId = token.selectedCompanyId // you could also add it to the `token.user` object from the `jwt` callback

  return session
},
```

And that's it! The client will receive an updated session whenever you call `getSession` (server-side) moving forward. 

When it comes to `useSession` (client-side), it will be updated on the next page refresh, so make sure you mostly use the session coming from `getServerSideProps`.

> Note: Unfortunately, the NextAuth route handler doesn't support `POST` request methods, so you will not be able to pass in complex data through a payload object. But since the callbacks are async, you can always complete the data by making an API call within them.

## Conclusion

This did require a bit of work, so hopefully I've explained it succinctly enough for you to remember the steps and why it's done this way.

I'm almost certain the NextAuth maintainers are working on improving the DX around this, so please, make sure you keep an eye on their docs before going this route.

Also, I cannot stress this enough: NEVER store sensitive data in the token, nor the session.

Additionally, if you're using TypeScript as well, you may want to follow this [Module Augmentation](https://next-auth.js.org/getting-started/typescript#module-augmentation) guide to update the `Session` and `JWT` types accordingly.