From 97f9fb5fad72620e6c9839841d69416f0bc73a52 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Mon, 9 Feb 2026 17:45:03 +0530 Subject: [PATCH 1/2] Refactor API key validation to return API key details --- common/apikey/api_key_hash_test.go | 16 +++++++++++----- common/apikey/store.go | 23 ++++++++++++----------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/common/apikey/api_key_hash_test.go b/common/apikey/api_key_hash_test.go index 2caa70257..44193396b 100644 --- a/common/apikey/api_key_hash_test.go +++ b/common/apikey/api_key_hash_test.go @@ -54,13 +54,19 @@ func TestAPIKeyHashedValidation(t *testing.T) { } // Test validation with correct plain text key - valid, err := store.ValidateAPIKey("api-123", "/test", "GET", plainAPIKey) + valid, apiKeyDetails, err := store.ValidateAPIKey("api-123", "/test", "GET", plainAPIKey) if err != nil { t.Fatalf("Validation failed with error: %v", err) } if !valid { t.Error("Validation should succeed with correct plain text API key") } + if apiKeyDetails == nil { + t.Error("Expected API key details to be returned") + } + if apiKeyDetails != nil && apiKeyDetails.CreatedBy != "test-user" { + t.Errorf("Expected CreatedBy to be 'test-user', got: %s", apiKeyDetails.CreatedBy) + } } func TestAPIKeyHashedValidationFailures(t *testing.T) { @@ -92,7 +98,7 @@ func TestAPIKeyHashedValidationFailures(t *testing.T) { // Test validation with wrong plain text key wrongKey := "apip_wrong399ef29761f92f4f6d2dbd6dcd78399b3bcb8c53417cb23726e5780ac999" - valid, err := store.ValidateAPIKey("api-456", "/test", "GET", wrongKey) + valid, _, err := store.ValidateAPIKey("api-456", "/test", "GET", wrongKey) if err != nil { if err != ErrNotFound { t.Fatalf("Expected ErrNotFound, got: %v", err) @@ -103,7 +109,7 @@ func TestAPIKeyHashedValidationFailures(t *testing.T) { } // Test validation with non-existent API - valid, err = store.ValidateAPIKey("non-existent-api", "/test", "GET", plainAPIKey) + valid, _, err = store.ValidateAPIKey("non-existent-api", "/test", "GET", plainAPIKey) if err == nil { t.Error("Expected error for non-existent API") } @@ -140,7 +146,7 @@ func TestAPIKeyHashedRevocation(t *testing.T) { } // Verify key works before revocation - valid, err := store.ValidateAPIKey("api-789", "/test", "GET", plainAPIKey) + valid, _, err := store.ValidateAPIKey("api-789", "/test", "GET", plainAPIKey) if err != nil { t.Fatalf("Validation failed before revocation: %v", err) } @@ -155,7 +161,7 @@ func TestAPIKeyHashedRevocation(t *testing.T) { } // Verify key no longer works after revocation - valid, err = store.ValidateAPIKey("api-789", "/test", "GET", plainAPIKey) + valid, _, err = store.ValidateAPIKey("api-789", "/test", "GET", plainAPIKey) if err != nil && err != ErrNotFound { t.Fatalf("Unexpected error during validation after revocation: %v", err) } diff --git a/common/apikey/store.go b/common/apikey/store.go index f2f84f3fa..fa206864d 100644 --- a/common/apikey/store.go +++ b/common/apikey/store.go @@ -167,10 +167,11 @@ func (aks *APIkeyStore) StoreAPIKey(apiId string, apiKey *APIKey) error { } // ValidateAPIKey validates the provided API key against the internal APIkey store -// Supports both local and external keys using unified hash-based lookup -func (aks *APIkeyStore) ValidateAPIKey(apiId, apiOperation, operationMethod, providedAPIKey string) (bool, error) { - aks.mu.RLock() - defer aks.mu.RUnlock() +// Supports both local keys (with format: key_id) and external keys (any format) +// Returns: (isValid bool, apiKey *APIKey, error) +func (aks *APIkeyStore) ValidateAPIKey(apiId, apiOperation, operationMethod, providedAPIKey string) (bool, *APIKey, error) { + aks.mu.Lock() + defer aks.mu.Unlock() // Normalize the provided API key providedAPIKey = strings.TrimSpace(providedAPIKey) @@ -192,18 +193,18 @@ func (aks *APIkeyStore) ValidateAPIKey(apiId, apiOperation, operationMethod, pro // Check if the API key belongs to the specified API if targetAPIKey.APIId != apiId { - return false, nil + return false, nil, nil } // Check if the API key is active if targetAPIKey.Status != Active { - return false, nil + return false, nil, nil } // Check if the API key has expired if targetAPIKey.Status == Expired || (targetAPIKey.ExpiresAt != nil && time.Now().After(*targetAPIKey.ExpiresAt)) { targetAPIKey.Status = Expired - return false, nil + return false, nil, nil } // Check if the API key has access to the requested operation @@ -211,13 +212,13 @@ func (aks *APIkeyStore) ValidateAPIKey(apiId, apiOperation, operationMethod, pro // Example: ["GET /{country_code}/{city}", "POST /data"], ["*"] for allow all operations var operations []string if err := json.Unmarshal([]byte(targetAPIKey.Operations), &operations); err != nil { - return false, fmt.Errorf("invalid operations format: %w", err) + return false, nil, fmt.Errorf("invalid operations format: %w", err) } // Check if wildcard is present for _, op := range operations { if strings.TrimSpace(op) == "*" { - return true, nil + return true, targetAPIKey, nil } } @@ -225,12 +226,12 @@ func (aks *APIkeyStore) ValidateAPIKey(apiId, apiOperation, operationMethod, pro requestedOperation := fmt.Sprintf("%s %s", operationMethod, apiOperation) for _, op := range operations { if strings.TrimSpace(op) == requestedOperation { - return true, nil + return true, targetAPIKey, nil } } // Operation not found in allowed list - return false, nil + return false, nil, nil } // RevokeAPIKey revokes a specific API key by plain text API key value From 50814793e5af996a48f6655d84589e60e9882b69 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Wed, 18 Feb 2026 11:21:01 +0530 Subject: [PATCH 2/2] Refactor AuthContext structure to use a typed representation for authentication --- .../internal/kernel/execution_context.go | 2 +- .../system-policies/analytics/analytics.go | 18 ++++---- sdk/gateway/policy/v1alpha/context.go | 42 +++++++++++++++++-- 3 files changed, 48 insertions(+), 14 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..ede1dd51b 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,7 @@ 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{}, } // 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..2498cf68c 100644 --- a/gateway/system-policies/analytics/analytics.go +++ b/gateway/system-policies/analytics/analytics.go @@ -31,8 +31,8 @@ const ( AIProviderNameMetadataKey = "ai:providername" AIProviderDisplayNameMetadataKey = "ai:providerdisplayname" - // AuthContext key for user ID (used for analytics) - AuthContextKeyUserID = "x-wso2-user-id" + // Analytics metadata key for user ID + AuthContextKeyUserID = "x-wso2-user-id" // Lazy resource type for LLM provider templates lazyResourceTypeLLMProviderTemplate = "LlmProviderTemplate" @@ -243,14 +243,12 @@ 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 { - if userID, ok := ctx.SharedContext.AuthContext[AuthContextKeyUserID]; ok && userID != "" { - analyticsMetadata[AuthContextKeyUserID] = userID - slog.Debug("Analytics system policy: User ID extracted from AuthContext", - "userID", userID, - ) - } + // Extract user ID from AuthContext if available (set by auth policies) + 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.Properties[AuthContextKeyUserID], + ) } // Based on the API kind, collect the analytics data diff --git a/sdk/gateway/policy/v1alpha/context.go b/sdk/gateway/policy/v1alpha/context.go index 0f019b58b..680271f87 100644 --- a/sdk/gateway/policy/v1alpha/context.go +++ b/sdk/gateway/policy/v1alpha/context.go @@ -54,9 +54,45 @@ type SharedContext struct { // with resolved parameters (e.g., "/petstore/v1.0.0/pets/123") 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 + // AuthContext stores typed authentication information set by auth policies + // Policies can read/write this to share auth data with downstream policies + AuthContext *AuthContext +} + +// AuthContext holds typed authentication information for a single auth layer. +// For chained authentication (e.g., API Key + JWT), use the Next field to form a linked list. +type AuthContext struct { + // Authenticated indicates whether authentication succeeded + Authenticated bool + + // AuthType identifies the authentication method (e.g., "jwt", "apikey", "basic") + AuthType string + + // UserID is the authenticated user's identifier + UserID string + + // Issuer is the token issuer (e.g., for JWT) + Issuer string + + // Audience contains the intended recipients of the token + Audience []string + + // Scopes holds granted permission scopes + Scopes map[string]bool + + // CredentialID is the credential-specific identifier: + // - JWT: OAuth consumer key + // - API Key: key name + // - mTLS: certificate thumbprint + // - Basic Auth: username + CredentialID string + + // Properties is an escape hatch for auth-type-specific metadata + // not covered by the typed fields above + Properties map[string]string + + // Next links the next AuthContext in a chain for multi-layer authentication + Next *AuthContext } // RequestContext is mutable context for request phase containing current request state