Auth.js (NextAuth.js v5)

authentication libraryAuth.jsNextAuth.jsJavaScriptTypeScriptOAuth 2.0OpenID Connectruntime-agnosticuniversal authentication

Authentication Library

Auth.js (NextAuth.js v5)

Overview

Auth.js (formerly NextAuth.js v5) is the most notable runtime-agnostic modern authentication library as of 2025. Significantly renewed from the Next.js-specific NextAuth.js, it now operates across diverse frameworks including Next.js, SvelteKit, Solid, Express, and Fastify. It supports OAuth 2.0, OpenID Connect, Magic Links, and Credentials authentication with pre-built integrations for 80+ providers (Google, GitHub, Discord, Apple, etc.). With Edge Runtime optimization, full TypeScript support, built-in security best practices, and database-agnostic design, Auth.js enables building enterprise-grade authentication systems in any JavaScript environment.

Details

Auth.js v5 operates across diverse JavaScript runtime environments including Node.js, Deno, Bun, Edge Runtime, and Cloudflare Workers through its runtime-agnostic architecture. The core API is unified, providing optimized integrations for each environment through framework-specific adapters (@auth/nextjs, @auth/sveltekit, @auth/express, etc.). It comes standard-equipped with JWT/JWE, session management, CSRF protection, State parameter, and PKCE support, achieving comprehensive protection against security vulnerabilities. Database adapters (Prisma, Drizzle, TypeORM, etc.) enable flexible data persistence with complete optimization for serverless environments.

Key Features

  • Runtime Agnostic: Node.js, Deno, Bun, Edge Runtime support
  • Cross-Framework: Unified API across Next.js, SvelteKit, Solid, Express, etc.
  • Rich Provider Support: 80+ pre-integrated authentication providers
  • Security-First: Comprehensive protection against CSRF, XSS, session fixation attacks
  • Full TypeScript Support: End-to-end type safety
  • Edge Optimized: High-speed operation on Vercel Edge, Cloudflare Workers, etc.

Advantages and Disadvantages

Advantages

  • Major evolution from NextAuth.js v5, freed from framework constraints
  • Runtime-agnostic enables use in any JavaScript environment
  • Rapid authentication implementation with 80+ pre-integrated providers
  • Excellent development-time type safety with full TypeScript support
  • Secure authentication systems with built-in security best practices
  • Continuous improvement through active open-source community

Disadvantages

  • Migration from NextAuth.js v4 requires learning cost and modification work
  • Potential stability challenges in some environments as a new library
  • Advanced customization requires understanding of internal architecture
  • Rich documentation but scattered information increases learning time
  • Complex authentication flows can lead to configuration complexity
  • Limited enterprise support (community-based)

Reference Pages

Usage Examples

Installation and Setup

# Install Auth.js core library and framework adapters

# For Next.js
npm install next-auth@beta

# For SvelteKit
npm install @auth/sveltekit

# For Express
npm install @auth/express

# For Solid
npm install @auth/solid-start

# Database adapter (example: Prisma)
npm install @auth/prisma-adapter prisma

Next.js App Router Configuration

// auth.ts - Auth.js Configuration
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import Discord from "next-auth/providers/discord"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID!,
      clientSecret: process.env.AUTH_GOOGLE_SECRET!,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID!,
      clientSecret: process.env.AUTH_GITHUB_SECRET!,
    }),
    Discord({
      clientId: process.env.AUTH_DISCORD_ID!,
      clientSecret: process.env.AUTH_DISCORD_SECRET!,
    }),
  ],
  session: {
    strategy: "database",
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60,   // 24 hours
  },
  callbacks: {
    authorized: async ({ auth }) => {
      // Protect pages requiring login
      return !!auth
    },
    session: async ({ session, user }) => {
      // Add custom data to session
      session.user.id = user.id
      session.user.role = user.role || "user"
      return session
    },
    jwt: async ({ token, user, account, profile }) => {
      // Add custom claims to JWT
      if (user) {
        token.role = user.role || "user"
      }
      return token
    },
  },
  pages: {
    signIn: "/auth/signin",
    signOut: "/auth/signout",
    error: "/auth/error",
    verifyRequest: "/auth/verify-request",
  },
  events: {
    async signIn(message) {
      console.log("User signed in:", message.user.email)
    },
    async signOut(message) {
      console.log("User signed out:", message.token?.email)
    },
  },
  debug: process.env.NODE_ENV === "development",
})

// middleware.ts - Next.js Middleware
import { auth } from "@/auth"

export default auth((req) => {
  // Check protected routes
  if (!req.auth && req.nextUrl.pathname.startsWith("/dashboard")) {
    const newUrl = new URL("/auth/signin", req.nextUrl.origin)
    return Response.redirect(newUrl)
  }
})

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
// app/api/auth/[...nextauth]/route.ts - API Route Handler
import { handlers } from "@/auth"

