fix(sso): bind Microsoft sign-in to the IdP identity, not the email claim

SSO picked the local account by the email/preferred_username claim, which
Entra does not verify. Anyone able to present a matching email (another
tenant with a multi-tenant issuer, or an editable attribute) could sign in
as that user without a password.

- The first SSO login links the account to "<iss>|<oid>" (new unique
  column User.ssoSubject); afterwards only that identity signs in, and a
  matching email from another identity is refused.
- Multi-tenant issuers (common/organizations/consumers) disable SSO.
- Emails are compared case-insensitively and stored lower-case, so
  [email protected] and [email protected] can no longer be two accounts; the migration
  lower-cases existing emails where that does not collide.
- An admin changing a user's email clears the link.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-01 01:20:49 +02:00
1 parent c4a11b4704
commit a25265b88a
13 files changed
+239 -19

No files matched your search

@@ -0,0 +1,15 @@
-- Microsoft sign-in is bound to the IdP identity ("<iss>|<oid>"), set on the
-- first SSO login; the unverified email claim no longer picks the account.
ALTER TABLE "User" ADD COLUMN "ssoSubject" TEXT;
CREATE UNIQUE INDEX "User_ssoSubject_key" ON "User"("ssoSubject");
-- Emails are compared case-insensitively from now on. Store existing ones
-- lower-case, except where that would collide with another account - those
-- stay as they are and are still found by the case-insensitive lookup.
UPDATE "User" AS u
SET "email" = lower(u."email")
WHERE u."email" <> lower(u."email")
AND NOT EXISTS (
SELECT 1 FROM "User" AS o
WHERE o."id" <> u."id" AND lower(o."email") = lower(u."email")
);
+5
View File
@@ -88,6 +88,11 @@ model User {
// nach einer Passwortaenderung nicht bis zu 15 Minuten weiter funktioniert.
passwordChangedAt DateTime?
// Unveraenderliche Identitaet beim Identity Provider ("<iss>|<oid>"),
// gesetzt bei der ersten Microsoft-Anmeldung. Danach entscheidet nur sie,
// nicht mehr der (unverifizierte) E-Mail-Claim, welches Konto sich anmeldet.
ssoSubject String? @unique
refreshTokens RefreshToken[]
inspections Inspection[]