惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Hacker News
The Hacker News
F
Full Disclosure
Cloudbric
Cloudbric
Blog — PlanetScale
Blog — PlanetScale
W
WeLiveSecurity
N
News and Events Feed by Topic
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
B
Blog
GbyAI
GbyAI
C
Check Point Blog
B
Blog RSS Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Recorded Future
Recorded Future
The Last Watchdog
The Last Watchdog
N
News and Events Feed by Topic
T
The Blog of Author Tim Ferriss
O
OpenAI News
V
V2EX
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
IT之家
IT之家
WordPress大学
WordPress大学
www.infosecurity-magazine.com
www.infosecurity-magazine.com
S
Security @ Cisco Blogs
C
Cisco Blogs
Security Latest
Security Latest
S
Security Affairs
V
Visual Studio Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Microsoft Azure Blog
Microsoft Azure Blog
Last Week in AI
Last Week in AI
AWS News Blog
AWS News Blog
雷峰网
雷峰网
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
博客园_首页
U
Unit 42
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
Project Zero
Project Zero
Cisco Talos Blog
Cisco Talos Blog
The Register - Security
The Register - Security
N
Netflix TechBlog - Medium
L
LINUX DO - 热门话题
H
Hacker News: Front Page

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
I Got the proxy.ts Matcher Wrong for Three Projects Before I Understood Why
Shubhra Pokhariya · 2026-06-17 · via DEV Community

A few days ago I published a post about the three-layer auth model and the invoice incident that made me rebuild how I think about Next.js 16 auth. More people had hit the same thing than I expected.

One comment stopped me. Someone pointed out a real gap in how the forwarded headers work when the matcher misses a route. They were right, and I want to cover it properly here. But before that, I want to go through proxy.ts end to end, because in my experience the matcher is where most auth setups quietly break first, and it breaks in the worst way, no error, no warning, nothing in the logs.

Why the Name Changed

If you've been building with Next.js for a while, the rename felt arbitrary at first. middleware.ts to proxy.ts. Same location in your project, different filename, different export.

The Next.js team has been direct about why. The word "middleware" created real confusion. Developers coming from Express thought of it as a pipeline, stack things up, run them in order, app.use everything. That's not what this is and it led to people using it for things it was never meant to do: database calls, heavy business logic, session management.

What it actually does is sit at the network boundary in front of your app and intercept requests before they reach your routes. That's a proxy. The rename is the team saying: this has a specific job. Stop treating it like a general-purpose request pipeline.

The official docs are pretty clear:

Proxy is meant to be invoked separately of your render code. You should not attempt relying on shared modules or globals.

No database calls. No heavy logic. That belongs in the layers behind it, which is exactly what the previous post was about.

The Runtime Change That Actually Matters for Auth

middleware.ts defaulted to the Edge runtime. The Edge runtime had limited crypto support. Verifying JWTs with certain algorithms meant lighter libraries, specific workarounds, and sometimes things that just didn't work depending on which signing algorithm your tokens used.

proxy.ts runs on the Node.js runtime by default in Next.js 16. Full crypto support. jose works completely. Any standard JWT library works. No workarounds.

From the official version history:

v16.0.0: Middleware is deprecated and renamed to Proxy. Proxy defaults to the Node.js runtime

The Node.js runtime in proxy.ts is not configurable. The docs are explicit: the edge runtime is not supported in proxy, the runtime is nodejs, and it cannot be configured. Don't try to change it.

Edge runtime is still available through middleware.ts, which still exists in Next.js 16 for edge-specific cases like geographic redirects or A/B testing at the CDN level. But middleware.ts is deprecated. For auth, you want proxy.ts.

Migrating From middleware.ts

If you haven't done this yet, two options.

Full upgrade codemod for all Next.js 16 breaking changes:

npx @next/codemod@canary upgrade latest

Only the middleware migration if that's all you need:

npx @next/codemod@canary middleware-to-proxy .

After running either one, check these three things manually. Don't trust the codemod alone:

  1. proxy.ts exists at your project root, same level as the app folder
  2. The exported function is named proxy, not middleware
  3. middleware.ts is gone

That third one is more important than it sounds. If you manually bumped the package version without the codemod, your old middleware.ts sits there, compiles clean, passes TypeScript checks, and does nothing at runtime. Routes that should be intercepted aren't. Redirects don't fire. No error anywhere. The file is just silently bypassed.

I covered this in the 4 places Next.js 16 broke my app post. This is the one that hurt the most because everything looks fine until a real redirect fails to fire in staging.

Also check next.config.js if you had skipMiddlewareUrlNormalize — configuration flags containing the middleware name are renamed in Next.js 16, so this is now skipProxyUrlNormalize. The codemod handles it, but worth verifying manually.

Building the proxy.ts Auth Gate

