[GH-ISSUE #9124] OAuth sign-in hard-requires email, blocking providers where email is absent (e.g. Discord phone-only accounts) #28601

Open
opened 2026-04-17 20:02:31 -05:00 by GiteaMirror · 0 comments
Owner

Originally created by @gustavovalverde on GitHub (Apr 11, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/9124

Reproduction

  1. Configure Better Auth with Discord as a social provider
  2. A user with a phone-only Discord account (no email associated) attempts to sign in via Discord OAuth
  3. Discord returns the user profile with email: undefined (requesting the email scope does not guarantee an email exists)
  4. Better Auth redirects with ?error=email_not_found

The user is completely blocked from signing in. The username plugin does not help because it only operates on the credential sign-in/sign-up layer and does not hook into the OAuth callback flow.

Current vs. Expected behavior

Current: OAuth sign-in fails with email_not_found for any provider user that lacks an email address. Three independent runtime guards reject the flow:

Guard File Line
if (!userInfo.email) packages/better-auth/src/api/routes/callback.ts 244
if (!userInfo.email) packages/better-auth/src/api/routes/sign-in.ts ~290 (idToken path)
if (!userInfo.email) packages/better-auth/src/api/routes/account.ts ~271 (account linking)

Additionally, handleOAuthUserInfo in packages/better-auth/src/oauth2/link-account.ts has six .toLowerCase() calls on userInfo.email (lines 24, 91, 132, 144, 146, 175) that would throw TypeError if email ever bypassed the guards.

The OAuth2UserInfo type in packages/core/src/oauth2/oauth-provider.ts already correctly types email as optional (email?: (string | null) | undefined), but the runtime rejects what the type contract allows.

The DiscordProfile type (packages/core/src/social-providers/discord.ts:44) incorrectly types email as string instead of string | undefined.

Expected: OAuth sign-in should succeed for users without an email address. Email should be optional in the user schema. Account linking should fall back to accountId + providerId lookup when email is absent (which findOAuthUser already does as its primary lookup strategy).

Affected layers

The fix requires changes across both packages/core and packages/better-auth:

  • User schema (Zod): email: z.string() in packages/core/src/db/schema/user.ts:10 needs to become optional/nullable
  • User schema (DB): required: true, unique: true in packages/core/src/db/get-tables.ts:163-169 needs to allow nullable email (NULLs must not violate uniqueness)
  • Three runtime guards: change from hard-block to pass-through when email is absent
  • handleOAuthUserInfo: guard all .toLowerCase() calls; skip email-based account linking when absent
  • findOAuthUser: make email parameter optional; skip email fallback when absent
  • createOAuthUser: allow creating users without email
  • DiscordProfile type: fix email: string to email?: string
  • Email verification flow: skip when email is absent (link-account.ts:184-199)

Areas affected

  • Backend
  • Types

Additional context

This affects any OAuth provider where email is not guaranteed: Discord (phone-only accounts), potentially others. The username plugin was designed for credential-based auth and does not address this; it does not modify the OAuth callback flow or relax the email requirement in the user schema.

This is a breaking change for the DB schema (existing deployments would need a migration to make the email column nullable).

Originally created by @gustavovalverde on GitHub (Apr 11, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/9124 ## Reproduction 1. Configure Better Auth with Discord as a social provider 2. A user with a phone-only Discord account (no email associated) attempts to sign in via Discord OAuth 3. Discord returns the user profile with `email: undefined` (requesting the `email` scope does not guarantee an email exists) 4. Better Auth redirects with `?error=email_not_found` The user is completely blocked from signing in. The `username` plugin does not help because it only operates on the credential sign-in/sign-up layer and does not hook into the OAuth callback flow. ## Current vs. Expected behavior **Current:** OAuth sign-in fails with `email_not_found` for any provider user that lacks an email address. Three independent runtime guards reject the flow: | Guard | File | Line | |-------|------|------| | `if (!userInfo.email)` | `packages/better-auth/src/api/routes/callback.ts` | 244 | | `if (!userInfo.email)` | `packages/better-auth/src/api/routes/sign-in.ts` | ~290 (idToken path) | | `if (!userInfo.email)` | `packages/better-auth/src/api/routes/account.ts` | ~271 (account linking) | Additionally, `handleOAuthUserInfo` in `packages/better-auth/src/oauth2/link-account.ts` has six `.toLowerCase()` calls on `userInfo.email` (lines 24, 91, 132, 144, 146, 175) that would throw `TypeError` if email ever bypassed the guards. The `OAuth2UserInfo` type in `packages/core/src/oauth2/oauth-provider.ts` already correctly types email as optional (`email?: (string | null) | undefined`), but the runtime rejects what the type contract allows. The `DiscordProfile` type (`packages/core/src/social-providers/discord.ts:44`) incorrectly types `email` as `string` instead of `string | undefined`. **Expected:** OAuth sign-in should succeed for users without an email address. Email should be optional in the user schema. Account linking should fall back to `accountId + providerId` lookup when email is absent (which `findOAuthUser` already does as its primary lookup strategy). ## Affected layers The fix requires changes across both `packages/core` and `packages/better-auth`: - **User schema (Zod):** `email: z.string()` in `packages/core/src/db/schema/user.ts:10` needs to become optional/nullable - **User schema (DB):** `required: true, unique: true` in `packages/core/src/db/get-tables.ts:163-169` needs to allow nullable email (NULLs must not violate uniqueness) - **Three runtime guards:** change from hard-block to pass-through when email is absent - **`handleOAuthUserInfo`:** guard all `.toLowerCase()` calls; skip email-based account linking when absent - **`findOAuthUser`:** make email parameter optional; skip email fallback when absent - **`createOAuthUser`:** allow creating users without email - **`DiscordProfile` type:** fix `email: string` to `email?: string` - **Email verification flow:** skip when email is absent (`link-account.ts:184-199`) ## Areas affected - Backend - Types ## Additional context This affects any OAuth provider where email is not guaranteed: Discord (phone-only accounts), potentially others. The `username` plugin was designed for credential-based auth and does not address this; it does not modify the OAuth callback flow or relax the email requirement in the user schema. This is a breaking change for the DB schema (existing deployments would need a migration to make the email column nullable).
GiteaMirror added the coreidentityoauthbug labels 2026-04-17 20:02:32 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28601