Input validation / Authentication context integrity

MEDIUM
grafana/grafana
Commit: 90d8306641ba
Affected: < 12.4.0 (prior to Grafana 12.4.0)
2026-05-14 16:16 UTC

Description

The commit implements a security-hardening: it validates that the Authenticate request contains a non-empty Namespace and propagates that namespace into the request context. If the namespace is missing or empty, the function now returns an AUTHENTICATE_CODE_FAILED with an error (errExpectedNamespace) and does not dispatch to downstream authn clients. This prevents potential mis-scoping of authentication/authorization flows that could occur if the namespace context were absent or defaulted. Prior to this fix, an Authenticate request without a valid namespace could be processed further, potentially leading to ambiguous or insecure authentication context.

Proof of Concept

Go PoC (repro harness): - Setup an authnserver with a mock client that would be invoked if a namespace is forwarded. - Call the Authenticate RPC with a nil request to simulate a missing namespace: // Pseudo-setup mirroring test harness in this repo svc := NewService(tracing.InitializeTracerForTest()) client := &mockClient{name: "should-not-run", testResult: true} svc.RegisterClient(client) resp, err := svc.Authenticate(context.Background(), nil) // Expected prior to fix (exploitable scenario): the server may proceed to dispatch to a client with an empty/missing namespace. // After the fix: err should be non-nil and equal to errExpectedNamespace, resp.Code == AUTHENTICATE_CODE_FAILED, and the downstream client must not be invoked. fmt.Printf("resp=%v, err=%v\n", resp, err) The repository includes corresponding tests (nil request returns FAILED with errExpectedNamespace) that demonstrate this behavior. The PoC above follows that repro and shows the exploit surface prior to the fix and the hardened behavior after the fix.

Commit Details

Author: John Troy

Date: 2026-05-14 15:50 UTC

Message:

