From 70440c4dfe73a0568de6c71572e26571aa8428fd Mon Sep 17 00:00:00 2001 From: user Date: Fri, 24 Apr 2026 19:27:47 -0400 Subject: [PATCH] fix(api-server): handle Keycloak service-account- prefix in OIDC username matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keycloak client credentials tokens have preferred_username set to "service-account-" rather than the raw client ID. The GRPC_SERVICE_ACCOUNT env var is populated from the clientId secret key (e.g. "ocm-ams-service"), but the JWT username is "service-account-ocm-ams-service". The direct comparison always failed, so CallerTypeService was never set and WatchSessionMessages returned PERMISSION_DENIED. Add isServiceAccount() helper that matches both the raw client ID and the Keycloak-prefixed form. Evidence: decoded the OIDC token from Red Hat SSO client credentials grant — preferred_username="service-account-ocm-ams-service". 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../pkg/middleware/bearer_token_grpc.go | 14 +++++++++-- .../pkg/middleware/bearer_token_test.go | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/components/ambient-api-server/pkg/middleware/bearer_token_grpc.go b/components/ambient-api-server/pkg/middleware/bearer_token_grpc.go index 7902784ca..fd139d573 100644 --- a/components/ambient-api-server/pkg/middleware/bearer_token_grpc.go +++ b/components/ambient-api-server/pkg/middleware/bearer_token_grpc.go @@ -29,7 +29,7 @@ func bearerTokenGRPCUnaryInterceptor(expectedToken, serviceAccountUsername strin return handler(withCallerType(ctx, CallerTypeService), req) } if username := usernameFromJWT(token); username != "" { - if serviceAccountUsername != "" && username == serviceAccountUsername { + if isServiceAccount(username, serviceAccountUsername) { ctx = withCallerType(ctx, CallerTypeService) } return handler(auth.SetUsernameContext(ctx, username), req) @@ -56,7 +56,7 @@ func bearerTokenGRPCStreamInterceptor(expectedToken, serviceAccountUsername stri } if username := usernameFromJWT(token); username != "" { ctx := auth.SetUsernameContext(ss.Context(), username) - if serviceAccountUsername != "" && username == serviceAccountUsername { + if isServiceAccount(username, serviceAccountUsername) { ctx = withCallerType(ctx, CallerTypeService) } return handler(srv, &serviceCallerStream{ServerStream: ss, ctx: ctx}) @@ -93,6 +93,16 @@ func usernameFromJWT(tokenString string) string { return "" } +const keycloakServiceAccountPrefix = "service-account-" + +func isServiceAccount(jwtUsername, configuredAccount string) bool { + if configuredAccount == "" { + return false + } + return jwtUsername == configuredAccount || + jwtUsername == keycloakServiceAccountPrefix+configuredAccount +} + type serviceCallerStream struct { grpc.ServerStream ctx context.Context diff --git a/components/ambient-api-server/pkg/middleware/bearer_token_test.go b/components/ambient-api-server/pkg/middleware/bearer_token_test.go index d2c7b2ed5..1d09087dc 100644 --- a/components/ambient-api-server/pkg/middleware/bearer_token_test.go +++ b/components/ambient-api-server/pkg/middleware/bearer_token_test.go @@ -38,6 +38,29 @@ func TestExtractBearerToken(t *testing.T) { } } +func TestIsServiceAccount(t *testing.T) { + tests := []struct { + name string + jwtUsername string + configuredAccount string + want bool + }{ + {"exact match", "ocm-ams-service", "ocm-ams-service", true}, + {"keycloak prefixed match", "service-account-ocm-ams-service", "ocm-ams-service", true}, + {"no match", "other-user", "ocm-ams-service", false}, + {"empty configured", "service-account-ocm-ams-service", "", false}, + {"empty jwt username", "", "ocm-ams-service", false}, + {"partial prefix no match", "service-account-other", "ocm-ams-service", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isServiceAccount(tt.jwtUsername, tt.configuredAccount); got != tt.want { + t.Errorf("isServiceAccount(%q, %q) = %v, want %v", tt.jwtUsername, tt.configuredAccount, got, tt.want) + } + }) + } +} + func TestBearerTokenAuth(t *testing.T) { const validToken = "test-secret-token" handler := BearerTokenAuth(validToken)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {