DEV Community

Nick
Nick

Posted on • Updated on

Handling Session Updates for Authenticated Users With NextAuth and JWT

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, 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):

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
},
Enter fullscreen mode Exit fullscreen mode

And here's how my session callback originally started:

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

  return session
},
Enter fullscreen mode Exit fullscreen mode

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):

import type { NextApiRequest, NextApiResponse, } from "next"

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

export default handler
Enter fullscreen mode Exit fullscreen mode

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:

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
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

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:

// 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

Enter fullscreen mode Exit fullscreen mode

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:

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
},
Enter fullscreen mode Exit fullscreen mode

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

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
},
Enter fullscreen mode Exit fullscreen mode

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 guide to update the Session and JWT types accordingly.

Top comments (6)

Collapse
 
designly profile image
Jay @ Designly

Thanks for this, saved me a lot of time! :-)

Collapse
 
fwm profile image
Alex Malikov

This is so mind blowing! So maybe you could answer me, is there a way to access (at least read) data from jwt not putting it into the session? As I understand in v4 there was a getToken function, but in v5 it’s everything done using session. Or no way?

Collapse
 
ali-raza764 profile image
Ali Raza Khalid

Even if I wrap the function it produces unexpected results.
Please share full code for our understanding.

Collapse
 
ali-raza764 profile image
Ali Raza Khalid

if (req.query?.companyId) {
token.selectedCompanyId = req.query.companyId as string
}
For Me It gives an error saying that the req is undefined

Collapse
 
rogercastaneda profile image
Roger Castañeda

Good approach Nick!, what do you recommend to persist some sensitive data like fullName, email, id in the session?

Collapse
 
nick profile image
Nick • Edited

Hi Roger, thank you for the comment!

As I emphasized in the Conclusion of the article, sensitive data - such as passwords and API keys - should NEVER be stored in a session, not even in the token. It's important to store such data in a secure database and encrypt it to protect the user's privacy.

On the other hand, it is generally considered safe to store non-sensitive data such as the user's full name, email, and ID in a session as they don't pose a risk to the user's privacy.