src/proxy.js (not middleware.js). It runs on the edge matcher below before App Router pages and route handlers.
/, /about, /login) is not in the matcher. GET /api/public/* is in the matcher but returns immediately without a session.
Request path
Super Admin page prefixes (level 5)
Super Admin page prefixes (level 5)
/admin/questions, /admin/logs, /admin/system, /admin/goals, /admin/submissionsSuper Admin API prefixes (level 5)
Super Admin API prefixes (level 5)
/api/admin/questions, /api/admin/health, /api/admin/goals, /api/admin/forms/rollover, /api/admin/forms/live, /api/admin/forms/exportSchool admin (level ≥ 4)
School admin (level ≥ 4)
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.JWT deny-list
Every JWT gets ajti (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 insrc/lib/auth.js:
hd: 'schools.nyc.gov'so the account picker prefers DOE Workspaceprompt: 'select_account'checks: ['pkce', 'state', 'nonce']- Secure cookies when
NEXTAUTH_URLishttps://
User with that email. hd is not a substitute for the MongoDB allowlist.