fix(auth): close nOAuth account takeover via email-based OAuth linking#5156
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview A before hook on Account linking Email verification is no longer assumed: Microsoft ID-token connectors use Reviewed by Cursor Bugbot for commit dc5cf93. Configure here. |
Greptile SummaryThis PR closes nOAuth-style account-takeover vectors by restricting the unauthenticated sign-in endpoints to a first-party allowlist (
Confidence Score: 5/5The 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
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
%%{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
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.
|
@greptile |
|
@cursor review |
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.
|
@greptile |
|
@cursor review |
There was a problem hiding this comment.
✅ 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.
Summary
microsoft-ad,salesforce,jira, and the rest) as a social provider, which made them reachable throughPOST /api/auth/sign-in/oauth2andPOST /api/auth/sign-in/social. Combined withtrustedProviders+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./sign-in/social,/sign-in/oauth2) to a first-party login allowlist (google,github,microsoft). Connectors are now only connectable through the authenticated/oauth2/linkflow, which binds to the current session user and never mints a session.trustedProvidersto providers that verify email ownership (google,github,email-password, SSO).microsoftis 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.emailVerified: true; honor the real verification claims for the Microsoft id-token connectors and Salesforce.Type of Change
Testing
apps/sim/lib/auth/constants.test.tscovering 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./oauth2/link(uses thelinkcallback branch, which doesn't consulttrustedProviders/emailVerified).Checklist