5. Callbacks, JWTs, and Sessions
Understand signIn, jwt, session, and redirect callbacks, event handlers, and safe custom claims.
Callback Lifecycle
The main NextAuth v4 callbacks are signIn, redirect, jwt, and session. signIn permits or denies login; redirect validates destinations; jwt adds claims when JWT sessions are used; session decides what is exposed to the client. Events are for side effects, not access-control decisions.
`signIn`: Allow or Deny a User
Return true to continue, false to deny, or a URL to redirect. Provider parameters differ. For Google, the profile commonly includes email_verified; do not assume every provider has that property. Check account status against a trusted source when required.
callbacks: {
async signIn({ user, account, profile }) {
if (account?.provider === "google") {
return Boolean(user?.email && profile?.email_verified);
}
return Boolean(user?.email);
},
},`jwt`: Store Selected Claims
With JWT sessions, jwt runs at initial sign-in and when the token is processed later. At initial sign-in, user and account may be present; later calls may only have the existing token. Add only necessary claims such as stable user ID, role, or provider. Never store passwords, password hashes, or secrets.
callbacks: {
async jwt({ token, user, account }) {
if (user) {
token.userId = user.id;
if (user.role) token.role = user.role;
}
if (account) token.provider = account.provider;
return token;
},
},`session`: Expose Safe Fields
The session callback controls what useSession() and session APIs return. By default, only a limited subset is exposed. Copy custom claims from token to the session explicitly. Anything returned in the session is readable by the browser, so never expose secrets.
callbacks: {
async session({ session, token }) {
if (session.user) {
session.user.id = token.userId ?? token.sub ?? "";
session.user.role = token.role ?? "user";
}
session.provider = token.provider ?? null;
return session;
},
},`redirect`: Restrict Destinations
The redirect callback prevents unsafe open redirects. Allow relative URLs and same-origin absolute URLs, and fall back to the base URL for invalid or external URLs. Keep the default behavior if you do not need customization.
callbacks: {
async redirect({ url, baseUrl }) {
if (url.startsWith("/")) return `${baseUrl}${url}`;
try {
if (new URL(url).origin === baseUrl) return url;
} catch {}
return baseUrl;
},
},Events vs Callbacks
Events such as signIn and signOut are suitable for audit logging and side effects. Use callbacks for allow/deny policy. Log only safe metadata such as user ID, provider, and outcome. Never log passwords, cookies, session tokens, or client secrets.
events: {
async signIn({ user, account, isNewUser }) {
console.info("auth.signIn", {
userId: user?.id ?? null,
provider: account?.provider ?? null,
isNewUser: Boolean(isNewUser),
});
},
},Combine the Callbacks Correctly
Put callback functions together inside one callbacks object. Do not define callbacks multiple times in the same JavaScript object, because later properties overwrite earlier ones. Test every provider, denied sign-ins, and redirect cases.