Next.js Proxy

Using Next.js 16 Proxy for zero-config authentication

Next.js 16 Proxy

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.

What is Proxy?

In Next.js 16, the Proxy function runs on the edge before requests reach your application. It's perfect for authentication because it can intercept, validate, and redirect requests efficiently.

Quick Start

1. Create the Proxy File

Create a proxy.ts file at the root of your project:

proxy.ts

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)(.*)",
  ],
};

2. Set Environment Variables

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

3. That's It!

Your authentication routes are now automatically available:

  • GET /api/auth/session - Get current session
  • POST /api/auth/signout - Sign out
  • GET /api/auth/signin/google - Start OAuth
  • GET /api/auth/callback/google - OAuth callback

Configuration Options

Authentication Config

The 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;
}

Proxy Options

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
  };
}

Advanced Examples

With Database Adapter

proxy.ts with Prisma

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();
}

With Email Magic Links

proxy.ts with email provider

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 16

With Two-Factor Authentication

proxy.ts with 2FA

Two-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 - Verify

With CAPTCHA Protection

proxy.ts with CAPTCHA

Bot 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",
  }
);

With MCP Tools (AI Agents)

proxy.ts with MCP

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();
}

Route Handling

Available Routes

The Proxy automatically creates these routes based on the provider:

MethodPathDescription
GET{basePath}/sessionGet current session
POST{basePath}/signoutSign out and clear session
GET{basePath}/signin/:providerStart OAuth flow
GET{basePath}/callback/:providerOAuth callback handler
POST{basePath}/signin/emailSend magic link (email provider)
GET{basePath}/signin/twofaSend/verify 2FA code

Custom Route Matching

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();
}

Callbacks

User Callback

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,
    };
  },
}

JWT Callback

The jwt callback customizes the JWT token payload:

callbacks: {
  jwt(token) {
    // Add custom claims
    token.role = "admin";
    token.subscriptionTier = "pro";

    return token;
  },
}

Session Callback

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;
  },
}

PKCE Support

The Proxy automatically handles PKCE (Proof Key for Code Exchange) for all OAuth providers:

  • Automatic Generation: PKCE verifier and challenge are generated automatically
  • Secure Storage: Verifier stored in HttpOnly cookie
  • S256 Method: Uses SHA-256 by default (most secure)
  • Cleanup: Verifier deleted after successful token exchange

PKCE by Default

All OAuth providers use PKCE with S256 by default, following OAuth 2.1 security best practices. No configuration needed!

Error Handling

The Proxy handles errors gracefully with automatic redirects:

  • OAuth Errors: Redirects to errorRedirect with error query param
  • Session Errors: Returns 401 for invalid sessions
  • CSRF Errors: Returns 403 for CSRF validation failures
// Example error redirect
// User denied OAuth: /login?error=access_denied
// Invalid callback: /login?error=callback_failed

Security Considerations

CSRF Protection

CSRF protection is automatic for OAuth flows:

  • State parameter generated and validated
  • In-memory storage with cookie fallback
  • Automatic cleanup after validation

Cookie Security

Session cookies use secure defaults:

  • httpOnly: true - Not accessible via JavaScript
  • secure: true - HTTPS only (production)
  • sameSite: "lax" - CSRF protection
  • maxAge: 30 days - Configurable expiration

Secret Management

# Generate a secure secret (min 32 chars)
openssl rand -base64 32

# Store in .env.local
AUTH_SECRET=your-generated-secret

Keep Secrets Secret

Never commit .env.local to version control. Use environment variables in production.

Troubleshooting

Proxy Not Running

Ensure the Proxy is properly exported:

// Must be named "proxy" (lowercase)
export function proxy(request: NextRequest) {
  // ...
}

// Must include matcher config
export const config = {
  matcher: [/* ... */],
};

OAuth Callback Errors

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 path

Session Not Persisting

Check cookie settings and domain:

// Development: localhost works fine
// Production: Ensure secure: true and proper domain

Performance

The Proxy is highly optimized:

  • Edge Runtime: Runs on Vercel Edge (super fast)
  • Minimal Overhead: Only processes auth routes
  • In-Memory CSRF: Fast state validation
  • JWT Tokens: Stateless session validation

Migration from Middleware

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
}

Naming Change

Next.js 16 renamed "Middleware" to "Proxy". The functionality is identical, just use proxy.ts instead of middleware.ts.

Next Steps

Next.js Proxy | @warpy-auth-sdk/core