Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
# ORY_OAUTH2_AUDIENCE=https://api.e2b.dev
### Ory project admin API token used by oryAuthAdmin (IdentityApi lookups)
# ORY_PROJECT_API_TOKEN=
### Self-hosted Ory admin endpoints (alternative to ORY_PROJECT_API_TOKEN).
### 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
### Dashboard API admin token used to bootstrap newly signed-in Ory users
# DASHBOARD_API_ADMIN_TOKEN=

Expand Down
35 changes: 34 additions & 1 deletion scripts/check-app-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ import { clientSchema, serverSchema, validateEnv } from '../src/lib/env'
const projectDir = process.cwd()
loadEnvConfig(projectDir)

// Always required when AUTH_PROVIDER=ory, regardless of deploy target.
const oryRequiredEnvVars = [
'AUTH_SECRET',
'ORY_SDK_URL',
'ORY_OAUTH2_CLIENT_ID',
'ORY_OAUTH2_CLIENT_SECRET',
'ORY_OAUTH2_AUDIENCE',
'ORY_PROJECT_API_TOKEN',
'DASHBOARD_API_ADMIN_TOKEN',
] as const

// Admin surface resolution must be mode-coherent:
// - Ory Network: ORY_PROJECT_API_TOKEN (bearer for the unified SDK host
// covers both Kratos and Hydra admin).
// - Self-hosted: BOTH ORY_KRATOS_ADMIN_URL and ORY_HYDRA_ADMIN_URL
// (each admin surface lives on its own port; either alone
// leaks the other call back to the public ORY_SDK_URL).

const schema = serverSchema
.merge(clientSchema)
.refine(
Expand Down Expand Up @@ -81,6 +88,32 @@ const schema = serverSchema
path: ['AUTH_PROVIDER'],
})
}

const hasKratosAdmin = !!data.ORY_KRATOS_ADMIN_URL
const hasHydraAdmin = !!data.ORY_HYDRA_ADMIN_URL
const isSelfHosted = hasKratosAdmin || hasHydraAdmin
const hasProjectToken = !!data.ORY_PROJECT_API_TOKEN

if (isSelfHosted) {
const missingSelfHostedVars: string[] = []
if (!hasKratosAdmin) missingSelfHostedVars.push('ORY_KRATOS_ADMIN_URL')
if (!hasHydraAdmin) missingSelfHostedVars.push('ORY_HYDRA_ADMIN_URL')

if (missingSelfHostedVars.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Self-hosted Ory is missing ${missingSelfHostedVars.join(', ')}`,
path: ['AUTH_PROVIDER'],
})
}
} else if (!hasProjectToken) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
'AUTH_PROVIDER=ory requires ORY_PROJECT_API_TOKEN (Ory Network) or both ORY_KRATOS_ADMIN_URL and ORY_HYDRA_ADMIN_URL (self-hosted)',
path: ['AUTH_PROVIDER'],
})
}
})

validateEnv(schema)
33 changes: 22 additions & 11 deletions src/core/server/auth/ory/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,46 @@ import { Configuration, IdentityApi, OAuth2Api } from '@ory/client-fetch'
let cachedIdentityApi: IdentityApi | null = null
let cachedOAuth2Api: OAuth2Api | null = null

// the IdentityApi requires the Ory project admin token (PAT). callers should
// ensure ORY_PROJECT_API_TOKEN is set at deploy time when AUTH_PROVIDER=ory.
// IdentityApi resolution:
// 1. ORY_KRATOS_ADMIN_URL — self-hosted Kratos admin (e.g. local devenv :4434).
// 2. ORY_SDK_URL — Ory Network (identity admin co-located on the SDK host).
//
// OAuth2Api resolution:
// 1. ORY_HYDRA_ADMIN_URL — self-hosted Hydra admin (e.g. local devenv :4445).
// 2. ORY_SDK_URL — Ory Network (OAuth2 admin co-located on the SDK host).
//
// The PAT is attached only when configured: Ory Network gates on it,
// self-hosted admin surfaces are gated by network reachability instead.
export function getOryIdentityApi(): IdentityApi {
if (cachedIdentityApi) return cachedIdentityApi

cachedIdentityApi = new IdentityApi(getOryConfiguration())
cachedIdentityApi = new IdentityApi(
getOryConfiguration(process.env.ORY_KRATOS_ADMIN_URL)
)

return cachedIdentityApi
}

export function getOryOAuth2Api(): OAuth2Api {
if (cachedOAuth2Api) return cachedOAuth2Api

cachedOAuth2Api = new OAuth2Api(getOryConfiguration())
cachedOAuth2Api = new OAuth2Api(
getOryConfiguration(process.env.ORY_HYDRA_ADMIN_URL)
)
return cachedOAuth2Api
}

function getOryConfiguration(): Configuration {
const basePath = process.env.ORY_SDK_URL
const accessToken = process.env.ORY_PROJECT_API_TOKEN
function getOryConfiguration(basePathOverride?: string): Configuration {
const basePath = basePathOverride ?? process.env.ORY_SDK_URL

if (!basePath) {
throw new Error('ORY_SDK_URL is not configured')
}
if (!accessToken) {
throw new Error('ORY_PROJECT_API_TOKEN is not configured')
}

const accessToken = process.env.ORY_PROJECT_API_TOKEN

return new Configuration({
basePath: basePath.replace(/\/$/, ''),
accessToken,
...(accessToken ? { accessToken } : {}),
Comment thread
drankou marked this conversation as resolved.
})
}
2 changes: 2 additions & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const serverSchema = z.object({
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(),
ORY_KRATOS_ADMIN_URL: z.url().optional(),
ORY_HYDRA_ADMIN_URL: z.url().optional(),

OTEL_SERVICE_NAME: z.string().optional(),
OTEL_EXPORTER_OTLP_ENDPOINT: z.url().optional(),
Expand Down
Loading