Skip to content

feat(auth): add local Hydra login/consent/logout provider#344

Open
tvi wants to merge 4 commits into
mainfrom
t/auth
Open

feat(auth): add local Hydra login/consent/logout provider#344
tvi wants to merge 4 commits into
mainfrom
t/auth

Conversation

@tvi
Copy link
Copy Markdown

@tvi tvi commented May 25, 2026

No description provided.

ben-fornefeld and others added 2 commits May 24, 2026 18:31
Wire the dashboard to Ory through Auth.js while preserving Supabase mode behind the auth provider switch.
@tvi tvi requested review from ben-fornefeld and drankou as code owners May 25, 2026 23:58
@cla-bot cla-bot Bot added the cla-signed label May 25, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented May 25, 2026

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

Project Deployment Actions Updated (UTC)
web Ready Ready Preview, Comment May 31, 2026 11:34pm
web-juliett Ready Ready Preview, Comment May 31, 2026 11:34pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 25, 2026

PR Summary

High Risk
Touches authentication, session handling, token refresh, sign-out, and API security across the app; misconfiguration or incomplete client token sourcing in Ory mode could block access or weaken auth boundaries.

Overview
This PR wires Ory (Hydra) + Auth.js as an alternate auth path when AUTH_PROVIDER=ory, while keeping Supabase as the default. It replaces the previous Ory stub with a real session provider, extends the dashboard API contract, and adds local Hydra login/consent/logout handlers for dev.

Auth.js / Ory client flow: New src/auth.ts configures the Ory Hydra provider at /api/auth/oauth, JWT sessions, token refresh, and a signIn event that calls bootstrapOryUser (admin POST /admin/users/bootstrap) from OIDC claims (with optional local env fallbacks). Sign-in/up/forgot-password pages redirect to hosted Ory when enabled; sign-out goes through /api/auth/oauth/signout-flow (Auth.js logout + Hydra RP-initiated logout + signed state callback).

Server auth integration: oryAuthProvider reads Auth.js sessions for getAuthContext; oryAuthAdmin uses Ory Identity API for user/email lookups. Middleware runs Auth.js when Ory is on. authHeaders switches API calls between Supabase headers and Authorization + X-Team-ID.

Dashboard API (OpenAPI): Adds bearer + team security schemes on existing routes, admin profile resolve/lookup endpoints, and /admin/users/bootstrap. Generated types updated.

Local Hydra provider routes: /oauth/login, /oauth/consent, and /oauth/logout auto-accept challenges (dev-oriented; ORY_LOCAL_LOGIN_SUBJECT and related env documented in .env.example / lib/env.ts). Dependencies: next-auth, @ory/client-fetch.

Note for reviewers: Some client features (e.g. terminal / sandbox inspect) still obtain tokens via supabase.auth.getSession() while only the header shape was generalized—worth verifying Ory mode end-to-end for those paths.

Reviewed by Cursor Bugbot for commit 8f3504b. Bugbot is set up for automated code reviews on this repo. Configure here.

loginChallenge: challenge,
})

