Using Next.js 16 Proxy for zero-config authentication
The Next.js 16 Proxy feature (formerly called Middleware) provides a zero-config, Clerk-like authentication experience. With a single proxy.ts file, you get automatic route handling, OAuth flows, session management, and more.
Create a proxy.ts file at the root of your project:
Basic Proxy configuration
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { authMiddleware } from "@warpy-auth-sdk/core/next";
import { google } from "@warpy-auth-sdk/core";
const handler = authMiddleware(
{
secret: process.env.AUTH_SECRET!,
provider: google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI!,
}),
callbacks: {
async user(u) {
return {
id: u.id || u.email,
email: u.email,
name: u.name,
picture: u.picture,
};
},
jwt: (t) => t,
session: (s) => s,
},
},
{
basePath: "/api/auth",
successRedirect: "/dashboard",
errorRedirect: "/login",
}
);
export function proxy(request: NextRequest) {
const p = request.nextUrl.pathname;
if (p.startsWith("/api/auth")) return handler(request);
return NextResponse.next();
}
export const config = {
matcher: [
"/((?!_next|[^?]*\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
],
};# .env.local
AUTH_SECRET=your-secret-key-at-least-32-characters
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/callback/googleYour authentication routes are now automatically available:
GET /api/auth/session - Get current sessionPOST /api/auth/signout - Sign outGET /api/auth/signin/google - Start OAuthGET /api/auth/callback/google - OAuth callbackThe first parameter to authMiddleware is the authentication configuration:
interface AuthConfig {
// Required: JWT signing secret (min 32 characters)
secret: string;
// Required: OAuth provider configuration
provider: Provider;
// Optional: Database adapter for session persistence
adapter?: Adapter;
// Optional: Customize user resolution, JWT claims, and session shape
callbacks?: {
user?: (oauthUser: OAuthUser) => Promise<User> | User;
jwt?: (token: JWTPayload) => JWTPayload;
session?: (session: Session) => Session;
};
// Optional: CAPTCHA configuration for bot protection
captcha?: CaptchaConfig;
// Optional: MCP configuration for AI agent authentication
mcp?: MCPConfig;
}The second parameter configures Proxy behavior:
interface ProxyOptions {
// Base path for all auth routes (default: "/api/auth")
basePath?: string;
// Redirect after successful authentication (default: "/")
successRedirect?: string;
// Redirect after authentication failure (default: "/login")
errorRedirect?: string;
// Enable/disable specific features
features?: {
session?: boolean; // GET /session endpoint
signout?: boolean; // POST /signout endpoint
oauth?: boolean; // OAuth flow endpoints
email?: boolean; // Email magic link endpoints
};
}Persist sessions to database
import { authMiddleware } from "@warpy-auth-sdk/core/next";
import { google } from "@warpy-auth-sdk/core";
import { PrismaAdapter } from "@warpy-auth-sdk/core";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const handler = authMiddleware(
{
secret: process.env.AUTH_SECRET!,
provider: google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI!,
}),
adapter: PrismaAdapter(prisma),
callbacks: {
async user(u) {
// Prisma adapter will handle user creation/update
return {
id: u.id || u.email,
email: u.email,
name: u.name,
picture: u.picture,
};
},
},
},
{
basePath: "/api/auth",
successRedirect: "/dashboard",
errorRedirect: "/login",
}
);
export function proxy(request: NextRequest) {
const p = request.nextUrl.pathname;
if (p.startsWith("/api/auth")) return handler(request);
return NextResponse.next();
}Email magic link authentication
import { authMiddleware } from "@warpy-auth-sdk/core/next";
import { email } from "@warpy-auth-sdk/core";
const handler = authMiddleware(
{
secret: process.env.AUTH_SECRET!,
provider: email({
from: process.env.EMAIL_FROM!,
service: {
type: "resend",
apiKey: process.env.RESEND_API_KEY!,
},
}),
callbacks: {
async user(u) {
return {
id: u.email,
email: u.email,
name: u.name,
};
},
},
},
{
basePath: "/api/auth",
successRedirect: "/dashboard",
errorRedirect: "/login",
}
);
// Note: Email endpoints require Node runtime
// Proxy runs on Node runtime by default in Next.js 16Two-factor email authentication
import { authMiddleware } from "@warpy-auth-sdk/core/next";
import { twofa } from "@warpy-auth-sdk/core";
const handler = authMiddleware(
{
secret: process.env.AUTH_SECRET!,
provider: twofa({
from: process.env.EMAIL_FROM!,
service: {
type: "resend",
apiKey: process.env.RESEND_API_KEY!,
},
expirationMinutes: 5,
}),
callbacks: {
async user(u) {
return {
id: u.email,
email: u.email,
name: u.name,
};
},
},
},
{
basePath: "/api/auth",
successRedirect: "/dashboard",
errorRedirect: "/login",
}
);
// Endpoints:
// GET /api/auth/signin/twofa?email=user@example.com - Send code
// GET /api/auth/signin/twofa?identifier=xxx&code=123456 - VerifyBot protection with reCAPTCHA
import { authMiddleware } from "@warpy-auth-sdk/core/next";
import { google } from "@warpy-auth-sdk/core";
import { createCaptchaProvider } from "@warpy-auth-sdk/core/captcha";
const handler = authMiddleware(
{
secret: process.env.AUTH_SECRET!,
provider: google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI!,
}),
captcha: {
provider: createCaptchaProvider({
type: "recaptcha-v3",
secretKey: process.env.RECAPTCHA_SECRET_KEY!,
}),
enabledMethods: ["email", "twofa"], // Enable for email/2FA only
scoreThreshold: 0.5, // For reCAPTCHA v3
},
callbacks: {
async user(u) {
return {
id: u.email,
email: u.email,
name: u.name,
};
},
},
},
{
basePath: "/api/auth",
successRedirect: "/dashboard",
errorRedirect: "/login",
}
);Enable AI agent authentication
import { authMiddleware } from "@warpy-auth-sdk/core/next";
import { google } from "@warpy-auth-sdk/core";
const handler = authMiddleware(
{
secret: process.env.AUTH_SECRET!,
provider: google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI!,
}),
mcp: {
enabled: true,
path: "/api/mcp",
warpy: {
apiKey: process.env.WARPY_API_KEY, // Optional: Warpy Cloud Shield
},
},
callbacks: {
async user(u) {
return {
id: u.email,
email: u.email,
name: u.name,
};
},
},
},
{
basePath: "/api/auth",
successRedirect: "/dashboard",
errorRedirect: "/login",
}
);
export function proxy(request: NextRequest) {
const p = request.nextUrl.pathname;
if (p.startsWith("/api/auth")) return handler(request);
if (p.startsWith("/api/mcp")) return handler(request); // MCP endpoint
return NextResponse.next();
}The Proxy automatically creates these routes based on the provider:
| Method | Path | Description |
|---|---|---|
| GET | {basePath}/session | Get current session |
| POST | {basePath}/signout | Sign out and clear session |
| GET | {basePath}/signin/:provider | Start OAuth flow |
| GET | {basePath}/callback/:provider | OAuth callback handler |
| POST | {basePath}/signin/email | Send magic link (email provider) |
| GET | {basePath}/signin/twofa | Send/verify 2FA code |
Control which requests the Proxy handles:
export function proxy(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Handle all auth routes
if (pathname.startsWith("/api/auth")) {
return handler(request);
}
// Protect specific routes
if (pathname.startsWith("/dashboard") || pathname.startsWith("/admin")) {
// Check authentication
const session = request.cookies.get("auth-session");
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
// Allow all other requests
return NextResponse.next();
}The user callback is called after OAuth authentication to resolve or create the user:
callbacks: {
async user(oauthUser) {
// oauthUser contains: id, email, name, picture
// Example: Save to database
const user = await db.user.upsert({
where: { email: oauthUser.email },
create: {
email: oauthUser.email,
name: oauthUser.name,
picture: oauthUser.picture,
},
update: {
name: oauthUser.name,
picture: oauthUser.picture,
},
});
return {
id: user.id,
email: user.email,
name: user.name,
picture: user.picture,
};
},
}The jwt callback customizes the JWT token payload:
callbacks: {
jwt(token) {
// Add custom claims
token.role = "admin";
token.subscriptionTier = "pro";
return token;
},
}The session callback shapes the session object returned to clients:
callbacks: {
session(session) {
// Add custom session data
session.preferences = {
theme: "dark",
language: "en",
};
return session;
},
}The Proxy automatically handles PKCE (Proof Key for Code Exchange) for all OAuth providers:
The Proxy handles errors gracefully with automatic redirects:
errorRedirect with error query param// Example error redirect
// User denied OAuth: /login?error=access_denied
// Invalid callback: /login?error=callback_failedCSRF protection is automatic for OAuth flows:
Session cookies use secure defaults:
httpOnly: true - Not accessible via JavaScriptsecure: true - HTTPS only (production)sameSite: "lax" - CSRF protectionmaxAge: 30 days - Configurable expiration# Generate a secure secret (min 32 chars)
openssl rand -base64 32
# Store in .env.local
AUTH_SECRET=your-generated-secret.env.local to version control. Use environment variables in production.Ensure the Proxy is properly exported:
// Must be named "proxy" (lowercase)
export function proxy(request: NextRequest) {
// ...
}
// Must include matcher config
export const config = {
matcher: [/* ... */],
};Verify redirect URI matches exactly:
# .env.local
GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/callback/google
# Google Cloud Console: Must match exactly, including protocol and pathCheck cookie settings and domain:
// Development: localhost works fine
// Production: Ensure secure: true and proper domainThe Proxy is highly optimized:
If you're migrating from Next.js 15 Middleware:
// Old: middleware.ts (Next.js 15)
export function middleware(request: NextRequest) {
// ...
}
// New: proxy.ts (Next.js 16)
export function proxy(request: NextRequest) {
// Same logic, just renamed
}proxy.ts instead of middleware.ts.