# 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 } // 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.