Fix: Auth.js (NextAuth) Not Working — Session Null, OAuth Callback Error, or CSRF Token Mismatch
Quick Answer
How to fix Auth.js and NextAuth.js issues — OAuth provider setup, session handling in App Router and Pages Router, JWT vs database sessions, middleware protection, and credential provider configuration.
The Problem
useSession() always returns null even after login:
const { data: session } = useSession();
console.log(session); // null — even after successful OAuth loginOr the OAuth callback fails:
[next-auth][error][OAUTH_CALLBACK_ERROR]
Error: oauth_callback_error — state mismatchOr you get a CSRF token mismatch:
[next-auth][error][CSRF_TOKEN_MISMATCH]Or the session exists in API routes but not in Server Components:
// app/page.tsx (Server Component)
const session = await auth();
console.log(session); // nullWhy This Happens
Auth.js (v5, formerly NextAuth.js v4) handles authentication through OAuth providers, credentials, and session management. Its configuration changed significantly between v4 and v5:
SessionProvideris required for client components —useSession()reads from a React context. WithoutSessionProviderwrapping your app, the hook always returnsnull. In App Router, the provider goes in a client component wrapper.- OAuth state/CSRF errors come from cookie misconfiguration — Auth.js stores state and CSRF tokens in cookies. If cookies are blocked (wrong domain, SameSite policy, missing HTTPS in production), the callback can’t verify the state parameter.
NEXTAUTH_URLmust match the actual URL — in production, Auth.js usesNEXTAUTH_URL(v4) orAUTH_URL(v5) to construct callback URLs. A mismatch between this variable and the actual domain causes OAuth providers to reject the redirect.- v5 uses
auth()instead ofgetServerSession()— the API changed.getServerSession(authOptions)is v4. In v5, you exportauthfrom your config and call it directly.
Fix 1: Auth.js v5 Setup (App Router)
npm install next-auth@beta @auth/core// auth.ts — root of your project
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import { db } from './src/db';
import bcrypt from 'bcryptjs';
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(db), // Optional: database sessions
providers: [
GitHub({
clientId: process.env.AUTH_GITHUB_ID!,
clientSecret: process.env.AUTH_GITHUB_SECRET!,
}),
Google({
clientId: process.env.AUTH_GOOGLE_ID!,
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
}),
Credentials({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await db.query.users.findFirst({
where: eq(users.email, credentials.email as string),
});
if (!user || !user.passwordHash) return null;
const valid = await bcrypt.compare(
credentials.password as string,
user.passwordHash,
);
if (!valid) return null;
return { id: user.id, name: user.name, email: user.email };
},
}),
],
session: {
strategy: 'jwt', // Use 'database' with an adapter for DB sessions
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.role = user.role;
}
return token;
},
async session({ session, token }) {
if (token) {
session.user.id = token.id as string;
session.user.role = token.role as string;
}
return session;
},
async authorized({ auth, request }) {
// Used by middleware — return true to allow, false to redirect
const isLoggedIn = !!auth?.user;
const isProtected = request.nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !isLoggedIn) return false;
return true;
},
},
pages: {
signIn: '/login', // Custom sign-in page
error: '/auth/error', // Custom error page
},
});// app/api/auth/[...nextauth]/route.ts — API route handler
import { handlers } from '@/auth';
export const { GET, POST } = handlers;// middleware.ts — protect routes
export { auth as middleware } from '@/auth';
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/api/protected/:path*'],
};Fix 2: Session Access in Server and Client Components
// app/layout.tsx — wrap with SessionProvider for client components
import { SessionProvider } from 'next-auth/react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<SessionProvider>
{children}
</SessionProvider>
</body>
</html>
);
}
// Server Component — use auth() directly
// app/dashboard/page.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await auth();
if (!session) redirect('/login');
return <h1>Welcome, {session.user.name}</h1>;
}
// Client Component — use useSession()
// components/UserMenu.tsx
'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
export function UserMenu() {
const { data: session, status } = useSession();
if (status === 'loading') return <div>Loading...</div>;
if (!session) {
return <button onClick={() => signIn()}>Sign In</button>;
}
return (
<div>
<span>{session.user.name}</span>
<button onClick={() => signOut()}>Sign Out</button>
</div>
);
}
// Server Action — use auth() in actions
// app/actions.ts
'use server';
import { auth } from '@/auth';
export async function createPost(formData: FormData) {
const session = await auth();
if (!session) throw new Error('Not authenticated');
const title = formData.get('title') as string;
// ... create post with session.user.id as author
}
// API Route — use auth() in route handlers
// app/api/posts/route.ts
import { auth } from '@/auth';
export async function GET() {
const session = await auth();
if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });
// ... return posts
}Fix 3: Fix OAuth Callback Errors
# .env.local — required environment variables
AUTH_SECRET=your-random-secret-at-least-32-characters # v5
# NEXTAUTH_SECRET=... # v4
AUTH_URL=http://localhost:3000 # Development
# AUTH_URL=https://myapp.com # Production
AUTH_GITHUB_ID=your-github-oauth-app-client-id
AUTH_GITHUB_SECRET=your-github-oauth-app-client-secret
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret# Generate a secret
npx auth secret
# Or: openssl rand -base64 32OAuth provider callback URL configuration:
| Provider | Callback URL |
|---|---|
| GitHub | https://yourapp.com/api/auth/callback/github |
https://yourapp.com/api/auth/callback/google | |
| Discord | https://yourapp.com/api/auth/callback/discord |
Common callback fixes:
- State mismatch — caused by cookie issues. Check that
AUTH_URLmatches your actual domain. In development, usehttp://localhost:3000, nothttp://127.0.0.1:3000. - CSRF mismatch — same cookie issue. Clear browser cookies and retry. If behind a reverse proxy, set
trustHost: truein your Auth.js config. - Redirect URI mismatch — the callback URL registered with the OAuth provider must exactly match what Auth.js sends. Check for trailing slashes and protocol (http vs https).
// auth.ts — fix for reverse proxy / load balancer
export const { handlers, auth } = NextAuth({
trustHost: true, // Trust the X-Forwarded-Host header
// ...
});Fix 4: TypeScript — Extend Session Types
// types/next-auth.d.ts
import { DefaultSession, DefaultUser } from 'next-auth';
import { DefaultJWT } from 'next-auth/jwt';
declare module 'next-auth' {
interface Session {
user: {
id: string;
role: string;
} & DefaultSession['user'];
}
interface User extends DefaultUser {
role: string;
}
}
declare module 'next-auth/jwt' {
interface JWT {
id: string;
role: string;
}
}Fix 5: NextAuth.js v4 (Pages Router)
If you’re still on v4:
// pages/api/auth/[...nextauth].ts
import NextAuth, { type NextAuthOptions } from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';
export const authOptions: NextAuthOptions = {
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
],
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async session({ session, token }) {
if (token.sub) session.user.id = token.sub;
return session;
},
},
};
export default NextAuth(authOptions);// Server-side (Pages Router)
import { getServerSession } from 'next-auth';
import { authOptions } from '@/pages/api/auth/[...nextauth]';
// In getServerSideProps
export async function getServerSideProps(context) {
const session = await getServerSession(context.req, context.res, authOptions);
if (!session) return { redirect: { destination: '/login', permanent: false } };
return { props: { session } };
}
// In API routes
export default async function handler(req, res) {
const session = await getServerSession(req, res, authOptions);
if (!session) return res.status(401).json({ error: 'Unauthorized' });
// ...
}Fix 6: Database Sessions with Drizzle or Prisma
npm install @auth/drizzle-adapter
# Or: npm install @auth/prisma-adapter// Drizzle adapter setup
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import { db } from './db';
export const { handlers, auth } = NextAuth({
adapter: DrizzleAdapter(db),
session: { strategy: 'database' }, // Store sessions in DB
providers: [GitHub({ /* ... */ })],
});
// Required Drizzle schema tables
// See: https://authjs.dev/getting-started/adapters/drizzle
import { pgTable, text, timestamp, primaryKey, integer } from 'drizzle-orm/pg-core';
export const users = pgTable('user', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text('name'),
email: text('email').notNull(),
emailVerified: timestamp('emailVerified', { mode: 'date' }),
image: text('image'),
});
export const accounts = pgTable('account', {
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
type: text('type').notNull(),
provider: text('provider').notNull(),
providerAccountId: text('providerAccountId').notNull(),
refresh_token: text('refresh_token'),
access_token: text('access_token'),
expires_at: integer('expires_at'),
token_type: text('token_type'),
scope: text('scope'),
id_token: text('id_token'),
session_state: text('session_state'),
}, (account) => ({
compoundKey: primaryKey({ columns: [account.provider, account.providerAccountId] }),
}));
export const sessions = pgTable('session', {
sessionToken: text('sessionToken').primaryKey(),
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
expires: timestamp('expires', { mode: 'date' }).notNull(),
});Still Not Working?
useSession() returns undefined instead of null — SessionProvider is missing. In App Router, you need a client component wrapper. Create components/Providers.tsx with 'use client' and SessionProvider, then use it in your root layout.
Session works in development but not in production — check AUTH_SECRET (v5) or NEXTAUTH_SECRET (v4) is set in your production environment. Without a secret, session tokens can’t be signed or verified. Also check AUTH_URL matches your production domain exactly.
Credentials provider doesn’t persist sessions — the Credentials provider doesn’t work with database sessions by default. Use session: { strategy: 'jwt' } with Credentials. If you need database sessions with Credentials, you must manually create the session in the authorize callback.
OAuth works but user data isn’t in the database — you need an adapter. Without an adapter, Auth.js uses JWT-only sessions and doesn’t store users in a database. Install and configure @auth/drizzle-adapter, @auth/prisma-adapter, or another supported adapter.
For related auth issues, see Fix: Lucia Auth Not Working and Fix: Next.js App Router Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Better Auth Not Working — Login Failing, Session Null, or OAuth Callback Error
How to fix Better Auth issues — server and client setup, email/password and OAuth providers, session management, middleware protection, database adapters, and plugin configuration.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.