Skip to content

fix(auth): close nOAuth account takeover via email-based OAuth linking#5156

Merged
waleedlatif1 merged 3 commits into
stagingfrom
worktree-fix+noauth-account-linking
Jun 20, 2026
Merged

fix(auth): close nOAuth account takeover via email-based OAuth linking#5156
waleedlatif1 merged 3 commits into
stagingfrom
worktree-fix+noauth-account-linking

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Hardens OAuth account-linking so integration connectors can't be used as sign-in methods. Better Auth registers every generic-OAuth connector (microsoft-ad, salesforce, jira, and the rest) as a social provider, which made them reachable through POST /api/auth/sign-in/oauth2 and POST /api/auth/sign-in/social. Combined with trustedProviders + requireLocalEmailVerified: false, an OAuth login whose email matched an existing account could auto-link to it — undesirable for connectors that authenticate against multi-tenant IdPs where the email claim isn't a proof of ownership.
  • Chokepoint: restrict the sign-in endpoints (/sign-in/social, /sign-in/oauth2) to a first-party login allowlist (google, github, microsoft). Connectors are now only connectable through the authenticated /oauth2/link flow, which binds to the current session user and never mints a session.
  • Trust scope: trim trustedProviders to providers that verify email ownership (google, github, email-password, SSO). microsoft is dropped from the trusted set since it authenticates against the multi-tenant /common/ endpoint — unverified emails no longer auto-link, while verified Microsoft sign-in still links and new signups still work.
  • Defense in depth: stop hardcoding emailVerified: true; honor the real verification claims for the Microsoft id-token connectors and Salesforce.

Type of Change

  • Bug fix

Testing

  • Added apps/sim/lib/auth/constants.test.ts covering the sign-in provider allowlist.
  • bunx vitest run lib/auth/ — 43 passing.
  • bunx tsc --noEmit — 0 errors. bun run check:api-validation — passing. bun run lint — clean.
  • Verified the legitimate flows stay intact: google/github/microsoft login, email + SSO sign-in (separate endpoints, ungated), and integration connect via /oauth2/link (uses the link callback branch, which doesn't consult trustedProviders/emailVerified).

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)

Restrict the unauthenticated sign-in endpoints to first-party login
providers, trim trustedProviders to providers that verify email
ownership, and stop hardcoding emailVerified for multi-tenant Microsoft
and Salesforce connectors.
@vercel

vercel Bot commented Jun 20, 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 Jun 20, 2026 9:47pm

Request Review

@cursor

cursor Bot commented Jun 20, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes authentication, sign-in allowlisting, and account-linking trust rules that directly mitigate account takeover; incorrect configuration could block legitimate sign-in or linking.

Overview
Closes nOAuth-style account takeover where integration OAuth connectors could sign in through unauthenticated endpoints and auto-link to existing users by email.

A before hook on /sign-in/social and /sign-in/oauth2 now allows only google, github, and microsoft (SIGN_IN_PROVIDER_IDS). Connectors such as Salesforce, Jira, and Microsoft work apps must use the authenticated /oauth2/link flow. getRequestedSignInProviderId reads the same body field Better Auth uses per path so a decoy field cannot bypass the guard.

Account linking trustedProviders is trimmed to Google, GitHub, email-password, and SSO—integration connectors and microsoft are removed so multi-tenant Microsoft emails do not auto-link without verification.

Email verification is no longer assumed: Microsoft ID-token connectors use deriveMicrosoftEmailVerified; Salesforce sets emailVerified only when email_verified === true.

Reviewed by Cursor Bugbot for commit dc5cf93. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes nOAuth-style account-takeover vectors by restricting the unauthenticated sign-in endpoints to a first-party allowlist (google, github, microsoft), trimming trustedProviders to providers that genuinely verify email ownership, and replacing the hardcoded emailVerified: true on Microsoft connector tokens and Salesforce with real claim-based derivation.

  • Sign-in gate: a before hook checks ctx.path against /sign-in/social / /sign-in/oauth2 and rejects any provider not in SIGN_IN_PROVIDER_IDS; the correct body field (provider vs providerId) is read per-path so a decoy field in the other slot cannot bypass the check.
  • trustedProviders scope: ~35 integration connectors and microsoft removed; connectors now use only the authenticated /oauth2/link flow; Microsoft sign-in still works but only auto-links when the IdP asserts a verified email.
  • emailVerified fixes: getMicrosoftUserInfoFromIdToken delegates to deriveMicrosoftEmailVerified (checks email_verified, verified_primary_email, verified_secondary_email); Salesforce getUserInfo changes from data.email_verified || true (always truthy) to data.email_verified === true.

Confidence Score: 5/5

The core security fix is correct and well-layered: the sign-in endpoint guard, trimmed trustedProviders, and real emailVerified derivation all work together to close the nOAuth account-takeover path.

