[GH-ISSUE #7052] SSO SAML acsEndpoint uses case-sensitive email lookup, causing login failures #10716

Closed
opened 2026-04-13 07:01:27 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @penguine-ip on GitHub (Dec 29, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7052

Originally assigned to: @Paola3stefania on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

To Reproduce

  1. User signs up via SAML SSO - email stored as user@example.com (lowercase)
  2. IdP (Okta) sends User@Example.com (mixed case) on subsequent login
  3. acsEndpoint queries for User@Example.com - no match found (PostgreSQL is case-sensitive)
  4. Code falls into else block and tries to create a new user
  5. Fails with Unique constraint failed on the fields: (email)

Error

PrismaClientKnownRequestError: 
Invalid `prisma.user.create()` invocation:
Unique constraint failed on the fields: (`email`)

Current vs. Expected behavior

Current Behavior

  • acsEndpoint searches for users using the email exactly as received from the IdP
  • PostgreSQL performs case-sensitive string comparison
  • User lookup fails when IdP sends User@Example.com but database has user@example.com
  • System attempts to create a new user, which fails due to unique constraint

Expected Behavior

  • The user lookup in acsEndpoint should be consistent with how the user was originally created
  • Since handleOAuthUserInfo lowercases emails when creating users, acsEndpoint should also lowercase emails when looking them up

Suggested Fix

Either:

Option A: Lowercase the email before the user lookup in acsEndpoint:

const existingUser = await ctx.context.adapter.findOne({
  model: "user",
  where: [{
    field: "email",
    value: userInfo.email.toLowerCase()  // Add .toLowerCase()
  }]
});

This should also be applied to userInfo.email when creating accounts and any other email comparisons in the SSO plugin.

Option B: Refactor acsEndpoint to use handleOAuthUserInfo instead of implementing its own user lookup/creation logic, ensuring consistent behavior across all OAuth/SSO flows.

What version of Better Auth are you using?

better-auth: 1.4.3 - @better-auth/sso: 1.4.3

System info

Database: PostgreSQL

Which area(s) are affected? (Select all that apply)

Package

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

There is an inconsistency between how users are created and how they are looked up in the SAML flow.

User Creation Flow

When a new user signs up via SAML SSO, acsEndpoint eventually stores the user with a lowercased email (either directly or through internal adapters). For example, a user signing up as User@Example.com gets stored as user@example.com.

User Lookup Flow (the bug)

On subsequent logins, acsEndpoint searches for the user using the email exactly as received from the IdP:

const existingUser = await ctx.context.adapter.findOne({
  model: "user",
  where: [{
    field: "email",
    value: userInfo.email  // ❌ Not lowercased - uses "User@Example.com"
  }]
});

Since PostgreSQL string comparison is case-sensitive:

  • Query searches for: User@Example.com
  • Database has: user@example.com
  • No match found

The code then falls into the else block and attempts to create a new user, which fails with a unique constraint error.

Comparison with handleOAuthUserInfo

The core handleOAuthUserInfo function (used by OIDC SSO, social OAuth, etc.) handles this correctly by lowercasing the email:

const dbUser = await c.context.internalAdapter.findOAuthUser(
  userInfo.email.toLowerCase(),  // ✅ Lowercased
  account.accountId,
  account.providerId
)

Why does acsEndpoint behave differently?

  • handleOAuthUserInfo is used by OIDC SSO callback, social OAuth providers, and generic OAuth flows
  • acsEndpoint (SAML callback at /sso/saml2/sp/acs/:providerId) implements its own user lookup logic instead of delegating to handleOAuthUserInfo

This custom implementation in acsEndpoint doesn't lowercase the email, causing the inconsistency between user creation and lookup.

I'm happy to submit a PR for this fix. What is your release cycle? We're currently experiencing this issue in production and need the fix urgently.

Thanks!

Originally created by @penguine-ip on GitHub (Dec 29, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7052 Originally assigned to: @Paola3stefania on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ## To Reproduce 1. User signs up via SAML SSO - email stored as `user@example.com` (lowercase) 2. IdP (Okta) sends `User@Example.com` (mixed case) on subsequent login 3. `acsEndpoint` queries for `User@Example.com` - no match found (PostgreSQL is case-sensitive) 4. Code falls into `else` block and tries to create a new user 5. Fails with `Unique constraint failed on the fields: (email)` ### Error ``` PrismaClientKnownRequestError: Invalid `prisma.user.create()` invocation: Unique constraint failed on the fields: (`email`) ``` ### Current vs. Expected behavior ### Current Behavior - `acsEndpoint` searches for users using the email exactly as received from the IdP - PostgreSQL performs case-sensitive string comparison - User lookup fails when IdP sends `User@Example.com` but database has `user@example.com` - System attempts to create a new user, which fails due to unique constraint ### Expected Behavior - The user lookup in `acsEndpoint` should be consistent with how the user was originally created - Since `handleOAuthUserInfo` lowercases emails when creating users, `acsEndpoint` should also lowercase emails when looking them up ### Suggested Fix Either: **Option A:** Lowercase the email before the user lookup in `acsEndpoint`: ```javascript const existingUser = await ctx.context.adapter.findOne({ model: "user", where: [{ field: "email", value: userInfo.email.toLowerCase() // Add .toLowerCase() }] }); ``` This should also be applied to `userInfo.email` when creating accounts and any other email comparisons in the SSO plugin. **Option B:** Refactor `acsEndpoint` to use `handleOAuthUserInfo` instead of implementing its own user lookup/creation logic, ensuring consistent behavior across all OAuth/SSO flows. ### What version of Better Auth are you using? `better-auth`: 1.4.3 - `@better-auth/sso`: 1.4.3 ### System info ```bash Database: PostgreSQL ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context There is an inconsistency between how users are created and how they are looked up in the SAML flow. ### User Creation Flow When a new user signs up via SAML SSO, `acsEndpoint` eventually stores the user with a lowercased email (either directly or through internal adapters). For example, a user signing up as `User@Example.com` gets stored as `user@example.com`. ### User Lookup Flow (the bug) On subsequent logins, `acsEndpoint` searches for the user using the email exactly as received from the IdP: ```javascript const existingUser = await ctx.context.adapter.findOne({ model: "user", where: [{ field: "email", value: userInfo.email // ❌ Not lowercased - uses "User@Example.com" }] }); ``` Since PostgreSQL string comparison is case-sensitive: - Query searches for: `User@Example.com` - Database has: `user@example.com` - **No match found** The code then falls into the `else` block and attempts to create a new user, which fails with a unique constraint error. ### Comparison with `handleOAuthUserInfo` The core `handleOAuthUserInfo` function (used by OIDC SSO, social OAuth, etc.) handles this correctly by lowercasing the email: ```javascript const dbUser = await c.context.internalAdapter.findOAuthUser( userInfo.email.toLowerCase(), // ✅ Lowercased account.accountId, account.providerId ) ``` ### Why does `acsEndpoint` behave differently? - **`handleOAuthUserInfo`** is used by OIDC SSO callback, social OAuth providers, and generic OAuth flows - **`acsEndpoint`** (SAML callback at `/sso/saml2/sp/acs/:providerId`) implements its **own user lookup logic** instead of delegating to `handleOAuthUserInfo` This custom implementation in `acsEndpoint` doesn't lowercase the email, causing the inconsistency between user creation and lookup. I'm happy to submit a PR for this fix. What is your release cycle? We're currently experiencing this issue in production and need the fix urgently. Thanks!
GiteaMirror added the lockedbug labels 2026-04-13 07:01:28 -05:00
Author
Owner

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

It looks like you've encountered an issue with the SAML SSO acsEndpoint where email lookups are case-sensitive, leading to login failures. This inconsistency arises because the email is stored in lowercase, but the IdP may send it in mixed case.

To address this problem, you can implement one of the following solutions:

  1. Lowercase the email before performing the user lookup in acsEndpoint:

    const existingUser = await ctx.context.adapter.findOne({
      model: "user",
      where: [{
        field: "email",
        value: userInfo.email.toLowerCase()
      }]
    });
    
  2. Refactor acsEndpoint to utilize handleOAuthUserInfo, maintaining consistency in how emails are handled.

It's important to note that there's an open issue related to this behavior, highlighting that the acsEndpoint should align with user creation practices to prevent such errors from occurring [1], [2].

Regarding your question about submitting a PR, it’s recommended to check in with the Better Auth team to get clarity on the release cycle and urgency for fixes, since production issues can require swift resolutions.

Have you considered detailing your experience with the issue in your PR to ensure it gets prioritized?

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3697039086 --> @better-auth-agent[bot] commented on GitHub (Dec 29, 2025): It looks like you've encountered an issue with the SAML SSO `acsEndpoint` where email lookups are case-sensitive, leading to login failures. This inconsistency arises because the email is stored in lowercase, but the IdP may send it in mixed case. To address this problem, you can implement one of the following solutions: 1. **Lowercase the email** before performing the user lookup in `acsEndpoint`: ```javascript const existingUser = await ctx.context.adapter.findOne({ model: "user", where: [{ field: "email", value: userInfo.email.toLowerCase() }] }); ``` 2. **Refactor `acsEndpoint`** to utilize `handleOAuthUserInfo`, maintaining consistency in how emails are handled. It's important to note that there's an open issue related to this behavior, highlighting that the `acsEndpoint` should align with user creation practices to prevent such errors from occurring [[1]](https://github.com/better-auth/better-auth/issues/3615), [[2]](https://www.better-auth.com/docs/guides/saml-sso-with-okta). Regarding your question about submitting a PR, it’s recommended to check in with the Better Auth team to get clarity on the release cycle and urgency for fixes, since production issues can require swift resolutions. Have you considered detailing your experience with the issue in your PR to ensure it gets prioritized? _If you need more help, tag @better-auth-agent 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

@bytaesu commented on GitHub (Jan 14, 2026):

@Paola3stefania could you check this 🙏

<!-- gh-comment-id:3752144643 --> @bytaesu commented on GitHub (Jan 14, 2026): @Paola3stefania could you check this 🙏
Author
Owner

@Paola3stefania commented on GitHub (Jan 19, 2026):

@penguine-ip thank you for raising this, working on it atm

<!-- gh-comment-id:3770378119 --> @Paola3stefania commented on GitHub (Jan 19, 2026): @penguine-ip thank you for raising this, working on it atm
Author
Owner

@penguine-ip commented on GitHub (Jan 20, 2026):

Hey @Paola3stefania no problem thanks for the fix - could you let me know when this change is out on a new release? Thank you.

<!-- gh-comment-id:3771121693 --> @penguine-ip commented on GitHub (Jan 20, 2026): Hey @Paola3stefania no problem thanks for the fix - could you let me know when this change is out on a new release? Thank you.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10716