1bc2de655a
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
42 lines
2.5 KiB
TypeScript
42 lines
2.5 KiB
TypeScript
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;
|
|
}
|