Skip to main content
Next.js 16 renamed middleware to proxy. The file is src/proxy.js (not middleware.js). It runs on the edge matcher below before App Router pages and route handlers.
Public HTML (/, /about, /login) is not in the matcher. GET /api/public/* is in the matcher but returns immediately without a session.

Request path

/admin/questions, /admin/logs, /admin/system, /admin/goals, /admin/submissions
/api/admin/questions, /api/admin/health, /api/admin/goals, /api/admin/forms/rollover, /api/admin/forms/live, /api/admin/forms/export
Other /admin/* and /api/admin/* paths (for example /admin/users and /api/admin/users/school) require a principal or Super Admin JWT. Route handlers still reload User from MongoDB for the real permission check.
The proxy is a coarse gate. Form-level RBAC (owner, same school, assignment, share) lives in each route handler. See Roles and access.

JWT deny-list

Every JWT gets a jti (UUID). On signOut, NextAuth writes sess:deny:{jti} in Redis until the token would have expired. Proxy and the JWT callback treat a denied jti as unauthenticated. Without Redis, logout cannot revoke the cookie early. The session still expires at maxAge (8 hours).

HTTP headers

next.config.js sets these on /:path*: CSP script-src includes 'unsafe-inline' and 'unsafe-eval' because Next.js and Once UI still need them in this build. Do not loosen frame-ancestors or form-action.

Indexing

metadataBase follows NEXTAUTH_URL.

OAuth hardening

Google provider in src/lib/auth.js:
  • hd: 'schools.nyc.gov' so the account picker prefers DOE Workspace
  • prompt: 'select_account'
  • checks: ['pkce', 'state', 'nonce']
  • Secure cookies when NEXTAUTH_URL is https://
Sign-in still requires a pre-registered, active User with that email. hd is not a substitute for the MongoDB allowlist.