Fix: NextAuth.js Not Working — Session Null, Callback Errors, or OAuth Redirect Issues
Part of: React & Frontend Errors
Quick Answer
How to fix NextAuth.js (Auth.js) issues — session undefined in server components, OAuth callback URL mismatch, JWT vs database sessions, middleware protection, and credentials provider.
The Problem
useSession() returns null or undefined even after signing in:
// app/dashboard/page.tsx
import { useSession } from 'next-auth/react';
export default function Dashboard() {
const { data: session } = useSession();
console.log(session); // null — even though the user is logged in
}Or a server component can’t access the session:
// app/profile/page.tsx (Server Component)
import { getServerSession } from 'next-auth';
export default async function Profile() {
const session = await getServerSession();
// session is null — getServerSession() without options always returns null
}Or OAuth login redirects to an error page:
https://your-app.com/api/auth/error?error=OAuthCallbackError
https://your-app.com/api/auth/error?error=ConfigurationOr the session works in development but not in production.
Why This Happens
NextAuth.js (rebranded as Auth.js for v5) has several configuration requirements that are easy to miss:
SessionProvidermissing —useSession()requires theSessionProvidercontext wrapper. Without it, session is alwaysnull.getServerSession()needs authOptions — callinggetServerSession()without passing yourauthOptionsconfig always returnsnullin App Router.NEXTAUTH_URLnot set in production — NextAuth usesNEXTAUTH_URLto construct callback URLs. Without it, OAuth providers redirect to the wrong URL.- OAuth callback URL mismatch — the redirect URI registered with your OAuth provider (Google, GitHub, etc.) must exactly match what NextAuth generates. Any mismatch causes
OAuthCallbackError. NEXTAUTH_SECRETmissing — required in production for signing JWT tokens. Without it, sessions can’t be created.
There is a second layer to this problem: the library has gone through three major API revolutions in five years, and tutorials, AI-generated snippets, and even your own previous code all coexist on the same npm dependency tree. NextAuth.js v3 (2020) used a [...nextauth].js API route in the Pages Router and a single _app.js <Provider> wrapper. NextAuth.js v4 (2021) introduced TypeScript-first typing, the getServerSession() helper, and next-auth/middleware. The 2022 rebrand to Auth.js signaled a framework-agnostic future and the start of the v5 redesign. next-auth@5 (beta throughout 2023, stable in 2024) replaced getServerSession(authOptions) with a unified auth() helper, dropped NEXTAUTH_URL requirements on Vercel, and switched providers to function-call form (GitHub({...}) instead of GitHubProvider({...})). If you copy a v4 snippet into a v5 project, almost every import path and call signature is wrong.
The App Router landing in Next.js 13.4 made things worse before they got better. Server Components, Server Actions, and Route Handlers each have a different session-access pattern, and the official examples lagged the stable Next.js release by months. Many of the “NextAuth not working” issues you see on GitHub Issues from 2023 are not bugs — they are someone running v4 code inside an App Router project and getting bitten by the missing authOptions argument, the cookies API change, or the fact that useSession() only works in Client Components.
The third layer is the OAuth provider side. Google, GitHub, Apple, and Microsoft each have their own redirect URI validation rules, secret rotation schedules, and dev/staging/prod separation requirements. The most common production failure — OAuthCallbackError — almost never comes from NextAuth itself. It comes from a redirect URI mismatch between what NextAuth generated (based on NEXTAUTH_URL or the auto-detected host) and what is whitelisted in the provider dashboard. Add a single trailing slash, forget to include https://, or use a preview URL that wasn’t registered, and the entire flow breaks at the very last redirect.
Fix 1: Add SessionProvider to the App
useSession() is a React hook that requires the SessionProvider context:
// app/providers.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}// Now useSession() works in any Client Component
'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
export function UserMenu() {
const { data: session, status } = useSession();
if (status === 'loading') return <p>Loading...</p>;
if (status === 'unauthenticated') {
return <button onClick={() => signIn()}>Sign in</button>;
}
return (
<div>
<p>Signed in as {session.user?.email}</p>
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}Fix 2: Get Session in Server Components
getServerSession() requires your authOptions to work correctly in the App Router:
// lib/auth.ts — export authOptions to reuse everywhere
import NextAuth, { NextAuthOptions } from 'next-auth';
import GithubProvider from 'next-auth/providers/github';
import GoogleProvider from 'next-auth/providers/google';
export const authOptions: NextAuthOptions = {
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
secret: process.env.NEXTAUTH_SECRET,
};
export default NextAuth(authOptions);// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };// app/profile/page.tsx (Server Component)
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function Profile() {
// WRONG — no authOptions, always returns null
// const session = await getServerSession();
// CORRECT — pass authOptions
const session = await getServerSession(authOptions);
if (!session) {
redirect('/api/auth/signin');
}
return <h1>Welcome, {session.user?.name}</h1>;
}Auth.js v5 (next-auth@5) — new API:
// auth.ts (Auth.js v5)
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
export const { auth, handlers, signIn, signOut } = NextAuth({
providers: [GitHub],
});
// app/profile/page.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
export default async function Profile() {
const session = await auth(); // No authOptions needed in v5
if (!session) redirect('/api/auth/signin');
return <h1>Welcome, {session.user?.name}</h1>;
}Fix 3: Fix OAuth Callback URL Mismatch
The redirect URI in your OAuth provider settings must match what NextAuth generates:
NextAuth callback URL format:
{NEXTAUTH_URL}/api/auth/callback/{provider}
Examples:
https://your-app.com/api/auth/callback/github
https://your-app.com/api/auth/callback/google
https://your-app.com/api/auth/callback/twitterSet these in your OAuth provider’s dashboard:
GitHub → Settings → Developer Settings → OAuth Apps → your app
Authorization callback URL: https://your-app.com/api/auth/callback/github
Google → Google Cloud Console → APIs & Services → Credentials → OAuth 2.0 Client
Authorized redirect URIs: https://your-app.com/api/auth/callback/google
For local development — add both:
http://localhost:3000/api/auth/callback/github
https://your-app.com/api/auth/callback/githubSet environment variables:
# .env.local (development)
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-random-secret-here
# Production (.env or hosting platform env vars)
NEXTAUTH_URL=https://your-app.com
NEXTAUTH_SECRET=your-production-secret-here
# Generate a secure secret:
openssl rand -base64 32Note: On Vercel,
NEXTAUTH_URLis often not needed because Auth.js detects the deployment URL automatically. But it’s required on most other platforms.
Fix 4: Protect Routes with Middleware
Use NextAuth middleware to protect routes without checking the session on every page:
// middleware.ts (at the root, not in app/)
export { default } from 'next-auth/middleware';
// Apply middleware to specific paths
export const config = {
matcher: [
'/dashboard/:path*',
'/profile/:path*',
'/api/protected/:path*',
// Exclude public paths and NextAuth's own routes
],
};Custom middleware with redirect:
// middleware.ts
import { withAuth } from 'next-auth/middleware';
import { NextResponse } from 'next/server';
export default withAuth(
function middleware(req) {
// Custom logic — e.g., role-based access
const token = req.nextauth.token;
if (req.nextUrl.pathname.startsWith('/admin') && token?.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', req.url));
}
},
{
callbacks: {
authorized: ({ token }) => !!token, // Require a token to access
},
}
);
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};Auth.js v5 middleware:
// middleware.ts
import { auth } from '@/auth';
import { NextResponse } from 'next/server';
export default auth((req) => {
if (!req.auth) {
return NextResponse.redirect(new URL('/login', req.url));
}
});
export const config = {
matcher: ['/dashboard/:path*'],
};Fix 5: Use Credentials Provider Correctly
The Credentials provider lets you authenticate with username/password:
// lib/auth.ts
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { db } from '@/lib/db';
export const authOptions: NextAuthOptions = {
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; // Return null to indicate failure
}
const user = await db.user.findUnique({
where: { email: credentials.email },
});
if (!user || !user.hashedPassword) {
return null;
}
const isValid = await bcrypt.compare(
credentials.password,
user.hashedPassword
);
if (!isValid) {
return null;
}
// Return user object — will be stored in JWT/session
return {
id: user.id,
email: user.email,
name: user.name,
};
},
}),
],
// Credentials provider requires JWT strategy
session: { strategy: 'jwt' },
pages: {
signIn: '/login', // Custom sign-in page
},
};Warning: The Credentials provider doesn’t support account linking, email verification, or password reset out of the box. For full auth flows, consider using the Email or OAuth providers alongside it.
Fix 6: Extend the Session with Custom Data
Add custom fields (like user role) to the session:
// lib/auth.ts
import { NextAuthOptions, Session } from 'next-auth';
import { JWT } from 'next-auth/jwt';
export const authOptions: NextAuthOptions = {
providers: [...],
callbacks: {
async jwt({ token, user }) {
// 'user' is only available on initial sign-in
if (user) {
token.id = user.id;
token.role = user.role; // Add role from database
}
return token;
},
async session({ session, token }) {
// Transfer JWT data to the session object
if (session.user) {
session.user.id = token.id as string;
session.user.role = token.role as string;
}
return session;
},
},
};// types/next-auth.d.ts — extend type definitions
import 'next-auth';
import 'next-auth/jwt';
declare module 'next-auth' {
interface Session {
user: {
id: string;
role: string;
email: string;
name?: string;
};
}
interface User {
role: string;
}
}
declare module 'next-auth/jwt' {
interface JWT {
id: string;
role: string;
}
}Version History: NextAuth.js to Auth.js
The package has been renamed, restructured, and rewritten enough times that “which version are you on?” is the first question to ask whenever a session-related error appears.
NextAuth.js v3 (2020) — Pages Router era. Configuration lived in pages/api/auth/[...nextauth].js with a default-export array of providers and a single _app.js wrapper using <Provider session={...}>. There was no getServerSession — server-side access went through getSession({ req }) in getServerSideProps. If you find a tutorial that imports from next-auth/client, you are reading v3 documentation and almost nothing you copy will compile against modern versions.
NextAuth.js v4 (Nov 2021) — TypeScript-first redesign. This is still the most widely deployed version in production today. It introduced NextAuthOptions, switched the React client export to next-auth/react, replaced <Provider> with <SessionProvider>, and added getServerSession(authOptions). The provider imports moved from next-auth/providers (default exports) to named providers in subpaths: next-auth/providers/github. Most “session is null in server components” Stack Overflow questions are v4 questions.
Auth.js rebrand (2022) — framework-agnostic future. The project rebranded to Auth.js to signal SvelteKit, SolidStart, and Express adapters were coming. The npm package stayed next-auth for backward compatibility, but the docs site moved to authjs.dev. This is when the v5 design started.
next-auth@5 beta (2023) — App Router-native API. The big break. Configuration moved from a route file to a top-level auth.ts that exports { auth, handlers, signIn, signOut }. The new auth() helper replaces getServerSession() in Server Components, Route Handlers, Server Actions, and middleware — same function, four contexts. Providers became factory calls: GitHub({ clientId, clientSecret }). Middleware switched from withAuth to the same auth() export. Edge runtime support was added for cookie-only sessions.
Auth.js v5 stable (early 2024). The v5 beta cycle ran for most of 2023 because each App Router minor release surfaced new edge cases (Server Actions in 13.4, cookies() becoming async in 15.0, params becoming async in 15.0). The stable v5 release locked the API for Next.js 14+ and added first-class TypeScript inference for session shape.
v5.x post-stable (late 2024 - 2025). Iterative additions: WebAuthn provider, account linking strategies, the useSession() hook gaining a required: true option that triggers a redirect when unauthenticated. Adapters were split into separate packages (@auth/prisma-adapter, @auth/drizzle-adapter) so the core stays lean.
Migration cheat-sheet. In v4: import { getServerSession } from 'next-auth'; const session = await getServerSession(authOptions);. In v5: import { auth } from '@/auth'; const session = await auth();. In v4: import { withAuth } from 'next-auth/middleware';. In v5: import { auth } from '@/auth'; export default auth;. In v4: NEXTAUTH_URL and NEXTAUTH_SECRET env vars. In v5: AUTH_URL and AUTH_SECRET (the old names still work as aliases). If your codebase still uses NEXTAUTH_SECRET, you are on v4 or running v5 in compatibility mode.
Still Not Working?
Session works in dev but not production — the two most common causes: NEXTAUTH_URL is not set for the production domain, or NEXTAUTH_SECRET is missing. Both are required in production.
useSession() returns { status: 'loading' } forever — the SessionProvider is present but the API route at /api/auth/[...nextauth] is missing or returning an error. Open DevTools Network tab and check the response from /api/auth/session.
Database session vs JWT session — by default, NextAuth uses database sessions (requires a database adapter). If you haven’t set up an adapter, switch to JWT strategy: session: { strategy: 'jwt' }. JWT sessions work without a database.
Cookies not working across subdomains — if your app is on app.example.com but the auth is on auth.example.com, cookies won’t share. Set cookies.sessionToken.options.domain = '.example.com' in authOptions to make the cookie available to all subdomains.
cookies() must be awaited after upgrading to Next.js 15 — in Next.js 15, the cookies(), headers(), and params APIs became asynchronous. NextAuth v4 still calls these synchronously in some code paths. Either upgrade to Auth.js v5 (which handles the async APIs natively) or pin Next.js to 14.x until you migrate. Trying to patch around it inside v4 leads to flaky session reads on the server.
Preview deployments (Vercel, Netlify) fail OAuth callback — each preview gets a unique URL like myapp-pr-42-myteam.vercel.app. That URL is not in your OAuth provider’s whitelist, so OAuthCallbackError fires. Solutions: add *.vercel.app to the provider (Google and GitHub don’t allow wildcards, but some others do), use a separate dev OAuth app per preview environment, or proxy auth through a stable subdomain like auth.yourdomain.com.
CredentialsSignin error with no detail — by default, Auth.js v5 hides the underlying reason credentials sign-in failed (to prevent user enumeration). To see the real error during development, log it inside the authorize() callback before returning null, or set debug: true in your auth config. Never expose the detailed reason to the client in production.
For related Next.js issues, see Fix: Next.js Middleware Not Running, Fix: Next.js Environment Variables Not Working, Fix: Next.js API Route Not Working, and Fix: Next.js Server Action 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: Clerk Not Working — Auth Not Loading, Middleware Blocking, or User Data Missing
How to fix Clerk authentication issues — ClerkProvider setup, middleware configuration, useUser and useAuth hooks, server-side auth, webhook handling, and organization features.
Fix: next-safe-action Not Working — Action Not Executing, Validation Errors Missing, or Type Errors
How to fix next-safe-action issues — action client setup, Zod schema validation, useAction and useOptimisticAction hooks, middleware, error handling, and authorization patterns.
Fix: nuqs Not Working — URL State Not Syncing, Type Errors, or Server Component Issues
How to fix nuqs URL search params state management — useQueryState and useQueryStates setup, parsers, server-side access, shallow routing, history mode, and Next.js App Router integration.
Fix: Vercel AI SDK Not Working — Streaming Not Rendering, useChat Stuck Loading, or Provider Errors
How to fix Vercel AI SDK issues — useChat and useCompletion hooks, streaming responses with streamText, provider configuration for OpenAI and Anthropic, tool calling, and Next.js integration.