The three-layer defence is correctly implemented and fully tested. The only finding is a missing Array.isArray guard in deriveMicrosoftEmailVerified, which only affects integration connectors that are no longer in trustedProviders and are reachable only via the authenticated link flow — so it has no exploitable impact under the current architecture.

apps/sim/lib/oauth/microsoft.ts — the verified-email claim handling uses an unsafe type cast; safe to merge as-is, but worth tightening for correctness.

Important Files Changed

Filename Overview
apps/sim/lib/auth/auth.ts Adds sign-in endpoint guard (allowlist check before hook), trims trustedProviders to first-party providers only, fixes Microsoft emailVerified to use deriveMicrosoftEmailVerified, and fixes Salesforce emailVerified from always-true to === true check.
apps/sim/lib/auth/constants.ts Introduces SIGN_IN_PROVIDER_IDS allowlist, isSignInProviderAllowed guard (type-safe, accepts unknown), and getRequestedSignInProviderId helper that maps each sign-in path to the correct body field.
apps/sim/lib/auth/constants.test.ts New test file covering the allowlist and provider-id extraction; tests the decoy-field bypass cases and rejects malformed inputs.
apps/sim/lib/oauth/microsoft.ts Adds deriveMicrosoftEmailVerified that checks email_verified, verified_primary_email, and verified_secondary_email claims; the verified-email array checks use an unsafe as-cast without a runtime Array.isArray guard.
apps/sim/lib/oauth/microsoft.test.ts New tests for deriveMicrosoftEmailVerified cover most cases, but the malformed-claim test uses a non-email string, missing the case where a plain string equals the email.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Hook as "Before Hook (auth.ts)"
    participant Allowlist as "isSignInProviderAllowed"
    participant Handler as "Sign-in Handler"
    participant LinkFlow as "/oauth2/link (authenticated)"

    Note over Client,Handler: First-party login (google / github / microsoft)
    Client->>Hook: "POST /sign-in/social { provider: "google" }"
    Hook->>Allowlist: "getRequestedSignInProviderId -> "google""
    Allowlist-->>Hook: allowed
    Hook->>Handler: proceed
    Handler-->>Client: OAuth redirect / session minted

    Note over Client,Hook: Integration connector blocked at sign-in
    Client->>Hook: "POST /sign-in/social { provider: "salesforce" }"
    Hook->>Allowlist: "getRequestedSignInProviderId -> "salesforce""
    Allowlist-->>Hook: blocked
    Hook-->>Client: 403 FORBIDDEN

    Note over Client,LinkFlow: Integration connector via authenticated link flow
    Client->>LinkFlow: POST /oauth2/link (active session + provider salesforce)
    LinkFlow-->>Client: OAuth redirect - binds to session user, no session minted
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"}}}%%
sequenceDiagram
    participant Client
    participant Hook as "Before Hook (auth.ts)"
    participant Allowlist as "isSignInProviderAllowed"
    participant Handler as "Sign-in Handler"
    participant LinkFlow as "/oauth2/link (authenticated)"

    Note over Client,Handler: First-party login (google / github / microsoft)
    Client->>Hook: "POST /sign-in/social { provider: "google" }"
    Hook->>Allowlist: "getRequestedSignInProviderId -> "google""
    Allowlist-->>Hook: allowed
    Hook->>Handler: proceed
    Handler-->>Client: OAuth redirect / session minted

    Note over Client,Hook: Integration connector blocked at sign-in
    Client->>Hook: "POST /sign-in/social { provider: "salesforce" }"
    Hook->>Allowlist: "getRequestedSignInProviderId -> "salesforce""
    Allowlist-->>Hook: blocked
    Hook-->>Client: 403 FORBIDDEN

    Note over Client,LinkFlow: Integration connector via authenticated link flow
    Client->>LinkFlow: POST /oauth2/link (active session + provider salesforce)
    LinkFlow-->>Client: OAuth redirect - binds to session user, no session minted
Loading

Reviews (3): Last reviewed commit: "fix(auth): check the provider field the ..." | Re-trigger Greptile

Extract the Microsoft ID-token email-verification logic into a pure
deriveMicrosoftEmailVerified helper and add unit coverage for explicit,
verified-claim, partial, absent, and malformed Azure AD claim
combinations.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/auth/auth.ts
The allowlist guard resolved the provider with `provider ?? providerId`,
but Better Auth reads `provider` on /sign-in/social and `providerId` on
/sign-in/oauth2. A request to /sign-in/oauth2 with an allowed `provider`
and a blocked `providerId` could pass the guard while the handler started
OAuth for the blocked connector. Resolve the field per path via
getRequestedSignInProviderId so the guard checks the same field the
handler acts on.
@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 dc5cf93. Configure here.

@waleedlatif1 waleedlatif1 merged commit aa57f10 into staging Jun 20, 2026
16 checks passed
@waleedlatif1 waleedlatif1 deleted the worktree-fix+noauth-account-linking branch June 20, 2026 22:00
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