Skip to content

feat(sso): SAML/OIDC single sign-on#3911

Open
0ski wants to merge 2 commits into
mainfrom
oskar/feat-sso
Open

feat(sso): SAML/OIDC single sign-on#3911
0ski wants to merge 2 commits into
mainfrom
oskar/feat-sso

Conversation

@0ski

@0ski 0ski commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

SAML/OIDC Single Sign-On

Vendor-neutral SSO feature for Trigger.dev, built as a plugin contract + host wiring. With no SSO plugin installed, everything degrades to a no-op fallback — OSS / self-hosted deployments are completely unaffected.

Plugin contract + fail-open fallback

  • Contract in @trigger.dev/plugins (packages/plugins/src/sso.ts) — the vendor-neutral interface.
  • Lazy loader + OSS fallback in the new internal-packages/sso package. It dynamically imports the cloud plugin on first use; if absent, it swaps in a no-op SsoFallback. Every method returns a neverthrow Result, and the fallback is fail-open (decideRouteForEmail → no_sso, mutations → feature_disabled).
  • Gated behind SSO_ENABLED (default off) so the feature ships dark until its backing plugin is available.

Login & sign-in flows

  • A "Sign in with SSO" entry on the login page, plus a dedicated /login/sso flow and /auth/sso + /auth/sso/callback routes (SP- and IdP-initiated).
  • Auto-discovery: magic-link, GitHub, Google, and Vercel onboarding all check whether the email's domain requires SSO and reroute into the SSO flow before establishing a session.
  • IP + email rate limiting on the SSO entry point, reusing the magic-link limiter infrastructure.

Org settings → SSO page

Delivers plan-tier upsell, connection status, verified-domain list, enforcement toggle, JIT provisioning config, default-role selection, and an admin-portal link dialog.

Session lifecycle & enforcement

  • AuthUser carries an optional signed sso marker, so SSO-established sessions are self-describing.
  • Those sessions are periodically re-validated against the IdP — single-flight (Redis SET NX), hard-timeout, fail-open, logging out only on an explicit invalid result. Cadence via SSO_SESSION_REVALIDATION_INTERVAL_SECONDS.
  • JIT org provisioning via a new ensureOrgMember helper (idempotent, seat-check-exempt, RBAC-role-aware).

Data model

A new UserSsoIdentity-style relation (Prisma schema + migration) linking host users to IdP identities, enabling the fast existing-user-by-IdP login path.

@changeset-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 0d5f19d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR introduces end-to-end SSO support across the monorepo. A new @trigger.dev/sso internal package provides a lazy plugin loader with a full OSS fallback controller. The webapp gains SSO environment variables, a database AuthenticationMethod.SSO enum value, a ssoController singleton, and new user/org-member server models for SSO-based JIT provisioning. Authentication services are extended with a SsoStrategy, auto-discovery helpers, and Redis-backed rate limiting. New Remix routes handle the SSO login page, authorization action, and callback with MFA carry-through. Existing GitHub, Google, and magic-link auth flows add domain-policy enforcement gates. Server-side SSO session revalidation runs on every authenticated request via a Redis-throttled check, with client-side window.fetch interception and EventSource probing for expired sessions. A new organization SSO settings route and updated navigation expose configuration to Enterprise org admins. An accounts webhook route and background worker process inbound IdP events.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch oskar/feat-sso

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@0ski 0ski marked this pull request as ready for review June 11, 2026 17:24
@0ski 0ski self-assigned this Jun 11, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@44a124f

trigger.dev

npm i https://pkg.pr.new/trigger.dev@44a124f

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@44a124f

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@44a124f

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@44a124f

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@44a124f

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@44a124f

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@44a124f

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@44a124f

commit: 44a124f

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@0ski 0ski force-pushed the oskar/feat-sso branch from 6bc31eb to 39e498a Compare June 12, 2026 10:34
coderabbitai[bot]

This comment was marked as resolved.

@0ski 0ski force-pushed the oskar/feat-sso branch 2 times, most recently from f0185b2 to fbf8172 Compare June 15, 2026 17:28
coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@0ski 0ski force-pushed the oskar/feat-sso branch 2 times, most recently from 5a2cf4b to e5012c1 Compare June 16, 2026 09:27
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@0ski 0ski force-pushed the oskar/feat-sso branch from e5012c1 to 416d947 Compare June 16, 2026 13:35
devin-ai-integration[bot]

