Skip to content

fix(sso): support skipping the OIDC UserInfo endpoint at registration#5386

Merged
waleedlatif1 merged 5 commits into
stagingfrom
worktree-sso-registration-fix
Jul 3, 2026
Merged

fix(sso): support skipping the OIDC UserInfo endpoint at registration#5386
waleedlatif1 merged 5 commits into
stagingfrom
worktree-sso-registration-fix

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Self-hosted Azure AD/Entra ID SSO breaks on better-auth 1.6.x: the OIDC callback now unconditionally prefers the UserInfo endpoint over ID token claims when one is configured, and our registration tooling was auto-populating userInfoEndpoint from OIDC discovery on every registration. Azure's Graph UserInfo endpoint doesn't reliably return email for all tenants, so login fails with missing_user_info.
  • Adds SSO_OIDC_SKIP_USERINFO_ENDPOINT to register-sso-provider.ts and a matching skipUserInfoEndpoint field on the in-app SSO registration API/contract, so operators can force the plugin onto the ID-token/JWKS verification path instead.
  • The in-app route now passes skipDiscovery: true into better-auth's own registerSSOProvider (since Sim already resolves/validates endpoints itself) instead of relying on better-auth's redundant internal discovery call, which was silently re-populating userInfoEndpoint even when we tried to omit it.
  • Preserves tokenEndpointAuthentication auto-detection from the issuer's discovery document (mirrors better-auth's own selectTokenEndpointAuthMethod) in both the discovery and all-endpoints-explicit paths, with a non-fatal fallback when discovery is unreachable — avoids silently defaulting to client_secret_basic, which has a documented invalid_client failure mode with secrets containing +.

Type of Change

  • Bug fix

Testing

  • Added 6 new tests to route.test.ts covering skipDiscovery, skipUserInfoEndpoint on/off, discovery-driven tokenEndpointAuthentication selection, and the non-fatal fallback when discovery is unreachable. All 16 tests pass.
  • tsc --noEmit clean on apps/sim and packages/db.
  • bun run check:api-validation passes.
  • Verified against the real @better-auth/sso@1.6.11 source on GitHub (not just our local dist bundle) to confirm the callback claim-precedence logic, overrideUserInfo semantics, and skipDiscovery/discovery-hydration behavior this fix relies on.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 3, 2026 6:41pm

Request Review

@cursor

cursor Bot commented Jul 3, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches enterprise SSO registration and OIDC discovery/SSRF validation paths; misconfiguration could break login or weaken endpoint checks, though UserInfo skipping is opt-in.

Overview
Fixes Entra/Azure-style OIDC login failures where better-auth prefers UserInfo over ID token claims by adding skipUserInfoEndpoint on the registration API/contract and SSO_OIDC_SKIP_USERINFO_ENDPOINT in the DB registration script (Azure example enables it). When set, userInfoEndpoint is omitted from the stored config—including skipping SSRF checks and discovery merge for UserInfo—so callbacks rely on verified ID token claims.

The in-app register route now always sets oidcConfig.skipDiscovery: true when calling better-auth, since Sim already resolves and SSRF-validates endpoints, avoiding redundant discovery that re-hydrates UserInfo.

OIDC discovery is refactored into fetchOIDCDiscoveryDocument (pinned fetch + timeout). Discovery runs even when all endpoints are explicit so tokenEndpointAuthentication can be chosen from token_endpoint_auth_methods_supported (prefer client_secret_post over basic); discovery failures are non-fatal on the explicit-endpoints path but surface specific errors when endpoints are still missing.

Reviewed by Cursor Bugbot for commit 45b6d31. Configure here.

Comment thread apps/sim/app/api/auth/sso/register/route.ts
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes Azure AD/Entra ID OIDC login failures on better-auth 1.6.x by giving operators a way to skip the UserInfo endpoint and force the ID-token/JWKS claims path. It also stops better-auth from silently re-populating userInfoEndpoint during its own internal registration-time re-discovery.

  • Adds SSO_OIDC_SKIP_USERINFO_ENDPOINT (DB script) and skipUserInfoEndpoint (in-app API) to omit the UserInfo endpoint from the stored config so better-auth falls back to ID-token claims.
  • Sets skipDiscovery: true on every in-app registration call so better-auth does not run its own internal OIDC re-discovery that was overwriting the carefully validated endpoint set.
  • Auto-detects tokenEndpointAuthentication from the IdP's discovery document in both code paths, preferring client_secret_post over client_secret_basic to avoid the +-in-secret invalid_client failure; falls back non-fatally when discovery is unreachable and all endpoints are explicit.

Confidence Score: 5/5

Safe to merge; all three previously-identified correctness issues have been addressed in this commit and the new tests provide good coverage of the edge cases.

