Fix: NextAuth.js Not Working — Session Null, Callback Errors, or OAuth Redirect Issues
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.
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;
}
}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.
For related Next.js issues, see Fix: Next.js Middleware Not Running and Fix: Next.js Environment Variables 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.