diff --git a/CLAUDE.md b/CLAUDE.md index a202c5e..04fde2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ Separate Vite+React SPA replacing Logto's default sign-in page. Visually matches - 13 OAuth2 scopes on the Logto API resource (`https://api.cameleer.local`): 10 platform scopes + 3 server scopes (`server:admin`, `server:operator`, `server:viewer`), served to the frontend from `GET /platform/api/config` - Server scopes map to server RBAC roles via JWT `scope` claim (server reads `rolesClaim: "scope"`) - Org role `admin` gets `server:admin`, org role `member` gets `server:viewer` -- Custom `JwtDecoder` in `SecurityConfig.java` — ES384 algorithm, `at+jwt` token type, split issuer-uri (string validation) / jwk-set-uri (Docker-internal fetch) +- Custom `JwtDecoder` in `SecurityConfig.java` — ES384 algorithm, `at+jwt` token type, split issuer-uri (string validation) / jwk-set-uri (Docker-internal fetch), audience validation (`https://api.cameleer.local`) ### Server integration (cameleer3-server env vars) @@ -68,7 +68,7 @@ Separate Vite+React SPA replacing Logto's default sign-in page. Visually matches |---------|-------|---------| | `CAMELEER_OIDC_ISSUER_URI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Token issuer claim validation | | `CAMELEER_OIDC_JWK_SET_URI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK fetch | -| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` | Skip cert verify for OIDC discovery (dev) | +| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` | Skip cert verify for OIDC discovery (dev only — disable in production) | | `CAMELEER_CORS_ALLOWED_ORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik | | `BASE_PATH` (server-ui) | `/server` | React Router basename + `` tag | diff --git a/docker-compose.yml b/docker-compose.yml index 750526b..94995e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,6 @@ services: - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik.yml:/etc/traefik/traefik.yml:ro - ./docker/traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro - - acme:/etc/traefik/acme - certs:/etc/traefik/certs:ro networks: - cameleer @@ -169,7 +168,7 @@ services: CAMELEER_TENANT_ID: ${CAMELEER_TENANT_SLUG:-default} CAMELEER_OIDC_ISSUER_URI: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}/oidc CAMELEER_OIDC_JWK_SET_URI: ${LOGTO_ENDPOINT:-http://logto:3001}/oidc/jwks - CAMELEER_OIDC_TLS_SKIP_VERIFY: "true" + CAMELEER_OIDC_TLS_SKIP_VERIFY: "true" # dev only — disable in production with real certs CAMELEER_OIDC_AUDIENCE: ${CAMELEER_OIDC_AUDIENCE:-https://api.cameleer.local} CAMELEER_CORS_ALLOWED_ORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost} healthcheck: @@ -179,15 +178,7 @@ services: retries: 30 start_period: 15s labels: - - traefik.enable=true - - traefik.http.routers.observe.rule=PathPrefix(`/observe`) - - traefik.http.routers.observe.service=observe - - traefik.http.services.observe.loadbalancer.server.port=8080 - - traefik.http.routers.dashboard.rule=PathPrefix(`/dashboard`) - - traefik.http.routers.dashboard.service=dashboard - - traefik.http.routers.dashboard.middlewares=dashboard-strip - - traefik.http.middlewares.dashboard-strip.stripprefix.prefixes=/dashboard - - traefik.http.services.dashboard.loadbalancer.server.port=8080 + - traefik.enable=false networks: - cameleer @@ -234,7 +225,6 @@ networks: volumes: pgdata: chdata: - acme: certs: jardata: bootstrapdata: diff --git a/docker/logto-bootstrap.sh b/docker/logto-bootstrap.sh index ee94394..e7f0f3d 100644 --- a/docker/logto-bootstrap.sh +++ b/docker/logto-bootstrap.sh @@ -541,9 +541,11 @@ cat > "$BOOTSTRAP_FILE" <( JWSAlgorithm.ES384, jwkSource); @@ -81,9 +87,15 @@ public class SecurityConfig { processor.setJWSTypeVerifier((type, context) -> { /* accept JWT and at+jwt */ }); var decoder = new NimbusJwtDecoder(processor); + var validators = new ArrayList>(); + validators.add(new JwtTimestampValidator()); if (issuerUri != null && !issuerUri.isEmpty()) { - decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri)); + validators.add(new JwtIssuerValidator(issuerUri)); } + if (audience != null && !audience.isEmpty()) { + validators.add(new JwtClaimValidator>("aud", aud -> aud != null && aud.contains(audience))); + } + decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); return decoder; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 417038e..0f9bb78 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,6 +39,7 @@ cameleer: m2m-client-id: ${LOGTO_M2M_CLIENT_ID:} m2m-client-secret: ${LOGTO_M2M_CLIENT_SECRET:} spa-client-id: ${LOGTO_SPA_CLIENT_ID:} + audience: ${CAMELEER_OIDC_AUDIENCE:https://api.cameleer.local} runtime: max-jar-size: 209715200 jar-storage-path: ${CAMELEER_JAR_STORAGE_PATH:/data/jars}