How to Add Auth.js (NextAuth v5) Authentication in Next.js 16 (2026)
Every real app eventually needs to answer one question: who is this user? The moment you add a dashboard, a settings page, or anything that shouldn't be public, you need authentication — and rolling your own login system from scratch (password hashing, sessions, CSRF protection, OAuth flows) is a rabbit hole most developers don't have time for.
That's exactly what Auth.js (the project formerly known as NextAuth) solves. It's free, open source, and gives you production-ready authentication — email/password, GitHub, Google, and dozens of other providers — without owning any of that security-critical code yourself.
In this guide, you'll set up Auth.js v5 in a Next.js 16 App Router project from scratch: installing it, configuring a provider, protecting routes with proxy.ts, and reading the logged-in user inside Server Components. By the end, you'll have a working "Sign in with GitHub" flow you can extend to any provider you want.
What you'll learn
By the end of this guide you'll know how to:
✓ Install Auth.js v5
✓ Configure GitHub OAuth
✓ Protect routes
✓ Read sessions
✓ Use auth() inside Server Components
✓ Secure Server Actions
What Is Auth.js, Exactly?
Auth.js is an authentication library built specifically for full-stack JavaScript frameworks. If you've read older tutorials, you've probably seen it called NextAuth — that's the same project. It was renamed to Auth.js when the team expanded support beyond Next.js to other frameworks like SvelteKit and Express, while the npm package name stayed next-auth for backwards compatibility.
Think of Auth.js as a middleman between your app and the identity providers (GitHub, Google, your own database, etc.). Instead of you writing the OAuth handshake, generating secure session cookies, and validating tokens by hand, Auth.js handles all of it — you just tell it which providers to use.
💡 Tip: If you've seen
getServerSession()in an older tutorial, that's the Auth.js v4 API. In v5 (what this guide uses), everything is exported from a singleauth()function instead — much simpler.
What We're Building
By the end of this guide, you'll have:
- Auth.js v5 installed and configured in a Next.js 16 App Router app
- A working GitHub OAuth sign-in flow (the pattern is the same for Google, Discord, etc.)
- A
proxy.tsfile that protects private routes automatically - A Navbar that shows "Sign in" or the user's avatar depending on auth state
- Access to the logged-in user inside Server Components using
auth()
Prerequisites
Before we start, make sure you have:
- Next.js 16 with the App Router
- TypeScript (strict mode)
- Tailwind CSS for styling
- Basic familiarity with React and the App Router
If your project doesn't have these yet, scaffold one first:
npx create-next-app@latest my-app
For this guide, we're using the src/ directory layout (choose Yes when the CLI asks). Everything works the same without it — just drop the src/ prefix from the file paths below.
Folder Structure
src/
├── app/
│ └── api/
│ └── auth/
│ └── [...nextauth]/
│ └── route.ts
├── actions/
│ └── create-post.ts
├── components/
│ └── auth-buttons.tsx
├── lib/
│ └── auth.ts
├── proxy.ts
.env.local
How does Auth.js work?
User clicks "Sign in"
↓
Redirect to GitHub
↓
User approves
↓
GitHub redirects back
↓
Auth.js validates
↓
Session cookie created
↓
auth() can now read the user
Step 1: Install Auth.js
Auth.js v5 is still installed under its old package name, next-auth, using the beta tag:
npm install next-auth@beta
⚠️ Common Mistake: Installing
next-authwithout@betagives you the old v4 API, which uses a completely different setup ([...nextauth].tspages,authOptions,getServerSession()). This guide is written entirely for v5.
Step 2: Create a GitHub OAuth App
We're using GitHub as our sign-in provider in this guide. In one sentence: OAuth lets users sign in using an existing account (like GitHub or Google) without ever sharing their password with your application. Instead, GitHub itself confirms the user's identity and hands your app a token proving it — your app never sees or stores their GitHub password.
Since we're using GitHub, GitHub needs to know about your app before it'll let anyone sign in with it.
- Go to github.com/settings/developers
- Click New OAuth App
- Fill in:
- Application name: anything, e.g.
My App (Dev) - Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/api/auth/callback/github
- Application name: anything, e.g.
- Click Register application
- Copy the Client ID, then generate and copy a Client Secret
💡 Tip: The callback URL pattern
/api/auth/callback/<provider>is fixed by Auth.js — don't change it, or GitHub won't be able to redirect back to your app correctly.
Step 3: Add Environment Variables
Create a .env.local file in your project root:
# .env.local
AUTH_SECRET=replace-this-with-a-random-string
AUTH_GITHUB_ID=your_github_client_id
AUTH_GITHUB_SECRET=your_github_client_secret
AUTH_SECRET is used to sign and encrypt session tokens — it must stay private. Generate a strong random value with:
npx auth secret
This command (from the auth CLI that ships with next-auth) automatically writes a secure AUTH_SECRET into your .env.local file for you.
⚠️ Common Mistake: Forgetting to add
.env.localto.gitignore. IfAUTH_SECRETor your OAuth secret leaks into a public repo, attackers can forge valid sessions for your app.
Step 4: Configure Auth.js
This is the heart of the setup. Create an auth.ts file inside src/lib/:
// src/lib/auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [GitHub],
});
What's happening here?
NextAuth(...)takes a config object and returns everything you need:handlers(for the API route),signIn/signOut(Server Actions you can call from forms or buttons), andauth()(a function to read the current session anywhere on the server).providers: [GitHub]tells Auth.js to use GitHub OAuth. Since our environment variables follow theAUTH_GITHUB_ID/AUTH_GITHUB_SECRETnaming convention, Auth.js picks them up automatically — no need to pass them manually.
💡 Tip: Want to add Google too? Just import
next-auth/providers/google, add matchingAUTH_GOOGLE_ID/AUTH_GOOGLE_SECRETenv variables, and drop it into theprovidersarray. Auth.js supports 80+ providers this way.
Step 5: Create the API Route Handler
Auth.js needs a route to handle sign-in, sign-out, and OAuth callbacks. Create this file:
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
Why a catch-all route?
The [...nextauth] folder name is a catch-all dynamic route — it matches every path under /api/auth/, like /api/auth/signin, /api/auth/callback/github, and /api/auth/session. Instead of writing a route for each one, Auth.js's handlers object already knows how to respond to all of them internally.
Step 6: Add the Proxy (Middleware)
In Next.js 16, what used to be called middleware.ts is now named proxy.ts — same concept, same file conventions, just a renamed file. This is the ideal place to check authentication before a request even reaches your page.
// src/proxy.ts
export { auth as proxy } from "@/lib/auth";
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
],
};
What's happening here?
Think of proxy.ts like a security guard standing at the entrance of your app — every matching request passes through it before reaching a page. By exporting auth as proxy, every request now has access to the session, which you can use to redirect unauthenticated users.
By default, this doesn't block anything yet — it just makes auth checking available. Let's actually protect a route next.
Step 7: Protect a Route
Let's say you have a /dashboard page that only logged-in users should see, and you also want to bounce already-logged-in users away from your login/home page. Update proxy.ts:
// src/proxy.ts
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const { nextUrl } = req
// Define routes that logged-in users shouldn't see
const isAuthPage = nextUrl.pathname === "/login" || nextUrl.pathname === "/"
if (isAuthPage) {
if (isLoggedIn) {
// Redirect authenticated users to the dashboard
return NextResponse.redirect(new URL("/dashboard", nextUrl))
}
return NextResponse.next()
}
// Protect private routes
if (!isLoggedIn && nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/", nextUrl))
}
});
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
]
};
Breaking this down
auth((req) => {...})wraps your proxy function, giving youreq.auth— the current session, ornullif nobody's logged in.isAuthPagecovers routes a logged-in user has no reason to revisit (like/loginor your home page) — if they're already signed in, we send them straight to/dashboardinstead.- The second check does the opposite: if someone isn't logged in and tries to visit
/dashboard, we redirect them back to/instead of letting the request through.
⚠️ Common Mistake: Relying on
proxy.tsalone for security. Middleware-level checks are great for UX (redirecting before a page even loads), but you should also verify the session inside Server Actions and Route Handlers that touch sensitive data — never trust the frontend redirect as your only line of defense.
Step 8: Add Sign In / Sign Out Buttons
Auth.js exposes signIn and signOut as Server Actions, so you can call them directly from a <form> without any client-side JavaScript.
// src/components/auth-buttons.tsx
import { auth, signIn, signOut } from "@/lib/auth";
export default async function AuthButtons() {
const session = await auth();
if (!session?.user) {
return (
<form
action={async () => {
"use server";
await signIn("github");
}}
>
<button
type="submit"
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
>
Sign in with GitHub
</button>
</form>
);
}
return (
<form
action={async () => {
"use server";
await signOut();
}}
>
<button
type="submit"
className="rounded-lg border px-4 py-2 text-sm font-medium"
>
Sign out
</button>
</form>
);
}
What's happening here?
AuthButtonsis an async Server Component — it callsauth()directly to check whether someone's logged in, nouseEffector loading state needed.- The inline
"use server"function insideaction={...}is an inline Server Action. When the form is submitted, this function runs on the server and callssignIn("github")orsignOut(). signIn("github")tells Auth.js exactly which provider to start the OAuth flow with — useful once you add more than one provider.
Drop this component into your navbar, and you've got a fully working auth toggle with zero client-side JavaScript.
Step 9: Read the Session in a Server Component
Anywhere on the server — a page, a layout, another component — you can call auth() to get the current user:
// src/app/dashboard/page.tsx
import { auth } from "@/lib/auth";
export default async function DashboardPage() {
const session = await auth();
return (
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-muted-foreground">
Welcome back, {session?.user?.name}!
</p>
</div>
);
}
Since we already protect /dashboard in proxy.ts, session is guaranteed to exist by the time this page renders — but it's still good practice to use optional chaining (session?.user?.name) in case that logic ever changes.
Step 10: Protect a Server Action Directly
Even with proxy.ts in place, always double-check authentication inside anything that mutates data:
// src/actions/create-post.ts
"use server";
import { auth } from "@/lib/auth";
export async function createPost(formData: FormData) {
const session = await auth();
if (!session?.user) {
return { error: "You must be signed in to do that." };
}
const title = formData.get("title") as string;
// Your database logic here
// await db.post.create({ data: { title, authorId: session.user.id } })
return { success: true };
}
This is the same idea as validating input on the server even though your form already validates on the client — never trust that a request only came from your UI.
Frequently Asked Questions
Is Auth.js free?
Yes, completely. Auth.js is open source and has no usage limits, unlike some hosted auth providers that charge per monthly active user. You're only responsible for hosting and maintaining your own database if you want persistent user records.
Why is Auth.js still installed as next-auth?
Because the npm package name never changed, even though the project itself rebranded. Renaming a package that thousands of projects already depend on would break every existing install, so the team kept next-auth as the package name and just changed the project's public name and docs to Auth.js. You'll see both names used interchangeably — they refer to the exact same library.
Why did NextAuth become Auth.js?
NextAuth started out Next.js-only, but the maintainers wanted the same authentication core to work with other frameworks too — SvelteKit, Express, and more, via a shared @auth/core package. "NextAuth" no longer described what the project did, so it was renamed to the framework-agnostic Auth.js, with next-auth continuing on as the Next.js-specific package built on top of that shared core.
Do I need a database?
Not for OAuth-only sign-in like the GitHub example above — Auth.js can run in a stateless JWT session mode with zero database. You'll need a database (with an Auth.js adapter, like the Drizzle or Prisma adapter) if you want persistent user records, email/password login, or multiple linked accounts per user.
What's the difference between Auth.js and Clerk?
Auth.js gives you full ownership of your data — everything lives in your own database (or nowhere, if you're stateless) — but you build your own UI. Clerk is a hosted service with prebuilt sign-in components and a dashboard, which is faster to set up but means your user data lives on Clerk's infrastructure. If data ownership matters for compliance reasons, Auth.js is the safer default.
💡 If you're deciding between the two, see Clerk vs NextAuth (Auth.js) in 2026 for a full comparison.
Can I add email/password login instead of OAuth?
Yes, using the Credentials provider. It requires you to write your own password hashing and verification logic (Auth.js won't do this for you, since it's security-sensitive), and you'll need a database to store user records.
Why is my session null even after signing in?
This is almost always a mismatched AUTH_SECRET or an incorrect callback URL. Double-check that your GitHub OAuth app's callback URL exactly matches http://localhost:3000/api/auth/callback/github (or your production domain), and that .env.local was loaded — restart your dev server after any environment variable change.
Wrapping Up
You've now got a complete Auth.js v5 setup in Next.js 16 — GitHub OAuth sign-in, route protection via proxy.ts, and session access in both Server Components and Server Actions. This same pattern extends cleanly: add more providers, connect a database adapter for persistent users, or layer role-based access control on top of the session object.
From here, a natural next step is wiring up a database adapter so user accounts persist across sign-ins, or adding a second provider like Google for more sign-in options.
📦 Source Code: View on GitHub
Useful Resources
Continue Learning
- Clerk vs NextAuth (Auth.js) in 2026: Which One Should You Choose?
- How to Add Clerk Authentication in Next.js 16 (2026 Guide)
- SaaS Starter Architecture with Next.js 2026
Helpful Free Tools
- Meta Tag Generator for Next.js & HTML — generate SEO meta tags, Open Graph tags, Twitter Cards, canonical URLs, and JSON-LD structured data instantly. Handy for wiring up per-post metadata like the
generateMetadata()function above. - Supabase RLS Policy Generator — if your next project pairs this blog setup with a Supabase backend, generate secure, performance-optimized Row Level Security policies in seconds.
- Zod Schema Generator — generate Zod schemas instantly from JSON or TypeScript. Includes smart inference, live validation, React Hook Form, Next.js API Routes, and Server Actions.