Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions components/ambient-api-server/pkg/middleware/bearer_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import (
pkgserver "github.com/openshift-online/rh-trex-ai/pkg/server"
)

const ambientAPITokenEnv = "AMBIENT_API_TOKEN"
const (
ambientAPITokenEnv = "AMBIENT_API_TOKEN"
grpcServiceAccountEnv = "GRPC_SERVICE_ACCOUNT"
)

var httpBypassPaths = map[string]bool{
"/healthcheck": true,
Expand All @@ -21,13 +24,19 @@ var httpBypassPaths = map[string]bool{

func init() {
token := os.Getenv(ambientAPITokenEnv)
if token == "" {
glog.Infof("Service token auth disabled: %s not set", ambientAPITokenEnv)
serviceAccount := os.Getenv(grpcServiceAccountEnv)
if token == "" && serviceAccount == "" {
glog.Infof("Service token auth disabled: neither %s nor %s set", ambientAPITokenEnv, grpcServiceAccountEnv)
return
}
glog.Infof("Service token auth enabled via %s (gRPC only)", ambientAPITokenEnv)
pkgserver.RegisterPreAuthGRPCUnaryInterceptor(bearerTokenGRPCUnaryInterceptor(token))
pkgserver.RegisterPreAuthGRPCStreamInterceptor(bearerTokenGRPCStreamInterceptor(token))
if token != "" {
glog.Infof("Service token auth enabled via %s (gRPC only)", ambientAPITokenEnv)
}
if serviceAccount != "" {
glog.Infof("OIDC service account username: %s", serviceAccount)
}
pkgserver.RegisterPreAuthGRPCUnaryInterceptor(bearerTokenGRPCUnaryInterceptor(token, serviceAccount))
pkgserver.RegisterPreAuthGRPCStreamInterceptor(bearerTokenGRPCStreamInterceptor(token, serviceAccount))
}

func extractBearerToken(header string) (string, error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var grpcBypassMethods = map[string]bool{
"/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo": true,
}

func bearerTokenGRPCUnaryInterceptor(expectedToken string) grpc.UnaryServerInterceptor {
func bearerTokenGRPCUnaryInterceptor(expectedToken, serviceAccountUsername string) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if grpcBypassMethods[info.FullMethod] {
return handler(ctx, req)
Expand All @@ -29,6 +29,9 @@ func bearerTokenGRPCUnaryInterceptor(expectedToken string) grpc.UnaryServerInter
return handler(withCallerType(ctx, CallerTypeService), req)
}
if username := usernameFromJWT(token); username != "" {
if serviceAccountUsername != "" && username == serviceAccountUsername {
ctx = withCallerType(ctx, CallerTypeService)
}
Comment on lines +32 to +34
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Where service caller privilege is assigned/checked:"
rg -n -C3 --type=go '\bwithCallerType\s*\(.*CallerTypeService|\bIsServiceCaller\s*\('

echo
echo "2) Which JWT claims are currently inspected for identity/elevation:"
rg -n -C3 --type=go 'preferred_username|username|sub|azp|client_id|clientId|aud|iss|issuer'

Repository: ambient-code/platform

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding usernameFromJWT implementation ==="
rg -n "func usernameFromJWT" --type=go -A 20

echo ""
echo "=== Finding JWT validation/parsing ==="
rg -n "jwt\.|JWT\." --type=go | head -30

echo ""
echo "=== Checking for JWT library usage ==="
grep -r "jwt" --include="*.go" components/ambient-api-server/pkg/middleware/ | head -20

Repository: ambient-code/platform

Length of output: 5098


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full bearer_token_grpc.go context around lines 25-65 ==="
sed -n '20,70p' components/ambient-api-server/pkg/middleware/bearer_token_grpc.go

echo ""
echo "=== Check if expectedToken is required for username path ==="
sed -n '20,70p' components/ambient-api-server/pkg/middleware/bearer_token_grpc.go | grep -A 5 -B 5 "usernameFromJWT\|serviceAccountUsername"

Repository: ambient-code/platform

Length of output: 3764


Remove unverified JWT claim path for service caller elevation; implement strict signed token validation.

Lines 31–34 and 57–61 elevate to CallerTypeService when an unverified JWT username claim matches serviceAccountUsername. This is a critical vulnerability: usernameFromJWT() (line 80) uses ParseUnverified(), allowing any attacker to forge a JWT with a matching username claim and bypass token signing entirely. Service elevation must require signature validation and strong claims (e.g., issuer, audience, client_id) beyond username matching. The constant-time token path (lines 28–29, 54–55) is secure; the unverified path should be removed or replaced with cryptographic verification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ambient-api-server/pkg/middleware/bearer_token_grpc.go` around
lines 32 - 34, The code elevates to CallerTypeService based on an unverified JWT
claim; remove the ParseUnverified-based path and replace it with strict
signature and claims validation: stop using usernameFromJWT()/ParseUnverified(),
instead validate the token signature (e.g., via jwt.ParseWithClaims or
equivalent) and verify required claims (issuer, audience/client_id, expiry)
before matching the validated subject/username to serviceAccountUsername, and
only then call withCallerType(ctx, CallerTypeService); keep the existing
constant-time token comparison path intact and ensure any new validation uses
the configured signing key and fails closed on errors.

return handler(auth.SetUsernameContext(ctx, username), req)
}
}
Expand All @@ -39,7 +42,7 @@ func bearerTokenGRPCUnaryInterceptor(expectedToken string) grpc.UnaryServerInter
}
}

func bearerTokenGRPCStreamInterceptor(expectedToken string) grpc.StreamServerInterceptor {
func bearerTokenGRPCStreamInterceptor(expectedToken, serviceAccountUsername string) grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if grpcBypassMethods[info.FullMethod] {
return handler(srv, ss)
Expand All @@ -53,6 +56,9 @@ func bearerTokenGRPCStreamInterceptor(expectedToken string) grpc.StreamServerInt
}
if username := usernameFromJWT(token); username != "" {
ctx := auth.SetUsernameContext(ss.Context(), username)
if serviceAccountUsername != "" && username == serviceAccountUsername {
ctx = withCallerType(ctx, CallerTypeService)
}
return handler(srv, &serviceCallerStream{ServerStream: ss, ctx: ctx})
}
}
Expand Down
6 changes: 6 additions & 0 deletions components/manifests/base/core/ambient-api-server-service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ spec:
env:
- name: AMBIENT_ENV
value: development
- name: GRPC_SERVICE_ACCOUNT
valueFrom:
secretKeyRef:
name: ambient-api-server
key: clientId
optional: true
ports:
- name: api
containerPort: 8000
Expand Down
Loading