export const { GET, POST } = handlers
# .env.local - Environment Variable Configuration
AUTH_SECRET="your-auth-secret" # Generate with openssl rand -base64 33
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
AUTH_GITHUB_ID="your-github-client-id"
AUTH_GITHUB_SECRET="your-github-client-secret"
AUTH_DISCORD_ID="your-discord-client-id"
AUTH_DISCORD_SECRET="your-discord-client-secret"
DATABASE_URL="postgresql://username:password@localhost:5432/authjs"

React Components Authentication Usage

// components/SignInButton.tsx - Sign-in Button
import { signIn, signOut } from "@/auth"
import { auth } from "@/auth"

export default async function SignInButton() {
  const session = await auth()

  if (session?.user) {
    return (
      <div className="flex items-center space-x-4">
        <img
          src={session.user.image || "/default-avatar.png"}
          alt="Profile"
          className="w-8 h-8 rounded-full"
        />
        <span>Hello, {session.user.name}!</span>
        <form
          action={async () => {
            "use server"
            await signOut()
          }}
        >
          <button
            type="submit"
            className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
          >
            Sign Out
          </button>
        </form>
      </div>
    )
  }

  return (
    <div className="space-x-2">
      <form
        action={async () => {
          "use server"
          await signIn("google")
        }}
      >
        <button
          type="submit"
          className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
        >
          Sign in with Google
        </button>
      </form>
      
      <form
        action={async () => {
          "use server"
          await signIn("github")
        }}
      >
        <button
          type="submit"
          className="bg-gray-800 hover:bg-gray-900 text-white px-4 py-2 rounded"
        >
          Sign in with GitHub
        </button>
      </form>
    </div>
  )
}

// components/UserProfile.tsx - User Profile Display
import { auth } from "@/auth"
import { redirect } from "next/navigation"

export default async function UserProfile() {
  const session = await auth()

  if (!session?.user) {
    redirect("/auth/signin")
  }

  return (
    <div className="max-w-md mx-auto bg-white shadow-lg rounded-lg p-6">
      <div className="text-center">
        <img
          src={session.user.image || "/default-avatar.png"}
          alt="Profile"
          className="w-24 h-24 rounded-full mx-auto mb-4"
        />
        <h1 className="text-2xl font-bold mb-2">{session.user.name}</h1>
        <p className="text-gray-600 mb-4">{session.user.email}</p>
        
        <div className="bg-gray-50 p-4 rounded-lg text-left">
          <h2 className="font-semibold mb-2">Session Information</h2>
          <div className="space-y-2 text-sm">
            <div>
              <span className="font-medium">User ID:</span>
              <span className="ml-2">{session.user.id}</span>
            </div>
            <div>
              <span className="font-medium">Role:</span>
              <span className="ml-2">{session.user.role}</span>
            </div>
            <div>
              <span className="font-medium">Session Expires:</span>
              <span className="ml-2">
                {new Date(session.expires).toLocaleDateString('en-US')}
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

SvelteKit Integration

// src/hooks.server.ts - SvelteKit Hooks
import { SvelteKitAuth } from "@auth/sveltekit"
import Google from "@auth/sveltekit/providers/google"
import GitHub from "@auth/sveltekit/providers/github"
import { GOOGLE_ID, GOOGLE_SECRET, GITHUB_ID, GITHUB_SECRET, AUTH_SECRET } from "$env/static/private"

export const { handle, signIn, signOut } = SvelteKitAuth({
  providers: [
    Google({ 
      clientId: GOOGLE_ID, 
      clientSecret: GOOGLE_SECRET 
    }),
    GitHub({ 
      clientId: GITHUB_ID, 
      clientSecret: GITHUB_SECRET 
    }),
  ],
  secret: AUTH_SECRET,
  trustHost: true,
})

// src/app.d.ts - Type Definitions
import type { DefaultSession } from "@auth/sveltekit"

declare module "@auth/sveltekit" {
  interface Session {
    user: {
      id: string
      role?: string
    } & DefaultSession["user"]
  }
}

// src/routes/+layout.server.ts - Layout Server
import type { LayoutServerLoad } from "./$types"

export const load: LayoutServerLoad = async (event) => {
  return {
    session: await event.locals.auth(),
  }
}

// src/routes/+layout.svelte - Layout Component
<script lang="ts">
  import { page } from "$app/stores"
  import { signIn, signOut } from "@auth/sveltekit/client"
  
  export let data
  
  $: session = data.session
</script>

<header class="bg-white shadow">
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
    <div class="flex justify-between h-16 items-center">
      <h1 class="text-xl font-semibold">My SvelteKit App</h1>
      
      {#if session?.user}
        <div class="flex items-center space-x-4">
          <img
            src={session.user.image || "/default-avatar.png"}
            alt="Profile"
            class="w-8 h-8 rounded-full"
          />
          <span>Hello, {session.user.name}!</span>
          <button
            on:click={() => signOut()}
            class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
          >
            Sign Out
          </button>
        </div>
      {:else}
        <div class="space-x-2">
          <button
            on:click={() => signIn("google")}
            class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
          >
            Sign in with Google
          </button>
          <button
            on:click={() => signIn("github")}
            class="bg-gray-800 hover:bg-gray-900 text-white px-4 py-2 rounded"
          >
            Sign in with GitHub
          </button>
        </div>
      {/if}
    </div>
  </div>
</header>

<main>
  <slot />
</main>

Express.js Integration

// server.js - Express.js Application
import express from "express"
import { ExpressAuth } from "@auth/express"
import Google from "@auth/express/providers/google"
import GitHub from "@auth/express/providers/github"

const app = express()

// Auth.js middleware
app.use("/auth/*", ExpressAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
  ],
  secret: process.env.AUTH_SECRET,
  trustHost: true,
}))

