|
| 1 | +import { NextResponse } from "next/server"; |
| 2 | + |
| 3 | +/** |
| 4 | + * Inbound authentication gate for every API route. |
| 5 | + * |
| 6 | + * AUTH_MODE controls how callers are authenticated: |
| 7 | + * - "easyauth": Azure Container Apps built-in authentication (EasyAuth) is |
| 8 | + * enabled in front of the app. Requests must carry the |
| 9 | + * X-MS-CLIENT-PRINCIPAL-ID header, which the platform sets for signed-in |
| 10 | + * callers and strips from any client-supplied request. |
| 11 | + * - "anonymous": no authentication. Only use this for local development or |
| 12 | + * when another trusted gateway authenticates every request. |
| 13 | + * |
| 14 | + * When AUTH_MODE is unset, a development build (`next dev`) allows anonymous |
| 15 | + * access for local development, while a production build (`next start`, the |
| 16 | + * container image) fails closed and rejects every API request. Unknown values |
| 17 | + * also fail closed. |
| 18 | + */ |
| 19 | +export type AuthMode = "easyauth" | "anonymous" | "unconfigured"; |
| 20 | + |
| 21 | +const PRINCIPAL_ID_HEADER = "x-ms-client-principal-id"; |
| 22 | + |
| 23 | +// `next build` inlines NODE_ENV, so a production build can't be switched back |
| 24 | +// to the anonymous development fallback by setting NODE_ENV at runtime. |
| 25 | +const IS_DEV_BUILD = process.env.NODE_ENV === "development"; |
| 26 | + |
| 27 | +let warned = false; |
| 28 | + |
| 29 | +export function resolveAuthMode( |
| 30 | + env: Record<string, string | undefined> = process.env, |
| 31 | +): AuthMode { |
| 32 | + const mode = env.AUTH_MODE?.trim().toLowerCase(); |
| 33 | + if (mode === "easyauth") return "easyauth"; |
| 34 | + if (mode === "anonymous") return "anonymous"; |
| 35 | + if (!mode && IS_DEV_BUILD) return "anonymous"; |
| 36 | + return "unconfigured"; |
| 37 | +} |
| 38 | + |
| 39 | +function warnOnce(message: string) { |
| 40 | + if (!warned) { |
| 41 | + warned = true; |
| 42 | + console.warn(`[auth] ${message}`); |
| 43 | + } |
| 44 | +} |
| 45 | + |
| 46 | +/** |
| 47 | + * Returns a 401 response when the request is not authenticated, or null when |
| 48 | + * the request may proceed. |
| 49 | + */ |
| 50 | +export function checkAuth(headers: Headers): NextResponse | null { |
| 51 | + const mode = resolveAuthMode(); |
| 52 | + |
| 53 | + if (mode === "anonymous") { |
| 54 | + if (!IS_DEV_BUILD) { |
| 55 | + warnOnce( |
| 56 | + "AUTH_MODE=anonymous: API routes are reachable without authentication.", |
| 57 | + ); |
| 58 | + } |
| 59 | + return null; |
| 60 | + } |
| 61 | + |
| 62 | + if (mode === "easyauth" && headers.get(PRINCIPAL_ID_HEADER)) { |
| 63 | + return null; |
| 64 | + } |
| 65 | + |
| 66 | + if (mode === "unconfigured") { |
| 67 | + warnOnce( |
| 68 | + "Rejecting API requests because authentication is not configured. " + |
| 69 | + "Enable Azure Container Apps authentication and set AUTH_MODE=easyauth.", |
| 70 | + ); |
| 71 | + } |
| 72 | + |
| 73 | + return NextResponse.json( |
| 74 | + { error: "Unauthorized" }, |
| 75 | + { status: 401, headers: { "Cache-Control": "no-store" } }, |
| 76 | + ); |
| 77 | +} |
0 commit comments