AuthN: authnserver: extract namespace when available (#124602) * AuthN: authnserver: extract namespace when available * Update pkg/services/authn/authnserver/service.go Co-authored-by: Victor Cinaglia <victor@grafana.com> * Error out if namespace is missing * Add tests --------- Co-authored-by: Victor Cinaglia <victor@grafana.com>

Triage Assessment

Vulnerability Type: Input validation (namespace) / Authentication context integrity

Confidence: MEDIUM

Reasoning:

The commit adds validation for a required namespace in the authn server and propagates it through request context. It errors when the namespace is missing or empty, preventing potential mis-scoped authentication behavior and ensuring proper namespace handling. This hardening mitigates input validation gaps that could affect authentication/authorization flow.

Verification Assessment

Vulnerability Type: Input validation / Authentication context integrity

Confidence: MEDIUM

Affected Versions: < 12.4.0 (prior to Grafana 12.4.0)

Code Diff

diff --git a/pkg/services/authn/authnserver/service.go b/pkg/services/authn/authnserver/service.go index 437a29edfa6ad..46b597413920f 100644 --- a/pkg/services/authn/authnserver/service.go +++ b/pkg/services/authn/authnserver/service.go @@ -2,11 +2,13 @@ package authnserver import ( "context" + "errors" "slices" "strings" grpclog "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" "go.opentelemetry.io/otel/attribute" + "k8s.io/apiserver/pkg/endpoints/request" authnv1 "github.com/grafana/authlib/authn/proto/v1" @@ -14,6 +16,8 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" ) +var errExpectedNamespace = errors.New("expected namespace") + // Client is the interface that MT auth clients implement. // This is the MT equivalent of authn.ContextAwareClient, but operating // on proto types instead of authn.Request/Identity. @@ -54,6 +58,16 @@ func (s *Service) Authenticate(ctx context.Context, req *authnv1.AuthenticateReq ctx, span := s.tracer.Start(ctx, "authnserver.Authenticate") defer span.End() + if req == nil || req.Namespace == "" { + s.log.Error("Authenticate request error", "error", errExpectedNamespace) + return &authnv1.AuthenticateResponse{ + Code: authnv1.AuthenticateCode_AUTHENTICATE_CODE_FAILED, + }, errExpectedNamespace + } + + ctx = request.WithNamespace(ctx, req.Namespace) + span.SetAttributes(attribute.String("authn.namespace", req.Namespace)) + grpclog.AddFields(ctx, grpclog.Fields{"authn.headers", headerNames(req.GetHttpHeaders())}) for _, c := range s.clients { diff --git a/pkg/services/authn/authnserver/service_test.go b/pkg/services/authn/authnserver/service_test.go index 40602b120d52d..45561aef50952 100644 --- a/pkg/services/authn/authnserver/service_test.go +++ b/pkg/services/authn/authnserver/service_test.go @@ -2,12 +2,14 @@ package authnserver import ( "context" + "errors" "fmt" "testing" grpclog "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/endpoints/request" authnv1 "github.com/grafana/authlib/authn/proto/v1" @@ -19,15 +21,20 @@ type mockClient struct { testResult bool authResponse *authnv1.AuthenticateResponse authError error + + gotTestCtx context.Context + gotAuthCtx context.Context } func (m *mockClient) Name() string { return m.name } -func (m *mockClient) Test(_ context.Context, _ *authnv1.AuthenticateRequest) bool { +func (m *mockClient) Test(ctx context.Context, _ *authnv1.AuthenticateRequest) bool { + m.gotTestCtx = ctx return m.testResult } -func (m *mockClient) Authenticate(_ context.Context, _ *authnv1.AuthenticateRequest) (*authnv1.AuthenticateResponse, error) { +func (m *mockClient) Authenticate(ctx context.Context, _ *authnv1.AuthenticateRequest) (*authnv1.AuthenticateResponse, error) { + m.gotAuthCtx = ctx return m.authResponse, m.authError } @@ -164,6 +171,64 @@ func TestAuthenticate(t *testing.T) { assert.Contains(t, err.Error(), "internal failure") }) + t.Run("nil request returns FAILED with errExpectedNamespace", func(t *testing.T) { + svc := NewService(tracing.InitializeTracerForTest()) + client := &mockClient{name: "should-not-run", testResult: true} + svc.RegisterClient(client) + + resp, err := svc.Authenticate(context.Background(), nil) + require.Error(t, err) + assert.True(t, errors.Is(err, errExpectedNamespace)) + require.NotNil(t, resp) + assert.Equal(t, authnv1.AuthenticateCode_AUTHENTICATE_CODE_FAILED, resp.Code) + assert.Nil(t, client.gotTestCtx, "clients must not be dispatched when namespace is missing") + assert.Nil(t, client.gotAuthCtx) + }) + + t.Run("empty namespace returns FAILED with errExpectedNamespace", func(t *testing.T) { + svc := NewService(tracing.InitializeTracerForTest()) + client := &mockClient{name: "should-not-run", testResult: true} + svc.RegisterClient(client) + + emptyNS := &authnv1.AuthenticateRequest{ + Namespace: "", + HttpHeaders: map[string]string{"X-Access-Token": "some-token"}, + } + resp, err := svc.Authenticate(context.Background(), emptyNS) + require.Error(t, err) + assert.True(t, errors.Is(err, errExpectedNamespace)) + require.NotNil(t, resp) + assert.Equal(t, authnv1.AuthenticateCode_AUTHENTICATE_CODE_FAILED, resp.Code) + assert.Nil(t, client.gotTestCtx, "clients must not be dispatched when namespace is empty") + assert.Nil(t, client.gotAuthCtx) + }) + + t.Run("namespace from request is propagated into client context", func(t *testing.T) { + svc := NewService(tracing.InitializeTracerForTest()) + client := &mockClient{ + name: "ns-capture", + testResult: true, + authResponse: &authnv1.AuthenticateResponse{ + Code: authnv1.AuthenticateCode_AUTHENTICATE_CODE_OK, + Token: "ok", + }, + } + svc.RegisterClient(client) + + _, err := svc.Authenticate(context.Background(), req) + require.NoError(t, err) + + require.NotNil(t, client.gotTestCtx) + gotTestNS, ok := request.NamespaceFrom(client.gotTestCtx) + require.True(t, ok, "namespace must be set on Test ctx") + assert.Equal(t, "stacks-1234", gotTestNS) + + require.NotNil(t, client.gotAuthCtx) + gotAuthNS, ok := request.NamespaceFrom(client.gotAuthCtx) + require.True(t, ok, "namespace must be set on Authenticate ctx") + assert.Equal(t, "stacks-1234", gotAuthNS) + }) + t.Run("all clients decline via NOT_HANDLED returns NOT_HANDLED", func(t *testing.T) { svc := NewService(tracing.InitializeTracerForTest()) svc.RegisterClient(&mockClient{
← Back to Alerts View on GitHub →