Three decisions in this code that look obvious but aren't. I'll walk through each one after.

// proxy.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { jwtVerify } from "jose"

// npm install jose
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!)

const PUBLIC_ROUTES = [
  "/login",
  "/register",
  "/forgot-password",
  "/reset-password",
  "/api/auth/login",
  "/api/auth/refresh",
  "/api/auth/logout",
  "/",
  "/about",
  "/pricing",
  "/blog",
]

const ROLE_ROUTES: Record<string, string[]> = {
  "/admin": ["admin"],
  "/dashboard": ["admin", "user", "moderator"],
  "/moderator": ["admin", "moderator"],
  "/api/admin": ["admin"],
  "/api/user": ["admin", "user", "moderator"],
}

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl

  if (PUBLIC_ROUTES.some((route) => pathname.startsWith(route))) {
    return NextResponse.next()
  }

  const tokenCookie = request.cookies.get("auth_tokens")?.value

  if (!tokenCookie) {
    const loginUrl = new URL("/login", request.url)
    loginUrl.searchParams.set("redirect", pathname)
    return NextResponse.redirect(loginUrl)
  }

  try {
    const tokens = JSON.parse(tokenCookie)
    const { payload } = await jwtVerify(tokens.accessToken, JWT_SECRET)
    const role = payload.role as string

    for (const [route, allowedRoles] of Object.entries(ROLE_ROUTES)) {
      if (
        (pathname === route || pathname.startsWith(route + "/")) &&
        !allowedRoles.includes(role)
      ) {
        return NextResponse.redirect(new URL("/unauthorized", request.url))
      }
    }

    // Headers go on the request, not the response
    // Server Components read incoming request headers via headers()
    // Setting them on the response sends them to the browser instead
    const requestHeaders = new Headers(request.headers)
    requestHeaders.set("x-user-id", payload.sub as string)
    requestHeaders.set("x-user-role", role)
    requestHeaders.set("x-user-email", (payload.email as string) ?? "")

    return NextResponse.next({
      request: { headers: requestHeaders },
    })
  } catch {
    // Expired token, malformed JWT, bad JSON — all redirect to login
    // Same response for all three, no information leak about which failed
    const loginUrl = new URL("/login", request.url)
    loginUrl.searchParams.set("redirect", pathname)
    return NextResponse.redirect(loginUrl)
  }
}

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.*\\.png$|.*\\.jpg$|.*\\.webp$|.*\\.svg$|.*\\.ico$).*)",
  ],
}

Decision 1: The Matcher

This is where most auth setups break. And the way it breaks is the worst possible kind.

Without a matcher, the proxy runs on every request. Every CSS file. Every JS bundle. Every image. That's a JWT verification attempt on each one. When an unauthenticated user hits a CSS request, they get redirected to login, which has no token, which redirects to login again. Infinite redirect loop on static assets. Your app loads for authenticated users but something always feels slow and wrong, and the logs don't explain it.

The negative lookahead in the matcher above excludes static files and keeps the proxy on actual routes:

  • _next/static : compiled JS and CSS bundles
  • _next/image : image optimization endpoint
  • favicon.ico, sitemap.xml, robots.txt : metadata files
  • .*\\.png$ and the other image extensions : public folder assets

One behavior worth knowing: even if you exclude _next/data in your matcher, the proxy still runs for _next/data routes. This is intentional by design. If you protect a page, the proxy deliberately still covers the corresponding data route so you can't accidentally leave it exposed.

Matcher values must be constants. Statically analyzed at build time. Dynamic values, variables, anything computed gets silently ignored. Another way auth gaps get introduced with zero error output.

Decision 2: The Header Direction

I got this backwards the first time I wrote it.

// This is correct. Headers reach your Server Components
return NextResponse.next({
  request: { headers: requestHeaders },
})

// This is wrong. Headers go to the browser instead
// No error produced
return NextResponse.next({
  headers: requestHeaders,
})

headers() in a Server Component returns incoming request headers. If you set the x-user-id header on the response instead of on the forwarded request, every headers().get("x-user-id") call in your pages returns null. Every authenticated user gets redirected to login. Nothing in the logs to explain it. Took me way longer to debug than it should have.

Decision 3: The try/catch

The catch block handles three failure modes with one redirect: the cookie is valid JSON but the tokens object is malformed, the JWT is structurally broken, or the JWT is expired. All three end at login with no indication of which failed.

Different error responses for different failure modes tell an attacker something about the state of your system. One generic redirect tells them nothing.

The Header Trust Boundary

The proxy sets x-user-id on the request headers before forwarding. Server Components read it with headers().get("x-user-id"). Works fine when the proxy runs.

When does the proxy not run? When the matcher has a gap.