This comment was marked as resolved.

@0ski 0ski force-pushed the oskar/feat-sso branch from 416d947 to c40572c Compare June 16, 2026 14:20
devin-ai-integration[bot]

This comment was marked as resolved.

@0ski 0ski force-pushed the oskar/feat-sso branch 3 times, most recently from fa71a08 to 19289bb Compare June 16, 2026 15:36
devin-ai-integration[bot]

This comment was marked as resolved.

@0ski 0ski force-pushed the oskar/feat-sso branch 2 times, most recently from 08a9e68 to 9d79e25 Compare June 18, 2026 11:43
devin-ai-integration[bot]

This comment was marked as resolved.

@0ski 0ski force-pushed the oskar/feat-sso branch from 9d79e25 to af75bc4 Compare June 18, 2026 13:54
0ski added a commit that referenced this pull request Jun 18, 2026
- vercel.onboarding: already-authenticated users whose domain requires SSO
  were redirected to /login/sso, which bounces authed users home and
  dropped the single-use Vercel code. Destroy the session first via
  authenticator.logout so /login/sso accepts them and the resume URL
  survives the round-trip (addresses PR #3911 review).
- auth.sso.callback: sanitize redirectTo at the exit point (IdP-initiated
  values come from relay-state and never passed the host sanitizer) and
  attribute the referral source, matching the other auth callbacks.
- ssoAuth: extract private early-return helpers (resolveSsoUserId,
  attachSsoIdentityBestEffort, provisionJitMembershipBestEffort,
  runPostAuthentication) and fail closed if the user can't be confirmed
  post-resolution instead of minting a session.
@0ski 0ski force-pushed the oskar/feat-sso branch from f432227 to eac52f8 Compare June 18, 2026 15:01
@0ski 0ski enabled auto-merge (squash) June 18, 2026 15:25
@0ski 0ski disabled auto-merge June 18, 2026 15:33
Vendor-neutral plugin contract plus the host
wiring that consumes it. With no SSO plugin installed, everything degrades
to a no-op fallback, so OSS deployments are unaffected.

- Plugin contract (@trigger.dev/plugins) + lazy loader/fallback in
  internal-packages/sso: status, portal-link, enforce/JIT config,
  route-decision, begin/complete authorization, identity resolution, JIT
  evaluation, and periodic session validation. All methods return
  neverthrow Results; the fallback is fail-open.
- Login: 'Sign in with SSO' entry + dedicated /login/sso flow and
  /auth/sso(.callback) routes, plus auto-discovery from magic-link/OAuth.
- Org settings -> SSO page: plan-tier upsell, connection status,
  verified-domain list, enforcement + JIT provisioning + default-role
  configuration, and an admin-portal link dialog.
- AuthUser carries an optional signed 'sso' marker; SSO-established
  sessions are periodically re-validated against the identity provider on
  a single-flight, throttled, fail-open basis and logged out only on an
  explicit invalid result.
- SSO_ENABLED gate (default off) so the feature ships dark until its
  backing plugin is available; SSO_SESSION_REVALIDATION_INTERVAL_SECONDS
  controls the cadence.
@0ski 0ski force-pushed the oskar/feat-sso branch from eac52f8 to 44a124f Compare June 18, 2026 15:34
@0ski 0ski enabled auto-merge (squash) June 18, 2026 15:34
devin-ai-integration[bot]

This comment was marked as resolved.

ensureOrgMember's create + rbac.setUserRole + compensating delete are not
transactional (the RBAC plugin writes on its own connection). The common
single-failure case already recovers — a failed setUserRole deletes the row,
so the next login retries cleanly. But if the compensating delete ALSO fails,
the placeholder MEMBER row is orphaned and the findFirst no-op short-circuits
every future login, stranding the user on the wrong role.

When an existing membership is found and a JIT role is requested, complete the
assignment if (and only if) the RBAC layer shows no role assigned. Gated on
'no role assigned' so it can never demote a deliberately-set role; best-effort
so it never throws or rolls back a valid pre-existing membership.

Addresses PR #3911 review.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment thread apps/webapp/app/routes/login._index/route.tsx
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