Skip to content

feat: auth flow via same-origin ory elements#438

Merged
drankou merged 30 commits into
mainfrom
ory-elements
Jun 23, 2026
Merged

feat: auth flow via same-origin ory elements#438
drankou merged 30 commits into
mainfrom
ory-elements

Conversation

@drankou

@drankou drankou commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the NextAuth (Auth.js) layer with a first-party, same-origin auth flow built directly on Ory. The dashboard now acts as Hydra's OAuth2 client and its consent/logout provider, drives an authorization-code + PKCE flow, and carries the resulting OIDC tokens in a single JWE-encrypted e2b_session cookie. Kratos owns the session; the cookie is just a token cache, never the auth gate.

This removes next-auth / @auth/core entirely and the Auth.js request-wrapping that had been fighting our sign-out and tRPC code paths.

Why

  • Auth.js wrapped every request and re-issued a JWT session cookie on the way out, which clobbered our sign-out cookie deletion and forced awkward boundary shims (authjs-boundary, authjs-session-boundary, authjs-callbacks).
  • The OAuth2 redirect/PKCE/state handling was opaque inside the Auth.js provider, making the Hydra ⇄ Kratos consent/logout dance hard to reason about.

What changed

Auth flow (new routes)

  • GET /api/auth/oauth/start — builds the Hydra authorization request (PKCE S256, state, nonce), stashes the verifier/state/nonce in a short-lived httpOnly flow cookie, and 307-redirects to /oauth2/auth.
  • GET /api/auth/oauth/callback/ory — validates state/nonce/PKCE, exchanges the code, bootstraps the dashboard user from the id_token, seals tokens into e2b_session. Failures route to the one-shot recover guard so a stale callback can't loop.
  • GET /consent — dashboard is Hydra's consent provider; accepts the consent challenge and folds the identity's profile traits into the id_token (without this, tokens carry sub/iss but no email/name and bootstrap rejects the login).
  • GET /logout — dashboard is Hydra's logout provider; accepts the logout challenge, routes through Kratos' own logout first so ory_kratos_session is cleared before Hydra finalizes.
  • sign-out route now reads the id_token from e2b_session to build the Hydra RP-logout URL and clears the cookie on the redirect.

Session / tokens

  • New e2b_session cookie: JWE (dir + A256GCM) via jose, carrying access/refresh/id tokens. Keyed by new E2B_SESSION_SECRET (replaces AUTH_SECRET); domain-scoped so it works across the cluster's subdomains.
  • New token-refresh module refreshes the Hydra access token edge-side (direct token-endpoint call, refresh skew, dead/unchanged/refreshed states).
  • New kratos-session-edge helper does an edge-safe Kratos whoami for the middleware redirect gate; authoritative enforcement stays in getAuthContext.

UI

  • Ory Elements auth pages (/login, /registration, /recovery, /verification) are now always same-origin — the NEXT_PUBLIC_ORY_CUSTOM_UI flag and the legacy redirect fallbacks are gone.
  • Auth footer links switched from next/link to plain <a> so the cross-origin start redirect stays a top-level document navigation (a soft nav would turn it into a CORS request Hydra rejects).

Plumbing

  • tRPC handler no longer wrapped by auth(); auth is resolved per-procedure by the auth middleware (Kratos whoami + e2b_session).
  • Deleted: src/auth.ts, the Auth.js boundary/callback shims, next-auth.d.ts, and the [...nextauth] / bootstrap-failed routes.

Dependencies

  • Removed next-auth (+ @auth/core and its transitive @panva/hkdf, preact*).
  • Added jose and oauth4webapi (direct, version-pinned).

Env

  • Added E2B_SESSION_SECRET, optional ORY_HYDRA_PUBLIC_URL (self-hosted Hydra issuer).
  • Removed AUTH_SECRET, AUTH_TRUST_HOST, NEXT_PUBLIC_ORY_CUSTOM_UI.

Testing

New unit + integration coverage for the rewritten flow: oauth-client, session-cookie, token-refresh, kratos-session-edge (unit) and auth-ory-callback, auth-ory-consent, auth-ory-logout, plus the retained entrypoints/account-security suites (integration). The Auth.js session-boundary test was removed with its module.

Notes for reviewers

  • Env / deploy: set E2B_SESSION_SECRET (openssl rand -hex 32) in every environment before merge — AUTH_SECRET is no longer read.
  • This is auth-critical; worth a careful pass on the callback failure/recover paths and the logout ordering (Kratos before Hydra finalize).

@cla-bot cla-bot Bot added the cla-signed label Jun 18, 2026
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
web Ready Ready Preview, Comment Jun 23, 2026 7:26pm

Request Review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the oryHandler existed for the following case:

  • token refresh happens one step before (inside the middleware)
  • if the middleware refreshed the token, it appended a Set-Cookie, which resulted in procedures that have been called after the refresh but before the Set-Cookie reached the client, to use an already invalidated access token.

does the new architecture account for this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this was a known issue with authjs and they added helpers for this, which we were using. it might be that we don't have this case anymore now, just want to make sure we have this tracked

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, I was addressing this explicitly during the implementation. worth double check

Comment thread src/app/consent/route.ts Outdated
Comment thread src/app/login/components/custom-card.tsx
Comment thread src/app/login/page.tsx
Comment thread src/core/server/auth/ory/identity.ts Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

up to exploration: is there certain libraries we should consider using around the oauth state management ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we're using oauth4webapi lib for oauth flow, the rest is handled by ory's useFlow()

Comment on lines +36 to +40
// Kratos owns the session. Identity (the gate) comes from `whoami`; the Hydra
// access token (API access only) comes from the e2b_session cookie, which the
// middleware keeps fresh. For E2B the OAuth subject equals the Kratos identity
// id, so kratos.identity.id is both the dashboard user id and the Kratos id used
// for admin operations. This module never refreshes — it is a pure reader.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the AuthUser.id we are using inside the dashboard state management should ideally map to E2B internal user id (public.users.id).

do we still account for that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we haven't matched it before afaik, and we shouldn't need this since kratos is source of truth for session, and API token has (iss, sub) pair included

@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.

Stale comment

Comment thread src/core/server/auth/ory/oauth-client.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b769ea78d

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/app/consent/route.ts
Comment thread src/core/server/proxy/runtime.ts Outdated
Comment thread src/app/login/components/custom-card.tsx
Comment thread src/app/api/auth/oauth/start/route.ts
Comment thread src/app/consent/route.ts
Comment thread tests/integration/auth-ory-callback.test.ts
Comment thread src/app/api/auth/oauth/callback/ory/route.ts Outdated
drankou and others added 6 commits June 23, 2026 14:23
AuthUser.id is now always public.users.id (the Kratos identity's
external_id) rather than the Kratos identity id, and a new identityId
field carries the Kratos id for admin/Kratos operations. This fixes
PostHog identify, tRPC telemetry, and team resolution, which all expect
public.users.id.

- identity.ts mappers require external_id (no silent fallback to the
  Kratos id) and set identityId.
- getAuthContext refuses a session whose identity has no external_id; the
  edge gate (isKratosSessionActive) rejects it too, so the user is routed
  to /sign-in instead of looping, where a fresh login re-runs bootstrap
  and backfills external_id.
- Drop the updateUser override that re-stamped the Kratos id over id.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@drankou drankou merged commit eec7c7b into main Jun 23, 2026
13 checks passed
@drankou drankou deleted the ory-elements branch June 23, 2026 19:32
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.

3 participants