import { NextRequest, NextResponse } from "next/server"; import { currentUser } from "@/lib/session"; import { allow, clientIp } from "@/lib/ratelimit"; import { buildAuthorizeUrl, facilityForEmail, mintState, ssoConfigured, STATE_COOKIE } from "@/lib/sso"; export const dynamic = "force-dynamic"; /* Begin single sign-on for the facility that owns this address's domain. * * Mints a nonce, keeps it in a signed, httpOnly cookie bound to that facility (and to whether a * wearer or a coordinator is expected back), echoes it as the OAuth `state`, and sends the browser * to the broker. The callback requires the returned state to be the cookie's nonce, so a forged * or replayed callback has nothing to match. The redirect target handed to the broker is the * product's one fixed callback address — never a request header. * * A cross-site link may not start this (login CSRF: a stranger's page must not be able to sign * you into an account of its choosing), and a signed-in coordinator is sent to the app instead. */ export async function GET(req: NextRequest) { if (!ssoConfigured()) return NextResponse.json({ error: "Single sign-on is not available." }, { status: 404 }); if (req.headers.get("sec-fetch-site") === "cross-site") return new NextResponse(null, { status: 303, headers: { Location: "/auth" } }); if (!allow("sso-start:" + clientIp(req.headers), 60, 15 * 60 * 1000)) return new NextResponse(null, { status: 303, headers: { Location: "/auth?error=sso_failed" } }); const cur = await currentUser(); if (cur && !cur.isDemo) return new NextResponse(null, { status: 303, headers: { Location: "/app" } }); const email = (req.nextUrl.searchParams.get("email") || "").trim().toLowerCase().slice(0, 160); const f = await facilityForEmail(email); if (!f) return new NextResponse(null, { status: 303, headers: { Location: "/auth?error=sso_unavailable" } }); const aud = req.nextUrl.searchParams.get("as") === "staff" ? "staff" : "user"; if (aud === "staff" && !f.ssoStaff) return new NextResponse(null, { status: 303, headers: { Location: "/auth?error=sso_unavailable" } }); const { nonce, cookie } = mintState(f.id, aud); const res = new NextResponse(null, { status: 302, headers: { Location: buildAuthorizeUrl(f.id, nonce) } }); res.cookies.set(STATE_COOKIE, cookie, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", // the broker returns by a top-level navigation, which lax still sends path: "/api/auth/sso", maxAge: 10 * 60, }); return res; }