Problem
When the clientregistration controller registers multiple agents concurrently, ensureAudienceMapper() can fail transiently (e.g., Keycloak rate-limiting, scope not yet visible after creation). The error is silently discarded, leaving the audience scope without a protocol mapper.
User tokens then lack the required aud claim for that agent, and AuthBridge returns 401:
JWT validation failed — "aud" not satisfied
Root Cause
In audience.go, both call sites discard the error:
// Line 90 — scope already existed
_ = a.ensureAudienceMapper(ctx, token, realm, scopeID, scopeName, audience)
// Line 108 — scope just created
_ = a.ensureAudienceMapper(ctx, token, realm, scopeID, scopeName, audience)
If the mapper POST fails for any reason other than 409 Conflict (e.g., 500, network timeout, Keycloak not yet consistent), the scope is returned without a mapper and the controller considers registration complete.
Observed Behavior
On a Kind cluster with two agents registered in the same reconcile cycle:
| Scope |
Mapper |
Token aud |
agent-team1-weather-service-2-aud |
oidc-audience-mapper with spiffe://...weather-service-2 |
Correct |
agent-team1-weather-service-aud |
empty — no mapper |
Missing → 401 |
Not Fixed By
PR #331 addressed the case where a mapper exists but has a stale included.custom.audience. It does not cover the case where no mapper was created at all.
Suggested Fix
- Propagate the error from
ensureAudienceMapper so the controller requeues:
if err := a.ensureAudienceMapper(ctx, token, realm, scopeID, scopeName, audience); err != nil {
return "", fmt.Errorf("audience scope %q created but mapper failed: %w", scopeName, err)
}
- Optionally add a reconcile-time check that verifies the mapper exists on the scope, and re-creates it if missing (defense in depth against Keycloak inconsistency).
Related
Assisted-By: Claude Code
Problem
When the
clientregistrationcontroller registers multiple agents concurrently,ensureAudienceMapper()can fail transiently (e.g., Keycloak rate-limiting, scope not yet visible after creation). The error is silently discarded, leaving the audience scope without a protocol mapper.User tokens then lack the required
audclaim for that agent, and AuthBridge returns 401:Root Cause
In
audience.go, both call sites discard the error:If the mapper POST fails for any reason other than 409 Conflict (e.g., 500, network timeout, Keycloak not yet consistent), the scope is returned without a mapper and the controller considers registration complete.
Observed Behavior
On a Kind cluster with two agents registered in the same reconcile cycle:
audagent-team1-weather-service-2-audoidc-audience-mapperwithspiffe://...weather-service-2agent-team1-weather-service-audNot Fixed By
PR #331 addressed the case where a mapper exists but has a stale
included.custom.audience. It does not cover the case where no mapper was created at all.Suggested Fix
ensureAudienceMapperso the controller requeues:Related
Assisted-By: Claude Code