Step-by-step
-
1
Install Auth.js v5
Auth.js v5 is published as
next-auth@beta. Install it and generate a secret — this secret signs the session token and must stay out of version control.bashnpm install next-auth@beta # Generate a random AUTH_SECRET and write it to .env.local npx auth secret # Adds AUTH_SECRET="<random>" to .env.local automatically -
2
Create the auth.ts config file
Create
auth.tsat the project root (next topackage.json). This file is the single source of truth — it exportsauth,signIn,signOut, andhandlers. Everything else imports from here, not fromnext-authdirectly.typescript// auth.ts import NextAuth from "next-auth" import Google from "next-auth/providers/google" export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ Google({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET, }), ], }) -
3
Wire up the API route handler
Auth.js needs a catch-all route at
/api/auth/[...nextauth]to handle OAuth callbacks, sign-in, sign-out, and session endpoints. Create the file and re-export the handlers from your config.typescript// app/api/auth/[...nextauth]/route.ts import { handlers } from "@/auth" export const { GET, POST } = handlers -
4
Set the required environment variables
Add these to
.env.local. For Google, get the client ID and secret from the Google Cloud Console under APIs & Services → Credentials → OAuth 2.0 Client IDs. Set the authorised redirect URI tohttp://localhost:3000/api/auth/callback/googlefor development (add your production URL separately).bash# .env.local AUTH_SECRET="generated-by-npx-auth-secret" AUTH_GOOGLE_ID="your-google-client-id.apps.googleusercontent.com" AUTH_GOOGLE_SECRET="your-google-client-secret" # NEXTAUTH_URL is not required in v5 — Auth.js infers it from the request -
5
Protect server components with auth()
In any Server Component or Server Action, call
auth()to get the session. It returnsnullwhen the user is not signed in. Redirect or render a gate accordingly.typescript// 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("/api/auth/signin") } return ( <main> <h1>Welcome, {session.user?.name}</h1> </main> ) } -
6
Add SessionProvider for client components
Client components cannot call
auth()— they use theuseSession()hook instead. Wrap your root layout withSessionProviderso the hook works anywhere in the client tree.typescript// app/layout.tsx import { SessionProvider } from "next-auth/react" import { auth } from "@/auth" export default async function RootLayout({ children }: { children: React.ReactNode }) { const session = await auth() return ( <html> <body> <SessionProvider session={session}> {children} </SessionProvider> </body> </html> ) } // In any client component: "use client" import { useSession } from "next-auth/react" export function UserAvatar() { const { data: session, status } = useSession() if (status === "loading") return <span>Loading…</span> if (!session) return <a href="/api/auth/signin">Sign in</a> return <img src={session.user?.image ?? ""} alt={session.user?.name ?? ""} /> } -
7
Add sign-in and sign-out buttons
Import
signInandsignOutfrom yourauth.tsfile and call them from Server Actions inside a<form>. This avoids exposing credentials to the client and works without JavaScript.typescript// components/auth-buttons.tsx import { signIn, signOut } from "@/auth" export function SignInButton() { return ( <form action={async () => { "use server"; await signIn("google") }}> <button type="submit">Sign in with Google</button> </form> ) } export function SignOutButton() { return ( <form action={async () => { "use server"; await signOut() }}> <button type="submit">Sign out</button> </form> ) } -
8
Protect routes with middleware
Create a
middleware.tsfile at the project root. Export theauthfunction as the middleware — it intercepts every request that matches thematcherconfig and redirects unauthenticated users to the sign-in page before the route handler even runs.typescript// middleware.ts export { auth as middleware } from "@/auth" export const config = { matcher: [ // Protect everything under /dashboard and /account "/dashboard/:path*", "/account/:path*", // Skip static files and API routes that handle auth themselves "/((?!api|_next/static|_next/image|favicon.ico).*)", ], } -
9
Use the Credentials provider for email/password
If you need email + password login instead of (or alongside) OAuth, add the Credentials provider to your
auth.ts. Compare the password against a hashed value from your database — never store plaintext passwords.typescript// auth.ts (Credentials provider addition) import Credentials from "next-auth/providers/credentials" import bcrypt from "bcryptjs" import { db } from "@/lib/db" // your DB client export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ Credentials({ credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) return null const user = await db.user.findUnique({ where: { email: credentials.email as string }, }) if (!user) return null const passwordMatch = await bcrypt.compare( credentials.password as string, user.passwordHash ) return passwordMatch ? user : null }, }), ], })
Tips & gotchas
- Use JWT sessions (the default) for stateless APIs. Switch to database sessions only when you need to invalidate sessions server-side (e.g. account bans).
- Extend the session object with custom fields (user ID, role) using the `callbacks.session` and `callbacks.jwt` hooks in `auth.ts` — this avoids extra DB round-trips on every request.
- In the Google Cloud Console, always add both your development (`localhost`) and production callback URLs to the authorised redirect URIs list before going live.
- Never call `signIn()` or `signOut()` from a Client Component directly — wrap them in a Server Action or use the built-in `/api/auth/signin` URL.
Wrapping up
Auth.js v5 cuts the authentication boilerplate down to a config file, a single API route, and a middleware export. Once it is wired up, adding new providers is a one-liner, and protecting new routes means adding a path to the middleware matcher. The combination of server-side auth() and client-side useSession() covers every rendering pattern the App Router offers.