Add Ory Auth.js integration#342
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryHigh Risk Overview Auth flow: New API & data: Dashboard UX: User profile and account updates move from server actions to a Dependencies: Reviewed by Cursor Bugbot for commit e344696. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Pull request overview
This PR introduces an Auth.js + Ory Hydra authentication mode (behind AUTH_PROVIDER) alongside the existing Supabase auth path, and updates request authentication/header wiring plus supporting dashboard-api contract types and tests.
Changes:
- Add Auth.js (NextAuth) Ory Hydra OAuth integration with session handling, sign-in bootstrap, and Ory sign-out flows.
- Unify API auth header generation via
authHeaders()and route server-side auth through a provider abstraction (auth/authAdmin). - Update OpenAPI spec + generated dashboard-api types and add unit/integration tests covering Ory auth paths.
Reviewed changes
Copilot reviewed 85 out of 86 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Inline Auth.js deps for Vitest resolution. |
| tests/unit/teams-repository.test.ts | Update mocks for new authAdmin dependency. |
| tests/unit/keys-repository.test.ts | Update expected error string for provider-agnostic auth. |
| tests/unit/auth-supabase-provider.test.ts | Add unit tests for SupabaseAuthProvider. |
| tests/unit/auth-supabase-admin.test.ts | Add unit tests for supabaseAuthAdmin. |
| tests/unit/auth-ory-provider.test.ts | Add unit tests for oryAuthProvider behavior. |
| tests/unit/auth-headers.test.ts | Add unit tests for header switching. |
| tests/setup.ts | Provide placeholder env for module-load clients. |
| tests/integration/proxy.test.ts | Extend proxy integration mocks for getSession. |
| tests/integration/auth-ory-bootstrap.test.ts | Add integration coverage for Ory bootstrap call. |
| src/proxy.ts | Add Ory-aware middleware auth integration. |
| src/lib/utils/server.ts | Switch token header generation to authHeaders(). |
| src/lib/utils/auth.ts | Remove Supabase-only provider extraction helper. |
| src/lib/env.ts | Add Ory/Auth.js env variables to schema. |
| src/features/dashboard/terminal/sandbox-session.ts | Use authHeaders() for E2B SDK calls. |
| src/features/dashboard/sandbox/inspect/context.tsx | Use authHeaders() for inspect sandbox headers. |
| src/features/dashboard/context.tsx | Use AuthUser instead of Supabase User. |
| src/features/dashboard/account/password-settings.tsx | Use AuthUser.providers for email provider checks. |
| src/features/dashboard/account/name-settings.tsx | Use AuthUser.name in forms and comparisons. |
| src/features/dashboard/account/email-settings.tsx | Use AuthUser.providers for email provider checks. |
| src/features/auth/ory-hosted-auth-redirect.tsx | Add client redirect component for hosted Ory auth. |
| src/core/shared/contracts/dashboard-api.types.ts | Regenerate dashboard-api types with new admin endpoints. |
| src/core/server/trpc/init.ts | Update TRPC context session/user types for AuthUser. |
| src/core/server/functions/sandboxes/get-team-metrics-max.ts | Switch to authHeaders() for infra calls. |
| src/core/server/functions/sandboxes/get-team-metrics-core.ts | Switch to authHeaders() for infra calls. |
| src/core/server/functions/auth/get-user-by-token.ts | Remove Supabase-specific cached token lookup. |
| src/core/server/functions/auth/get-session.ts | Remove Supabase getSessionInsecure helper. |
| src/core/server/auth/types.ts | Introduce provider-agnostic auth types. |
| src/core/server/auth/supabase/user.ts | Map Supabase User -> AuthUser. |
| src/core/server/auth/supabase/server-client.ts | Add SSR client wrappers for proxy/headers contexts. |
| src/core/server/auth/supabase/provider.ts | Add SupabaseAuthProvider implementing AuthProvider. |
| src/core/server/auth/supabase/flows.ts | Centralize Supabase auth flows behind helpers. |
| src/core/server/auth/supabase/admin.ts | Add supabaseAuthAdmin implementing AuthAdmin. |
| src/core/server/auth/provider.ts | Define AuthProvider interface. |
| src/core/server/auth/ory/signout.ts | Add signed state + redirect helpers for Ory logout. |
| src/core/server/auth/ory/provider.ts | Add oryAuthProvider using Auth.js session. |
| src/core/server/auth/ory/identity.ts | Map Auth.js session/Ory identity -> AuthUser. |
| src/core/server/auth/ory/client.ts | Add cached Ory Identity API client. |
| src/core/server/auth/ory/bootstrap.ts | Add dashboard-api bootstrap on Auth.js sign-in event. |
| src/core/server/auth/ory/admin.ts | Add oryAuthAdmin for identity/email lookups. |
| src/core/server/auth/index.ts | Wire auth/authAdmin selection + helpers. |
| src/core/server/auth/admin.ts | Define AuthAdmin interface. |
| src/core/server/api/middlewares/telemetry.ts | Update telemetry middleware to AuthUser. |
| src/core/server/api/middlewares/auth.ts | Switch TRPC auth middleware to provider-based auth. |
| src/core/server/actions/user-actions.ts | Use supabaseAuthFlows + provider signOut. |
| src/core/server/actions/sandbox-actions.ts | Switch to authHeaders() for infra DELETE. |
| src/core/server/actions/ory-auth-actions.ts | Add server action wrapper for Auth.js signIn. |
| src/core/server/actions/client.ts | Switch safe-action auth context to provider-based. |
| src/core/server/actions/auth-actions.ts | Route Supabase flows through supabaseAuthFlows; add Ory sign-out redirect. |
| src/core/modules/webhooks/repository.server.ts | Replace SUPABASE_AUTH_HEADERS with authHeaders. |
| src/core/modules/users/auth-user-emails.server.ts | Resolve creator emails via authAdmin (provider-agnostic). |
| src/core/modules/templates/repository.server.ts | Replace SUPABASE_AUTH_HEADERS with authHeaders. |
| src/core/modules/teams/user-teams-repository.server.ts | Replace SUPABASE_AUTH_HEADERS with authHeaders. |
| src/core/modules/teams/teams-repository.server.ts | Replace Supabase admin lookups with authAdmin.getUserById. |
| src/core/modules/sandboxes/repository.server.ts | Replace SUPABASE_AUTH_HEADERS with authHeaders. |
| src/core/modules/keys/repository.server.ts | Replace SUPABASE_AUTH_HEADERS with authHeaders. |
| src/core/modules/builds/repository.server.ts | Replace SUPABASE_AUTH_HEADERS with authHeaders. |
| src/core/modules/billing/repository.server.ts | Replace SUPABASE_AUTH_HEADERS with authHeaders. |
| src/core/modules/auth/repository.server.ts | Route verifyOtp via supabaseAuthFlows helper. |
| src/configs/flags.ts | Add isOryAuthEnabled() feature switch. |
| src/configs/api.ts | Introduce authHeaders() + Ory team header constant. |
| src/auth.ts | Add Auth.js (NextAuth) Ory Hydra provider configuration + token refresh. |
| src/app/sbx/new/route.ts | Switch route auth to provider-based auth + authHeaders. |
| src/app/dashboard/terminal/page.tsx | Switch server auth to provider-based auth + authHeaders. |
| src/app/dashboard/route.ts | Switch server auth to provider-based auth + Ory sign-out on missing team. |
| src/app/dashboard/account/route.ts | Switch server auth to provider-based auth + Ory sign-out on missing team. |
| src/app/dashboard/[teamSlug]/team-gate.tsx | Update prop type to AuthUser. |
| src/app/dashboard/[teamSlug]/layout.tsx | Switch server auth to provider-based auth. |
| src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts | Switch server auth to provider-based auth + authHeaders. |
| src/app/api/teams/[teamSlug]/metrics/route.ts | Switch route auth to provider-based auth. |
| src/app/api/auth/oauth/signout-flow/route.ts | Add Ory logout flow endpoint. |
| src/app/api/auth/oauth/signed-out/route.ts | Add Ory post-logout callback endpoint. |
| src/app/api/auth/oauth/[...nextauth]/route.ts | Add NextAuth handlers route. |
| src/app/api/auth/email-callback/route.tsx | Route exchangeCodeForSession via supabaseAuthFlows and log errors. |
| src/app/api/auth/callback/route.ts | Route exchangeCodeForSession via supabaseAuthFlows. |
| src/app/(auth)/sign-up/signup-form.tsx | Extract existing signup form into client component. |
| src/app/(auth)/sign-up/page.tsx | Redirect to hosted Ory auth when enabled. |
| src/app/(auth)/sign-in/page.tsx | Redirect to hosted Ory auth when enabled. |
| src/app/(auth)/sign-in/login-form.tsx | Extract existing login form into client component. |
| src/app/(auth)/forgot-password/page.tsx | Redirect to hosted Ory auth when enabled. |
| src/app/(auth)/forgot-password/forgot-password-form.tsx | Extract existing forgot-password form into client component. |
| src/app/(auth)/auth/cli/page.tsx | Switch server auth to provider-based auth for CLI flow. |
| spec/openapi.dashboard-api.yaml | Add auth provider security schemes + new admin endpoints. |
| package.json | Add next-auth and @ory/client-fetch dependencies. |
| bun.lock | Lockfile updates for new dependencies. |
| .env.example | Document Ory/Auth.js env variables and usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export const authHeaders = ( | ||
| token: string, | ||
| teamId?: string | ||
| ): Record<string, string> => { | ||
| const isOry = isOryAuthEnabled() | ||
| const headers: Record<string, string> = isOry | ||
| ? { Authorization: `Bearer ${token}` } | ||
| : { [SUPABASE_TOKEN_HEADER]: token } | ||
| if (teamId) { | ||
| headers[isOry ? AUTH_PROVIDER_TEAM_HEADER : SUPABASE_TEAM_HEADER] = teamId | ||
| } | ||
| return headers |
| const proxyWithOryAuth = authjsMiddleware(async (req, _event: NextFetchEvent) => | ||
| proxyCore(req, !!req.auth) | ||
| ) |
| export async function signInWithOryAction(formData: FormData) { | ||
| const returnTo = formData.get('returnTo') | ||
| const redirectTo = | ||
| typeof returnTo === 'string' && returnTo.length > 0 | ||
| ? returnTo | ||
| : '/dashboard' | ||
| await signIn('ory', { redirectTo }) |
| AUTH_PROVIDER: z.enum(['supabase', 'ory']).optional(), | ||
| AUTH_SECRET: z.string().min(1).optional(), | ||
| AUTH_TRUST_HOST: z.string().optional(), | ||
| ORY_SDK_URL: z.url().optional(), | ||
| ORY_OAUTH2_CLIENT_ID: z.string().min(1).optional(), | ||
| ORY_OAUTH2_CLIENT_SECRET: z.string().min(1).optional(), | ||
| ORY_OAUTH2_AUDIENCE: z.string().min(1).optional(), | ||
| ORY_PROJECT_API_TOKEN: z.string().min(1).optional(), | ||
|
|
| try { | ||
| const credentials = btoa(`${clientId}:${clientSecret}`) | ||
| const tokenEndpoint = `${sdkUrl.replace(/\/$/, '')}/oauth2/token` | ||
| const response = await fetch(tokenEndpoint, { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: `Basic ${credentials}`, | ||
| 'Content-Type': 'application/x-www-form-urlencoded', | ||
| }, |
| }; | ||
| }; | ||
| }; | ||
| put?: never; |
Wire the dashboard to Ory through Auth.js while preserving Supabase mode behind the auth provider switch.
0b18e9c to
77a5a85
Compare
| } | ||
|
|
||
| const proxyWithOryAuth = authjsMiddleware(async (req, _event: NextFetchEvent) => | ||
| proxyCore(req, !!req.auth) |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
The Ory middleware path marks a request authenticated using !!req.auth instead of the stricter provider validation used elsewhere (getAuthContext() rejects sessions with missing user.id, missing accessToken, or session.error). This creates an auth-context mismatch in proxy routing decisions.
Impact: Requests tied to invalid/errored sessions can be treated as authenticated for middleware redirect logic, weakening the intended authentication gate on protected navigation paths.
Move the sign-out redirect target into the AuthProvider contract so route handlers drop their isOryAuthEnabled() branches. Delete the HMAC-state sign-out machinery (we no longer thread a "you were signed out because X" message through Hydra). Extract refreshOryToken into its own module and convert the bootstrap import to a static one.
Drop the per-member authAdmin.getUserById fanout in listTeamMembers. dashboard-api now returns name, profilePictureUrl, and providers on each TeamMember directly, so the repository just maps the response shape. Removes the auth admin dependency from createTeamsRepository. Re-syncs openapi.dashboard-api.yaml from infra and regenerates the typed contracts.
Freezes user/team membership churn while the identity store migrates. - New NEXT_PUBLIC_AUTH_MIGRATION_IN_PROGRESS env in src/configs/flags.ts. - signUpAction returns a "paused" error when the flag is on. - /sign-up route renders a migration notice instead of the form/OAuth buttons. - tRPC teams.addMember procedure throws FORBIDDEN when the flag is on, and the members page hides the Add new member dialog. Sign-in (returning users) is intentionally unaffected. To also block brand-new OIDC account creation via the sign-in page, toggle "Registration" off in the Ory Console for the duration of the migration; the dashboard flag is the front-end and add-member chokepoint.
Server route handlers that drive the Ory OAuth2 flow: - oauth-start: server-side signIn entry (sets state/PKCE cookies), mapping intent -> prompt (registration/login) for signup and reauth. - oauth-recover: catches Auth.js OAuth errors, logs them, and bounces to /sign-in with a short-lived loop guard. - signout-flow: Auth.js sign-out + Kratos session revocation (by the resolved identityId, not the OIDC subject) + Hydra RP-initiated logout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the unused ory-auth-actions module and the OryHostedAuthRedirect component; the redirect to the Ory hosted UI now happens at the middleware layer (getOryAuthRouteRedirect). The (auth) sign-in/sign-up/forgot-password pages no longer branch on the Ory flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extracts the middleware-redirect / route-rewrite / middleware-rewrite / auth-gate concerns into named handlers in core/server/http/proxy.ts, so proxyCore reads as a first-non-null pipeline. Collapses the awkward isAuthenticated threading into handleAuthGate(knownAuth) and names the poisoned-session guard isSessionAuthenticated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Server auth core for the dashboard-as-Hydra-OIDC-client setup: - find-identity/auth-callbacks resolve the Kratos identity (profile.sub -> token.sub -> verified email, with external_id fallback) and cache it on the Auth.js session as identityId; the OIDC subject (token.sub) stays the E2B user id used by dashboard-api/infra. - AuthProvider gains getUserProfile; the Ory and Supabase providers implement it. - jwt-claims/freshness/ory-error/flows/kratos-session/build-start-url/ auth-route-redirect helpers; fromOryIdentity normalizes the password credential to the email provider so the account UI gate is provider-agnostic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the user-update/access-token server actions with a tRPC user router: a cached profile query (live Kratos lookup with a timeout fallback to the session user), an update mutation returning a discriminated result, and createAccessToken. The dashboard layout prefetches the profile and team-gate injects it into DashboardContext; the account settings forms consume the mutations and refresh the profile cache. Reauth remains a redirect-throwing server action. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| const url = new URL(request.url) | ||
| const intent = url.searchParams.get('intent') | ||
| const returnTo = url.searchParams.get('returnTo') | ||
| const redirectTo = returnTo && returnTo.length > 0 ? returnTo : '/dashboard' |
There was a problem hiding this comment.
Unvalidated user-controlled returnTo enables open redirect
High Severity
The returnTo query parameter is read directly from user input and passed to Auth.js signIn as redirectTo without any server-side validation. Other auth entry points in the codebase validate returnTo via relativeUrlSchema, but this route skips that check entirely. While Auth.js's default redirect callback provides same-origin protection, this creates an inconsistency in the security model — any same-origin path is accepted, and the defense-in-depth validation applied everywhere else is bypassed. The relativeUrlSchema or equivalent validation needs to be applied before passing returnTo to signIn.
Reviewed by Cursor Bugbot for commit 4ad2019. Configure here.
A JSON-Patch write to /credentials/password/config/password is accepted with 200 but stored raw — hashed_password is left untouched, so the change appeared to succeed while the OLD password kept working and the new one never did. Route password changes through updateIdentity (the credential import path), which Kratos hashes; trait-only changes keep the lighter patch. Re-sends schema_id/state/traits/external_id/metadata so the full update doesn't clobber them, and preserves existing non-password credentials (e.g. oidc). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nges) - Reauth returns the oauth-start URL for a client window.location navigation instead of a server-action redirect(). The soft RSC navigation was prefetching/re-invoking the side-effecting oauth-start GET, corrupting the OAuth state/callback-url cookies so the post-reauth callback fell back to '/'. - Require fresh re-authentication for EMAIL changes too (not just password): otherwise a stolen session could take over the account by swapping the email and resetting the password via the attacker's inbox. Wire the email form to the reauth dialog and make the (now shared) dialog copy generic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| }, | ||
| 'Failed to check whether Ory user already has a dashboard team' | ||
| ) | ||
| return false |
There was a problem hiding this comment.
Team check failure retriggers bootstrap
Medium Severity
When listing teams fails during sign-in, hasBootstrappedUserTeam returns false and ensureOryUserBootstrapped calls the admin bootstrap endpoint again, even for users who already have teams, which can cause duplicate bootstrap attempts or spurious sign-in denials on transient API errors.
Reviewed by Cursor Bugbot for commit ee1554a. Configure here.
…ra-oidc-client-ory-elements-integration-eng-4125 # Conflicts: # src/app/(auth)/sign-up/page.tsx # src/core/modules/webhooks/repository.server.ts # src/core/server/api/routers/index.ts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e344696. Configure here.
| }) | ||
| } | ||
|
|
||
| return identity ? fromOryIdentity(identity) : null |
There was a problem hiding this comment.
Profile uses Kratos id not E2B
High Severity
getUserProfile returns fromOryIdentity(identity), which sets AuthUser.id to the Kratos identity id, while the session and APIs use the OIDC subject as the E2B user id when those differ.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e344696. Configure here.


Summary
AUTH_PROVIDERswitch.Test plan
bun run tsc --noEmitbun test tests/unit/auth-headers.test.ts tests/unit/auth-ory-provider.test.ts tests/integration/auth-ory-bootstrap.test.ts