Skip to content

Commit acdcc60

Browse files
committed
feat: add ory auth provider integration
1 parent c539a4a commit acdcc60

63 files changed

Lines changed: 3349 additions & 151 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,28 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
2424
### Auth provider: supabase (default) or ory
2525
# AUTH_PROVIDER=supabase
2626

27-
### Ory Network SDK URL (required when AUTH_PROVIDER=ory)
27+
### Ory Network configuration (required when AUTH_PROVIDER=ory)
28+
### SDK URL of the Ory Network project (or custom domain like https://auth.e2b.dev)
2829
# ORY_SDK_URL=https://your-project.projects.oryapis.com
30+
### OAuth2 client credentials issued by Ory for this dashboard deployment
31+
# ORY_OAUTH2_CLIENT_ID=
32+
# ORY_OAUTH2_CLIENT_SECRET=
33+
### Access-token audience requested from Ory. Must match infra AUTH_PROVIDER_CONFIG.jwt[].issuer.audiences.
34+
# ORY_OAUTH2_AUDIENCE=https://api.e2b.dev
35+
### Ory project admin API token used by oryAuthAdmin (IdentityApi lookups)
36+
# ORY_PROJECT_API_TOKEN=
37+
### Dashboard API admin token used to bootstrap newly signed-in Ory users
38+
# DASHBOARD_API_ADMIN_TOKEN=
39+
40+
### Auth.js configuration (required when AUTH_PROVIDER=ory)
41+
### Generate with `npx auth secret` or `openssl rand -hex 32`. Used to encrypt the JWT session cookie.
42+
# AUTH_SECRET=
43+
### Set to 1 outside Vercel-hosted production to allow Auth.js to trust the Host header
44+
# AUTH_TRUST_HOST=1
45+
46+
### Legacy Supabase bootstrap fallback used by dashboard route team resolution.
47+
### Ory sign-in bootstrap does not depend on this flag.
48+
# ENABLE_USER_BOOTSTRAP=0
2949

3050
### Billing API URL (Required if NEXT_PUBLIC_INCLUDE_BILLING=1)
3151
# BILLING_API_URL=https://billing.e2b.dev

bun.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@opentelemetry/sdk-metrics": "^2.0.1",
6161
"@opentelemetry/sdk-node": "^0.203.0",
6262
"@opentelemetry/semantic-conventions": "^1.36.0",
63+
"@ory/client-fetch": "^1.22.37",
6364
"@radix-ui/react-avatar": "^1.1.4",
6465
"@radix-ui/react-checkbox": "^1.3.3",
6566
"@radix-ui/react-dialog": "^1.1.15",
@@ -111,6 +112,7 @@
111112
"motion": "^12.23.25",
112113
"nanoid": "^5.0.9",
113114
"next": "^16.2.7",
115+
"next-auth": "^5.0.0-beta.31",
114116
"next-safe-action": "^8.0.11",
115117
"next-themes": "^0.4.6",
116118
"nuqs": "^2.7.0",

scripts/check-app-env.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,25 @@ const schema = serverSchema
5858
path: ['PLAIN_API_KEY'],
5959
}
6060
)
61+
.refine(
62+
(data) => {
63+
if (data.AUTH_PROVIDER !== 'ory') return true
64+
65+
return Boolean(
66+
data.AUTH_SECRET &&
67+
data.ORY_SDK_URL &&
68+
data.ORY_OAUTH2_CLIENT_ID &&
69+
data.ORY_OAUTH2_CLIENT_SECRET &&
70+
data.ORY_OAUTH2_AUDIENCE &&
71+
data.ORY_PROJECT_API_TOKEN &&
72+
data.DASHBOARD_API_ADMIN_TOKEN
73+
)
74+
},
75+
{
76+
message:
77+
'AUTH_PROVIDER=ory requires AUTH_SECRET, ORY_SDK_URL, ORY_OAUTH2_CLIENT_ID, ORY_OAUTH2_CLIENT_SECRET, ORY_OAUTH2_AUDIENCE, ORY_PROJECT_API_TOKEN, and DASHBOARD_API_ADMIN_TOKEN',
78+
path: ['AUTH_PROVIDER'],
79+
}
80+
)
6181

