NextAuth.js Authentication Series — Part 8 ~ Understanding JWT and Session Management in Auth.js
In the previous article, we built a complete user registration system using MongoDB, bcrypt, and the Credentials Provider.Users can now create accounts and...

In the previous article, we built a complete user registration system using MongoDB, bcrypt, and the Credentials Provider.
Users can now create accounts and authenticate with their email and password.
But what happens after a successful login?
How does the application remember that a user is authenticated?
This is where sessions come in.
In this article, we'll explore how Auth.js manages authentication sessions, understand the difference between JWT-based sessions and database sessions, and learn how to customize session data using callbacks.
What Is a Session?
A session represents the authenticated state of a user.
After a successful login, the application needs a way to know:
"Is this user authenticated?"
and:
"Which user is currently logged in?"
A session provides this information.
A simplified flow looks like this:
User Login
│
▼
Credentials Validation
│
▼
Authentication Successful
│
▼
Session Created
│
▼
Browser Maintains Session
│
▼
Protected Resources
Without a session, the user would effectively have to authenticate again on every request.
JWT vs Database Sessions
Auth.js supports different session strategies.
The two important concepts are:
JWT-based sessions
Database-backed sessions
Let's understand the difference.
JWT-Based Sessions
With a JWT session, authentication information is stored in an encrypted/signed token associated with the user's session.
Conceptually:
User
│
▼
Login
│
▼
Auth.js
│
▼
JWT Session
│
▼
Browser Cookie
The server can use the session token to determine the authenticated user's state without storing every session in a database.
This can be useful for applications where you don't want to maintain a separate session table.
Database Sessions
With database sessions, the server stores session information in a database.
Conceptually:
User
│
▼
Login
│
▼
Auth.js
│
├── Session ──► Database
│
▼
Session Cookie
The cookie identifies the session, while the server retrieves the associated session information from the database.
Database sessions can be useful when you need server-side control over active sessions.
Which One Should You Use?
There isn't a universal answer.
For many applications, JWT-based sessions are convenient and simple.
Database sessions can be preferable when you need functionality such as:
Server-side session management
Session revocation
Tracking active sessions
More centralized control over sessions
The right choice depends on the application's requirements.
For this series, we'll use the JWT session strategy so that we can clearly understand the callback-based authentication flow.
Step 1 — Configure the Session Strategy
Open:
lib/auth.ts
Add the session configuration:
session: {
strategy: "jwt",
},
Your configuration can look like:
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { connectDB } from "@/lib/mongodb";
import { comparePassword } from "@/lib/password";
export const authOptions: NextAuthOptions = {
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: {
label: "Email",
type: "email",
},
password: {
label: "Password",
type: "password",
},
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const db = await connectDB();
const user = await db.collection("users").findOne({
email: credentials.email,
});
if (!user || !user.password) {
return null;
}
const isPasswordValid = await comparePassword(
credentials.password as string,
user.password
);
if (!isPasswordValid) {
return null;
}
return {
id: user._id.toString(),
name: user.name,
email: user.email,
};
},
}),
],
};
Step 2 — Understanding the JWT Callback
Auth.js provides callbacks that allow us to customize the authentication flow.
One of the most important callbacks is:
callbacks: {
async jwt({ token, user }) {
return token;
},
},
The JWT callback runs when Auth.js creates or updates the JWT.
When the user first signs in, the user object is available.
For example:
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
}
Now the user's ID is added to the token.
Step 3 — Understanding the Session Callback
The JWT isn't automatically exposed directly to your React components.
Instead, we can use the session callback to transfer selected information from the token to the session.
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
}
return session;
}
Now the client-side session can contain the user's ID.
The overall flow becomes:
User Login
│
▼
authorize()
│
▼
user object
│
▼
jwt()
│
▼
JWT token
│
▼
session()
│
▼
Session
Step 4 — Add Custom User Data
Let's combine the callbacks:
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
}
return session;
},
},
Now our authentication system knows how to transfer the user's ID from the authentication result into the session.
TypeScript Session Types
TypeScript may complain about:
session.user.id
because the default Auth.js type may not include a custom id property.
We can extend the types.
Create:
types/next-auth.d.ts
Add:
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
}
declare module "next-auth/jwt" {
interface JWT {
id: string;
}
}
Now TypeScript understands that our session user has an id.
Accessing the Session on the Server
In a server-side environment, you may need the authenticated user's session to protect server-side functionality.
With the modern Auth.js architecture, this is commonly handled through the exported auth() function.
The exact setup depends on how your Auth.js configuration is structured, which we'll standardize in a later article when we move toward route protection.
Conceptually:
const session = await auth();
if (!session?.user) {
// User is not authenticated
}
This is one of the most useful patterns when protecting server-side resources.
Accessing Session Data on the Client
For client components, Auth.js provides session utilities.
For example:
"use client";
import { useSession } from "next-auth/react";
export default function Profile() {
const { data: session, status } = useSession();
if (status === "loading") {
return <p>Loading...</p>;
}
if (!session) {
return <p>You are not signed in.</p>;
}
return (
<div>
<h1>Welcome {session.user?.name}</h1>
<p>{session.user?.email}</p>
<p>User ID: {session.user?.id}</p>
</div>
);
}
This allows client components to react to the user's authentication state.
Don't Put Sensitive Information in the Session
A very important security rule is:
Only put information in the session that the application actually needs.
For example, don't add:
token.password = user.password;
or:
session.user.password = user.password;
Even though the password is hashed, there is no reason to expose it.
Avoid putting sensitive internal information into client-accessible session data.
A good session might contain:
id
name
email
image
role
depending on your application's requirements.
Adding a User Role
Later in the series, we'll implement role-based authentication.
For example, a user might have:
{
"role": "admin"
}
We could pass that role through the JWT:
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.role = user.role;
}
return token;
}
And then expose it through the session:
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
session.user.role = token.role as string;
}
return session;
}
We'll implement the complete role system later.
Session Expiration
Authentication sessions shouldn't remain valid forever.
Session expiration limits the damage if a session token is compromised.
Auth.js provides configuration options for session lifetime.
For example:
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60,
},
The value represents seconds.
In this example, the session lifetime is approximately 30 days.
Choose a lifetime appropriate for your application's security requirements.
Authentication State vs User Data
It's useful to understand the difference between these concepts:
User
│
├── Database Record
│
└── Authentication Session
The database contains persistent user information.
The session represents the user's current authenticated state.
They are related, but they are not the same thing.
Common Mistakes
Here are some common session-related mistakes.
Putting passwords in JWTs
Never do this.
token.password = user.password;
Putting unnecessary database data into sessions
Don't copy the entire MongoDB user document into the session.
Only expose what the application needs.
Forgetting TypeScript augmentation
If you add custom fields such as id or role, update your Auth.js TypeScript definitions.
Assuming the session is permanent
Sessions expire according to their configuration.
Your application should handle expired sessions gracefully.
Key Takeaways
In this article, we learned:
What authentication sessions are.
The difference between JWT and database sessions.
How JWT-based sessions work.
How the
jwtcallback works.How the
sessioncallback works.How to add custom user information to sessions.
How to extend Auth.js types with TypeScript.
Why sensitive information shouldn't be included in sessions.
How session expiration works.
The authentication flow now looks like:
Registration
│
▼
MongoDB
│
▼
Login
│
▼
Credentials Provider
│
▼
JWT
│
▼
Session
│
▼
Authenticated User
What's Next?
We now know how authentication sessions work, but our application still doesn't prevent unauthenticated users from accessing protected pages.
For example:
/dashboard
/admin
/profile
/settings
should not be accessible to everyone.
In the next article, we'll learn how to protect routes and pages with Auth.js.
We'll implement authentication checks and build a protected dashboard that only authenticated users can access.
Series Navigation
NextAuth.js Authentication Series
✅ Part 1 — Introduction to Authentication with NextAuth.js
✅ Part 2 — Installing & Configuring NextAuth.js
✅ Part 3 — Google OAuth Authentication
✅ Part 4 — Credentials Provider with Email & Password
✅ Part 5 — Integrating MongoDB with Credentials Authentication
✅ Part 6 — Secure Password Authentication with bcrypt
✅ Part 7 — Building a User Registration System
✅ Part 8 — JWT & Session Management ← You are here


