404 Not Found

404 Not Found


nginx

Authentication and Authorization: A Hands-On Guide to the NextAuth.js/Clerk Dual-Solution Approach

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


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 /dashboard page 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.

TS
// 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:

100%
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

BASH
npm install next-auth@beta
npx auth secret    # Generate AUTH_SECRET
TS
// 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

TS
// 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
    }
  }
})
💡 Tip: If the 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
100%
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

TS
// 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

TSX
// 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>
  )
}
💻 Output:

TEXT
<h1>Welcome back,Alice</h1>
<p>Email:alice@taskflow.io</p>
<p>Characters: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:

100%
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

TS
// 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

TS
// 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' }] })
}
🔥 Common Mistake: 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

BASH
npm install @clerk/nextjs
# In .env.local, add:
# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=...
# CLERK_SECRET_KEY=...
TSX
// 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

TSX
// 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>
  )
}
TSX
// 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>
}
💡 Tip: Clerk's auth() is a server-side function; you don't need 'use client' when using it in a Server Component.

(3) Clerk Middleware

TS
// 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

100%
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

TSX
// 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}</>
}
TSX
// 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

TSX
// 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>
  )
}
TS
// 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 })
}
💻 Output (DELETE request without logging in):

JSON
{ "error": "unauthorized" }
💻 Output (DELETE request by the "viewer" role):

JSON
{ "error": "forbidden" }

❓ FAQ

Q What is the difference between Auth.js v4 (NextAuth) and v5?
A v5 uses the App Router’s Route Handler (app/api/auth/[...nextauth]/route.ts), supports directly retrieving the session in Server Components via auth(), and no longer requires getSession() or SessionProvider.
Q Which should I choose: JWT sessions or database sessions?
A For small applications or those that don’t require immediate session revocation, choose JWT (zero DB queries). For enterprise applications that require immediate user logout or multi-device management, choose database sessions. In JWT mode, session revocation must wait until the JWT expires (30 days by default).
Q When should I choose Clerk and when should I choose Auth.js?
A If you need to launch quickly and don’t want to build your own UI → Clerk (30-minute integration). If you need a fully customizable UI and want to build your own account system → Auth.js. Clerk’s free tier is capped at 5,000 MAU; beyond that, it’s €25/month.
Q What is the difference between auth() and getToken() in Middleware?
A 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.
Q How do I secure statically exported pages?
A Static sites built with 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


📝 Exercises

  1. 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 /dashboard page.

  2. Advanced Exercise (⭐⭐): Add RBAC logic to the middleware: admin can access /admin/*, and editor can access the /projects/* edit interface; return a 403 page if any other role attempts to access these resources.

  3. Challenge (⭐⭐⭐): Combining Auth.js (Credentials Provider) and Prisma: The user enters their email and password on the login page → authorize Query the database to validate → Write role and orgId into the JWT → Middleware routes to the corresponding organization’s workspace based on orgId.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