const { redirect_to } = await hydra.acceptOAuth2LoginRequest({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Agentic Security Review
Severity: HIGH

The login provider route accepts any login_challenge and unconditionally authenticates it as a fixed ORY_LOCAL_LOGIN_SUBJECT, but there is no runtime enforcement that this path can only run in local/dev mode.

If this route is ever wired outside an isolated dev setup, an attacker can start an OAuth flow and receive tokens as that fixed subject, resulting in authentication bypass/account impersonation.

Comment thread package.json Outdated
className="text-fg underline underline-offset-[3px]"
>
Continue if you are not redirected automatically
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Raw button element instead of Button primitive

Low Severity

A new raw <button> element with manual Tailwind styling (text-fg underline underline-offset-[3px]) is used instead of the Button component from @/ui/primitives/button. This is new feature code that violates the rule requiring use of the Button primitive for consistent focus rings, disabled states, and accessibility.

Fix in Cursor Fix in Web

Triggered by learned rule: Use Button primitive instead of raw button elements

Reviewed by Cursor Bugbot for commit 95f62c7. Configure here.

name: keyof OryTokenClaims
): string | null {
return readStringClaim(claims, name)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

readRequiredStringClaim is identical to readStringClaim

Low Severity

readRequiredStringClaim is a wrapper that does nothing beyond calling readStringClaim — it returns string | null without enforcing any "required" semantics. The name is misleading: callers reading the code at line 29 (readRequiredStringClaim(accessClaims, 'sub')) would expect it to throw or assert on missing claims, but it silently returns null just like the non-required variant.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 95f62c7. Configure here.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95f62c79a9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/configs/api.ts
Comment on lines +15 to +17
const isOry = isOryAuthEnabled()
const headers: Record<string, string> = isOry
? { Authorization: `Bearer ${token}` }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid server-only env checks in shared auth header helper

authHeaders now switches behavior via isOryAuthEnabled(), which reads process.env.AUTH_PROVIDER, but this helper is also consumed by client-side code paths introduced in this commit (for example terminal/inspect browser SDK calls). In Next.js, non-NEXT_PUBLIC_ env vars are not available in browser bundles, so client code will not reliably select the Ory branch and can keep emitting Supabase-style headers; in AUTH_PROVIDER=ory deployments that causes sandbox API calls from the browser to be unauthorized.

Useful? React with 👍 / 👎.

Wire the dashboard as Hydra's login provider so the OIDC flow can complete
end-to-end against a self-hosted Hydra (e.g. ../infra devenv) without
requiring a separate IdP UI.

- src/app/oauth/login: auto-accept login challenges as ORY_LOCAL_LOGIN_SUBJECT.
- src/app/oauth/consent: defensive auto-accept (never hit while the seeded
  client has skip_consent=true; kept for misconfiguration safety).
- src/app/oauth/logout: auto-accept logout challenges.
- src/core/server/auth/ory/hydra-admin.ts: OAuth2Api client that targets
  ORY_HYDRA_ADMIN_URL (self-hosted, no PAT) or ORY_SDK_URL (Ory Network, PAT).
- src/lib/env.ts: new optional ORY_HYDRA_ADMIN_URL and ORY_LOCAL_LOGIN_SUBJECT.
- package.json: pin 'next dev' to :3001 so it doesn't collide with the
  infra api on :3000 and matches the seeded client's redirect_uri.

Modeled on ory/hydra-login-consent-node. Intended for local/dev only;
production deployments delegate login to Ory Network / Kratos.
Cookies are scoped by host+path+name, not port, so multiple local
dashboards on different localhost ports shared the default
authjs.session-token cookie and clobbered each other's sessions.
AUTH_COOKIE_PREFIX lets each instance use a distinct cookie name;
unset in prod/preview keeps the standard name.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 4 potential issues.

There are 6 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8f3504b. Configure here.


const userId = data.session.user.id
const headers = SUPABASE_AUTH_HEADERS(data.session.access_token, teamId)
const headers = authHeaders(data.session.access_token, teamId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Terminal still uses Supabase session

High Severity

With AUTH_PROVIDER=ory, the dashboard terminal and sandbox inspect flows still load credentials via supabase.auth.getSession(), while API calls use authHeaders() and send a Bearer Ory access token. There is no Supabase session in Ory mode, so connect/create fails or users are sent back to sign-in despite an active Auth.js session.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8f3504b. Configure here.

Comment thread src/proxy.ts
}

const proxyWithOryAuth = authjsMiddleware(async (req, _event: NextFetchEvent) =>
proxyCore(req, !!req.auth)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Middleware ignores session error state

Medium Severity

In Ory mode, middleware treats any truthy req.auth as authenticated, but oryAuthProvider.getAuthContext() returns null when session.error is set (e.g. refresh failure) or when accessToken is missing. Users can keep accessing protected routes while server code treats them as signed out.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8f3504b. Configure here.

Comment thread src/configs/api.ts
if (teamId) {
headers[isOry ? AUTH_PROVIDER_TEAM_HEADER : SUPABASE_TEAM_HEADER] = teamId
}
return headers
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Client cannot detect Ory mode

High Severity

authHeaders() chooses Bearer vs Supabase headers via isOryAuthEnabled(), which reads process.env.AUTH_PROVIDER. That variable is not exposed to the browser bundle, so client code (terminal, sandbox inspect) always takes the Supabase branch even when the deployment runs AUTH_PROVIDER=ory.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8f3504b. Configure here.

typeof returnTo === 'string' && returnTo.length > 0
? returnTo
: '/dashboard'
await signIn('ory', { redirectTo })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ory sign-in skips returnTo validation

Medium Severity

signInWithOryAction forwards the form returnTo field straight to Auth.js signIn('ory', { redirectTo }) with no relativeUrlSchema check. Supabase sign-in/up flows validate returnTo the same way; the Ory hosted redirect hidden input is user-controlled.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8f3504b. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants