mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(auth): link single sign-on to an existing account only after its own sign-in
The SSO callback bound a verified provider email to the local account with the same address on the spot. The local address is not verified: anyone could register with it, or type it into their profile, and the SSO user then landed in an account the other person still held the password to. The callback now links nothing. It records the link it would make in a sealed, ten-minute, HttpOnly cookie and sends the browser to the sign-in page, and the link is made only when that browser then signs in to that same account with its password (and second factor) or passkey. Under an SSO-only policy the password form opens for exactly that account. The native sign-in answers oidc_link_required; the link is confirmed once on the web. Moving an account to a new email address now also needs a fresh proof: a sign-in or second factor in the last five minutes, or the current password sent alongside the change. Without it the address is left as it was and reported in rejectedFields; the rest of the save still lands.
This commit is contained in:
1 parent
7b5bc130d8
commit
8da7583ec6
27 files changed
+878
-76
No files matched your search
+23
-2
@@ -1853,8 +1853,9 @@ paths:
|
||||
- $ref: "#/components/schemas/MfaRequiredResponse"
|
||||
"401": *a1
|
||||
"403":
|
||||
description: Password login is disabled — the operator runs `OIDC_ONLY=true`. `meta.errorCode` = `oidc_only`; sign in
|
||||
through SSO instead.
|
||||
description: "Password login is disabled — the operator runs `OIDC_ONLY=true`. `meta.errorCode` = `oidc_only`; sign in
|
||||
through SSO instead. The one exception is a browser the SSO callback sent back with a pending link to
|
||||
confirm an existing account: it may sign in with that one account's password, which links the account."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -3294,6 +3295,16 @@ paths:
|
||||
configure: the scheme is a compile-time constant, so the open-redirect class is removed by construction.
|
||||
|
||||
|
||||
**An account that exists but is not linked yet.** When the provider's verified email matches a local account
|
||||
with no SSO identity, nothing is linked and nobody is signed in: the local address was never verified, so
|
||||
matching it proves nothing about who owns the account. The browser arm redirects to
|
||||
`/auth/login?error=oidc_link_required` with a sealed, ten-minute, HttpOnly pending-link cookie; the person then
|
||||
signs in to that account once with its own password (and second factor) or passkey, and that sign-in makes the
|
||||
link. Under `OIDC_ONLY=true` the password sign-in is admitted for exactly the account the pending link names.
|
||||
The native arm redirects to `healthlog://oidc-callback?error=oidc_link_required`; the link is confirmed once on
|
||||
the web, after which the app's SSO sign-in works.
|
||||
|
||||
|
||||
Two PKCE exchanges run here and must not be confused: the server↔provider verifier the server generates for
|
||||
itself, and the app↔server challenge supplied above. The native flag and the app's challenge travel only inside
|
||||
an AES-256-GCM state cookie, so the callback branches on tamper-authenticated state rather than on anything the
|
||||
@@ -22435,6 +22446,16 @@ components:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
currentPassword:
|
||||
description: "The account password, sent only alongside a NEW `email`. Moving the account to a new address needs a fresh
|
||||
proof because single sign-on links an existing account by it: a browser session that signed in or completed
|
||||
a second factor in the last five minutes needs nothing more, otherwise this field must carry the current
|
||||
password. Without an accepted proof the address is not changed and comes back in `rejectedFields` as
|
||||
`reproof_required` or `reproof_failed` (or, when the address was the only field sent, 401
|
||||
`auth.reproof.required` / `auth.reproof.failed`). Attempts draw on the account's shared re-proof budget of
|
||||
five per fifteen minutes. Never stored."
|
||||
type: string
|
||||
maxLength: 512
|
||||
description: Partial profile update. Every field is optional and an omitted field is left untouched. An explicit null
|
||||
clears any nullable field; an empty string also clears `displayName`, `dateOfBirth`, `gender`, `fullName`,
|
||||
`insurerName`, `insuranceNumber` and `insurerIkNumber`. `email` is not among them — it is validated as an
|
||||
|
||||
Reference in new issue
Block a user