6282
validateEnv(schema)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { NextRequest } from 'next/server'
2+
import { NextResponse } from 'next/server'
3+
import { AUTH_URLS } from '@/configs/urls'
4+
import { l } from '@/core/shared/clients/logger/logger'
5+
6+
// Auth.js renders its built-in `${basePath}/error` page when something fails
7+
// during the OAuth dance (most commonly a stale state/PKCE/nonce cookie that
8+
// expired while the user lingered on the Ory hosted UI). We point
9+
// `pages.error` here so the user never sees that page - we log the failure
10+
// for observability and bounce them back to /sign-in, which restarts the
11+
// flow with fresh cookies via the middleware -> oauth-start chain.
12+
//
13+
// A short-lived cookie prevents tight loops when the underlying failure is
14+
// genuinely persistent (e.g. ORY_SDK_URL misconfigured). After one recovery
15+
// attempt in the window, subsequent failures fall back to the marketing
16+
// root so the user isn't bounced indefinitely.
17+
const RECOVERY_COOKIE = 'auth_recover_attempted'
18+
const RECOVERY_COOKIE_MAX_AGE_SECONDS = 30
19+
20+
export async function GET(request: NextRequest) {
21+
const errorCode = request.nextUrl.searchParams.get('error') ?? 'unknown'
22+
const alreadyAttempted = request.cookies.get(RECOVERY_COOKIE)?.value === '1'
23+
24+
l.error(
25+
{
26+
key: 'oauth_recover:auth_js_error',
27+
context: { error_code: errorCode, already_attempted: alreadyAttempted },
28+
},
29+
'Auth.js OAuth flow failed; recovering user'
30+
)
31+
32+
const destination = alreadyAttempted ? '/' : AUTH_URLS.SIGN_IN
33+
const response = NextResponse.redirect(new URL(destination, request.url))
34+
35+
if (alreadyAttempted) {
36+
response.cookies.delete(RECOVERY_COOKIE)
37+
} else {
38+
response.cookies.set(RECOVERY_COOKIE, '1', {
39+
maxAge: RECOVERY_COOKIE_MAX_AGE_SECONDS,
40+
httpOnly: true,
41+
sameSite: 'lax',
42+
path: '/',
43+
secure: process.env.NODE_ENV === 'production',
44+
})
45+
}
46+
47+
return response
48+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { signIn } from '@/auth'
2+
3+
// Server-side entry point for the Ory OAuth2 flow. Pages redirect here
4+
// instead of rendering a client-side form so that Auth.js can set its
5+
// state/PKCE cookies (only allowed in route handlers / server actions
6+
// / middleware) without any client JS in the loop.
7+
//
8+
// `intent=signup` forwards `prompt=registration` to Hydra, which routes
9+
// to its registration UI (`urls.registration`, default `/ui/registration`)
10+
// instead of the login UI.
11+
//
12+
// `intent=reauth` forwards `prompt=login`, forcing Hydra to redo the login
13+
// flow even with an active session so we get a fresh `auth_time`. Used to
14+
// re-authenticate before sensitive account changes (password).
15+
// https://www.ory.com/docs/oauth2-oidc/authorization-code-flow
16+
export async function GET(request: Request) {
17+
const url = new URL(request.url)
18+
const intent = url.searchParams.get('intent')
19+
const returnTo = url.searchParams.get('returnTo')
20+
const redirectTo = returnTo && returnTo.length > 0 ? returnTo : '/dashboard'
21+
22+
const authorizationParams =
23+
intent === 'signup'
24+
? { prompt: 'registration' }
25+
: intent === 'reauth'
26+
? { prompt: 'login' }
27+
: undefined
28+
29+
await signIn('ory', { redirectTo }, authorizationParams)
30+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { handlers } from '@/auth'
2+
3+
export const { GET, POST } = handlers
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'server-only'
2+
3+
import type { NextRequest } from 'next/server'
4+
import { NextResponse } from 'next/server'
5+
import { signOut } from '@/auth'
6+
import {
7+
buildOryLogoutUrl,
8+
ORY_BOOTSTRAP_FAILURE_ID_TOKEN_COOKIE,
9+
ORY_POST_LOGOUT_PATH,
10+
} from '@/core/server/auth/ory/signout'
11+
import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'
12+
13+
export async function GET(request: NextRequest) {
14+
const origin = request.nextUrl.origin
15+
const idToken = request.cookies.get(
16+
ORY_BOOTSTRAP_FAILURE_ID_TOKEN_COOKIE
17+
)?.value
18+
19+
try {
20+
await signOut({ redirect: false })
21+
} catch (error) {
22+
l.warn(
23+
{
24+
key: 'oauth_bootstrap_failed:authjs_sign_out:error',
25+
error: serializeErrorForLog(error),
26+
},
27+
'Auth.js signOut() failed after Ory bootstrap failure'
28+
)
29+
}
30+
31+
const logoutUrl = idToken ? buildOryLogoutUrl({ idToken, origin }) : null
32+
33+
if (!logoutUrl) {
34+
l.error(
35+
{
36+
key: 'oauth_bootstrap_failed:missing_logout_context',
37+
context: {
38+
has_id_token: !!idToken,
39+
has_ory_sdk_url: !!process.env.ORY_SDK_URL,
40+
},
41+
},
42+
'Could not perform Ory logout after bootstrap failure'
43+
)
44+
}
45+
46+
const response = NextResponse.redirect(
47+
logoutUrl ?? new URL(ORY_POST_LOGOUT_PATH, origin)
48+
)
49+
response.cookies.delete(ORY_BOOTSTRAP_FAILURE_ID_TOKEN_COOKIE)
50+
return response
51+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import 'server-only'
2+
3+
import type { NextRequest } from 'next/server'
4+
import { NextResponse } from 'next/server'
5+
import { auth, signOut } from '@/auth'
6+
import { revokeKratosSessionsForIdentity } from '@/core/server/auth/ory/kratos-session'
7+
import {
8+
buildOryLogoutUrl,
9+
ORY_POST_LOGOUT_PATH,
10+
} from '@/core/server/auth/ory/signout'
11+
import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'
12+
13+
export async function GET(request: NextRequest) {
14+
const origin = request.nextUrl.origin
15+
const postLogoutUrl = new URL(ORY_POST_LOGOUT_PATH, origin)
16+
17+
let idToken: string | undefined
18+
let identityId: string | undefined
19+
try {
20+
const session = await auth()
21+
idToken = session?.idToken
22+
// The Kratos identity id resolved at sign-in — NOT the OIDC subject (which
23+
// is the E2B user id) — so we revoke the right identity's Kratos sessions.
24+
identityId = session?.identityId
25+
} catch (error) {
26+
l.warn(
27+
{
28+
key: 'oauth_signout:read_session:error',
29+
error: serializeErrorForLog(error),
30+
},
31+
'failed to read Auth.js session before sign-out'
32+
)
33+
}
34+
35+
try {
36+
await signOut({ redirect: false })
37+
} catch (error) {
38+
l.warn(
39+
{
40+
key: 'oauth_signout:authjs_sign_out:error',
41+
error: serializeErrorForLog(error),
42+
},
43+
'Auth.js signOut() failed'
44+
)
45+
}
46+
47+
if (identityId) {
48+
await revokeKratosSessionsForIdentity(identityId)
49+
}
50+
51+
const logoutUrl = idToken ? buildOryLogoutUrl({ idToken, origin }) : null
52+
if (!logoutUrl) {
53+
return NextResponse.redirect(postLogoutUrl)
54+
}
55+
56+
return NextResponse.redirect(logoutUrl.toString())
57+
}

src/app/dashboard/(resolvers)/inspect/sandbox/[sandboxId]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cookies } from 'next/headers'
22
import { type NextRequest, NextResponse } from 'next/server'
3-
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
3+
import { authHeaders } from '@/configs/api'
44
import { COOKIE_KEYS } from '@/configs/cookies'
55
import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
66
import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server'
@@ -59,7 +59,7 @@ async function hasSandboxInTeam(
5959
},
6060
},
6161
headers: {
62-
...SUPABASE_AUTH_HEADERS(accessToken, teamId),
62+
...authHeaders(accessToken, teamId),
6363
},
6464
cache: 'no-store',
6565
})

0 commit comments

Comments
 (0)