DEV Community

Peter Jacxsens
Peter Jacxsens

Posted on • Updated on

7/ NextAuth callback functions explained

The final code for this chapter is available on github (branch callbacksForGoogleProvider).

NextAuth creates a JWT token and sets that in a cookie on our client (browser). On top of that, NextAuth lets us read this token by using either the client component useSession hook or the server component getServerSession function.

NextAuth also gives us the tools to customize what we put on this token and what we get from the session. The session is what useSession or getServerSession returns. These tools are called callbacks and we define them in our authOptions object.

Overview

There are four callbacks:

  1. signIn
  2. redirect
  3. jwt
  4. session

The jwt callback is responsible for putting data onto the JWT token. NextAuth puts data on the token by default. We can use the jwt callback to put other data on the token, customizing this process.

As you might guess, the session callback handles what is put on the session (the return from useSession or getServerSession). Again, we can customize these values.

We won't be using the other 2 callbacks. Note that the signIn callback is not the same as the signIn function we used earlier to redirect us to the sign in page or start the auth flow. The signIn callback's purpose is to controle if a user is allowed to sign in.

Finally, the redirect callback lets us customize the redirect behaviour on f.e. signing in. You can read more about these last two callbacks in the NextAuth docs.

Strapi

Before we start coding, let's think about what we need to do. There's 2 things we need to achieve. Firstly, somewhere in our sign up/sign in process with Google provider, we want to put the user into the Strapi database as a User.

Secondly, when we create a Strapi User, Strapi will generate an JWT access token for this user. When we request non public content from Strapi we send this access token along as a header so Strapi can authenticate the user. Only authenticated users can request non public content.

Where do we save this Strapi token? Inside the JWT token. So, we will put the Strapi JWT token inside the NextAuth JWT token. This is something we need to configure. We need to customize the NextAuth token and that is our second goal.

authOptions

Before we get into the callbacks I'm gonna first put some other settings in authOptions. We add these 2 properties:

// frontend/src/api/auth/[...nextAuth]/authOptions.ts

{
  // ...
  session: {
    strategy: 'jwt',
  },
  secret: process.env.NEXTAUTH_SECRET,
  // ...
}
Enter fullscreen mode Exit fullscreen mode

These are actually just the defaults but I like to explicitly set them. The session strategy indicates that we are using JWT tokens. The alterative would be database but that is a whole other story. The secret just refers to our .env file that we created earlier.

The callback syntax

In authOptions, add callback property with the session and jwt callbacks:

