fix(auth): refuse a sign-in over plain http before it spends a factor (#1097)

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.
This commit is contained in:
Marc-André Bombeck committed 2026-10-03 18:21:19 +02:00
1 parent 911e642c24
commit b100c73a62
22 files changed
+1325 -463

No files matched your search

+523 -441
View File
File diff suppressed because it is too large. Load diff
+7
View File
@@ -207,6 +207,13 @@ ps` — the `app` healthcheck polls `/api/health` every 30 seconds
the container unhealthy if Postgres is still
starting. Tail `docker compose logs db` to confirm Postgres came up
cleanly.
- **The login page says "Signing in cannot complete on this page".** The
server sets its session cookie with the `Secure` flag (the production
default) and the page was opened over plain `http://`, where the browser
would drop it. Set `SESSION_COOKIE_SECURE=false` for a server reached only
over HTTP on a private network, or open the `https://` address (behind a TLS
proxy, make sure it forwards `X-Forwarded-Proto: https`). The sign-in is
refused before any password, code or ticket is used.
- **OAuth callbacks loop back to localhost.** Confirm `APP_URL` and
`NEXT_PUBLIC_APP_URL` both point at the public hostname, not at
`localhost`, and restart `app` after the change.