From 67b35a25d6e7a4adb4e6e5a7b9620e9149a2e94b Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:17:04 +0200 Subject: [PATCH] feat: configure Logto Custom JWT and update server OIDC rolesClaim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change rolesClaim from "scope" to "roles" to match the custom claim injected by the Logto Custom JWT script - Add Phase 7b: configure Logto Custom JWT for access tokens that maps org roles (admin→server:admin, member→server:viewer) and global roles (platform-admin→server:admin) into a standard "roles" claim - Add additionalScopes field to OIDC config Co-Authored-By: Claude Opus 4.6 (1M context) --- docker/logto-bootstrap.sh | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/docker/logto-bootstrap.sh b/docker/logto-bootstrap.sh index 2cce4fa..615bc1b 100644 --- a/docker/logto-bootstrap.sh +++ b/docker/logto-bootstrap.sh @@ -554,8 +554,9 @@ if [ "$SERVER_HEALTHY" = "yes" ] && [ -n "$TRAD_SECRET" ]; then \"autoSignup\": true, \"defaultRoles\": [\"VIEWER\"], \"displayNameClaim\": \"name\", - \"rolesClaim\": \"scope\", - \"audience\": \"$API_RESOURCE_INDICATOR\" + \"rolesClaim\": \"roles\", + \"audience\": \"$API_RESOURCE_INDICATOR\", + \"additionalScopes\": [] }") log "OIDC config response: $(echo "$OIDC_RESPONSE" | head -c 200)" log "cameleer3-server OIDC configured." @@ -566,6 +567,39 @@ else log "WARNING: cameleer3-server not available or no Traditional app secret — skipping OIDC config" fi +# ============================================================ +# PHASE 7b: Configure Logto Custom JWT for access tokens +# ============================================================ +# Adds a 'roles' claim to access tokens based on user's org roles and global roles. +# This allows the server to extract roles from the access token using rolesClaim: "roles". + +log "Configuring Logto Custom JWT for access tokens..." +CUSTOM_JWT_SCRIPT='const getCustomJwtClaims = async ({ token, context, environmentVariables }) => { + const roleMap = { admin: "server:admin", member: "server:viewer" }; + const roles = new Set(); + if (context?.user?.organizationRoles) { + for (const orgRole of context.user.organizationRoles) { + const mapped = roleMap[orgRole.roleName]; + if (mapped) roles.add(mapped); + } + } + if (context?.user?.roles) { + for (const role of context.user.roles) { + if (role.name === "platform-admin") roles.add("server:admin"); + } + } + return roles.size > 0 ? { roles: [...roles] } : {}; +};' + +CUSTOM_JWT_PAYLOAD=$(jq -n --arg script "$CUSTOM_JWT_SCRIPT" '{ script: $script }') +CUSTOM_JWT_RESPONSE=$(api_put "/api/configs/jwt-customizer/access-token" "$CUSTOM_JWT_PAYLOAD" 2>&1) +if echo "$CUSTOM_JWT_RESPONSE" | jq -e '.script' >/dev/null 2>&1; then + log "Custom JWT configured for access tokens." +else + log "WARNING: Custom JWT configuration failed — server OIDC login may fall back to local roles" + log "Response: $(echo "$CUSTOM_JWT_RESPONSE" | head -c 200)" +fi + # ============================================================ # PHASE 8: Configure sign-in branding # ============================================================