Skip to content

Commit 6af0e2a

Browse files
authored
feat: support self-hosted ory envs (#368)
Today the dashboard's `AUTH_PROVIDER=ory` path assumes Ory Network: it requires `ORY_PROJECT_API_TOKEN` and points every admin call at `ORY_SDK_URL` (the unified SDK host). That makes local devenv and self-hosted deployments — where Kratos and Hydra are separate services on separate admin ports — impossible to configure. This PR adds first-class support for self-hosted Ory by letting the IdentityApi and OAuth2Api clients each target an explicit admin base path, with the env check enforcing that one valid admin surface is configured per service. ### Supported configurations | Deployment | Required env | |---|---| | Ory Network | `ORY_PROJECT_API_TOKEN` (covers both Kratos and Hydra admin via the unified SDK host) | | Self-hosted | `ORY_KRATOS_ADMIN_URL` **and** `ORY_HYDRA_ADMIN_URL` (each gated by network reachability) |
1 parent 4af7723 commit 6af0e2a

4 files changed

Lines changed: 62 additions & 12 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
3434
# ORY_OAUTH2_AUDIENCE=https://api.e2b.dev
3535
### Ory project admin API token used by oryAuthAdmin (IdentityApi lookups)
3636
# ORY_PROJECT_API_TOKEN=
37+
### Self-hosted Ory admin endpoints (alternative to ORY_PROJECT_API_TOKEN).
38+
### Set both when running self-hosted; leave unset to use Ory Network with the PAT above.
39+
# ORY_KRATOS_ADMIN_URL=http://localhost:4434
40+
# ORY_HYDRA_ADMIN_URL=http://localhost:4445
3741
### Dashboard API admin token used to bootstrap newly signed-in Ory users
3842
# DASHBOARD_API_ADMIN_TOKEN=
3943

scripts/check-app-env.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,23 @@ import { clientSchema, serverSchema, validateEnv } from '../src/lib/env'
55
const projectDir = process.cwd()
66
loadEnvConfig(projectDir)
77

8+
// Always required when AUTH_PROVIDER=ory, regardless of deploy target.
89
const oryRequiredEnvVars = [
910
'AUTH_SECRET',
1011
'ORY_SDK_URL',
1112
'ORY_OAUTH2_CLIENT_ID',
1213
'ORY_OAUTH2_CLIENT_SECRET',
1314
'ORY_OAUTH2_AUDIENCE',
14-
'ORY_PROJECT_API_TOKEN',
1515
'DASHBOARD_API_ADMIN_TOKEN',
1616
] as const
1717

18+
// Admin surface resolution must be mode-coherent:
19+
// - Ory Network: ORY_PROJECT_API_TOKEN (bearer for the unified SDK host
20+
// covers both Kratos and Hydra admin).
21+
// - Self-hosted: BOTH ORY_KRATOS_ADMIN_URL and ORY_HYDRA_ADMIN_URL
22+
// (each admin surface lives on its own port; either alone
23+
// leaks the other call back to the public ORY_SDK_URL).
24+
1825
const schema = serverSchema
1926
.merge(clientSchema)
2027
.refine(
@@ -81,6 +88,32 @@ const schema = serverSchema
8188
path: ['AUTH_PROVIDER'],
8289
})
8390
}
91+
92+
const hasKratosAdmin = !!data.ORY_KRATOS_ADMIN_URL
93+
const hasHydraAdmin = !!data.ORY_HYDRA_ADMIN_URL
94+
const isSelfHosted = hasKratosAdmin || hasHydraAdmin
95+
const hasProjectToken = !!data.ORY_PROJECT_API_TOKEN
96+
97+
if (isSelfHosted) {
98+
const missingSelfHostedVars: string[] = []
99+
if (!hasKratosAdmin) missingSelfHostedVars.push('ORY_KRATOS_ADMIN_URL')
100+
if (!hasHydraAdmin) missingSelfHostedVars.push('ORY_HYDRA_ADMIN_URL')
101+
102+
if (missingSelfHostedVars.length > 0) {
103+
ctx.addIssue({
104+
code: z.ZodIssueCode.custom,
105+
message: `Self-hosted Ory is missing ${missingSelfHostedVars.join(', ')}`,
106+
path: ['AUTH_PROVIDER'],
107+
})
108+
}
109+
} else if (!hasProjectToken) {
110+
ctx.addIssue({
111+
code: z.ZodIssueCode.custom,
112+
message:
113+
'AUTH_PROVIDER=ory requires ORY_PROJECT_API_TOKEN (Ory Network) or both ORY_KRATOS_ADMIN_URL and ORY_HYDRA_ADMIN_URL (self-hosted)',
114+
path: ['AUTH_PROVIDER'],
115+
})
116+
}
84117
})
85118

86119
validateEnv(schema)

src/core/server/auth/ory/client.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,46 @@ import { Configuration, IdentityApi, OAuth2Api } from '@ory/client-fetch'
55
let cachedIdentityApi: IdentityApi | null = null
66
let cachedOAuth2Api: OAuth2Api | null = null
77

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

13-
cachedIdentityApi = new IdentityApi(getOryConfiguration())
21+
cachedIdentityApi = new IdentityApi(
22+
getOryConfiguration(process.env.ORY_KRATOS_ADMIN_URL)
23+
)
24+
1425
return cachedIdentityApi
1526
}
1627

1728
export function getOryOAuth2Api(): OAuth2Api {
1829
if (cachedOAuth2Api) return cachedOAuth2Api
1930

20-
cachedOAuth2Api = new OAuth2Api(getOryConfiguration())
31+
cachedOAuth2Api = new OAuth2Api(
32+
getOryConfiguration(process.env.ORY_HYDRA_ADMIN_URL)
33+
)
2134
return cachedOAuth2Api
2235
}
2336

24-
function getOryConfiguration(): Configuration {
25-
const basePath = process.env.ORY_SDK_URL
26-
const accessToken = process.env.ORY_PROJECT_API_TOKEN
37+
function getOryConfiguration(basePathOverride?: string): Configuration {
38+
const basePath = basePathOverride ?? process.env.ORY_SDK_URL
2739

2840
if (!basePath) {
2941
throw new Error('ORY_SDK_URL is not configured')
3042
}
31-
if (!accessToken) {
32-
throw new Error('ORY_PROJECT_API_TOKEN is not configured')
33-
}
43+
44+
const accessToken = process.env.ORY_PROJECT_API_TOKEN
3445

3546
return new Configuration({
3647
basePath: basePath.replace(/\/$/, ''),
37-
accessToken,
48+
...(accessToken ? { accessToken } : {}),
3849
})
3950
}

src/lib/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export const serverSchema = z.object({
2222
ORY_OAUTH2_CLIENT_SECRET: z.string().min(1).optional(),
2323
ORY_OAUTH2_AUDIENCE: z.string().min(1).optional(),
2424
ORY_PROJECT_API_TOKEN: z.string().min(1).optional(),
25+
ORY_KRATOS_ADMIN_URL: z.url().optional(),
26+
ORY_HYDRA_ADMIN_URL: z.url().optional(),
2527

2628
OTEL_SERVICE_NAME: z.string().optional(),
2729
OTEL_EXPORTER_OTLP_ENDPOINT: z.url().optional(),

0 commit comments

Comments
 (0)