# Auth.js
Auth.js is a comprehensive, open-source authentication library for modern web applications. Built on standard Web APIs (Request/Response), it provides a framework-agnostic authentication solution that works across Next.js, SvelteKit, Express, SolidStart, Qwik, and other frameworks. The library supports multiple authentication strategies including OAuth 2.0/OIDC (with 50+ built-in providers), email/passwordless authentication, WebAuthn, and traditional credentials-based login.
Auth.js emphasizes security by default with encrypted JWT sessions (using A256CBC-HS512), CSRF protection via double-submit cookies, and secure cookie policies. It offers flexible session strategies (JWT or database-backed), comprehensive callback systems for customization, and works with 30+ database adapters including Prisma, MongoDB, Firebase, and DynamoDB. The library is runtime-agnostic, running seamlessly in Node.js, Edge runtimes, and serverless environments.
## Core Authentication Handler
Main entry point for handling authentication requests
```typescript
import { Auth } from "@auth/core"
import GitHub from "@auth/core/providers/github"
import Google from "@auth/core/providers/google"
// Basic authentication configuration
const request = new Request("https://example.com/api/auth/signin")
const response = await Auth(request, {
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
secret: process.env.AUTH_SECRET,
trustHost: true,
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async signIn({ user, account, profile }) {
// Control who can sign in
if (profile?.email?.endsWith("@company.com")) {
return true
}
return false
},
async jwt({ token, user, account }) {
// Add custom properties to JWT
if (account) {
token.accessToken = account.access_token
token.userId = user.id
}
return token
},
async session({ session, token }) {
// Expose data to client
session.accessToken = token.accessToken
session.user.id = token.userId
return session
},
},
})
console.log(response.status) // 200, 302, etc.
```
## Next.js Integration
Complete Next.js authentication setup
```typescript
// auth.ts
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import Google from "next-auth/providers/google"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
session: { strategy: "database" },
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
callbacks: {
async session({ session, user }) {
session.user.id = user.id
return session
},
},
})
// app/api/auth/[...nextauth]/route.ts
export { handlers as GET, handlers as POST } from "@/auth"
// middleware.ts - Protect routes
export { auth as middleware } from "@/auth"
export const config = {
matcher: ["/dashboard/:path*", "/admin/:path*"],
}
// Server component usage
import { auth } from "@/auth"
export default async function ServerPage() {
const session = await auth()
if (!session) {
return
Not authenticated
}
return Welcome {session.user.name}
}
// Client component usage
"use client"
import { useSession } from "next-auth/react"
export default function ClientComponent() {
const { data: session, status } = useSession()
if (status === "loading") {
return Loading...
}
if (!session) {
return Not authenticated
}
return Signed in as {session.user.email}
}
```
## JWT Token Operations
Encoding, decoding, and retrieving JWT tokens
```typescript
import { encode, decode, getToken } from "@auth/core/jwt"
// Encode a JWT token
const token = await encode({
token: {
sub: "user-123",
name: "John Doe",
email: "john@example.com",
role: "admin",
},
secret: process.env.AUTH_SECRET,
salt: "authjs.session-token",
maxAge: 30 * 24 * 60 * 60, // 30 days
})
console.log(token) // Encrypted JWT string
// Decode a JWT token
const decoded = await decode({
token: token,
secret: process.env.AUTH_SECRET,
salt: "authjs.session-token",
})
console.log(decoded)
// {
// sub: "user-123",
// name: "John Doe",
// email: "john@example.com",
// role: "admin",
// iat: 1234567890,
// exp: 1237159890,
// jti: "uuid-here"
// }
// Get token from request (API route handler)
import { NextRequest } from "next/server"
export async function GET(request: NextRequest) {
const token = await getToken({
req: request,
secret: process.env.AUTH_SECRET,
})
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
return Response.json({
userId: token.sub,
email: token.email,
})
}
// Get raw JWT string
const rawToken = await getToken({
req: request,
secret: process.env.AUTH_SECRET,
raw: true,
})
// Get token from Authorization header
const authToken = await getToken({
req: new Request("https://api.example.com", {
headers: {
Authorization: `Bearer ${token}`,
},
}),
secret: process.env.AUTH_SECRET,
})
```
## OAuth Provider Configuration
Setting up OAuth/OIDC providers
```typescript
import { Auth } from "@auth/core"
import Google from "@auth/core/providers/google"
import GitHub from "@auth/core/providers/github"
import Auth0 from "@auth/core/providers/auth0"
const response = await Auth(request, {
providers: [
// Google OAuth with custom scopes
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization: {
params: {
scope: "openid email profile https://www.googleapis.com/auth/calendar",
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
// GitHub with profile customization
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name || profile.login,
email: profile.email,
image: profile.avatar_url,
role: profile.company ? "member" : "guest",
}
},
}),
// Auth0 with custom domain
Auth0({
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
issuer: process.env.AUTH0_ISSUER,
wellKnown: `${process.env.AUTH0_ISSUER}/.well-known/openid-configuration`,
}),
// Generic OAuth2 provider
{
id: "custom-oauth",
name: "Custom OAuth Provider",
type: "oauth",
clientId: process.env.CUSTOM_CLIENT_ID,
clientSecret: process.env.CUSTOM_CLIENT_SECRET,
authorization: {
url: "https://provider.com/oauth/authorize",
params: { scope: "read write" },
},
token: "https://provider.com/oauth/token",
userinfo: "https://provider.com/oauth/userinfo",
profile(profile) {
return {
id: profile.user_id,
name: profile.display_name,
email: profile.email,
image: profile.avatar,
}
},
},
],
secret: process.env.AUTH_SECRET,
trustHost: true,
})
```
## Credentials Provider
Username/password authentication
```typescript
import { Auth } from "@auth/core"
import Credentials from "@auth/core/providers/credentials"
import { CredentialsSignin } from "@auth/core/errors"
import bcrypt from "bcryptjs"
import { z } from "zod"
class InvalidCredentialsError extends CredentialsSignin {
code = "invalid_credentials"
}
const response = await Auth(request, {
providers: [
Credentials({
name: "Credentials",
credentials: {
email: {
label: "Email",
type: "email",
placeholder: "you@example.com",
},
password: {
label: "Password",
type: "password",
},
},
async authorize(credentials, req) {
// Validate input with Zod
const parsedCredentials = z
.object({
email: z.string().email(),
password: z.string().min(6),
})
.safeParse(credentials)
if (!parsedCredentials.success) {
throw new InvalidCredentialsError()
}
const { email, password } = parsedCredentials.data
// Fetch user from database
const user = await db.user.findUnique({
where: { email },
})
if (!user) {
throw new InvalidCredentialsError()
}
// Verify password
const isValid = await bcrypt.compare(password, user.hashedPassword)
if (!isValid) {
throw new InvalidCredentialsError()
}
// Return user object (will be available in JWT callback)
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
}
},
}),
],
session: {
strategy: "jwt", // Required for credentials provider
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
token.role = user.role
}
return token
},
async session({ session, token }) {
session.user.id = token.id
session.user.role = token.role
return session
},
},
secret: process.env.AUTH_SECRET,
trustHost: true,
})
```
## Email Provider
Passwordless email authentication
```typescript
import { Auth } from "@auth/core"
import Nodemailer from "@auth/core/providers/nodemailer"
import Resend from "@auth/core/providers/resend"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
const response = await Auth(request, {
adapter: PrismaAdapter(prisma), // Required for email provider
providers: [
// Nodemailer provider
Nodemailer({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,
},
},
from: process.env.EMAIL_FROM,
}),
// Or use Resend
Resend({
apiKey: process.env.RESEND_API_KEY,
from: "noreply@example.com",
}),
],
session: {
strategy: "database",
},
callbacks: {
async signIn({ user, email }) {
// Prevent signIn for blocklisted emails
if (email?.verificationRequest) {
const blocklist = ["spam@example.com"]
if (blocklist.includes(user.email)) {
return false
}
}
return true
},
},
pages: {
verifyRequest: "/auth/verify-request",
},
secret: process.env.AUTH_SECRET,
trustHost: true,
})
// Custom email template (sent via Nodemailer)
Nodemailer({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
sendVerificationRequest: async ({ identifier, url, provider }) => {
const { host } = new URL(url)
const transport = nodemailer.createTransport(provider.server)
await transport.sendMail({
to: identifier,
from: provider.from,
subject: `Sign in to ${host}`,
text: `Sign in to ${host}\n\n${url}\n\n`,
html: `
Sign in to ${host}
Click the link below to sign in:
Sign in
`,
})
},
})
```
## Database Adapter Integration
Connect Auth.js to your database
```typescript
import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { MongoDBAdapter } from "@auth/mongodb-adapter"
import { DynamoDBAdapter } from "@auth/dynamodb-adapter"
import { PrismaClient } from "@prisma/client"
import { MongoClient } from "mongodb"
import { DynamoDB } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"
// Prisma Adapter
const prisma = new PrismaClient()
export const { handlers, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
/* providers */
],
session: {
strategy: "database",
maxAge: 30 * 24 * 60 * 60,
updateAge: 24 * 60 * 60,
},
})
// MongoDB Adapter
const client = new MongoClient(process.env.MONGODB_URI)
const clientPromise = client.connect()
export const { handlers, auth } = NextAuth({
adapter: MongoDBAdapter(clientPromise, {
databaseName: "myapp",
}),
providers: [
/* providers */
],
})
// DynamoDB Adapter
const dynamoClient = DynamoDBDocument.from(
new DynamoDB({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
}),
{
marshallOptions: {
convertEmptyValues: true,
removeUndefinedValues: true,
convertClassInstanceToMap: true,
},
}
)
export const { handlers, auth } = NextAuth({
adapter: DynamoDBAdapter(dynamoClient, {
tableName: "auth",
}),
providers: [
/* providers */
],
})
```
## Session Management
Reading and updating sessions
```typescript
// Server-side session retrieval (Next.js App Router)
import { auth } from "@/auth"
import { redirect } from "next/navigation"
export default async function ProtectedPage() {
const session = await auth()
if (!session) {
redirect("/auth/signin")
}
return (
Welcome {session.user.name}
Email: {session.user.email}
)
}
// API route with session
import { auth } from "@/auth"
import { NextResponse } from "next/server"
export async function GET(request: Request) {
const session = await auth()
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const data = await fetchUserData(session.user.id)
return NextResponse.json(data)
}
// Update session (client-side)
"use client"
import { useSession } from "next-auth/react"
export default function UpdateProfile() {
const { data: session, update } = useSession()
const handleUpdate = async () => {
await update({
user: {
...session?.user,
name: "New Name",
},
})
}
return Update Name
}
// Manual session update triggers jwt callback with trigger: "update"
callbacks: {
async jwt({ token, trigger, session }) {
if (trigger === "update" && session) {
token.name = session.user.name
}
return token
},
}
// Programmatic sign in/out (server actions)
import { signIn, signOut } from "@/auth"
export async function doSignIn() {
"use server"
await signIn("github", { redirectTo: "/dashboard" })
}
export async function doSignOut() {
"use server"
await signOut({ redirectTo: "/" })
}
```
## Middleware Protection
Route protection with middleware
```typescript
// middleware.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"
export default auth((req) => {
const { pathname } = req.nextUrl
const session = req.auth
// Protect admin routes
if (pathname.startsWith("/admin")) {
if (!session || session.user.role !== "admin") {
return NextResponse.redirect(new URL("/auth/signin", req.url))
}
}
// Protect dashboard
if (pathname.startsWith("/dashboard")) {
if (!session) {
return NextResponse.redirect(new URL("/auth/signin", req.url))
}
}
// Redirect authenticated users away from auth pages
if (pathname.startsWith("/auth/signin") && session) {
return NextResponse.redirect(new URL("/dashboard", req.url))
}
return NextResponse.next()
})
export const config = {
matcher: [
"/dashboard/:path*",
"/admin/:path*",
"/auth/signin",
],
}
// Advanced middleware with custom logic
import { auth } from "@/auth"
export default auth(async (req) => {
const session = req.auth
const { pathname } = req.nextUrl
// API rate limiting by user
if (pathname.startsWith("/api/")) {
if (session) {
const rateLimit = await checkRateLimit(session.user.id)
if (rateLimit.exceeded) {
return NextResponse.json(
{ error: "Rate limit exceeded" },
{ status: 429 }
)
}
}
}
// Role-based access control
const protectedRoutes = {
"/admin": ["admin"],
"/billing": ["admin", "billing"],
"/dashboard": ["admin", "user", "billing"],
}
for (const [route, allowedRoles] of Object.entries(protectedRoutes)) {
if (pathname.startsWith(route)) {
if (!session || !allowedRoles.includes(session.user.role)) {
return NextResponse.redirect(new URL("/unauthorized", req.url))
}
}
}
return NextResponse.next()
})
```
## Express.js Integration
Auth.js with Express server
```typescript
import express from "express"
import { Auth } from "@auth/core"
import { ExpressAuth } from "@auth/express"
import GitHub from "@auth/core/providers/github"
const app = express()
// Method 1: Using ExpressAuth helper
app.use(
"/auth/*",
ExpressAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
secret: process.env.AUTH_SECRET,
trustHost: true,
})
)
// Method 2: Manual integration with Auth core
app.all("/api/auth/*", async (req, res) => {
const request = new Request(
`${req.protocol}://${req.get("host")}${req.originalUrl}`,
{
method: req.method,
headers: new Headers(req.headers as HeadersInit),
body: req.method !== "GET" ? JSON.stringify(req.body) : undefined,
}
)
const response = await Auth(request, {
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
secret: process.env.AUTH_SECRET,
trustHost: true,
})
response.headers.forEach((value, key) => {
res.setHeader(key, value)
})
res.status(response.status)
const body = await response.text()
res.send(body)
})
// Protected route middleware
import { getToken } from "@auth/core/jwt"
async function requireAuth(req, res, next) {
const token = await getToken({
req: {
headers: req.headers,
},
secret: process.env.AUTH_SECRET,
})
if (!token) {
return res.status(401).json({ error: "Unauthorized" })
}
req.user = token
next()
}
app.get("/api/protected", requireAuth, (req, res) => {
res.json({
message: "Protected data",
user: req.user,
})
})
app.listen(3000)
```
## Advanced Callbacks
Custom authentication flows and data handling
```typescript
import NextAuth from "next-auth"
export const { handlers, auth } = NextAuth({
providers: [
/* providers */
],
callbacks: {
// Control sign-in access
async signIn({ user, account, profile, email, credentials }) {
// Email domain restriction
if (account?.provider === "google") {
return profile?.email?.endsWith("@company.com") ?? false
}
// Custom credential validation
if (account?.provider === "credentials") {
const isAllowed = await checkUserAllowlist(user.email)
if (!isAllowed) {
return "/auth/error?error=AccessDenied"
}
}
// Email verification check
if (email?.verificationRequest) {
const isDomainAllowed = await checkDomain(user.email)
return isDomainAllowed
}
return true
},
// Customize redirect behavior
async redirect({ url, baseUrl }) {
// Allow relative callback URLs
if (url.startsWith("/")) {
return `${baseUrl}${url}`
}
// Allow callback URLs on the same origin
if (new URL(url).origin === baseUrl) {
return url
}
return baseUrl
},
// Modify JWT token
async jwt({ token, user, account, profile, trigger, session }) {
// Initial sign in
if (account && user) {
token.accessToken = account.access_token
token.refreshToken = account.refresh_token
token.accessTokenExpires = account.expires_at
token.userId = user.id
token.role = user.role
}
// Refresh token rotation
if (Date.now() < token.accessTokenExpires) {
return token
}
return await refreshAccessToken(token)
// Session update from client
if (trigger === "update" && session) {
token.name = session.user.name
token.role = session.user.role
}
return token
},
// Modify session object sent to client
async session({ session, token, user }) {
// JWT strategy
if (token) {
session.user.id = token.userId
session.user.role = token.role
session.accessToken = token.accessToken
session.error = token.error
}
// Database strategy
if (user) {
session.user.id = user.id
session.user.role = user.role
}
return session
},
},
// Events for audit logging
events: {
async signIn({ user, account, profile, isNewUser }) {
await auditLog("sign_in", {
userId: user.id,
provider: account?.provider,
isNewUser,
})
},
async signOut({ token, session }) {
await auditLog("sign_out", {
userId: token?.sub || session?.userId,
})
},
async createUser({ user }) {
await auditLog("user_created", { userId: user.id })
},
async linkAccount({ user, account }) {
await auditLog("account_linked", {
userId: user.id,
provider: account.provider,
})
},
},
})
// Token refresh helper
async function refreshAccessToken(token) {
try {
const response = await fetch("https://oauth2.provider.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
}),
})
const refreshedTokens = await response.json()
if (!response.ok) {
throw refreshedTokens
}
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
}
} catch (error) {
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
```
## TypeScript Module Augmentation
Extend Auth.js types with custom properties
```typescript
// types/next-auth.d.ts
import { DefaultSession, DefaultUser } from "next-auth"
import { JWT as DefaultJWT } from "next-auth/jwt"
declare module "next-auth" {
interface Session {
user: {
id: string
role: "admin" | "user" | "billing"
twoFactorEnabled: boolean
} & DefaultSession["user"]
accessToken?: string
error?: string
}
interface User extends DefaultUser {
role: "admin" | "user" | "billing"
twoFactorEnabled: boolean
}
}
declare module "next-auth/jwt" {
interface JWT extends DefaultJWT {
userId: string
role: "admin" | "user" | "billing"
accessToken?: string
refreshToken?: string
accessTokenExpires?: number
error?: string
}
}
// Usage in components
import { useSession } from "next-auth/react"
export default function Component() {
const { data: session } = useSession()
// TypeScript now knows about custom properties
const userRole = session?.user.role // "admin" | "user" | "billing"
const userId = session?.user.id // string
return {userRole}
}
```
## Auth.js is designed for production-ready authentication with minimal configuration while offering deep customization when needed. The library handles complex authentication flows, session management, and security concerns automatically, allowing developers to focus on building their applications. Common use cases include adding OAuth social login to web apps, implementing passwordless email authentication, building multi-tenant SaaS applications with role-based access control, and creating secure APIs with JWT authentication.
The integration patterns are consistent across frameworks: configure providers and callbacks, set up route handlers, protect routes with middleware or server-side checks, and access session data in components. Database adapters enable session persistence and user management, while the callback system provides hooks into every stage of the authentication lifecycle. The library's framework-agnostic core means the same authentication logic can power Next.js apps, Express APIs, SvelteKit applications, and more, making it ideal for organizations with diverse tech stacks or developers who want portable authentication knowledge.