// Authentication status verification middleware
const requireAuth = async (req, res, next) => {
  const session = await getSession(req, res)
  if (!session?.user) {
    return res.status(401).json({ error: "Authentication required" })
  }
  req.user = session.user
  next()
}

// Protected route
app.get("/api/profile", requireAuth, (req, res) => {
  res.json({
    message: "Authenticated user profile",
    user: req.user,
  })
})

// Public route
app.get("/", (req, res) => {
  res.send(`
    <html>
      <body>
        <h1>Express + Auth.js</h1>
        <a href="/auth/signin">Sign In</a>
      </body>
    </html>
  `)
})

const port = process.env.PORT || 3000
app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`)
})

Custom Providers and Adapters

// lib/custom-provider.ts - Custom Authentication Provider
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers"

interface CustomProfile {
  id: string
  email: string
  name: string
  avatar_url: string
}

export default function CustomProvider<P extends CustomProfile>(
  options: OAuthUserConfig<P>
): OAuthConfig<P> {
  return {
    id: "custom-provider",
    name: "Custom Provider",
    type: "oauth",
    authorization: {
      url: "https://api.custom-provider.com/oauth/authorize",
      params: {
        scope: "user:email user:profile",
        response_type: "code",
      },
    },
    token: "https://api.custom-provider.com/oauth/token",
    userinfo: "https://api.custom-provider.com/user",
    profile(profile) {
      return {
        id: profile.id,
        name: profile.name,
        email: profile.email,
        image: profile.avatar_url,
      }
    },
    ...options,
  }
}

// lib/custom-adapter.ts - Custom Database Adapter
import type { Adapter } from "next-auth/adapters"

export function CustomDatabaseAdapter(client: any): Adapter {
  return {
    async createUser(user) {
      const result = await client.user.create({
        data: {
          name: user.name,
          email: user.email,
          image: user.image,
          emailVerified: user.emailVerified,
        },
      })
      return result
    },
    
    async getUser(id) {
      const user = await client.user.findUnique({
        where: { id },
      })
      return user
    },
    
    async getUserByEmail(email) {
      const user = await client.user.findUnique({
        where: { email },
      })
      return user
    },
    
    async getUserByAccount({ providerAccountId, provider }) {
      const account = await client.account.findUnique({
        where: {
          provider_providerAccountId: {
            provider,
            providerAccountId,
          },
        },
        include: { user: true },
      })
      return account?.user ?? null
    },
    
    async updateUser({ id, ...user }) {
      const result = await client.user.update({
        where: { id },
        data: user,
      })
      return result
    },
    
    async deleteUser(userId) {
      await client.user.delete({
        where: { id: userId },
      })
    },
    
    async linkAccount(account) {
      const result = await client.account.create({
        data: {
          userId: account.userId,
          provider: account.provider,
          type: account.type,
          providerAccountId: account.providerAccountId,
          access_token: account.access_token,
          expires_at: account.expires_at,
          id_token: account.id_token,
          refresh_token: account.refresh_token,
          scope: account.scope,
          session_state: account.session_state,
          token_type: account.token_type,
        },
      })
      return result
    },
    
    async unlinkAccount({ providerAccountId, provider }) {
      await client.account.delete({
        where: {
          provider_providerAccountId: {
            provider,
            providerAccountId,
          },
        },
      })
    },
    
    async createSession({ sessionToken, userId, expires }) {
      const result = await client.session.create({
        data: {
          sessionToken,
          userId,
          expires,
        },
      })
      return result
    },
    
    async getSessionAndUser(sessionToken) {
      const userAndSession = await client.session.findUnique({
        where: { sessionToken },
        include: { user: true },
      })
      
      if (!userAndSession) return null
      
      const { user, ...session } = userAndSession
      return { user, session }
    },
    
    async updateSession({ sessionToken, ...session }) {
      const result = await client.session.update({
        where: { sessionToken },
        data: session,
      })
      return result
    },
    
    async deleteSession(sessionToken) {
      await client.session.delete({
        where: { sessionToken },
      })
    },
  }
}