mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
A server that issues Secure session cookies cannot sign anyone in on a plain http:// page: the browser drops the cookie. The login request was still verified, the MFA ticket minted and then claimed, and the next attempt answered "Invalid or expired challenge". The web client now sends an optional clientTransport on the browser sign-in requests (password login, passkey start, both MFA completions). When the server issues Secure cookies and the page is plain http on a non-loopback host, the request is refused with 409 auth.session.insecure_transport before the password is checked, a ticket is minted or claimed, a code is spent, or a passkey challenge is issued. A request without the field is never refused. The SSO start takes client_proto/client_host and sends such a page back to the login page instead of to the provider. GET /api/auth/registration-status publishes sessionCookieSecure, and the login page shows the reason with both fixes and disables the sign-in controls before anything is sent; on localhost it shows a note only. SESSION_COOKIE_SECURE keeps its meaning; the header heuristic log line stays.
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
/**
|
|
* What the browser knows and the server does not: whether this page was
|
|
* opened over https://. A `Secure` cookie only sticks on an https:// page
|
|
* (and, in most browsers, on http://localhost). The server knows whether it
|
|
* will issue `Secure` cookies (`shouldEmitSecureCookie`, published as
|
|
* `sessionCookieSecure` on `GET /api/auth/registration-status`); combining the
|
|
* two facts says, before any credential leaves the browser, whether a sign-in
|
|
* here can finish.
|
|
*
|
|
* Pure and dependency-free: imported by the login page and by the server
|
|
* refusal (`./transport-refusal.ts`) so both sides apply the same rule.
|
|
*/
|
|
|
|
export interface ClientTransport {
|
|
protocol: "http" | "https";
|
|
/** `location.host`: hostname plus an optional port. */
|
|
host: string;
|
|
}
|
|
|
|
/**
|
|
* Loopback names a browser treats as a secure context. A `Secure` cookie set
|
|
* on one of them usually sticks even over http://, so a sign-in there gets a
|
|
* soft note, never a refusal.
|
|
*/
|
|
export function isLocalhostHost(host: string): boolean {
|
|
let name = host.trim().toLowerCase();
|
|
if (name.startsWith("[")) {
|
|
// [::1] or [::1]:3000
|
|
name = name.slice(
|
|
1,
|
|
name.indexOf("]") === -1 ? undefined : name.indexOf("]"),
|
|
);
|
|
} else if (name.split(":").length === 2) {
|
|
name = name.split(":")[0];
|
|
}
|
|
if (name === "localhost" || name.endsWith(".localhost")) return true;
|
|
if (name === "::1") return true;
|
|
return /^127(?:\.\d{1,3}){3}$/.test(name);
|
|
}
|
|
|
|
/**
|
|
* - `blocked`: the server sets `Secure` cookies and this page is plain http on
|
|
* a non-loopback host; a sign-in cannot keep its cookie, so it must not start.
|
|
* - `soft`: the same on a loopback host; most browsers accept it, say so.
|
|
* - `ok`: nothing to say.
|
|
*/
|
|
export type TransportVerdict = "ok" | "soft" | "blocked";
|
|
|
|
export function transportVerdict(
|
|
sessionCookieSecure: boolean,
|
|
transport: ClientTransport | undefined,
|
|
): TransportVerdict {
|
|
if (!sessionCookieSecure || !transport || transport.protocol !== "http") {
|
|
return "ok";
|
|
}
|
|
return isLocalhostHost(transport.host) ? "soft" : "blocked";
|
|
}
|
|
|
|
/** The current page's transport, for the request bodies the server checks. */
|
|
export function currentClientTransport(): ClientTransport | undefined {
|
|
if (typeof window === "undefined") return undefined;
|
|
const protocol = window.location.protocol === "https:" ? "https" : "http";
|
|
return { protocol, host: window.location.host };
|
|
}
|