Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7fd76fb
feat: migrate to ory/elements and replace nextauth logic
drankou Jun 18, 2026
dec4af8
fix links
drankou Jun 19, 2026
ed28caa
domain-wise cookie
drankou Jun 19, 2026
665e72c
Use Link component with prefetch false
drankou Jun 21, 2026
a79dc14
parse ory identity traits and populate avatarUrl from public metadata
drankou Jun 21, 2026
2dcb3ce
style: apply biome formatting
drankou Jun 21, 2026
5d65626
encrypt pkce flow cookie for consistency
drankou Jun 22, 2026
576a071
fix envs for integration tests
drankou Jun 22, 2026
4f9a33e
redirect to recover path on error
drankou Jun 22, 2026
16d07ff
address Link target
drankou Jun 22, 2026
771c4e5
check for loopback url for oauth client
drankou Jun 22, 2026
a42e110
fix test
drankou Jun 22, 2026
26bfa33
fix test
drankou Jun 22, 2026
55fd1fa
improve relative url schema check
drankou Jun 22, 2026
e40566a
add clientId check before auto consent scopes
drankou Jun 22, 2026
8702932
filter out app cookies for kratos whoami
drankou Jun 22, 2026
981dc6d
fix session logout
drankou Jun 22, 2026
65328f0
don't rely on /logout and clear ory_session cookie on sign-out
drankou Jun 22, 2026
de6cab0
fix(auth): source AuthUser.id from Kratos external_id, add identityId
ben-fornefeld Jun 23, 2026
36fc59a
add login/logout relay for previews
drankou Jun 23, 2026
68ad9f4
fix unit test
drankou Jun 23, 2026
ad6d09e
fix integration tests
drankou Jun 23, 2026
c24747a
proxy oauth2 requests to hydra
drankou Jun 23, 2026
8d509d0
drop ory prefix from e2b session cookie management
drankou Jun 23, 2026
21386f2
style: apply biome formatting
drankou Jun 23, 2026
ff95136
add forgot password option to login card
drankou Jun 23, 2026
e576feb
improve login and reauth flow, cards style update
drankou Jun 23, 2026
69f9707
style: apply biome formatting
drankou Jun 23, 2026
4178ff0
improve message padding
drankou Jun 23, 2026
e7274bc
Merge branch 'main' into ory-elements
drankou Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@
### Dashboard API admin token used for user bootstrap and creator email hydration
DASHBOARD_API_ADMIN_TOKEN=your_dashboard_api_admin_token

### Auth.js configuration
### Generate with `npx auth secret` or `openssl rand -hex 32`.
AUTH_SECRET=your_auth_secret
### JWE key for the encrypted e2b_session cookie (carries the Hydra OIDC tokens).
### Generate with `openssl rand -hex 32`.
E2B_SESSION_SECRET=your_e2b_session_secret

### Ory Network configuration
ORY_SDK_URL=https://your-project.projects.oryapis.com
### OIDC issuer for the OAuth client. Falls back to ORY_SDK_URL on Ory Network;
### set explicitly for self-hosted Hydra (e.g. http://localhost:4444).
# ORY_HYDRA_PUBLIC_URL=http://localhost:4444
ORY_OAUTH2_CLIENT_ID=your_ory_oauth2_client_id
ORY_OAUTH2_CLIENT_SECRET=your_ory_oauth2_client_secret
### Access-token audience requested from Ory. Must match the backend JWT audience configuration.
ORY_OAUTH2_AUDIENCE=https://api.e2b.dev
### Ory project admin API token used for IdentityApi lookups
ORY_PROJECT_API_TOKEN=your_ory_project_api_token

### Custom Ory UI: "true" on Preview/Staging, unset on Production.
# NEXT_PUBLIC_ORY_CUSTOM_UI=true
### Kratos public URL for the custom UI (self-hosted :4433; falls back to ORY_SDK_URL).
### Browser-facing Kratos public URL for the Elements UI (self-hosted :4433; falls back to ORY_SDK_URL).
# NEXT_PUBLIC_ORY_SDK_URL=http://localhost:4433

### Domain for the E2B cluster
Expand All @@ -41,8 +42,13 @@ NEXT_PUBLIC_E2B_DOMAIN=e2b.dev
### Set both when running self-hosted; leave unset to use Ory Network with the PAT above.
# ORY_KRATOS_ADMIN_URL=http://localhost:4434
# ORY_HYDRA_ADMIN_URL=http://localhost:4445
### Set to 1 outside Vercel-hosted production to allow Auth.js to trust the Host header
# AUTH_TRUST_HOST=1

