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:
- signIn
- redirect
- jwt
- 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,
// ...
}
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;
},
},
// ...
}
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;
},
}
}
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);
}
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.
- token
- trigger
- account
- profile
- user
- 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
*/
},
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
*/
}
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,
*/
}
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
},
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
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;
},
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
*/
},
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
}
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:
- Putting a user in
Strapi
- Putting a
Strapi
jwt token
onto ourNextAuth 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;
},
},
}
We continue in the next chapter.
If you want to support my writing, you can donate with paypal.