Skip to content

feat(security): config-driven multi-IdP OAuth2 resource server (v26.06.107)#139

Merged
ancongui merged 2 commits into
mainfrom
feat/oauth2-resource-server-parity
Jun 16, 2026
Merged

feat(security): config-driven multi-IdP OAuth2 resource server (v26.06.107)#139
ancongui merged 2 commits into
mainfrom
feat/oauth2-resource-server-parity

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

Summary

Reworks the OAuth2 resource server to work out of the box with Keycloak, Microsoft Entra ID (v1.0 + v2.0) and AWS Cognito via configuration alone (no subclassing), reaching Spring-Security parity — and fixes 8 findings surfaced by an adversarial audit + a hermetic multi-IdP test harness. Motivated by the cdm-mexico (FAES México) resource-server use case.

Added

  • issuer-uri OIDC discovery — derive jwks-uri + issuer from /.well-known/openid-configuration.
  • Config-driven claim mapping (ResourceServerProperties / ClaimMappings): principal-claim-names, authorities-claim-names, scope-claim-names, attribute-claims, authority-prefix. Claim names accept dotted paths with a * wildcard and are colon-safe → authorities resolve from realm_access.roles, resource_access.*.roles (Keycloak), roles+groups (Entra), cognito:groups (Cognito) with zero code.
  • audiences (list; aud must match any) + validate-audience toggle (Cognito access tokens carry no aud). Configurable algorithms / clock-skew-seconds / jwks-timeout-seconds / jwks-cache-seconds.

Fixed

  • Clock-skew leeway (default 60s) — was 0, causing intermittent 401s on real IdP tokens.
  • Blocking JWKS I/O now offloaded to a worker thread (anyio.to_thread) instead of stalling the event loop.
  • Multi-IdP claim coverageresource_access roles, Entra groups/scp, Cognito cognito:groups, token attributes were silently dropped; now mapped.
  • Case-insensitive Bearer scheme (RFC 7235).
  • Opt-in strict 401 mode (authenticate-error-mode: "401") → WWW-Authenticate: Bearer error="invalid_token" (RFC 6750). Default "anonymous" — no behaviour change.

@conditional_on_missing_bean(JWKSTokenValidator) is subclass-aware, so an app can still register its own validator subclass (the cdm-mexico EntraClaimsValidator pattern) — covered by a wiring test.

Tests

  • Hermetic multi-IdP suite — real localhost JWKS + real RS256 tokens shaped like Keycloak/Entra/Cognito; leeway, audiences, rotation, OIDC discovery, claim mapping, negatives.
  • Filter tests — offload, anonymous vs strict-401, case-insensitive Bearer, exclude-patterns.
  • End-to-end wiring through create_app, incl. subclass backoff.
  • Real Keycloak integration test (testcontainers, tests/integration/): boots Keycloak, provisions realm/client/role/user, mints a real token, validates via OIDC discovery + the filter. Runs in the CI integration lane (dispatched on this branch).

Verification

  • ruff + ruff format + mypy --strict (678 files) clean; 558 security/web tests green; new OAuth2 suite (40 tests) green.
  • Real-Keycloak job dispatched on this branch (results linked in a comment).

Bump v26.06.106 → v26.06.107.

🤖 Generated with Claude Code

Andrés Contreras Guillén added 2 commits June 16, 2026 15:47
…6.107)

Reworks the OAuth2 resource server to work out of the box with Keycloak,
Microsoft Entra ID (v1.0 + v2.0) and AWS Cognito via configuration alone (no
subclassing), reaching Spring-Security parity, and fixes 8 findings surfaced by
an adversarial audit + a hermetic multi-IdP test harness.

Added
- issuer-uri OIDC discovery (derive jwks-uri + issuer from
  /.well-known/openid-configuration).
- Config-driven claim mapping (ResourceServerProperties / ClaimMappings):
  principal-claim-names, authorities-claim-names, scope-claim-names,
  attribute-claims, authority-prefix. Claim names accept dotted paths with a '*'
  wildcard and are colon-safe, so authorities resolve from realm_access.roles,
  resource_access.*.roles (Keycloak), roles+groups (Entra), cognito:groups
  (Cognito) with zero code.
- audiences (list; aud must match any) + validate-audience toggle (Cognito
  access tokens carry no aud). Configurable algorithms / clock-skew /
  jwks-timeout / jwks-cache.

Fixed
- Clock-skew leeway (default 60s) — was 0, causing intermittent 401s on real
  IdP tokens whose iat/nbf were slightly ahead of the server clock.
- Blocking JWKS I/O now offloaded to a worker thread (anyio.to_thread) instead
  of stalling the event loop on a cache miss.
- Multi-IdP claim coverage: resource_access roles, Entra groups/scp, Cognito
  groups and token attributes were silently dropped; now mapped.
- Case-insensitive Bearer scheme (RFC 7235).
- Opt-in authenticate-error-mode "401" rejects a present-but-invalid token at
  the filter with WWW-Authenticate: Bearer error="invalid_token" (RFC 6750);
  default "anonymous" (no behaviour change).

conditional_on_missing_bean(JWKSTokenValidator) backoff is subclass-aware, so an
app can still register its own validator subclass (cdm-mexico EntraClaimsValidator
pattern) — covered by a wiring test.

Tests
- Hermetic multi-IdP suite: real localhost JWKS + real RS256 tokens shaped like
  Keycloak/Entra/Cognito; leeway, audiences, rotation, OIDC discovery, claim
  mapping, negatives.
- Filter tests: offload, anonymous vs strict-401, case-insensitive Bearer,
  exclude-patterns.
- End-to-end wiring through create_app incl. subclass backoff.
- Real Keycloak integration test (testcontainers) under tests/integration:
  boots Keycloak, provisions realm/client/role/user, mints a real token, and
  validates it via OIDC discovery + the filter. Runs in the CI integration lane.

Docs + CHANGELOG updated; bump v26.06.106 -> v26.06.107.
…oak test

Validated the resource server against a REAL Keycloak locally (testcontainers,
4/4: OIDC discovery, JWKS signature verification, iss/aud/exp validation, realm-
role claim mapping, and the full filter chain end-to-end). Per request, the
Docker/testcontainers test is removed — the durable coverage is the hermetic
multi-IdP suite (real localhost JWKS + real RS256 tokens, no Docker), which runs
fast in the default lane and covers Keycloak/Entra/Cognito.

Adds an explicit cdm-mexico (FAES México) parity test: an Entra-shaped token
(CdM.Gn role + group + scp + oid + tid + cdm_entidad_id) maps correctly via pure
config (roles+groups, scp->permissions, oid principal, attributes), the admin
gate's raw-claim has_role("CdM.Gn") exact match works, and a rep principal is
denied the gn gate — covering the use case without requiring the EntraClaimsValidator
subclass (which still works via the conditional_on_missing_bean backoff).
@ancongui ancongui merged commit 52c2af3 into main Jun 16, 2026
6 checks passed
@ancongui ancongui deleted the feat/oauth2-resource-server-parity branch June 16, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant