Skip to content

Commit 1667435

Browse files
committed
Implement API key generation
1 parent 9a2db4a commit 1667435

File tree

7 files changed

+704
-112
lines changed

7 files changed

+704
-112
lines changed

gateway/gateway-controller/api/openapi.yaml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,82 @@ paths:
376376
schema:
377377
$ref: "#/components/schemas/ErrorResponse"
378378

379+
/apis/{name}/{version}/api-key:
380+
post:
381+
summary: Generate API key for an API
382+
description: |
383+
Generate a new API key for the specified API. The generated key can be
384+
used by clients to authenticate requests to the API if API Key validation
385+
policy is applied. The key is a 32-byte random value encoded in hexadecimal
386+
and prefixed with "gw_".
387+
operationId: generateAPIKey
388+
tags:
389+
- API Management
390+
parameters:
391+
- name: name
392+
in: path
393+
required: true
394+
description: Name of the API to generate the API key for
395+
schema:
396+
type: string
397+
example: weather-api
398+
- name: version
399+
in: path
400+
required: true
401+
description: Version of the API
402+
schema:
403+
type: string
404+
example: v1.0
405+
responses:
406+
'201':
407+
description: API key generated successfully
408+
content:
409+
application/json:
410+
schema:
411+
type: object
412+
properties:
413+
status:
414+
type: string
415+
example: success
416+
message:
417+
type: string
418+
example: API key generated successfully
419+
name:
420+
type: string
421+
description: Name of the API
422+
example: weather-api
423+
version:
424+
type: string
425+
description: Version of the API
426+
example: v1.0
427+
api_key:
428+
type: string
429+
description: Generated API key with gw_ prefix
430+
example: "gw_1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
431+
created_at:
432+
type: string
433+
format: date-time
434+
description: Timestamp when the API key was generated
435+
example: "2025-12-08T10:30:00Z"
436+
expires_at:
437+
type: string
438+
format: date-time
439+
nullable: true
440+
description: Expiration timestamp (null if no expiration)
441+
example: null
442+
'404':
443+
description: API configuration not found
444+
content:
445+
application/json:
446+
schema:
447+
$ref: '#/components/schemas/ErrorResponse'
448+
'500':
449+
description: Internal server error
450+
content:
451+
application/json:
452+
schema:
453+
$ref: '#/components/schemas/ErrorResponse'
454+
379455
/certificates:
380456
get:
381457
summary: List all custom certificates

gateway/gateway-controller/pkg/api/generated/generated.go

