Skip to content
Open
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
12 changes: 12 additions & 0 deletions gateway/configs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,18 @@ gateway_controller:
# Expected issuer value in the JWT `iss` claim
issuer: ""

# API key hashing configuration
# Used to hash API keys before storing/comparing them
# By default, API keys hashing is enabled with SHA-256 algorithm
# Supported algorithms: "sha256", "bcrypt", "argon2id"
# We recommend using a strong hashing algorithm like argon2id for production deployments
# if `api_key_hashing.enabled` is set to false, API keys will be stored and compared in plain text (not recommended for production)
api_key_hashing:
# Enable or disable API key hashing
enabled: true
# Hashing algorithm: "sha256", "bcrypt", "argon2id"
algorithm: sha256

# Logging configuration
logging:
# Log level: "debug", "info", "warn", or "error"
Expand Down
6 changes: 5 additions & 1 deletion gateway/gateway-builder/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ require (
gopkg.in/yaml.v3 v3.0.1
)

require gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
require (
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/sys v0.39.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

// Local module replacements for Docker builds
replace github.com/wso2/api-platform/sdk => ../../sdk
4 changes: 4 additions & 0 deletions gateway/gateway-builder/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
16 changes: 8 additions & 8 deletions gateway/gateway-controller/cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func main() {

// Initialize in-memory API key store for xDS
apiKeyStore := storage.NewAPIKeyStore(log)
apiKeySnapshotManager := apikeyxds.NewAPIKeySnapshotManager(apiKeyStore, log)
apiKeyXDSManager := apikeyxds.NewAPIKeyStateManager(apiKeyStore, apiKeySnapshotManager, log)

// Load configurations from database on startup (if persistent mode)
if cfg.IsPersistentMode() && db != nil {
Expand All @@ -123,7 +125,7 @@ func main() {
if err := storage.LoadAPIKeysFromDatabase(db, configStore, apiKeyStore); err != nil {
log.Fatal("Failed to load API keys from database", zap.Error(err))
}
log.Info("Loaded API keys", zap.Int("count", apiKeyStore.Count()))
log.Info("Loaded API keys", zap.Int("count", apiKeyXDSManager.GetAPIKeyCount()))
}

// Initialize xDS snapshot manager with router config
Expand Down Expand Up @@ -166,12 +168,10 @@ func main() {
}
}()

apiKeySnapshotManager := apikeyxds.NewAPIKeySnapshotManager(apiKeyStore, log)
apiKeyXDSManager := apikeyxds.NewAPIKeyStateManager(apiKeyStore, apiKeySnapshotManager, log)

// Generate initial API key snapshot if API keys were loaded from database
if cfg.IsPersistentMode() && apiKeyStore.Count() > 0 {
log.Info("Generating initial API key snapshot for policy engine", zap.Int("api_key_count", apiKeyStore.Count()))
if cfg.IsPersistentMode() && apiKeyXDSManager.GetAPIKeyCount() > 0 {
log.Info("Generating initial API key snapshot for policy engine",
zap.Int("api_key_count", apiKeyXDSManager.GetAPIKeyCount()))
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
if err := apiKeySnapshotManager.UpdateSnapshot(ctx); err != nil {
log.Warn("Failed to generate initial API key snapshot", zap.Error(err))
Expand Down Expand Up @@ -300,8 +300,8 @@ func main() {
router.Use(gin.Recovery())

// Initialize API server with the configured validator and API key manager
apiServer := handlers.NewAPIServer(configStore, db, snapshotManager, policyManager, log, cpClient,
policyDefinitions, templateDefinitions, validator, &cfg.GatewayController.Router, apiKeyXDSManager)
apiServer := handlers.NewAPIServer(configStore, db, snapshotManager, policyManager, log, cpClient, policyDefinitions,
templateDefinitions, validator, &cfg.GatewayController.Router, apiKeyXDSManager, &cfg.GatewayController.APIKeyHashing)

// Register API routes (includes certificate management endpoints from OpenAPI spec)
api.RegisterHandlers(router, apiServer)
Expand Down
2 changes: 1 addition & 1 deletion gateway/gateway-controller/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/wso2/api-platform/sdk v0.0.0
github.com/xeipuuv/gojsonschema v1.2.0
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.46.0
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -79,7 +80,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions gateway/gateway-controller/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
Expand Down
3 changes: 2 additions & 1 deletion gateway/gateway-controller/pkg/api/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func NewAPIServer(
validator config.Validator,
routerConfig *config.RouterConfig,
apiKeyXDSManager *apikeyxds.APIKeyStateManager,
apiKeyHashingConfig *config.APIKeyHashingConfig,
) *APIServer {
deploymentService := utils.NewAPIDeploymentService(store, db, snapshotManager, validator, routerConfig)
server := &APIServer{
Expand All @@ -97,7 +98,7 @@ func NewAPIServer(
mcpDeploymentService: utils.NewMCPDeploymentService(store, db, snapshotManager),
llmDeploymentService: utils.NewLLMDeploymentService(store, db, snapshotManager, templateDefinitions,
deploymentService, routerConfig),
apiKeyService: utils.NewAPIKeyService(store, db, apiKeyXDSManager),
apiKeyService: utils.NewAPIKeyService(store, db, apiKeyXDSManager, apiKeyHashingConfig),
apiKeyXDSManager: apiKeyXDSManager,
controlPlaneClient: controlPlaneClient,
routerConfig: routerConfig,
Expand Down
25 changes: 3 additions & 22 deletions gateway/gateway-controller/pkg/apikeyxds/apikey_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,23 @@ func (asm *APIKeyStateManager) StoreAPIKey(apiId, apiName, apiVersion string, ap
}

// RevokeAPIKey revokes an API key and updates the policy engine with the complete state
func (asm *APIKeyStateManager) RevokeAPIKey(apiId, apiName, apiVersion, apiKeyValue, correlationID string) error {
func (asm *APIKeyStateManager) RevokeAPIKey(apiId, apiName, apiVersion, apiKeyID, apiKeyValue, correlationID string) error {
asm.logger.Info("Revoking API key with state-of-the-world update",
zap.String("api_id", apiId),
zap.String("api_name", apiName),
zap.String("api_version", apiVersion),
zap.String("correlation_id", correlationID))

// Revoke the API key and update the snapshot
if err := asm.snapshotManager.RevokeAPIKey(apiKeyValue); err != nil {
if err := asm.snapshotManager.RevokeAPIKey(apiId, apiKeyID, apiKeyValue); err != nil {
asm.logger.Error("Failed to revoke API key and update snapshot",
zap.String("api_key_value", asm.MaskAPIKey(apiKeyValue)),
zap.Error(err))
return fmt.Errorf("failed to revoke API key: %w", err)
}

asm.logger.Info("Successfully revoked API key and updated policy engine state",
zap.String("api_id", apiId),
zap.String("api_key_value", asm.MaskAPIKey(apiKeyValue)),
zap.String("correlation_id", correlationID))

Expand Down Expand Up @@ -113,26 +114,6 @@ func (asm *APIKeyStateManager) RemoveAPIKeysByAPI(apiId, apiName, apiVersion, co
return nil
}

// GetAPIKeyByValue retrieves an API key by its value
func (asm *APIKeyStateManager) GetAPIKeyByValue(value string) (*models.APIKey, bool) {
return asm.store.GetByValue(value)
}

// GetAPIKeyByID retrieves an API key by its ID
func (asm *APIKeyStateManager) GetAPIKeyByID(id string) (*models.APIKey, bool) {
return asm.store.GetByID(id)
}

// GetAPIKeysByAPI retrieves all API keys for a specific API
func (asm *APIKeyStateManager) GetAPIKeysByAPI(apiId string) []*models.APIKey {
return asm.store.GetByAPI(apiId)
}

// GetAllAPIKeys retrieves all API keys
func (asm *APIKeyStateManager) GetAllAPIKeys() []*models.APIKey {
return asm.store.GetAll()
}

// GetAPIKeyCount returns the total number of API keys
func (asm *APIKeyStateManager) GetAPIKeyCount() int {
return asm.store.Count()
Expand Down
13 changes: 9 additions & 4 deletions gateway/gateway-controller/pkg/apikeyxds/apikey_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,17 @@ func (sm *APIKeySnapshotManager) StoreAPIKey(apiKey *models.APIKey) error {
}

// RevokeAPIKey revokes an API key and updates the snapshot
func (sm *APIKeySnapshotManager) RevokeAPIKey(apiKeyValue string) error {
sm.logger.Info("Revoking API key", zap.String("api_key_value", MaskAPIKey(apiKeyValue)))
func (sm *APIKeySnapshotManager) RevokeAPIKey(apiId, apiKeyID, apiKeyValue string) error {
sm.logger.Info("Revoking API key",
zap.String("api_id", apiId),
zap.String("api_key_value", MaskAPIKey(apiKeyValue)))

// Revoke in the API key store
if !sm.store.Revoke(apiKeyValue) {
return fmt.Errorf("API key not found: %s", MaskAPIKey(apiKeyValue))
if !sm.store.Revoke(apiId, apiKeyID, apiKeyValue) {
sm.logger.Warn("API key not found for revocation",
zap.String("api_id", apiId),
zap.String("api_key_value", MaskAPIKey(apiKeyValue)))
return nil
}

// Update the snapshot to reflect the new state
Expand Down
109 changes: 109 additions & 0 deletions gateway/gateway-controller/pkg/config/api_key_hashing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package config

import (
"testing"

"github.com/wso2/api-platform/gateway/gateway-controller/pkg/constants"
)

func TestValidateAPIKeyHashingConfig(t *testing.T) {
tests := []struct {
name string
enabled bool
algorithm string
expectError bool
}{
{
name: "hashing disabled",
enabled: false,
algorithm: "",
expectError: false,
},
{
name: "hashing disabled with algorithm set",
enabled: false,
algorithm: constants.HashingAlgorithmSHA256,
expectError: false,
},
{
name: "hashing enabled without algorithm - should default to SHA256",
enabled: true,
algorithm: "",
expectError: false,
},
{
name: "hashing enabled with valid SHA256 algorithm",
enabled: true,
algorithm: constants.HashingAlgorithmSHA256,
expectError: false,
},
{
name: "hashing enabled with valid bcrypt algorithm",
enabled: true,
algorithm: constants.HashingAlgorithmBcrypt,
expectError: false,
},
{
name: "hashing enabled with valid Argon2id algorithm",
enabled: true,
algorithm: constants.HashingAlgorithmArgon2ID,
expectError: false,
},
{
name: "hashing enabled with invalid algorithm",
enabled: true,
algorithm: "invalid-algorithm",
expectError: true,
},
{
name: "hashing enabled with case-insensitive valid algorithm",
enabled: true,
algorithm: "SHA256", // uppercase
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a minimal config with API key hashing settings
config := &Config{
GatewayController: GatewayController{
APIKeyHashing: APIKeyHashingConfig{
Enabled: tt.enabled,
Algorithm: tt.algorithm,
},
},
}

err := config.validateAPIKeyHashingConfig()

if tt.expectError {
if err == nil {
t.Errorf("Expected error but got none")
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
}
})
}
}
Loading
Loading