HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
7 Topics
Chapter•30 Minutes

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.

Callback flow

`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.

Sign-in policy
callbacks: {
  async signIn({ user, account, profile }) {
    if (account?.provider === "google") {
      return Boolean(user?.email && profile?.email_verified);
    }
    return Boolean(user?.email);
  },
},
💡 Adapt the policy to your actual provider and identity-verification requirements.

`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.

JWT callback
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;
  },
},
💡 NextAuth's default JWT session token is encrypted as JWE. Do not assume it is a plain three-part signed JWT.

`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.

Session callback
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;
  },
},
💡 A session role can guide UI rendering, but privileged server operations must verify authorization independently.

`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.

Safe redirect callback
callbacks: {
  async redirect({ url, baseUrl }) {
    if (url.startsWith("/")) return `${baseUrl}${url}`;
    try {
      if (new URL(url).origin === baseUrl) return url;
    } catch {}
    return baseUrl;
  },
},
💡 Do not return an arbitrary query-string URL.

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.

Safe audit event
events: {
  async signIn({ user, account, isNewUser }) {
    console.info("auth.signIn", {
      userId: user?.id ?? null,
      provider: account?.provider ?? null,
      isNewUser: Boolean(isNewUser),
    });
  },
},
💡 Use a structured logger and your organization's privacy policy; avoid unnecessary personal data.

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.