Skip to content

Commit 4d097b8

Browse files
Strehkclaude
andauthored
fix: impersonation fails on self-hosted Logto (JWKS key mismatch) (#417)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 72f9974 commit 4d097b8

File tree

2 files changed

+27
-9
lines changed

2 files changed

+27
-9
lines changed

.env.example

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ NODE_ENV=development
2121
# [OPTIONAL] Server port (default: 3000)
2222
PORT=3000
2323

24+
# [OPTIONAL] Public origin of the application (e.g. https://delegator.munify.cloud)
25+
# SvelteKit uses this for CSRF protection on form submissions and POST requests.
26+
# If not set, SvelteKit infers it from the Host header, which works when your reverse
27+
# proxy forwards headers correctly (e.g. Traefik, Caddy). If you're self-hosting behind
28+
# a proxy that doesn't reliably set the Host header, set this to your public URL to
29+
# prevent CSRF validation failures.
30+
# ORIGIN=https://delegator.munify.cloud
31+
2432
# ───────────────────────────────────────────────────────────────────────────────
2533
# DATABASE
2634
# ───────────────────────────────────────────────────────────────────────────────
@@ -93,14 +101,24 @@ OIDC_SCOPES="openid profile offline_access email phone identity role custom_data
93101
# With the custom JWT claims above, roles are available at the "roles" claim
94102
OIDC_ROLE_CLAIM=roles
95103

104+
# [OPTIONAL] OIDC resource indicator for your application's API
105+
# When set, this is passed as the `resource` parameter during authorization and token requests,
106+
# and used as the expected `audience` when verifying access tokens.
107+
# Required for features that need API access tokens (e.g. reading resources with "resource:read"
108+
# scopes). Without it, Logto issues opaque tokens instead of JWTs, and access token verification
109+
# will be skipped.
110+
# Set this to the API resource indicator you configured in Logto Console → API Resources.
111+
# OIDC_RESOURCE=https://delegator.munify.cloud/api
112+
96113
# [OPTIONAL] Machine-to-machine app credentials for Logto Management API
97114
# Required for user impersonation feature (token exchange via subject tokens)
98115
# Create an M2M app in Logto Console with access to the Management API resource
99116
# OIDC_M2M_CLIENT_ID=your-m2m-app-id
100117
# OIDC_M2M_CLIENT_SECRET=your-m2m-app-secret
101-
# [OPTIONAL] Logto Management API resource indicator
118+
# [OPTIONAL] Logto Management API resource indicator (audience claim in the M2M token)
119+
# This is NOT the API URL — the actual API URL is always derived from PUBLIC_OIDC_AUTHORITY.
102120
# For Logto Cloud: https://<tenant-id>.logto.app/api
103-
# For self-hosted: check your Logto API Resources settings (e.g. https://default.logto.app/api)
121+
# For self-hosted: check your Logto API Resources settings (typically https://default.logto.app/api)
104122
# If not set, derived from PUBLIC_OIDC_AUTHORITY by replacing /oidc with /api
105123
# OIDC_M2M_RESOURCE=https://default.logto.app/api
106124

src/api/services/OIDC.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,9 @@ async function getM2MAccessToken(): Promise<string> {
285285
);
286286
}
287287

288-
// Use configured resource or derive from OIDC authority
289-
// For Logto Cloud: https://<tenant>.logto.app/api
290-
// For self-hosted: must be set explicitly via OIDC_M2M_RESOURCE
288+
// Resource indicator (audience claim) — may differ from the actual API URL.
289+
// For self-hosted Logto the default resource identifier is https://default.logto.app/api
290+
// but the actual API lives at the instance's own origin.
291291
const issuer = config.serverMetadata().issuer;
292292
const managementApiResource =
293293
configPrivate.OIDC_M2M_RESOURCE ?? `${issuer.replace(/\/oidc\/?$/, '')}/api`;
@@ -324,12 +324,12 @@ async function getM2MAccessToken(): Promise<string> {
324324
async function createSubjectToken(subjectUserId: string): Promise<string> {
325325
const m2mToken = await getM2MAccessToken();
326326

327-
// Use configured resource or derive from OIDC authority (mirrors getM2MAccessToken)
327+
// The actual API URL is always derived from the issuer (the real instance origin),
328+
// NOT from OIDC_M2M_RESOURCE which is only a resource indicator / audience claim.
328329
const issuer = config.serverMetadata().issuer;
329-
const managementApiBase =
330-
configPrivate.OIDC_M2M_RESOURCE ?? `${issuer.replace(/\/oidc\/?$/, '')}/api`;
330+
const managementApiUrl = `${issuer.replace(/\/oidc\/?$/, '')}/api`;
331331

332-
const response = await fetch(`${managementApiBase}/subject-tokens`, {
332+
const response = await fetch(`${managementApiUrl}/subject-tokens`, {
333333
method: 'POST',
334334
headers: {
335335
Authorization: `Bearer ${m2mToken}`,

0 commit comments

Comments
 (0)