Lines changed: 152 additions & 110 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gateway/gateway-controller/pkg/api/handlers/handlers.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package handlers
2121
import (
2222
"context"
2323
"encoding/json"
24+
"errors"
2425
"fmt"
2526
"io"
2627
"net/http"
@@ -1437,3 +1438,95 @@ func (s *APIServer) GetConfigDump(c *gin.Context) {
14371438
zap.Int("policies", len(policies)),
14381439
zap.Int("certificates", len(certificates)))
14391440
}
1441+
1442+
// GenerateAPIKey implements ServerInterface.GenerateAPIKey
1443+
// (POST /apis/{name}/{version}/api-key)
1444+
func (s *APIServer) GenerateAPIKey(c *gin.Context, name string, version string) {
1445+
// Get correlation-aware logger from context
1446+
log := middleware.GetLogger(c, s.logger)
1447+
1448+
// Check if API exists
1449+
_, err := s.store.GetByNameVersion(name, version)
1450+
if err != nil {
1451+
log.Warn("API configuration not found for API Key generation",
1452+
zap.String("name", name),
1453+
zap.String("version", version))
1454+
c.JSON(http.StatusNotFound, api.ErrorResponse{
1455+
Status: "error",
1456+
Message: fmt.Sprintf("API configuration with name '%s' and version '%s' not found", name, version),
1457+
})
1458+
return
1459+
}
1460+
1461+
// Generate new API key
1462+
apiKey, err := models.GenerateAPIKey(name, version)
1463+
if err != nil {
1464+
log.Error("Failed to generate API key",
1465+
zap.Error(err),
1466+
zap.String("name", name),
1467+
zap.String("version", version))
1468+
c.JSON(http.StatusInternalServerError, api.ErrorResponse{
1469+
Status: "error",
1470+
Message: "Failed to generate API key",
1471+
})
1472+
return
1473+
}
1474+
1475+
// Save API key to database (only if persistent mode)
1476+
if s.db != nil {
1477+
if err := s.db.SaveAPIKey(apiKey); err != nil {
1478+
if errors.Is(err, storage.ErrConflict) {
1479+
// This should be extremely rare with 32-byte random keys
1480+
log.Warn("API key collision detected, retrying",
1481+
zap.String("name", name),
1482+
zap.String("version", version))
1483+
1484+
// Retry once with a new key
1485+
apiKey, err = models.GenerateAPIKey(name, version)
1486+
if err != nil {
1487+
log.Error("Failed to regenerate API key after collision", zap.Error(err))
1488+
c.JSON(http.StatusInternalServerError, api.ErrorResponse{
1489+
Status: "error",
1490+
Message: "Failed to generate unique API key",
1491+
})
1492+
return
1493+
}
1494+
1495+
if err := s.db.SaveAPIKey(apiKey); err != nil {
1496+
log.Error("Failed to save API key after retry", zap.Error(err))
1497+
c.JSON(http.StatusInternalServerError, api.ErrorResponse{
1498+
Status: "error",
1499+
Message: "Failed to save API key",
1500+
})
1501+
return
1502+
}
1503+
} else {
1504+
log.Error("Failed to save API key to database",
1505+
zap.Error(err),
1506+
zap.String("name", name),
1507+
zap.String("version", version))
1508+
c.JSON(http.StatusInternalServerError, api.ErrorResponse{
1509+
Status: "error",
1510+
Message: "Failed to save API key",
1511+
})
1512+
return
1513+
}
1514+
}
1515+
}
1516+
1517+
log.Info("API key generated successfully",
1518+
zap.String("name", name),
1519+
zap.String("version", version),
1520+
zap.String("key_id", apiKey.ID))
1521+
1522+
// Return success response
1523+
c.JSON(http.StatusCreated, gin.H{
1524+
"status": "success",
1525+
"message": "API key generated successfully",
1526+
"name": apiKey.APIName,
1527+
"version": apiKey.APIVersion,
1528+
"api_key": apiKey.APIKey,
1529+
"created_at": apiKey.CreatedAt.Format(time.RFC3339),
1530+
"expires_at": nil, // No expiration by default
1531+
})
1532+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
package models
20+
21+
import (
22+
"crypto/rand"
23+
"encoding/hex"
24+
"fmt"
25+
"time"
26+
27+
"github.com/google/uuid"
28+
)
29+
30+
// APIKeyStatus represents the status of an API key
31+
type APIKeyStatus string
32+
33+
const (
34+
APIKeyStatusActive APIKeyStatus = "active"
35+
APIKeyStatusRevoked APIKeyStatus = "revoked"
36+
APIKeyStatusExpired APIKeyStatus = "expired"
37+
)
38+
39+
// APIKey represents an API key for an API
40+
type APIKey struct {
41+
ID string `json:"id" db:"id"`
42+
APIKey string `json:"api_key" db:"api_key"`
43+
APIName string `json:"api_name" db:"api_name"`
44+
APIVersion string `json:"api_version" db:"api_version"`
45+
Status APIKeyStatus `json:"status" db:"status"`
46+
CreatedAt time.Time `json:"created_at" db:"created_at"`
47+
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
48+
ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
49+
}
50+
51+
// GenerateAPIKey creates a new API key with a random 32-byte value prefixed with "gw_"
52+
func GenerateAPIKey(apiName, apiVersion string) (*APIKey, error) {
53+
// Generate UUID for the record ID
54+
id := uuid.New().String()
55+
56+
// Generate 32 random bytes
57+
randomBytes := make([]byte, 32)
58+
if _, err := rand.Read(randomBytes); err != nil {
59+
return nil, fmt.Errorf("failed to generate random bytes: %w", err)
60+
}
61+
62+
// Encode to hex and prefix with "gw_"
63+
apiKey := "gw_" + hex.EncodeToString(randomBytes)
64+
65+
now := time.Now()
66+
67+
return &APIKey{
68+
ID: id,
69+
APIKey: apiKey,
70+
APIName: apiName,
71+
APIVersion: apiVersion,
72+
Status: APIKeyStatusActive,
73+
CreatedAt: now,
74+
UpdatedAt: now,
75+
ExpiresAt: nil, // No expiration by default
76+
}, nil
77+
}
78+
79+
// IsValid checks if the API key is valid (active and not expired)
80+
func (ak *APIKey) IsValid() bool {
81+
if ak.Status != APIKeyStatusActive {
82+
return false
83+
}
84+
85+
if ak.ExpiresAt != nil && time.Now().After(*ak.ExpiresAt) {
86+
return false
87+
}
88+
89+
return true
90+
}
91+
92+
// IsExpired checks if the API key has expired
93+
func (ak *APIKey) IsExpired() bool {
94+
return ak.ExpiresAt != nil && time.Now().After(*ak.ExpiresAt)
95+
}

gateway/gateway-controller/pkg/storage/gateway-controller-db.sql

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,35 @@ CREATE TABLE IF NOT EXISTS deployment_configs (
8383
FOREIGN KEY(id) REFERENCES deployments(id) ON DELETE CASCADE
8484
);
8585

86-
-- Set schema version to 3
87-
PRAGMA user_version = 3;
86+
-- Table for API keys
87+
CREATE TABLE IF NOT EXISTS api_keys (
88+
-- Primary identifier (UUID)
89+
id TEXT PRIMARY KEY,
90+
91+
-- The generated API key
92+
api_key TEXT NOT NULL UNIQUE,
93+
94+
-- API reference
95+
api_name TEXT NOT NULL,
96+
api_version TEXT NOT NULL,
97+
98+
-- Key status
99+
status TEXT NOT NULL CHECK(status IN ('active', 'revoked', 'expired')) DEFAULT 'active',
100+
101+
-- Timestamps
102+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
103+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
104+
expires_at TIMESTAMP NULL, -- NULL means no expiration
105+
106+
-- Foreign key relationship to deployments
107+
FOREIGN KEY (api_name, api_version) REFERENCES deployments(name, version) ON DELETE CASCADE
108+
);
109+
110+
-- Indexes for API key lookups
111+
CREATE INDEX IF NOT EXISTS idx_api_key ON api_keys(api_key);
112+
CREATE INDEX IF NOT EXISTS idx_api_key_api ON api_keys(api_name, api_version);
113+
CREATE INDEX IF NOT EXISTS idx_api_key_status ON api_keys(status);
114+
CREATE INDEX IF NOT EXISTS idx_api_key_expiry ON api_keys(expires_at);
115+
116+
-- Set schema version to 4
117+
PRAGMA user_version = 4;

gateway/gateway-controller/pkg/storage/interface.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,35 @@ type Storage interface {
105105
// May be expensive for large datasets; consider pagination in future versions.
106106
GetAllConfigsByKind(kind string) ([]*models.StoredConfig, error)
107107

108+
// SaveAPIKey persists a new API key.
109+
//
110+
// Returns an error if an API key with the same key value already exists.
111+
// Implementations should ensure this operation is atomic (all-or-nothing).
112+
SaveAPIKey(apiKey *models.APIKey) error
113+
114+
// GetAPIKeyByKey retrieves an API key by its key value.
115+
//
116+
// Returns an error if the API key is not found.
117+
// This is used for API key validation during authentication.
118+
GetAPIKeyByKey(key string) (*models.APIKey, error)
119+
120+
// GetAPIKeysByAPI retrieves all API keys for a specific API (name and version).
121+
//
122+
// Returns an empty slice if no API keys exist for the API.
123+
// Used for listing API keys associated with an API.
124+
GetAPIKeysByAPI(apiName, apiVersion string) ([]*models.APIKey, error)
125+
126+
// UpdateAPIKey updates an existing API key (e.g., to revoke or expire it).
127+
//
128+
// Returns an error if the API key does not exist.
129+
// Implementations should ensure this operation is atomic and thread-safe.
130+
UpdateAPIKey(apiKey *models.APIKey) error
131+
132+
// DeleteAPIKey removes an API key by its key value.
133+
//
134+
// Returns an error if the API key does not exist.
135+
DeleteAPIKey(key string) error
136+
108137
// SaveCertificate persists a new certificate.
109138
//
110139
// Returns an error if a certificate with the same name already exists.

0 commit comments

Comments
 (0)