The changes are surgical and well-tested: skipDiscovery is set before every registerSSOProvider call, tokenEndpointAuthentication is now resolved in both the discovery and explicit-endpoint branches, and the SSRF error detail is correctly surfaced. The only remaining finding is a misleading log message with no runtime impact.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/api/auth/sso/register/route.ts Core logic refactor: discovery is now always attempted (non-fatally when endpoints are explicit) to detect token_endpoint_auth_methods_supported; skipUserInfoEndpoint clears the endpoint before the final call; skipDiscovery: true is injected unconditionally. One cosmetic issue: the "Fetching OIDC discovery document" log fires after the fetch is already complete.
apps/sim/app/api/auth/sso/register/route.test.ts Six new tests added covering skipDiscovery, skipUserInfoEndpoint (on/off, explicit + discovery paths), tokenEndpointAuthentication selection, and the non-fatal discovery-failure fallback. mockSecureFetchWithPinnedIP is now a named mock with a sensible default rejection in beforeEach, preventing silent undefined returns in untouched tests.
apps/sim/lib/api/contracts/auth.ts Adds skipUserInfoEndpoint: z.boolean().default(false) to the OIDC discriminated-union arm; default preserves backward compatibility.
packages/db/scripts/register-sso-provider.ts Adds SSO_OIDC_SKIP_USERINFO_ENDPOINT env var, skipUserInfoEndpoint to the OIDCConfig interface, and clears userInfoEndpoint before writing to the DB when the flag is set. Azure AD example env vars are updated to include the new flag.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[POST /api/auth/sso/register] --> B{providerType === 'oidc'?}
    B -- No --> C[Build SAML config]
    B -- Yes --> D[SSRF-validate explicit endpoints\nexclude userInfoEndpoint if skipUserInfoEndpoint]
    D --> E[fetchOIDCDiscoveryDocument\nalways called]
    E --> F{needsDiscovery?\nany of auth/token/jwks missing}
    F -- Yes --> G{discoveryResult.ok?}
    G -- No --> H[400: surfaced discovery error]
    G -- Yes --> I[Merge missing endpoints from discovery\nSSRF-validate discovered endpoints\nselect tokenEndpointAuthentication]
    F -- No --> J[selectTokenEndpointAuthentication\nfrom discovery or fallback to client_secret_post]
    I --> K{skipUserInfoEndpoint?}
    J --> K
    K -- Yes --> L[oidcConfig.userInfoEndpoint = undefined]
    K -- No --> M[keep userInfoEndpoint]
    L --> N[Validate required endpoints present]
    M --> N
    N --> O[oidcConfig.skipDiscovery = true]
    O --> P[auth.api.registerSSOProvider]
    C --> P
    P --> Q[200 success]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[POST /api/auth/sso/register] --> B{providerType === 'oidc'?}
    B -- No --> C[Build SAML config]
    B -- Yes --> D[SSRF-validate explicit endpoints\nexclude userInfoEndpoint if skipUserInfoEndpoint]
    D --> E[fetchOIDCDiscoveryDocument\nalways called]
    E --> F{needsDiscovery?\nany of auth/token/jwks missing}
    F -- Yes --> G{discoveryResult.ok?}
    G -- No --> H[400: surfaced discovery error]
    G -- Yes --> I[Merge missing endpoints from discovery\nSSRF-validate discovered endpoints\nselect tokenEndpointAuthentication]
    F -- No --> J[selectTokenEndpointAuthentication\nfrom discovery or fallback to client_secret_post]
    I --> K{skipUserInfoEndpoint?}
    J --> K
    K -- Yes --> L[oidcConfig.userInfoEndpoint = undefined]
    K -- No --> M[keep userInfoEndpoint]
    L --> N[Validate required endpoints present]
    M --> N
    N --> O[oidcConfig.skipDiscovery = true]
    O --> P[auth.api.registerSSOProvider]
    C --> P
    P --> Q[200 success]
Loading

Reviews (3): Last reviewed commit: "fix(sso): always resolve token auth meth..." | Re-trigger Greptile

Comment thread apps/sim/app/api/auth/sso/register/route.ts
Comment thread apps/sim/app/api/auth/sso/register/route.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Re: skipDiscovery not written by the DB script path — verified directly against better-auth's real source (packages/sso/src/routes/sso.ts in the better-auth/better-auth repo at the @better-auth/sso@1.6.11 tag). skipDiscovery appears in exactly 3 places in the entire plugin: the request-body schema declaration, and two checks inside the /sso/register handler that decide whether to run discovery during that request. It is never written into the persisted oidc_config JSON in either the skipDiscovery branch or the hydrated branch of buildOIDCConfig() — both explicitly enumerate every field they store, and skipDiscovery isn't one of them. It's also never referenced anywhere in discovery.ts or the OIDC callback handler. So it's a registration-request-only hint, not a stored runtime flag — the DB script (which writes oidc_config directly and never goes through registerSSOProvider at all) has no parity gap to close here. Both paths persist an identical config shape, and the callback path's behavior depends only on what's actually in that JSON (userInfoEndpoint, tokenEndpoint, jwksEndpoint, authorizationEndpoint), not on any registration-time hint.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Re: mockSecureFetchWithPinnedIP missing a default in beforeEach — good catch, fixed in 2bdcd52. Tests were implicitly relying on the unset vi.fn() rejecting, which happened to produce the right non-fatal fallback but wasn't an intentional assertion. Added an explicit mockRejectedValue(new Error('discovery not mocked for this test')) default so the behavior is documented and won't silently change if fetchOIDCDiscoveryDocument's error handling is refactored later.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/auth/sso/register/route.ts
Comment thread apps/sim/app/api/auth/sso/register/route.ts
Comment thread apps/sim/app/api/auth/sso/register/route.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 45b6d31. Configure here.

@waleedlatif1 waleedlatif1 merged commit e1b8200 into staging Jul 3, 2026
18 checks passed
@waleedlatif1 waleedlatif1 deleted the worktree-sso-registration-fix branch July 3, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant