Uh oh!
There was an error while loading. Please reload this page.
fix(auth): let Microsoft sign-in link via Entra's domain-verified email claim - #6546
Conversation
…il claim Microsoft is excluded from accountLinking.trustedProviders because the email claim is attacker-controllable on /common/ (nOAuth). Entra never emits email_verified for work/school accounts, so Better Auth refused to link a Microsoft identity onto any existing user row, permanently stranding those users on account_not_linked. Derive emailVerified from the xms_edov optional claim, which Entra emits only when the email's domain belongs to the user's tenant and an admin verified it — the one email signal a hostile tenant cannot forge. Microsoft stays untrusted; the guard now passes on its own merits. The mapper returns an empty object when unverified, so it can only ever promote unverified to verified, never downgrade.
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview Microsoft social login now uses The Reviewed by Cursor Bugbot for commit 5675934. Configure here. |
Greptile SummaryThe PR updates Microsoft authentication to recognize Entra’s domain-verified email claim while keeping Microsoft excluded from trusted account-linking providers.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains in the eligible follow-up review scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/oauth/microsoft.ts | Adds a narrowly scoped mapper that promotes Microsoft email verification only for accepted xms_edov values. |
| apps/sim/lib/auth/auth.ts | Wires the mapper and explicit account-selection prompt into the Microsoft social provider configuration. |
| apps/sim/lib/oauth/microsoft.test.ts | Tests verified, unverified, absent, string, and numeric encodings of the Entra claim. |
| apps/sim/app/oauth-error/page.tsx | Adds recovery-oriented messages for account-linking and missing-email failures. |
Sequence Diagram
sequenceDiagram
participant User
participant Sim as Sim Auth
participant Entra as Microsoft Entra
participant BA as Better Auth
User->>Sim: Sign in with Microsoft
Sim->>Entra: "Authorize with prompt=select_account"
Entra-->>BA: ID token with email and optional xms_edov
BA->>Sim: Map Microsoft profile
alt xms_edov is verified
Sim-->>BA: "emailVerified=true"
BA->>BA: Evaluate existing-account linking
else claim absent or unverified
Sim-->>BA: No profile override
BA->>BA: Preserve existing verification result
end
Reviews (3): Last reviewed commit: "chore(auth): tighten the Microsoft linki..." | Re-trigger Greptile
Hosted Sim serves many Entra tenants, so it must stay on the multi-tenant endpoint — pinning is only meaningful for a self-hoster restricting sign-in to their own directory, and nobody is asking for that yet. The xms_edov fix is independent of the tenant setting, so this removes surface without touching behavior.
waleedlatif1
commented
Aug 11, 2026
waleedlatif1
commented
Aug 11, 2026
@cursor review |
waleedlatif1
commented
Aug 11, 2026
waleedlatif1
commented
Aug 11, 2026
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 5675934. Configure here.
Summary
microsoftis excluded fromaccountLinking.trustedProvidersbecause the email claim is attacker-controllable on/common/(nOAuth). Entra never emitsemail_verifiedfor work/school accounts, so Better Auth refused to link a Microsoft identity onto any existing user row — those users hitaccount_not_linkedpermanently, with no recovery pathemailVerifiedfrom Entra'sxms_edovoptional claim, emitted only when the email's domain belongs to the user's tenant and an admin verified that domain. It's the one email signal a hostile tenant cannot forge, and it's Microsoft's documented nOAuth mitigation.microsoftstays untrusted — the linking guard now passes on its own merits{}when unverified, so it only ever promotes unverified → verified, never downgrades. With the claim absent, Better Auth's computation is byte-identical to todayprompt=select_account, so/common/stops silently reusing whichever Microsoft session the browser already holds/oauth-errorcopy foraccount_not_linkedandemail_not_found, which currently dead-end on "Please try again" — advice that can never succeedRequires an Entra change to take effect
Inert until
xms_edovandemailare added as optional claims on the ID token for the app registration (xms_edovrequiresemailto be present). Until then the mapper returns{}and behavior is unchanged.Known limitation:
xms_edovisfalsefor SAML/WS-Fed federated domains and for B2B guests, so those tenants remain unlinkable and should use Entra SSO, which is trusted for linking.Verified against the Better Auth source
callback.mjs:140-152passesgetUserInfo's result intohandleOAuthUserInfo;microsoft-entra-id.mjs:91-107builds it fromdecodeJwt(idToken), soxms_edovis visible tomapProfileToUser, whose result is spread last (:116)overrideUserInfoOnSignInis unset, so the branch that rewritesemail/emailVerifiedon every sign-in (link-account.mjs:67-77) never runsemailVerifiedupgraded on next sign-in (link-account.mjs:49,65), guarded on exact email match — an upgrade, never a downgradeauth.ts:348gates onemailVerified); after this they get one at creation.afterEmailVerificationonly runs in the verification flow, which OAuth signups don't enter — no duplicatelink-account.mjs:105) needs bothsendOnSignUpandsendVerificationEmail; we set neither, so it stays deadgenericOAuthentries with their ownproviderIdandgetUserInfoType of Change
Testing
bun run type-checkclean; 297 tests across 18 files inlib/auth+lib/oauthpass; verified the new tests fail when the mapper is stubbed out.Checklist