OAuth Flow
Social login with Next-Auth v5 covering the full OAuth 2.0 cycle: provider setup, callback handling, session management, and protected routes.
Principle
OAuth delegates authentication to a trusted provider — you never handle passwords. The key is understanding the two-step flow: (1) redirect user to provider, (2) exchange the authorization code for tokens on your server. Never exchange codes on the client.
Store the minimal session payload on the JWT — just userId and role. Fetch full user data from your DB on demand. Bloated JWTs slow down every request.
Rules
- check_circleServer-Side Code ExchangeThe OAuth code-for-token exchange must happen on the server. Never expose your client_secret to the browser.
- check_circleMinimal JWT PayloadOnly store userId and role in the JWT. Everything else (name, avatar, permissions) should be fetched from your DB when needed.
- check_circleProtect Routes at Middleware LevelUse Next.js middleware to check session before the page renders. Avoids flash of unauthenticated content.
- check_circleHandle Token RefreshImplement silent refresh: detect expiry in the JWT callback and call the provider's token endpoint to get a new access token.
Pattern
import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; import Google from 'next-auth/providers/google'; export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), ], callbacks: { jwt({ token, account, profile }) { // Persist minimal data to JWT on first sign-in if (account) { token.userId = profile?.sub; token.provider = account.provider; } return token; }, session({ session, token }) { session.user.id = token.userId as string; return session; }, }, });
Implementation
Version Compatibility
Requires React 19+ and the latest stable versions of all dependencies shown.
Next-Auth v5 integrates natively with App Router. Expose the handlers at a catch-all route, use the auth() helper in Server Components, and protect pages via middleware.
import { auth } from '@/auth'; import { NextResponse } from 'next/server'; export default auth((req) => { const isAuthenticated = !!req.auth; const isProtected = req.nextUrl.pathname.startsWith('/dashboard'); if (isProtected && !isAuthenticated) { return NextResponse.redirect(new URL('/login', req.url)); } }); export const config = { matcher: ['/dashboard/:path*', '/settings/:path*'], }; // app/api/auth/[...nextauth]/route.ts import { handlers } from '@/auth'; export const { GET, POST } = handlers; // app/dashboard/page.tsx (Server Component) import { auth } from '@/auth'; import { redirect } from 'next/navigation'; export default async function DashboardPage() { const session = await auth(); if (!session) redirect('/login'); return <Dashboard user={session.user} />; }