From 986b25eab51e85493846061c699a3423ad93ae16 Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Thu, 12 Feb 2026 12:14:21 +0530 Subject: [PATCH 1/3] Modify AuthContext to the new format --- .../internal/kernel/execution_context.go | 5 +- .../system-policies/analytics/analytics.go | 61 +++++++++---------- sdk/gateway/policy/v1alpha/context.go | 45 +++++++++++++- 3 files changed, 76 insertions(+), 35 deletions(-) diff --git a/gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go b/gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go index dfdd00028..bfad31932 100644 --- a/gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go +++ b/gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go @@ -359,7 +359,10 @@ func (ec *PolicyExecutionContext) buildRequestContext(headers *extprocv3.HttpHea APIContext: routeMetadata.Context, OperationPath: routeMetadata.OperationPath, Metadata: make(map[string]interface{}), - AuthContext: make(map[string]string), + AuthContext: &policy.AuthContext{ + Properties: make(map[string]string), + Scopes: make(map[string]bool), + }, } // Add template handle to metadata for LLM provider/proxy scenarios if routeMetadata.TemplateHandle != "" { diff --git a/gateway/system-policies/analytics/analytics.go b/gateway/system-policies/analytics/analytics.go index 693e09291..3511c0822 100644 --- a/gateway/system-policies/analytics/analytics.go +++ b/gateway/system-policies/analytics/analytics.go @@ -32,7 +32,7 @@ const ( AIProviderDisplayNameMetadataKey = "ai:providerdisplayname" // AuthContext key for user ID (used for analytics) - AuthContextKeyUserID = "x-wso2-user-id" + AuthContextKeyUserID = "x-wso2-user-id" // Lazy resource type for LLM provider templates lazyResourceTypeLLMProviderTemplate = "LlmProviderTemplate" @@ -59,22 +59,22 @@ var ( type AnalyticsPolicy struct{} type McpRequestAnalyticsProperties struct { - JsonRpcMethod string `json:"jsonRpcMethod,omitempty"` - Capability string `json:"capability,omitempty"` - CapabilityName string `json:"capabilityName,omitempty"` - ClientInfo *McpClientInfo `json:"clientInfo,omitempty"` + JsonRpcMethod string `json:"jsonRpcMethod,omitempty"` + Capability string `json:"capability,omitempty"` + CapabilityName string `json:"capabilityName,omitempty"` + ClientInfo *McpClientInfo `json:"clientInfo,omitempty"` ServerInfo *McpServerInfo `json:"serverInfo,omitempty"` } type McpClientInfo struct { - RequestedProtocolVersion string `json:"requestedProtocolVersion"` - Name string `json:"name"` - Version string `json:"version"` + RequestedProtocolVersion string `json:"requestedProtocolVersion"` + Name string `json:"name"` + Version string `json:"version"` } type McpServerInfo struct { - ProtocolVersion string `json:"protocolVersion,omitempty"` - ServerInfo *McpServerInfoDetails `json:"serverInfo,omitempty"` + ProtocolVersion string `json:"protocolVersion,omitempty"` + ServerInfo *McpServerInfoDetails `json:"serverInfo,omitempty"` } type McpServerInfoDetails struct { @@ -83,8 +83,8 @@ type McpServerInfoDetails struct { } type McpResponseAnalyticsProperties struct { - IsError bool `json:"isError,omitempty"` - ErrorCode int `json:"errorCode,omitempty"` + IsError bool `json:"isError,omitempty"` + ErrorCode int `json:"errorCode,omitempty"` } // LLMTokenInfo holds extracted token-related information from LLM provider responses @@ -123,7 +123,7 @@ func (a *AnalyticsPolicy) Mode() policy.ProcessingMode { func (a *AnalyticsPolicy) OnRequest(ctx *policy.RequestContext, params map[string]interface{}) policy.RequestAction { slog.Debug("Analytics system policy: OnRequest called") allowPayloads := getAllowPayloadsFlag(params) - // Store tokenInfo in analytics metadata for publishing + // Store tokenInfo in analytics metadata for publishing analyticsMetadata := make(map[string]any) // When allow_payloads is enabled, capture the raw request body into analytics metadata. @@ -132,7 +132,6 @@ func (a *AnalyticsPolicy) OnRequest(ctx *policy.RequestContext, params map[strin analyticsMetadata["request_payload"] = string(ctx.Body.Content) } - // Extract common analytics data from the request // Based on the API kind, collect the analytics data apiKind := ctx.SharedContext.APIKind @@ -147,7 +146,7 @@ func (a *AnalyticsPolicy) OnRequest(ctx *policy.RequestContext, params map[strin // Currently no data is collected case KindMCP: // Collect analytics data specific for MCP scenario from request - if ctx.Headers != nil && len(ctx.Headers.GetAll()) > 0 { + if ctx.Headers != nil && len(ctx.Headers.GetAll()) > 0 { // Need to get the mcp-session-id from headers sessionIDs := ctx.Headers.Get("mcp-session-id") if len(sessionIDs) > 0 { @@ -184,9 +183,9 @@ func (a *AnalyticsPolicy) OnRequest(ctx *policy.RequestContext, params map[strin // Populate client info clientInfo := McpClientInfo{ - RequestedProtocolVersion: extractStringFromJsonpath(mcpPayload,ProtocolVersionJsonPath), - Name: extractStringFromJsonpath(mcpPayload,ClientNameJsonPath), - Version: extractStringFromJsonpath(mcpPayload,ClientVersionJsonPath), + RequestedProtocolVersion: extractStringFromJsonpath(mcpPayload, ProtocolVersionJsonPath), + Name: extractStringFromJsonpath(mcpPayload, ClientNameJsonPath), + Version: extractStringFromJsonpath(mcpPayload, ClientVersionJsonPath), } // Only set ClientInfo pointer if at least one field is non-empty so that omitempty can exclude it from JSON if clientInfo.RequestedProtocolVersion != "" || clientInfo.Name != "" || clientInfo.Version != "" { @@ -244,13 +243,11 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str analyticsMetadata := make(map[string]any) // Extract user ID from AuthContext if available (set by jwt-auth policy) - if ctx.SharedContext.AuthContext != nil { - if userID, ok := ctx.SharedContext.AuthContext[AuthContextKeyUserID]; ok && userID != "" { - analyticsMetadata[AuthContextKeyUserID] = userID - slog.Debug("Analytics system policy: User ID extracted from AuthContext", - "userID", userID, - ) - } + if ctx.SharedContext.AuthContext != nil && ctx.SharedContext.AuthContext.UserID != "" { + analyticsMetadata[AuthContextKeyUserID] = ctx.SharedContext.AuthContext.UserID + slog.Debug("Analytics system policy: User ID extracted from AuthContext", + "userID", ctx.SharedContext.AuthContext.UserID, + ) } // Based on the API kind, collect the analytics data @@ -315,7 +312,7 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str } case KindMCP: // Collect the analytics data specific for MCP specific scenario - if ctx.ResponseHeaders != nil && len(ctx.ResponseHeaders.GetAll()) > 0 { + if ctx.ResponseHeaders != nil && len(ctx.ResponseHeaders.GetAll()) > 0 { if analyticsMetadata["mcp_session_id"] == nil { sessionIDs := ctx.ResponseHeaders.Get("mcp-session-id") if len(sessionIDs) > 0 { @@ -328,7 +325,7 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str if ctx != nil && ctx.ResponseBody != nil && len(ctx.ResponseBody.Content) > 0 { var mcpResponsePayload map[string]interface{} responseContent := ctx.ResponseBody.Content - + // Check if response is in SSE format by inspecting content-type or content structure isSSE := false if ctx.ResponseHeaders != nil { @@ -337,12 +334,12 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str isSSE = true } } - + // Also check content structure if header check didn't confirm SSE if !isSSE && (strings.HasPrefix(string(responseContent), "event:") || strings.Contains(string(responseContent), "\ndata:")) { isSSE = true } - + // Parse SSE format if detected if isSSE { jsonData, err := parseSSEResponse(responseContent) @@ -352,7 +349,7 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str responseContent = jsonData } } - + // Unmarshal the JSON (either from SSE data field or direct response) if err := json.Unmarshal(responseContent, &mcpResponsePayload); err != nil { slog.Error("Failed to unmarshal MCP response body for server info analytics", "error", err) @@ -362,12 +359,12 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str Name: extractStringFromJsonpath(mcpResponsePayload, ServerInfoNameJsonPath), Version: extractStringFromJsonpath(mcpResponsePayload, ServerInfoVersionJsonPath), } - + // Populate server info serverInfo := McpServerInfo{ ProtocolVersion: extractStringFromJsonpath(mcpResponsePayload, ServerProtocolVersionJsonPath), } - + // Only set ServerInfo pointer if at least one field is non-empty if serverInfoDetails.Name != "" || serverInfoDetails.Version != "" { serverInfo.ServerInfo = &serverInfoDetails diff --git a/sdk/gateway/policy/v1alpha/context.go b/sdk/gateway/policy/v1alpha/context.go index 0f019b58b..0c4083ac2 100644 --- a/sdk/gateway/policy/v1alpha/context.go +++ b/sdk/gateway/policy/v1alpha/context.go @@ -15,6 +15,47 @@ type Body struct { Present bool } +// AuthContext holds authentication data produced by auth policies (jwt, oauth2, apikey, basic-auth) +// and consumed by downstream policies (analytics, rate limiting, etc.). +type AuthContext struct { + // Authenticated indicates whether the request passed authentication. + Authenticated bool + + // AuthType identifies the mechanism that authenticated the request. + // Values: "jwt", "oauth2", "apikey", "basic", or empty if unauthenticated. + AuthType string + + // Subject is the authenticated principal identifier. + // JWT "sub" claim, basic-auth username, API key owner/app ID, etc. + Subject string + + // Issuer is the token issuer (JWT "iss" claim, IdP URL). + // Empty for non-token auth types. + Issuer string + + // Audience is the intended audience (JWT "aud" claim). + // Empty for non-token auth types. + Audience string + + // Scopes contains granted OAuth2/JWT scopes as a set for O(1) lookup. + // Nil for non-token auth types. + Scopes map[string]bool + + // AppID is the application identifier associated with the authenticated request. + // For API key auth, this is the application that owns the key. + // For OAuth2, this may be the client_id. Empty if not applicable. + AppID string + + // UserID is the user identifier associated with the authenticated request. + // By default, this is the same as Subject, but can be set separately by auth policies if needed. + UserID string + + // Properties holds additional auth-related key-value data for + // inter-policy communication that does not fit the typed fields above. + // Examples: email, custom JWT claims, API key tier. + Properties map[string]string +} + // SharedContext contains data shared across request and response phases type SharedContext struct { // ProjectID is the project ID which the API is associated with @@ -55,8 +96,8 @@ type SharedContext struct { OperationPath string // AuthContext stores authentication-related information - // Policies can read/write this map to share auth data (e.g., user ID) - AuthContext map[string]string + // Populated by auth policies, consumed by downstream policies + AuthContext *AuthContext } // RequestContext is mutable context for request phase containing current request state From db2b557c66e9062bb8f530896e54e3ae07a416d9 Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Sun, 15 Feb 2026 15:09:44 +0530 Subject: [PATCH 2/3] Move userID extraction login to kernal analytics --- .../policy-engine/internal/kernel/analytics.go | 5 +++++ gateway/system-policies/analytics/analytics.go | 12 ++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/gateway/gateway-runtime/policy-engine/internal/kernel/analytics.go b/gateway/gateway-runtime/policy-engine/internal/kernel/analytics.go index 9a891e002..30d5ba19b 100644 --- a/gateway/gateway-runtime/policy-engine/internal/kernel/analytics.go +++ b/gateway/gateway-runtime/policy-engine/internal/kernel/analytics.go @@ -36,6 +36,7 @@ const ( OperationPathKey = Wso2MetadataPrefix + "operation-path" APIKindKey = Wso2MetadataPrefix + "api-kind" ProjectIDKey = Wso2MetadataPrefix + "project-id" + UserIDKey = Wso2MetadataPrefix + "user-id" ) // convertToStructValue converts a value to structpb.Value, handling complex types like map[string][]string @@ -96,6 +97,10 @@ func buildAnalyticsStruct(analyticsData map[string]any, execCtx *PolicyExecution if sharedCtx.ProjectID != "" { fields[ProjectIDKey] = structpb.NewStringValue(sharedCtx.ProjectID) } + // Extract UserID from AuthContext if available + if sharedCtx.AuthContext != nil && sharedCtx.AuthContext.UserID != "" { + fields[UserIDKey] = structpb.NewStringValue(sharedCtx.AuthContext.UserID) + } } return &structpb.Struct{Fields: fields}, nil diff --git a/gateway/system-policies/analytics/analytics.go b/gateway/system-policies/analytics/analytics.go index 3511c0822..1ac3c8ec3 100644 --- a/gateway/system-policies/analytics/analytics.go +++ b/gateway/system-policies/analytics/analytics.go @@ -31,9 +31,6 @@ const ( AIProviderNameMetadataKey = "ai:providername" AIProviderDisplayNameMetadataKey = "ai:providerdisplayname" - // AuthContext key for user ID (used for analytics) - AuthContextKeyUserID = "x-wso2-user-id" - // Lazy resource type for LLM provider templates lazyResourceTypeLLMProviderTemplate = "LlmProviderTemplate" // Lazy resource type for provider-to-template mapping @@ -242,13 +239,8 @@ func (p *AnalyticsPolicy) OnResponse(ctx *policy.ResponseContext, params map[str // Store tokenInfo in analytics metadata for publishing analyticsMetadata := make(map[string]any) - // Extract user ID from AuthContext if available (set by jwt-auth policy) - if ctx.SharedContext.AuthContext != nil && ctx.SharedContext.AuthContext.UserID != "" { - analyticsMetadata[AuthContextKeyUserID] = ctx.SharedContext.AuthContext.UserID - slog.Debug("Analytics system policy: User ID extracted from AuthContext", - "userID", ctx.SharedContext.AuthContext.UserID, - ) - } + // Note: UserID from AuthContext is directly extracted by the policy engine's analytics filter + // No need to copy it to analyticsMetadata here // Based on the API kind, collect the analytics data apiKind := ctx.SharedContext.APIKind From a907f4a962b86537cbd4f9e081dd864f12518f1d Mon Sep 17 00:00:00 2001 From: Osura Viduranga Date: Sun, 15 Feb 2026 18:02:40 +0530 Subject: [PATCH 3/3] Changing the authcontext structure --- sdk/gateway/policy/v1alpha/context.go | 63 +++++++++++++++++---------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/sdk/gateway/policy/v1alpha/context.go b/sdk/gateway/policy/v1alpha/context.go index 0c4083ac2..7b92f1182 100644 --- a/sdk/gateway/policy/v1alpha/context.go +++ b/sdk/gateway/policy/v1alpha/context.go @@ -15,6 +15,33 @@ type Body struct { Present bool } +// JWTAuthDetails holds fields specific to JWT/OAuth2 token-based auth. +type JWTAuthDetails struct { + // Subject is the "sub" claim from the token. + Subject string + + // Issuer is the token issuer ("iss" claim, IdP URL). + Issuer string + + // Audience is the intended audience ("aud" claim). Can be multiple values. + Audience []string + + // Claims holds additional token claims that don't fit the typed fields. + Claims map[string]string +} + +// APIKeyAuthDetails holds fields specific to API key authentication. +// Placeholder for future auth-type-specific fields. +type APIKeyAuthDetails struct { + // Todo: API key tier, rate limit info, etc. +} + +// BasicAuthDetails holds fields specific to basic authentication. +// Placeholder for future auth-type-specific fields. +type BasicAuthDetails struct { + // Todo: Basic auth specific metadata +} + // AuthContext holds authentication data produced by auth policies (jwt, oauth2, apikey, basic-auth) // and consumed by downstream policies (analytics, rate limiting, etc.). type AuthContext struct { @@ -25,34 +52,24 @@ type AuthContext struct { // Values: "jwt", "oauth2", "apikey", "basic", or empty if unauthenticated. AuthType string - // Subject is the authenticated principal identifier. - // JWT "sub" claim, basic-auth username, API key owner/app ID, etc. - Subject string - - // Issuer is the token issuer (JWT "iss" claim, IdP URL). - // Empty for non-token auth types. - Issuer string + // UserID is the user identifier extracted from the authentication source. + UserID string - // Audience is the intended audience (JWT "aud" claim). - // Empty for non-token auth types. - Audience string + // AppID is the application identifier associated with the request. + // e.g., client_id for OAuth2, application owning the API key, etc. + AppID string - // Scopes contains granted OAuth2/JWT scopes as a set for O(1) lookup. - // Nil for non-token auth types. + // Scopes contains granted scopes as a set for O(1) lookup. + // Applicable for OAuth2/JWT and potentially API key auth. Scopes map[string]bool - // AppID is the application identifier associated with the authenticated request. - // For API key auth, this is the application that owns the key. - // For OAuth2, this may be the client_id. Empty if not applicable. - AppID string - - // UserID is the user identifier associated with the authenticated request. - // By default, this is the same as Subject, but can be set separately by auth policies if needed. - UserID string + // Auth-type-specific details. Only the relevant one is non-nil. + JWT *JWTAuthDetails + APIKey *APIKeyAuthDetails + Basic *BasicAuthDetails - // Properties holds additional auth-related key-value data for - // inter-policy communication that does not fit the typed fields above. - // Examples: email, custom JWT claims, API key tier. + // Properties holds additional key-value data for inter-policy communication + // that does not fit the typed fields above. Properties map[string]string }