Authentication & Authorization
Adding authentication to a full-stack application is like installing an access control system in an office building—you need precise control over who can enter and which floors they can access.
1. What You'll Learn
- Auth.js v5 (NextAuth) Integration with Credentials / GitHub / Google Multi-Provider
- Selecting and Configuring JWT and Database Session Strategies
- Middleware Routing Protection and
matcherExcluding Common Paths - Clerk Third-Party Authentication Integration (
<SignIn />/<SignUp />/auth()helper) - RBAC: Role-Based Access Control (admin / editor / viewer)
getToken()Authentication for Protected API Routes
2. A True Story of a Full-Stack Developer
(1) Pain Point: We didn’t realize we lacked certification until right before launch
Alice works at a 15-person SaaS startup. She developed "TaskFlow," a fully featured team collaboration platform, using Next.js 16. However, during a security audit prior to launch, Bob, the technical lead, pointed out:
"Your
/dashboardpage is accessible to anyone; the API endpoint lacks token authentication; user data is exposed in plain text."
The audit report lists the following issues:
| Issue | Scope | Risk Level |
|---|---|---|
| No Login Page | All Routes | 🔴 High |
| API Route: No Authentication | /api/projects/* |
🔴 High |
| No role distinction | All users can view the admin panel | 🟡 Medium |
| Session never expires | Log in once and stay logged in forever | 🔴 High |
(2) The Auth.js + Clerk Solution
Implement the standard authentication flow using Auth.js v5, with Clerk as a zero-configuration alternative.
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
import Credentials from 'next-auth/providers/credentials'
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
GitHub,
Google,
Credentials({
credentials: { email: {}, password: {} },
async authorize(credentials) {
const user = { id: '1', name: 'Alice', email: 'alice@taskflow.io', role: 'admin' }
return credentials.email === 'alice@taskflow.io' && credentials.password === 'pass123' ? user : null
}
})
],
callbacks: { session: ({ session, token }) => ({ ...session, user: { ...session.user, role: token.role } }) }
})
(3) Revenue
| Dimension | Before Implementation | After Implementation |
|---|---|---|
Access /dashboard without logging in |
Accessible | Redirect to login page |
| API Endpoint Security | No Validation | getToken() JWT Validation |
| Role Control | None | Three levels: admin, editor, viewer |
| Session Expiration | Permanent | Expires automatically after 30 days |
| Integration Time | — | Auth.js: 2h / Clerk: 30min |
3. Auth.js v5 (NextAuth) Provider Integration
(1) Provider Architecture
The Provider abstraction layer in Auth.js v5 allows you to integrate with multiple authentication sources through a unified interface:
graph TB
A[Request Authentication] --> B{NextAuth Route Handler}
B --> C[Credentials<br/>Email+Password]
B --> D[OAuth<br/>GitHub / Google]
B --> E[Other Provider<br/>Auth0 / Azure AD]
C --> F[JWT Callback<br/>token + session]
D --> F
E --> F
F --> G[Session Return to the client]
G --> H[Middleware Verification]
H --> I[Protected Page]
H --> J[Public Page]
style B fill:#cce5ff
style F fill:#d4edda
| Provider Type | Implementation Complexity | User Experience | Applicable Scenarios |
|---|---|---|---|
| Credentials | Medium (requires a custom login page) | Standard form | Proprietary account system |
| GitHub OAuth | Low | One-click login | Developer tools |
| Google OAuth | Low | One-click login | For general users |
| OIDC / SAML | High | Enterprise SSO | Enterprise intranet |
(2) Installation and Initialization
npm install next-auth@beta
npx auth secret # Generate AUTH_SECRET
// auth.ts — Centralized Authentication Configuration
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET! }),
Google({ clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET! })
]
})
▶ Example: Complete Configuration of a Credentials Provider
Output:
TypeScript code executed successfully.
// auth.ts — Credentials + OAuth Mixed
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import GitHub from 'next-auth/providers/github'
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Credentials({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials) {
const { email, password } = credentials as { email: string; password: string }
// Actual Project Query Database
const user = { id: '1', name: 'Alice', email, role: 'admin' }
if (email === 'admin@taskflow.io' && password === 'admin123') return user
return null
}
}),
GitHub
],
callbacks: {
async jwt({ token, user }) {
if (user) token.role = (user as any).role
return token
},
async session({ session, token }) {
session.user.role = token.role as string
return session
}
}
})
Output:
TypeScript configuration loaded successfully.
authorize function returns null, it indicates that the login failed, and Auth.js automatically returns a 401 response.
4. Session Management: JWT vs Database
(1) Comparison of the Two Strategies
Auth.js v5 supports two session storage strategies:
| Dimension | JWT (default) | Database |
|---|---|---|
| Storage Location | Encrypted JWT in a cookie | Database Session table |
| Query Overhead | Zero (no DB queries) | One DB query per request |
| Immediate Expiration | Depends on JWT Expiration Time | Can Be Revoked Immediately |
| Scalability | No database required | Requires an ORM such as Prisma |
| Suitable Scale | Small and Medium-Sized Applications | Large Enterprise Applications |
graph LR
subgraph JWT Pattern
A1[Log In] --> B1[Generate JWT<br/>with user + role]
B1 --> C1[Write Cookie]
C1 --> D1[Request → middleware<br/>Decrypt JWT → Verification]
end
subgraph Database Pattern
A2[Log In] --> B2[Create Session<br/>Write DB]
B2 --> C2[Session ID → Cookie]
C2 --> D2[Request → middleware<br/>Search DB → Verification]
end
style A1 fill:#d4edda
style A2 fill:#cce5ff
(2) Database Session Configuration
// auth.ts — Database Session + Prisma
import NextAuth from 'next-auth'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: 'database' },
providers: [GitHub, Google]
})
▶ Example: Retrieving a Session and Displaying User Information
Output:
TypeScript module executes successfully.
// app/dashboard/page.tsx — Server-Side Retrieval Session
import { auth } from '@/auth'
export default async function DashboardPage() {
const session = await auth()
if (!session?.user) return <p>Please log in first</p>
return (
<div>
<h1>Welcome back,{session.user.name}</h1>
<p>Email:{session.user.email}</p>
<p>Characters:{session.user.role}</p>
<img src={session.user.image!} alt="avatar" width={48} height={48} />
</div>
)
}
<h1>Welcome back,Alice</h1>
<p>Email:alice@taskflow.io</p>
<p>Characters:admin</p>
Output:
<h1>Welcome back, Alice</h1>
<p>Email: alice@taskflow.io</p>
<p>Role: admin</p>
5. Middleware Routing Protection
(1) Matcher Configuration Mode
middleware.ts Intercept the request before it reaches the page to check the session's validity:
graph TB
A[User Request /dashboard/*] --> B{middleware.ts}
B -->|Valid Session| C[Clearance → page.tsx]
B -->|No Session| D[Redirect /login]
D --> E{Public Paths?}
E -->|/api/auth/*| F[Clearance]
E -->|/_next/*| F
E -->|/favicon.ico| F
style B fill:#fff3cd
style C fill:#d4edda
style D fill:#f8d7da
| Matcher Pattern | Matching Path | Description |
|---|---|---|
/dashboard/:path* |
/dashboard/* |
Protect the Dashboard |
/api/projects/:path* /api/projects/* Protection API |
||
| `/((?!auth | _next | favicon).*)` |
(2) Complete Middleware Implementation
// middleware.ts
import { auth } from '@/auth'
import { NextResponse } from 'next/server'
export default auth((req) => {
const { pathname } = req.nextUrl
const isLoggedIn = !!req.auth
const isPublicPath = pathname.startsWith('/login') ||
pathname.startsWith('/register') ||
pathname.startsWith('/api/auth')
if (!isLoggedIn && !isPublicPath) {
return NextResponse.redirect(new URL('/login', req.url))
}
// RBAC:Prevent non- admin Visit /admin
if (pathname.startsWith('/admin') && req.auth?.user?.role !== 'admin') {
return NextResponse.redirect(new URL('/dashboard', req.url))
}
return NextResponse.next()
})
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|images).*)']
}
▶ Example: Adding Authentication Protection to an API Route
Output:
Middleware intercepts requests and redirects based on conditions.
// app/api/projects/route.ts — Protected API
import { getToken } from 'next-auth/jwt'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(req: NextRequest) {
const token = await getToken({ req })
if (!token) {
return NextResponse.json({ error: 'Not logged in' }, { status: 401 })
}
// token.role From JWT callback
if (token.role !== 'admin' && token.role !== 'editor') {
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
}
return NextResponse.json({ projects: [{ id: 1, name: 'TaskFlow' }] })
}
Output:
GET app/api/projects/route.ts → Verifies auth token, returns protected data as JSON.
getToken() requires the AUTH_SECRET environment variable to be set; otherwise, it returns null.
6. Clerk Third-Party Certification
(1) Comparison of Clerk Features
| Feature | Auth.js | Clerk |
|---|---|---|
| Installation Complexity | Medium (requires Provider configuration) | Low (npm + environment variables) |
| UI Components | Custom Login Page | <SignIn /> / <SignUp /> Ready to Use |
| Multi-factor authentication | Requires manual integration | Built-in support |
| Free Tier Limits | None | 5,000 MAU (Free) |
| Custom UI | Complete freedom | Many restrictions |
(2) Steps for Integrating Clerk
npm install @clerk/nextjs
# In .env.local, add:
# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=...
# CLERK_SECRET_KEY=...
// app/layout.tsx — ClerkProvider Wrapper Root Layout
import { ClerkProvider } from '@clerk/nextjs'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="zh">
<body>{children}</body>
</html>
</ClerkProvider>
)
}
▶ Example: Clerk Login Page + Protected Page
Output:
Renders the RootLayout component UI.
// app/page.tsx — Login Page
import { SignIn, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
export default function HomePage() {
return (
<div>
<SignedOut>
<SignIn routing="hash" />
</SignedOut>
<SignedIn>
<div>
<UserButton afterSignOutUrl="/" />
<h1>Welcome to TaskFlow</h1>
</div>
</SignedIn>
</div>
)
}
Output:
Renders: Welcome to TaskFlow
Visible text: Welcome to TaskFlow
// app/dashboard/page.tsx — Clerk auth() Protection
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const { userId } = auth()
if (!userId) redirect('/sign-in')
return <div>Dashboard — UserID: {userId}</div>
}
auth() is a server-side function; you don't need 'use client' when using it in a Server Component.
(3) Clerk Middleware
// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = {
matcher: ['/((?!_next|sign-in|sign-up|favicon.ico).*)']
}
7. RBAC Role-Based Access Control
(1) Character Model Design
graph TB
A[User] --> B{Role}
B --> C[admin<br/>Full Access]
B --> D[editor<br/>Reading and Writing Project]
B --> E[viewer<br/>Read-only]
C --> F[Create/Delete Item]
C --> G[Management Team]
C --> H[Change Settings]
D --> I[Edit Task]
D --> J[Add a comment]
E --> K[View Dashboard]
E --> L[Read the document]
style C fill:#d4edda
style D fill:#cce5ff
style E fill:#f8d7da
| Role | Permission Level | Accessible Pages |
|---|---|---|
| admin | 100 | All (including /admin) |
| editor | 50 | /dashboard, /projects (editable) |
| viewer | 20 | /dashboard (read-only) |
▶ Example: RBAC Permission Check Component
Output:
Diagram: User; admin Full Access; editor Reading and Writing Project; viewer Read-only; Create/Delete Item; Management Team.
// components/PermissionGuard.tsx
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
type Role = 'admin' | 'editor' | 'viewer'
const roleHierarchy: Record<Role, number> = { admin: 100, editor: 50, viewer: 20 }
export async function PermissionGuard({
children,
minRole
}: {
children: React.ReactNode
minRole: Role
}) {
const session = await auth()
const userRole = (session?.user?.role as Role) || 'viewer'
if (roleHierarchy[userRole] < roleHierarchy[minRole]) {
redirect('/dashboard')
}
return <>{children}</>
}
Output:
Renders the PermissionGuard component UI.
// app/admin/page.tsx — Usage PermissionGuard
import { PermissionGuard } from '@/components/PermissionGuard'
export default function AdminPage() {
return (
<PermissionGuard minRole="admin">
<h1>Management Console</h1>
<p>This page is only admin As can be seen。</p>
</PermissionGuard>
)
}
8. Complete Example: Comprehensive Implementation of Multi-Provider Authentication + RBAC
// app/dashboard/layout.tsx — Protected Layout + RBAC
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { PermissionGuard } from '@/components/PermissionGuard'
export default async function DashboardLayout({
children,
analytics,
team
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
const session = await auth()
if (!session) redirect('/login')
const user = session.user!
return (
<div>
<header>
<h1>TaskFlow</h1>
<p>Welcome, {user.name} ({user.role})</p>
<nav>
<a href="/dashboard">Overview</a>
{user.role === 'admin' && <a href="/admin">Management</a>}
<a href="/api/auth/signout">Exit</a>
</nav>
</header>
<div style={{ display: 'flex', gap: '2rem' }}>
<main>{children}</main>
<aside>
{analytics}
<PermissionGuard minRole="editor">{team}</PermissionGuard>
</aside>
</div>
</div>
)
}
// app/api/projects/[id]/route.ts — Complete API Protection
import { getToken } from 'next-auth/jwt'
import { NextRequest, NextResponse } from 'next/server'
const roles = { admin: 100, editor: 50, viewer: 20 }
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const token = await getToken({ req })
const { id } = await params
if (!token) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
if (roles[token.role as keyof typeof roles] < 50) {
return NextResponse.json({ error: 'forbidden' }, { status: 403 })
}
// Actually Delete the Item
return NextResponse.json({ id, deleted: true })
}
{ "error": "unauthorized" }
{ "error": "forbidden" }
❓ FAQ
app/api/auth/[...nextauth]/route.ts), supports directly retrieving the session in Server Components via auth(), and no longer requires getSession() or SessionProvider.auth() and getToken() in Middleware?auth() is a wrapper function for Auth.js v5 that returns a complete Session object. getToken() is derived from next-auth/jwt and parses only JWT tokens, offering better performance. We recommend auth() for its simplicity in middleware; for API routes, we recommend getToken() for its lightweight nature.output: 'export' cannot use middleware (which requires the Node.js runtime). In this case, you should use the useSession() hook in a client component to check the login status, or use Clerk’s <SignedIn> / <SignedOut> conditional rendering.📖 Summary
- Auth.js v5 uses the Route Handler pattern;
auth()can retrieve the session directly within a server component - The Provider supports multiple sources such as Credentials, GitHub, and Google, and allows custom data to be injected via a callback mechanism
- JWT sessions with zero database queries are suitable for small applications, while database sessions with support for instant revocation are suitable for enterprises.
- Middleware uses the
matcherconfiguration to protect paths and excludes public resources through reverse matching. - Clerk provides out-of-the-box UI components ideal for rapid prototyping, with a free tier for up to 5,000 MAU
- RBAC implements permission control through role value comparisons and is used declaratively in conjunction with the PermissionGuard component
📝 Exercises
-
Basic Exercise (⭐): Create a new Next.js project, integrate the GitHub Provider from Auth.js v5, and display the user’s avatar and email address on the
/dashboardpage. -
Advanced Exercise (⭐⭐): Add RBAC logic to the middleware:
admincan access/admin/*, andeditorcan access the/projects/*edit interface; return a 403 page if any other role attempts to access these resources. -
Challenge (⭐⭐⭐): Combining Auth.js (Credentials Provider) and Prisma: The user enters their email and password on the login page →
authorizeQuery the database to validate → WriteroleandorgIdinto the JWT → Middleware routes to the corresponding organization’s workspace based onorgId.