Add a new route, forget to check it falls inside the matcher pattern, the proxy never runs for that route. Nobody set x-user-id. Now a client sends their own x-user-id: someone_elses_id header on that unmatched route. The Server Component reads it. From inside the Server Component, a proxy-set header and a client-sent header look identical — there's no way to tell the difference.

What breaks: if you use that userId to query data, the data layer's AND user_id = $2 still scopes the query correctly. Actual records don't leak. But getUserPermissions(userId) now runs against the wrong user entirely. The attacker gets a different user's permissions back. No records exposed, but a real authorization failure on a specific route.

The fix is verifying the JWT directly from the cookie in the Server Component instead of trusting the forwarded header.

// lib/auth-server.ts
import { cookies } from "next/headers"
import { jwtVerify } from "jose"

const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!)

type AuthUser = {
  userId: string
  role: string
  email: string
}

export async function getVerifiedUser(): Promise<AuthUser | null> {
  const cookieStore = await cookies()
  const tokenCookie = cookieStore.get("auth_tokens")?.value

  if (!tokenCookie) return null

  try {
    const tokens = JSON.parse(tokenCookie)
    const { payload } = await jwtVerify(tokens.accessToken, JWT_SECRET)

    return {
      userId: payload.sub as string,
      role: payload.role as string,
      email: (payload.email as string) ?? "",
    }
  } catch {
    return null
  }
}

Using it in a Server Component:

// app/dashboard/billing/page.tsx
import { redirect } from "next/navigation"
import { getVerifiedUser } from "@/lib/auth-server"
import { getUserPermissions, getUserInvoices } from "@/lib/data"

export default async function BillingPage() {
  const user = await getVerifiedUser()

  if (!user) {
    redirect("/login")
  }

  const permissions = await getUserPermissions(user.userId)

  if (!permissions.includes("billing:read")) {
    redirect("/unauthorized")
  }

  const invoices = await getUserInvoices(user.userId)
  return <BillingView invoices={invoices} />
}

Yes, this verifies the JWT twice on requests that go through the proxy. The proxy verifies at the network boundary, the Server Component verifies again at render time. That feels redundant. It isn't.

The proxy verification is the fast gate. The Server Component verification removes the trust dependency on the proxy having run at all. On a route where the proxy ran normally, the second verification takes a few milliseconds. On a route where the matcher had a gap, it's what closes the hole. Trusting a header any client can send on any unmatched route is not worth the few milliseconds saved.

Server Functions and the Proxy

Something in the official docs tucked into the execution order section that gets missed:

Server Functions are not separate routes in this chain. They are handled as POST requests to the route where they are used, so a Proxy matcher that excludes a path will also skip Server Function calls on that path.

If your matcher excludes a path, Server Actions on that path run without proxy coverage too. The docs explicitly say to verify authentication and authorization inside each Server Function, not rely on proxy coverage alone.

Same principle as the three-layer model. The proxy is the fast gate, not the complete answer.

Where proxy.ts Sits in the Request Flow

From the official docs, the actual execution order:

  1. headers from next.config.js
  2. redirects from next.config.js
  3. proxy.ts
  4. beforeFiles rewrites from next.config.js
  5. Filesystem routes (public/, _next/static/, pages/, app/)
  6. afterFiles rewrites from next.config.js
  7. Dynamic routes
  8. fallback rewrites from next.config.js

Third. After next.config.js headers and redirects, before anything in the filesystem renders. That's why it's cheap, it intercepts before any React component work starts.

Three Things to Check Before You Ship

JWT_SECRET in every environment. Always in .env.local. Easy to forget in staging or production. The jwtVerify call throws an opaque error when it's missing and authenticated users get sent to login with nothing in the logs that explains why.

Actually verify the migration ran. Check that proxy.ts exists at project root, the export is named proxy, and middleware.ts is deleted. Running the codemod and assuming it worked is not the same as checking. Manual upgrade with no codemod means the old file sits there silently doing nothing with zero warning.

Check the matcher every time you add a protected route. New route, check it falls inside the matcher pattern, check it appears in ROLE_ROUTES. Most auth gaps I've seen get introduced weeks after the initial proxy setup when someone adds a route and nobody goes back to verify coverage.

The proxy is the fast gate. It's essential and does its job well at the network boundary.

It can't answer ownership questions. It can't stop one user from seeing another user's data. And the forwarded header pattern has a real trust boundary issue on any route where the matcher has a gap.

Next post covers the Server Component authorization layer, roles versus permissions, why permissions need a database call that roles don't, and how the independent check at render time catches what the proxy structurally cannot.

Full implementation with all three layers: shubhra.dev/tutorials/nextjs-16-authentication-3-layer-security. The proxy setup in this post maps to Step 2 there. The getVerifiedUser utility above updates the Server Component pattern in Step 3 to close the header trust boundary gap.