mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
feat(auth): pin OIDC accounts to their (issuer, sub) identity
An OIDC login now resolves the account by the durable (issuer, sub) pair instead of the email claim. First contact stamps the pair — by auto-provision or a one-time link against an existing account matched by verified email — and every later login matches on it alone, so an IdP-side email change refreshes the display email but can never re-point the login at a different account. An email that matches an account already bound to a different identity is rejected and audited (auth.oidc.link_conflict), never silently re-bound. The email gate tightens with it: linking and provisioning now require an explicit email_verified: true — an absent claim is a reject (oidc_email_unverified) rather than a benefit of the doubt, since an IdP that does not assert verification cannot anchor the account match. Provisioned accounts get a lowercased email and the same server-default timezone resolution the register route uses. Migration 0240 adds the nullable users.oidc_issuer / users.oidc_sub columns with a composite unique index.
This commit is contained in:
1 parent
f815e4a83f
commit
fa326c398a
11 files changed
+348
-141
No files matched your search
@@ -0,0 +1,13 @@
|
||||
-- v1.28 — OIDC SSO identity pinning.
|
||||
--
|
||||
-- The (oidc_issuer, oidc_sub) pair pins an account to exactly one IdP
|
||||
-- identity: stamped on first OIDC login (auto-provision or one-time link by
|
||||
-- verified email), matched on every later OIDC login. Email stays a display
|
||||
-- field after the stamp — an IdP-side email change can never re-point the
|
||||
-- login at a different account. Both columns are NULL on accounts that never
|
||||
-- signed in via SSO; Postgres unique indexes ignore NULL pairs, so the
|
||||
-- constraint only bites on actual duplicate identities.
|
||||
ALTER TABLE "users" ADD COLUMN "oidc_issuer" TEXT;
|
||||
ALTER TABLE "users" ADD COLUMN "oidc_sub" TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX "users_oidc_issuer_oidc_sub_key" ON "users"("oidc_issuer", "oidc_sub");
|
||||
@@ -42,6 +42,13 @@ model User {
|
||||
email String? @unique
|
||||
passwordHash String? @map("password_hash") // argon2id, optional fallback
|
||||
role String @default("USER") // "ADMIN" | "USER"
|
||||
// OIDC SSO — the (issuer, sub) pair pins the account to exactly one IdP
|
||||
// identity. Stamped on first OIDC login (auto-provision or one-time link
|
||||
// by verified email); every later OIDC login matches on this pair, never
|
||||
// on the email, so an IdP-side email change can update the display email
|
||||
// but can never re-point the login at a different account.
|
||||
oidcIssuer String? @map("oidc_issuer")
|
||||
oidcSub String? @map("oidc_sub")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@ -867,6 +874,10 @@ model User {
|
||||
// v1.16.0 — per-signup invite redemption ledger (multi-use history).
|
||||
inviteRedemptions InviteRedemption[] @relation("InviteRedemptions")
|
||||
|
||||
// One account per IdP identity — the (issuer, sub) pair is the durable
|
||||
// OIDC key (both NULL on accounts that never signed in via SSO; Postgres
|
||||
// unique indexes ignore NULL pairs).
|
||||
@@unique([oidcIssuer, oidcSub])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user