Adil Haddaoui

/ Home/ Uses

Customizing JWT Session in NextAuth: A Practical Guide to Adding custom User Data

Cover Image for Customizing JWT Session in NextAuth: A Practical Guide to Adding custom User Data
Photo of Adil Haddaoui
Adil Haddaoui

Whenever I use NextAuth for authentication with NextJS, I waste a ton of time trying to figure out how to add extra data to the user session (role, status, isBanned ...) to be able to access it directly from the session whenever calling useSession() hook on the client side or getServerSession(req,res, authOptions) on the server side. And whenever I go through Stack Overflow or Github issues I find a ton of devs struggling with that and no answer is direct to the point so I decided to document this for everyone, to remove that dark side of handling JWT session data appending on NextAuth

First I'd like to point out how NextAuth flow works using this simple illustration:

So when I try to login to my application authorize() callback function is where the logic is for when a user logs in to make sure they're authorized to login, Now since we're talking about JWT I assume you are already marking your provider to use JWT as the session strategy like so session: {strategy: "jwt",} in the NextAuth options, then the jwt() callback function will be called just after the authorize() cb function and right after that the session() callback is called right after that.

Let's get to the code, First thing and right from the NextAuth documentation this is the initial implementation of NextAuth credentials provider

1import CredentialsProvider from "next-auth/providers/credentials"
2...
3session: {
4 strategy: "jwt",
5},
6providers: [
7 CredentialsProvider({
8 // The name to display on the sign in form (e.g. "Sign in with...")
9 name: "Credentials",
10 // `credentials` is used to generate a form on the sign in page.
11 // You can specify which fields should be submitted, by adding keys to the `credentials` object.
12 // e.g. domain, username, password, 2FA token, etc.
13 // You can pass any HTML attribute to the <input> tag through the object.
14 credentials: {
15 username: { label: "Username", type: "text", placeholder: "jsmith" },
16 password: { label: "Password", type: "password" }
17 },
18 async authorize(credentials, req) {
19 // Add logic here to look up the user from the credentials supplied
20 const user = { id: "1", name: "J Smith", email: "jsmith@example.com" }
21
22 if (user) {
23 // Any object returned will be saved in `user` property of the JWT
24 return user
25 } else {
26 // If you return null then an error will be displayed advising the user to check their details.
27 return null
28
29 // You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter
30 }
31 }
32 })
33]
34...

Added session: {strategy: "jwt",} to make sure we're in the same page.

Let's clean up the comments a bit and write a more real-world scenario for the authorize() callback function:

1import CredentialsProvider from "next-auth/providers/credentials"
2
3export const authOptions = {
4 ...
5 session: {
6 strategy: "jwt",
7 },
8 providers: [
9 CredentialsProvider({
10 name: "Credentials",
11 credentials: {
12 username: { label: "Username", type: "text", placeholder: "jsmith" },
13 password: { label: "Password", type: "password" }
14 },
15 async authorize(credentials {
16 if (!credentials.username || !credentials?.password) {
17 throw new Error("Incorrect credentials")
18 }
19
20 // service can be your db client or whatever to fetch user by username
21 const user = await service.findUserByUsername(credentials.username)
22
23 // assuming you're encrypting your password in db 🫣 using bcrypt
24 if (user && bcrypt.compareSync(credentials.password, user.password)) {
25 return {
26 // default data we get from session
27 name: user.name,
28 email: user.email,
29 image: user.image,
30 // our data we want to append to the session object
31 id: user.id,
32 username: user.username,
33 role: user.role,
34 isBanned: user.isBanned,
35 // ...
36 }
37 } else {
38 throw new Error("No user matches the provided credentials in our database")
39 }
40 }
41 })
42 ]
43 ...
44}

Line: 15 Now that our authorize() cb function is more clear and that we return from it the data needed to fill in the session object.

as I demonstrated in the illustration above the returned that from authorize() cb function will go directly to jwt() cb function and its going to look like that:

1import CredentialsProvider from "next-auth/providers/credentials"
2
3export const authOptions = {
4 ...
5 session: {
6 strategy: "jwt",
7 },
8 callbacks: {
9 jwt: ({token, user, profile, account, isNewUser}) => {
10 return token
11 }
12 },
13 providers: [
14 CredentialsProvider({
15 name: "Credentials",
16 credentials: {
17 username: { label: "Username", type: "text", placeholder: "jsmith" },
18 password: { label: "Password", type: "password" }
19 },
20 async authorize(credentials {
21 if (!credentials.username || !credentials?.password) {
22 throw new Error("Incorrect credentials")
23 }
24
25 // service can be your db client or whatever to fetch user by username
26 const user = await service.findUserByUsername(credentials.username)
27
28 // assuming you're encrypting your password in db 🫣 using bcrypt
29 if (user && bcrypt.compareSync(credentials.password, user.password)) {
30 return {
31 // default data we get from session
32 name: user.name,
33 email: user.email,
34 image: user.image,
35 // our data we want to append to the session object
36 id: user.id,
37 username: user.username,
38 role: user.role,
39 isBanned: user.isBanned,
40 // ...
41 }
42 } else {
43 throw new Error("No user matches the provided credentials in our database")
44 }
45 }
46 })
47 ]
48 ...
49}

Line: 9, that's its default behavior it just returns the JWT token we're using during the current session and it's stored in a cookie.

The token param is always available but others like user, profile, account and isNewUser are only available the first time the user logs in so whenever its called next (eg. Requests to /api/auth/signin, /api/auth/session and calls to getSession(), getServerSession() and useSession()callbacks) we're going to lose the user we just built on authorize() cb, therefor we're going make sure to override the token user object when first the user logs in so that we can access it in the session() cb later, as following:

Note: From now on I will be just updating the callbacks option so it s what I will be highlighting in the code snippets to make it more readable

1...
2callbacks: {
3 jwt: ({token, user, profile, account, isNewUser}) => {
4 if(user) {
5 return {...token, ...user}
6 }
7 return token
8 }
9}
10...

In this way we made sure on first login we are going to append the user data we got from authorize() callback to the token data else we gonna just return the JWT token.

Now let's access that token on the session() callback and extract our user data from it.

1
2...
3callbacks: {
4 jwt: ({token, user}) => {
5 if(user) {
6 return {...token, ...user}
7 }
8 return token
9 },
10 session: ({session, token}) => {
11 return {
12 ...session,
13 user: {
14 ...session.user,
15 id: token.id,
16 name: token.name,
17 email: token.email,
18 username: token.username,
19 image: token.image,
20 role: token.role,
21 isBanned: token.isBanned
22 }
23 }
24 }
25}
26...

And that's it 🎉 Like that we've added extra data to our session object the first time the user logs in by passing it by from authorize() cb through jwt() cb to our session() cb that build the session object we're going to have hands on whenever we call useSession() hook on the client side or getServerSession(req, res, authOptions) on the server side.


More to read

React Redefined: Unleashing it's Power with a new Compiler

The React's game-changing update: a brand-new compiler that simplifies coding, making it cleaner and more efficient. This major leap forward promises an easier and more enjoyable development experience. In this post, we're diving into the big changes and what they mean for us developers. It's a thrilling time for the React community, and we can't wait to see where this update takes us.

Photo of Adil Haddaoui
Adil Haddaoui

HTMX: Redefining Simplicity in Web Development

Discover the simplicity of web development with HTMX. Dive into practical examples that showcase HTMX's ability to enhance HTML for dynamic updates, form submissions, and real-time search—effortlessly and with minimal code. Learn how HTMX streamlines development, making it easier to create interactive, maintainable web applications.

Photo of Adil Haddaoui
Adil Haddaoui