### Fixed host whose OAuth callback/logout relays are registered in Hydra. Set on
### preview deployments only (dynamic preview hosts can't register their own
### redirect URIs); leave unset on staging/production/local, where sign-in stays
### host-direct. The relay paths (/api/auth/oauth/relay, /api/auth/oauth/logout-relay)
### must be added to the OAuth2 client's redirect_uris / post_logout_redirect_uris.
# ORY_OAUTH_RELAY_ORIGIN=https://e2b-staging.dev

# ENABLE_USER_BOOTSTRAP=0

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-latest
needs: unit-tests
env:
AUTH_SECRET: test-auth-secret
E2B_SESSION_SECRET: test-session-secret
ORY_SDK_URL: https://test-ory.projects.oryapis.com
ORY_OAUTH2_CLIENT_ID: test-ory-client-id
ORY_OAUTH2_CLIENT_SECRET: test-ory-client-secret
Expand Down
13 changes: 2 additions & 11 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,15 @@
"fast-xml-parser": "^5.3.5",
"immer": "^10.1.1",
"input-otp": "^1.4.2",
"jose": "^6.2.3",
"micromatch": "^4.0.8",
"motion": "^12.23.25",
"nanoid": "^5.0.9",
"next": "^16.2.7",
"next-auth": "^5.0.0-beta.31",
"next-safe-action": "^8.0.11",
"next-themes": "^0.4.6",
"nuqs": "^2.7.0",
"oauth4webapi": "^3.8.6",
"openapi-fetch": "^0.14.0",
"pathe": "^2.0.3",
"pino": "^9.7.0",
Expand Down
10 changes: 2 additions & 8 deletions src/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { redirect } from 'next/navigation'
import { buildOryStartURL } from '@/core/server/auth/ory/build-start-url'

type PageProps = {
searchParams: Promise<{ returnTo?: string }>
}

