[GH-ISSUE #6480] SAML account linking requires trustedProviders but OIDC does not (inconsistent behavior) #27850

Closed
opened 2026-04-17 19:05:21 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @joeyorlando on GitHub (Dec 3, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6480

Description

When a user logs in via SSO and an account with their email already exists in the database, Better Auth handles OIDC and SAML account linking differently. OIDC automatically links accounts based on email match, while SAML requires the provider to be in trustedProviders or have domainVerified: true.

Expected Behavior

Both OIDC and SAML should have the same account linking behavior. Either:

  1. Both should require trusted providers for automatic linking, OR
  2. Both should allow automatic linking based on email match (current OIDC behavior)

Current Behavior

OIDC Callback (~line 1719-1750 in @better-auth/sso/src/routes/sso.ts):

if (existingUser) {
  user = existingUser;
}
// ... later ...
if (!account) {
  // Just creates the account link - no trusted provider check!
  await ctx.context.internalAdapter.createAccount({...});
}

SAML Callback (~line 2076-2105 in @better-auth/sso/src/routes/sso.ts):

if (existingUser) {
  const account = await findAccount(...);
  if (!account) {
    // Requires trusted provider OR domain verification!
    const isTrustedProvider = trustedProviders?.includes(provider.providerId) ||
      ("domainVerified" in provider && provider.domainVerified && validateEmailDomain(...));
    
    if (!isTrustedProvider) {
      throw ctx.redirect(`${callbackUrl}?error=account_not_found`);
    }
    await ctx.context.internalAdapter.createAccount({...});
  }
}

Steps to Reproduce

  1. Create a user account with email user@example.com
  2. Configure a custom SAML provider (not in trustedProviders)
  3. Login via SAML with a user whose email is user@example.com
  4. Observe: Redirected with ?error=account_not_found

Compare with OIDC:

  1. Create a user account with email user@example.com
  2. Configure a custom OIDC provider (not in trustedProviders)
  3. Login via OIDC with a user whose email is user@example.com
  4. Observe: Account is linked successfully

Impact

  • Custom OIDC providers work fine for account linking
  • Custom SAML providers fail with account_not_found unless explicitly added to trustedProviders or domain verification is enabled and completed
  • This creates a confusing UX where OIDC "just works" but SAML requires extra configuration

Workarounds

  1. Add all custom provider IDs to trustedProviders (requires knowing them ahead of time)
  2. Enable domainVerification and complete the verification flow (complex)
  3. Don't use SAML with existing accounts (poor UX)

Environment

  • better-auth: 1.4.4-beta.3
  • @better-auth/sso: 1.4.4-beta.3

Suggested Fix

Apply the same account linking logic to both OIDC and SAML callbacks. If the security concern is that SAML assertions could be spoofed, the same concern applies to OIDC tokens, so both should be treated consistently.

Originally created by @joeyorlando on GitHub (Dec 3, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6480 ## Description When a user logs in via SSO and an account with their email already exists in the database, Better Auth handles OIDC and SAML account linking differently. OIDC automatically links accounts based on email match, while SAML requires the provider to be in `trustedProviders` or have `domainVerified: true`. ## Expected Behavior Both OIDC and SAML should have the **same** account linking behavior. Either: 1. Both should require trusted providers for automatic linking, OR 2. Both should allow automatic linking based on email match (current OIDC behavior) ## Current Behavior **OIDC Callback** (~line 1719-1750 in `@better-auth/sso/src/routes/sso.ts`): ```typescript if (existingUser) { user = existingUser; } // ... later ... if (!account) { // Just creates the account link - no trusted provider check! await ctx.context.internalAdapter.createAccount({...}); } ``` **SAML Callback** (~line 2076-2105 in `@better-auth/sso/src/routes/sso.ts`): ```typescript if (existingUser) { const account = await findAccount(...); if (!account) { // Requires trusted provider OR domain verification! const isTrustedProvider = trustedProviders?.includes(provider.providerId) || ("domainVerified" in provider && provider.domainVerified && validateEmailDomain(...)); if (!isTrustedProvider) { throw ctx.redirect(`${callbackUrl}?error=account_not_found`); } await ctx.context.internalAdapter.createAccount({...}); } } ``` ## Steps to Reproduce 1. Create a user account with email `user@example.com` 2. Configure a custom SAML provider (not in `trustedProviders`) 3. Login via SAML with a user whose email is `user@example.com` 4. Observe: Redirected with `?error=account_not_found` Compare with OIDC: 1. Create a user account with email `user@example.com` 2. Configure a custom OIDC provider (not in `trustedProviders`) 3. Login via OIDC with a user whose email is `user@example.com` 4. Observe: Account is linked successfully ✅ ## Impact - Custom OIDC providers work fine for account linking - Custom SAML providers fail with `account_not_found` unless explicitly added to `trustedProviders` or domain verification is enabled and completed - This creates a confusing UX where OIDC "just works" but SAML requires extra configuration ## Workarounds 1. Add all custom provider IDs to `trustedProviders` (requires knowing them ahead of time) 2. Enable `domainVerification` and complete the verification flow (complex) 3. Don't use SAML with existing accounts (poor UX) ## Environment - `better-auth`: 1.4.4-beta.3 - `@better-auth/sso`: 1.4.4-beta.3 ## Suggested Fix Apply the same account linking logic to both OIDC and SAML callbacks. If the security concern is that SAML assertions could be spoofed, the same concern applies to OIDC tokens, so both should be treated consistently.
GiteaMirror added the locked label 2026-04-17 19:05:21 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Dec 3, 2025):

(tag @better-auth-agent if you would like an answer from the Agent)

If you need more help, tag @Skyvern in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3604479289 --> @better-auth-agent[bot] commented on GitHub (Dec 3, 2025): (tag @better-auth-agent if you would like an answer from the Agent) _If you need more help, tag @Skyvern in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) • [Discord](https://discord.gg/fG2XXEuQX3) • [GitHub](https://github.com/Skyvern-AI/Skyvern) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/fG2XXEuQX3) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/Skyvern-AI/Skyvern)
Author
Owner

@dosubot[bot] commented on GitHub (Dec 3, 2025):

You're right—there is an inconsistency: OIDC account linking in Better Auth is more permissive and does not require the provider to be in trustedProviders or have domainVerified: true, while SAML requires one of those checks for automatic linking. This is by design: recent changes introduced domain verification for SSO providers, and the SAML flow enforces this as a security measure to prevent account takeover via untrusted SAML assertions. The rationale is that SAML assertions are considered more susceptible to spoofing or misconfiguration, so explicit trust or domain verification is required before linking accounts automatically, whereas OIDC flows are generally considered less risky in this context, though this is debated in the security community [PR #5910].

If you want consistent behavior, you can either (1) add all custom provider IDs to trustedProviders, or (2) enable and complete domain verification for your SAML providers. Both options will allow SAML to link accounts automatically, just like OIDC. Be aware that relaxing these checks for SAML (or OIDC) can increase the risk of account takeover if untrusted providers are allowed to link by email alone [PR #5755].

There are no existing tickets or discussions directly tracking this inconsistency, but the stricter SAML logic is a deliberate security policy, not a technical limitation. If you want to unify the logic, you would need to adjust the SAML callback to match the OIDC flow, but this is not recommended unless you fully understand and accept the security tradeoffs.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3604485723 --> @dosubot[bot] commented on GitHub (Dec 3, 2025): <!-- Answer --> You're right—there is an inconsistency: OIDC account linking in Better Auth is more permissive and does not require the provider to be in `trustedProviders` or have `domainVerified: true`, while SAML requires one of those checks for automatic linking. This is by design: recent changes introduced domain verification for SSO providers, and the SAML flow enforces this as a security measure to prevent account takeover via untrusted SAML assertions. The rationale is that SAML assertions are considered more susceptible to spoofing or misconfiguration, so explicit trust or domain verification is required before linking accounts automatically, whereas OIDC flows are generally considered less risky in this context, though this is debated in the security community [[PR #5910](https://github.com/better-auth/better-auth/pull/5910)]. If you want consistent behavior, you can either (1) add all custom provider IDs to `trustedProviders`, or (2) enable and complete domain verification for your SAML providers. Both options will allow SAML to link accounts automatically, just like OIDC. Be aware that relaxing these checks for SAML (or OIDC) can increase the risk of account takeover if untrusted providers are allowed to link by email alone [[PR #5755](https://github.com/better-auth/better-auth/pull/5755)]. There are no existing tickets or discussions directly tracking this inconsistency, but the stricter SAML logic is a deliberate security policy, not a technical limitation. If you want to unify the logic, you would need to adjust the SAML callback to match the OIDC flow, but this is not recommended unless you fully understand and accept the security tradeoffs. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/bc866469-bd7c-4795-a235-85ee4411cdc6?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/bc866469-bd7c-4795-a235-85ee4411cdc6?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/bc866469-bd7c-4795-a235-85ee4411cdc6?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/bc866469-bd7c-4795-a235-85ee4411cdc6?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/bc866469-bd7c-4795-a235-85ee4411cdc6?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/bc866469-bd7c-4795-a235-85ee4411cdc6?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/bc866469-bd7c-4795-a235-85ee4411cdc6?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=You%27re%20right%E2%80%94there%20is%20an%20inconsistency%3A%20OIDC%20account%20linking%20in%20Better%20Auth%20is%20more%20permissive%20and%20does%20not%20require%20the%20provider%20to%20be%20in%20%60trustedProviders%60%20or%20have%20%60domainVerified%3A%20true%60%2C%20while%20SAML%20requires%20one%20of%20those%20checks%20for%20automatic%20linking.%20This%20is%20by%20design%3A%20recent%20changes%20introduced%20domain%20verification%20for%20SSO%20providers%2C%20and%20the%20SAML%20flow%20enforces%20this%20as%20a%20security%20measure%20to%20prevent%20account%20takeover%20via%20untrusted%20SAML%20assertions.%20The%20rationale%20is%20that%20SAML%20assertions%20are%20considered%20more%20susceptible%20to%20spoofing%20or%20misconfiguration%2C%20so%20explicit%20trust%20or%20domain%20verification%20is%20required%20before%20linking%20accounts%20automatically%2C%20whereas%20OIDC%20flows%20are%20generally%20considered%20less%20risky%20in%20this%20context%2C%20though%20this%20is%20debated%20in%20the%20security%20community%20%5B%5BPR%20%235910%5D%28https%3A//github.com/better-auth/better-auth/pull/5910%29%5D.%0A%0AIf%20you%20want%20consistent%20behavior%2C%20you%20can%20either%20%281%29%20add%20all%20custom%20provider%20IDs%20to%20%60trustedProviders%60%2C%20or%20%282%29%20enable%20and%20complete%20domain%20verification%20for%20your%20SAML%20providers.%20Both%20options%20will%20allow%20SAML%20to%20link%20accounts%20automatically%2C%20just%20like%20OIDC.%20Be%20aware%20that%20relaxing%20these%20checks%20for%20SAML%20%28or%20OIDC%29%20can%20increase%20the%20risk%20of%20account%20takeover%20if%20untrusted%20providers%20are%20allowed%20to%20link%20by%20email%20alone%20%5B%5BPR%20%235755%5D%28https%3A//github.com/better-auth/better-auth/pull/5755%29%5D.%0A%0AThere%20are%20no%20existing%20tickets%20or%20discussions%20directly%20tracking%20this%20inconsistency%2C%20but%20the%20stricter%20SAML%20logic%20is%20a%20deliberate%20security%20policy%2C%20not%20a%20technical%20limitation.%20If%20you%20want%20to%20unify%20the%20logic%2C%20you%20would%20need%20to%20adjust%20the%20SAML%20callback%20to%20match%20the%20OIDC%20flow%2C%20but%20this%20is%20not%20recommended%20unless%20you%20fully%20understand%20and%20accept%20the%20security%20tradeoffs.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/6480)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27850