// frontend/src/api/auth/[...nextAuth]/authOptions.ts
{
  // ...
  callbacks: {
    async jwt({ token, trigger, account, profile, user, session }) {
      // do some stuff
      return token;
    },
    async session({ token, user, session, newSession, trigger }) {
      // do some stuff
      return session;
    },
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

This is the syntax from the docs and initially it really confused me. Are we calling this function? But then why does it have a body? It played tricks with my mind. I managed to understand was going on by writing it longhand:

{
  callbacks: {
    jwt: async function jwt({ token, user, account, profile, session, trigger }) {
      // do stuff
      return token;
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

I also had to recall what a callback function actually does. Consider this example:

// the would be the part NextAuth is doing
const myArr = [1, 2, 3, 4];
myArr.map(myCallback);

// this would be one of our callbacks in authOptions
function myCallback(item) {
  console.log(item);
}
Enter fullscreen mode Exit fullscreen mode

Given this example, it should be clear where the arguments come from in our jwt and session callbacks: NextAuth just passes them when it calls the callback and that is how we have access to the parameters in our function body.

That was just a side note, let's get back to customizing. Note that we will be using the shorthand like in the docs.

NextAuth jwt callback

jwt is called every time we create a token (by signing in) or when we update or read a token (f.e. using useSession). This callback is used to put extra information on the JWT token that NextAuth creates. It must always return the token argument.

The callback takes a lot of arguments and that's confusing but there is some good news also:

  • We don't need all of these arguments so we will be leaving some out. Yay!
  • We're running in TypeScript and when we hover the arguments, we get an explanation of what they are.

To really understand these arguments it helps to put them in sequence. Except for token, these arguments will usually be undefined. Only on certain events like sign in or update will they be populated with data. This makes sense, NextAuth makes an auth request on singing in. When the user is already logged in, there is no need to call the provider.

  1. token
  2. trigger
  3. account
  4. profile
  5. user
  6. session

Token is always populated. When there is a certain event - a trigger - like signing in, NextAuth will go to the provider. The provider info will be saved in the account argument. The provider sends over user info, this will be safed in profile argument. NextAuth cleans this up and saves some properties into user argument. The session is the exception because it only gets populated on an 'update' event (trigger). Careful, it is not related to the NextAuth session (returned from f.e. getServerSession) as one could expect. All of these arguments come together to populate the token.

I hope this makes sense. It's a flow of different stages and data in the auth process. Let's us take a short look at each of them:

1. Token argument

Token is what will be put on our JWT token. Right now, it holds the values that NextAuth puts on them by default:

{
  name: 'Peter Jacxsens',
  email: string,

  // ignore the rest
  /*
  picture: string,
  sub: string,
  iat: number,
  exp: number,
  jti: string
  */
},
Enter fullscreen mode Exit fullscreen mode

In our case we just ignore everything but name and email. So, this is what NextAuth puts on our token by default.

2. Trigger argument: "signIn" | "signUp" | "update" | undefined

Trigger is like an NextAuth event. When the user is already signed in it is undefined. We will use this later on.

3. Account argument

Account contains provider info when certain triggers happen. When signing in with GoogleProvider it will look like this:

{
  // this we use
  provider: 'google',
  access_token: string,

  // the rest we don't need
  /*
  type: 'oauth',
  providerAccountId: string,
  expires_at: string,
  scope: string,
  token_type: 'Bearer',
  id_token: string
  */
}
Enter fullscreen mode Exit fullscreen mode

4. Profile argument

This is the raw user information we get back from our provider. It's only populated on sign in and undefined else. NextAuth uses this to create a User. We don't use it and hence ignore it.

{
  /*
  iss: 'https://accounts.google.com',
  azp: string,
  aud: string,
  sub: string,
  email: string,
  email_verified: boolean,
  at_hash: string,
  name: 'Peter Jacxsens',
  picture: string,
  given_name: string,
  family_name: string,
  locale: string,
  iat: number,
  exp: another,
  */
}
Enter fullscreen mode Exit fullscreen mode

5. User argument

This is like a cleaned up version of profile. It will only be populated on signing in.

{
  id: string,
  name: 'Peter Jacxsens',
  email: string,
  image: string
},
Enter fullscreen mode Exit fullscreen mode

Remember, this is a flow. The token will be populated with some things by default. When there is a trigger, there will be some raw provider data in account and some raw user data in profile. NextAuth takes some data from profile and puts it into the user argument. Here is a concrete (but useless example) of the flow:

account.providerAccountId -> profile.sub -> user.id -> token.sub
Enter fullscreen mode Exit fullscreen mode

6. session argument

We will cover this one later when working with the update trigger.

Recap

Let's do a quick recap. We're talking about the jwt callback. It gets called when there is a special event like sign in but also every time useSession or getServerSession is used. The main goal of this callback is populating the NextAuth jwt token. It's a callback function that exposes a whole series a arguments to us: token will always be populated. On certain triggers, other arguments will be populated. We can listen for these triggers and then conditionally customize the token.

NextAuth session callback

Let's turn to our second callback: session. The goals of this callback is simple, we populate what gets returned from the useSession hook and the getServerSession function. So, we use the session callback to customize our NextAuth session. Here is our callback once more. Note that we must always return session from this callback:

async session({ token, user, session, newSession, trigger }) {
  // do some stuff
  return session;
},
Enter fullscreen mode Exit fullscreen mode

As with the jwt callback, the token argument will always be populated. In fact, token here equals the return value from the jwt callback. jwt is called first, followed by the session callback. This is what the token argument looks like:

{
  name: 'Peter Jacxsens',
  email: string,

  // ignore the rest
  /*
  picture: string,
  sub: string,
  iat: number,
  exp: number,
  jti: string
  */
},
Enter fullscreen mode Exit fullscreen mode

Inside the session callback, the session argument is also always populated, regardless of wether we're are signed in or signing in. Session argument:

{
  user: {
    name: 'Peter Jacxsens',
    email: string,
    image: string,
  },
  expires: Date
}
Enter fullscreen mode Exit fullscreen mode

We have seen this before when we console.logged useSession or getServerSession. This is what NextAuth puts into our session by default when running GoogleProvider. This of course is the whole point!

The other arguments of the session callback: user and newSession are only available when using authOptions session.strategy === database. (We use session.strategy jwt) Hence we will ignore them. We will ignore trigger aswell as we don't need it.

So, next time we use the session callback it will look like this: async session({ token, session }) {}. And that's all.

Wrapping up

We just had a deep dive into the jwt and session callback functions. In the next chapter we will start using these to solve the 2 problems we started this chapter with:

  1. Putting a user in Strapi
  2. Putting a Strapi jwt token onto our NextAuth jwt token

With all the theory from above, you will probably have a vague clue as how we will do this. Don't worry if you forget about all these arguments. You just need to struggle with them as you are customizing the token or session.

Finally, I updated both the jwt and session callbacks with a log function, so you can easily check the logs and see what's what. These do flood your terminal so comment them out when done.

// frontend/src/api/auth/[...nextAuth]/authOptions.ts

{
  callbacks: {
    async jwt({ token, trigger, profile, user, session }) {
      console.log('jwt callback', {
        token,
        trigger,
        profile,
        user,
        session,
      });
      return token;
    },
    async session({ token, session }) {
      console.log('session callback', {
        token,
        session,
      });
      return session;
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

We continue in the next chapter.


If you want to support my writing, you can donate with paypal.

Top comments (0)