export default async function Page({ searchParams }: PageProps) {
const { returnTo } = await searchParams
redirect(buildOryStartURL('signin', returnTo))
export default function Page() {
redirect('/recovery')
}
5 changes: 0 additions & 5 deletions src/app/api/auth/oauth/[...nextauth]/route.ts

This file was deleted.

66 changes: 0 additions & 66 deletions src/app/api/auth/oauth/bootstrap-failed/route.ts

This file was deleted.

115 changes: 115 additions & 0 deletions src/app/api/auth/oauth/callback/ory/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import 'server-only'

import { type NextRequest, NextResponse } from 'next/server'
import { PROTECTED_URLS } from '@/configs/urls'
import { ensureOryUserBootstrapped } from '@/core/server/auth/ory/dashboard-bootstrap'
import { exchangeOryCallback } from '@/core/server/auth/ory/oauth-client'
import {
E2B_OAUTH_FLOW_COOKIE,
ORY_RECOVER_PATH,
openOryFlowState,
} from '@/core/server/auth/ory/oauth-flow'
import { resolveOryRedirectUri } from '@/core/server/auth/ory/oauth-relay'
import {
E2B_SESSION_COOKIE,
ORY_SIGNUP_METADATA_COOKIE,
sealSessionCookie,
sessionCookieOptions,
} from '@/core/server/auth/ory/session-cookie'
import {
buildOryLogoutUrl,
ORY_POST_LOGOUT_PATH,
} from '@/core/server/auth/ory/signout'
import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'
import { relativeUrlSchema } from '@/core/shared/schemas/url'

// Hydra redirects here with ?code after Kratos created the session. We exchange
// the code (validating state/nonce/PKCE), provision the dashboard user from the
// id_token, then seal the OIDC tokens into e2b_session. Kratos already owns the
// session at this point — this cookie only carries tokens for API access.
export async function GET(request: NextRequest) {
const origin = request.nextUrl.origin
const flow = await openOryFlowState(
request.cookies.get(E2B_OAUTH_FLOW_COOKIE)?.value
)

if (!flow) {
l.warn(
{ key: 'oauth_callback:missing_flow_state' },
'Ory callback hit without a valid flow-state cookie'
)
return finalize(NextResponse.redirect(new URL(ORY_RECOVER_PATH, origin)))
}

let tokens: Awaited<ReturnType<typeof exchangeOryCallback>>
try {
tokens = await exchangeOryCallback({
// A genuine global URL — oauth4webapi rejects NextURL (not `instanceof URL`).
currentUrl: new URL(request.url),
expectedState: flow.state,
expectedNonce: flow.nonce,
codeVerifier: flow.codeVerifier,
// Must be byte-identical to the authorize-time value (the registered
// relay URI on previews), not the host the code was delivered to.
redirectUri: resolveOryRedirectUri(origin).redirectUri,
})
} catch (error) {
l.error(
{
key: 'oauth_callback:exchange_failed',
error: serializeErrorForLog(error),
},
'Ory authorization code exchange failed'
)
return finalize(NextResponse.redirect(new URL(ORY_RECOVER_PATH, origin)))
}

const bootstrapped = await ensureOryUserBootstrapped({
accessToken: tokens.accessToken,
idToken: tokens.idToken,
provider: 'ory',
})

if (!bootstrapped) {
l.error(
{ key: 'oauth_callback:bootstrap_failed' },
'dashboard bootstrap failed; ending the Ory session without a dashboard cookie'
)
// Don't strand the user with a half-provisioned login: end the Ory + Kratos
// session via RP-logout (falling back to home if no id_token is available).
const logoutUrl = tokens.idToken
? await buildOryLogoutUrl({ idToken: tokens.idToken, origin })
: null
return finalize(
NextResponse.redirect(logoutUrl ?? new URL(ORY_POST_LOGOUT_PATH, origin))
)
}

const sealed = await sealSessionCookie({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
idToken: tokens.idToken,
expiresAt: tokens.expiresAt,
})

// Re-validate here too: the flow cookie is read back as a raw string, and
// `new URL()` would otherwise escape the origin on a crafted returnTo.
const parsedReturnTo = relativeUrlSchema.safeParse(flow.returnTo)
const destination = parsedReturnTo.success
? parsedReturnTo.data
: PROTECTED_URLS.DASHBOARD
const response = finalize(NextResponse.redirect(new URL(destination, origin)))
response.cookies.set(
E2B_SESSION_COOKIE,
sealed,
sessionCookieOptions(request.nextUrl.host)
)
return response
}

// Clears the one-shot transient cookies on every exit path.
function finalize(response: NextResponse): NextResponse {
response.cookies.delete(E2B_OAUTH_FLOW_COOKIE)
response.cookies.delete(ORY_SIGNUP_METADATA_COOKIE)
return response
}
29 changes: 29 additions & 0 deletions src/app/api/auth/oauth/logout-relay/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'server-only'

import { type NextRequest, NextResponse } from 'next/server'
import {
isAllowedRelayTarget,
openRelayState,
} from '@/core/server/auth/ory/oauth-relay'
import { ORY_POST_LOGOUT_PATH } from '@/core/server/auth/ory/signout'
import { l } from '@/core/shared/clients/logger/logger'

// Fixed-host post-logout relay (mirror of the login relay). Hydra returns here
// after ending the session, with the sealed `state` carrying the preview origin;
// we bounce the browser back to that preview's home. The sign-out route already
// cleared the cookies on the preview before the Hydra hop, so this is a pure
// redirect. See oauth-relay.ts.
export async function GET(request: NextRequest) {
const origin = request.nextUrl.origin
const target = await openRelayState(request.nextUrl.searchParams.get('state'))

if (!target || !isAllowedRelayTarget(target)) {
l.warn(
{ key: 'oauth_logout_relay:invalid_target' },
'Ory logout relay hit without a valid sealed target'
)
return NextResponse.redirect(new URL(ORY_POST_LOGOUT_PATH, origin))
}

return NextResponse.redirect(new URL(ORY_POST_LOGOUT_PATH, target))
}
4 changes: 2 additions & 2 deletions src/app/api/auth/oauth/recover/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export async function GET(request: NextRequest) {

l.error(
{
key: 'oauth_recover:auth_js_error',
key: 'oauth_recover:flow_failed',
context: { error_code: errorCode, already_attempted: alreadyAttempted },
},
'Auth.js OAuth flow failed; recovering user'
'OAuth flow failed; recovering user once before bailing to home'
)

const destination = alreadyAttempted ? '/' : AUTH_URLS.SIGN_IN
Expand Down
Loading
Loading