-
Notifications
You must be signed in to change notification settings - Fork 1
API Key Management
Secure API key creation, encryption, rotation, and management
Enterprise-grade API key system with:
- Fernet encryption for storage
- HMAC-SHA256 hashing for validation
- Scope-based permissions
- Environment tags (live, test, staging, dev)
- Expiration dates
- IP allowlists
- Domain restrictions
- Request limits per key
curl -X POST http://localhost:8000/user/api-keys \
-H "Authorization: Bearer EXISTING_KEY" \
-H "Content-Type: application/json" \
-d '{
"key_name": "Production API",
"environment_tag": "live",
"expiration_days": 365,
"max_requests": 100000
}'Response:
{
"id": 1,
"api_key": "gw_live_abc123...",
"key_name": "Production API",
"environment_tag": "live",
"expires_at": "2025-12-15T10:30:00Z",
"created_at": "2024-12-15T10:30:00Z"
}Save the api_key - it's only shown once!
curl http://localhost:8000/user/api-keys \
-H "Authorization: Bearer YOUR_KEY"curl -X PUT http://localhost:8000/user/api-keys/1 \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"key_name": "Updated Name",
"max_requests": 200000
}'curl -X DELETE http://localhost:8000/user/api-keys/1 \
-H "Authorization: Bearer YOUR_KEY"| Environment | Prefix | Example |
|---|---|---|
| Production | gw_live_ |
gw_live_abc123... |
| Test | gw_test_ |
gw_test_def456... |
| Staging | gw_staging_ |
gw_staging_ghi789... |
| Development | gw_dev_ |
gw_dev_jkl012... |
gw_live_<32_random_chars>
│ │ │
│ │ └─ URL-safe random string
│ └────── Environment tag
└────────── Prefix
POST /user/api-keys
Request:
{
"key_name": "My API Key",
"environment_tag": "live",
"scope_permissions": {
"read": ["*"],
"write": ["chat", "images"]
},
"expiration_days": 365,
"max_requests": 100000,
"ip_allowlist": ["192.168.1.1", "10.0.0.0/24"],
"domain_referrers": ["example.com", "*.example.com"]
}Optional Parameters:
-
scope_permissions: Permission scopes (default: all access) -
expiration_days: Days until expiration (optional) -
max_requests: Request limit per month (optional) -
ip_allowlist: Allowed IP addresses/CIDR (optional) -
domain_referrers: Allowed HTTP referers (optional)
GET /user/api-keys
Returns all keys for authenticated user (encrypted keys hidden).
GET /user/api-keys/{key_id}
PUT /user/api-keys/{key_id}
Updatable Fields:
key_namescope_permissionsmax_requestsip_allowlistdomain_referrersis_active
Cannot update: environment_tag, expiration_date
DELETE /user/api-keys/{key_id}
Soft delete - key becomes inactive.
POST /user/api-keys/{key_id}/rotate
Generates new key, invalidates old one.
Algorithm: Fernet (AES-128)
# Key storage
encrypted_key = fernet.encrypt(api_key.encode())
# Validation
decrypted_key = fernet.decrypt(encrypted_key)Environment Variable: SECRET_KEY
Algorithm: HMAC-SHA256
key_hash = hmac.new(
secret_key.encode(),
api_key.encode(),
hashlib.sha256
).hexdigest()Fast lookup without decryption.
Stored for easy identification:
last4 = api_key[-4:] # Display as "...abc123"{
"read": ["*"],
"write": ["*"],
"admin": ["*"]
}{
"read": ["models", "balance", "usage"],
"write": ["chat", "images"],
"admin": []
}Location: src/security/deps.py
def check_scope(key: APIKey, required_scope: str):
if "*" in key.scope_permissions.get("read", []):
return True
return required_scope in key.scope_permissions.get("read", [])Restrict key usage to specific IPs:
{
"ip_allowlist": [
"192.168.1.100",
"10.0.0.0/24",
"2001:db8::/32"
]
}Supports IPv4, IPv6, and CIDR notation.
Restrict to specific domains:
{
"domain_referrers": [
"app.example.com",
"*.example.com",
"https://secure.example.com"
]
}Checks HTTP Referer header.
Per-key monthly limits:
{
"max_requests": 100000,
"requests_used": 5234,
"requests_remaining": 94766
}Resets monthly based on creation date.
Auto-disable after expiration:
{
"expires_at": "2025-12-15T00:00:00Z",
"is_expired": false,
"days_until_expiry": 365
}CREATE TABLE api_keys_new (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
key_name TEXT NOT NULL,
encrypted_key TEXT NOT NULL, -- Fernet encrypted
key_hash TEXT NOT NULL UNIQUE, -- HMAC-SHA256 hash
last4 TEXT, -- Last 4 chars (display)
environment_tag TEXT DEFAULT 'live',
scope_permissions JSONB,
expiration_date TIMESTAMP,
max_requests INTEGER,
requests_used INTEGER DEFAULT 0,
ip_allowlist TEXT[],
domain_referrers TEXT[],
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
last_used_at TIMESTAMP,
UNIQUE(user_id, key_name)
);
-- Indexes
CREATE INDEX idx_api_keys_user_id ON api_keys_new(user_id);
CREATE INDEX idx_api_keys_hash ON api_keys_new(key_hash);
CREATE INDEX idx_api_keys_active ON api_keys_new(is_active);-
DB Layer:
src/db/api_keys.py -
Routes:
src/routes/api_keys.py -
Security:
src/security/security.py -
Crypto:
src/utils/crypto.py
import secrets
def generate_api_key(environment_tag: str = "live") -> str:
prefix = f"gw_{environment_tag}_"
random_part = secrets.token_urlsafe(32)
return prefix + random_part1. Extract key from Authorization header
2. Compute key_hash = HMAC-SHA256(key)
3. Lookup key by hash in database
4. Check is_active, expiration, IP, domain
5. Verify scope permissions
6. Update last_used_at, requests_used
7. Allow request
- Name keys descriptively: "Production Web App", "Mobile App iOS"
- Use environment tags: Separate live/test/staging
- Set expiration dates: Force rotation annually
- Limit scopes: Grant minimum necessary permissions
- Rotate regularly: Every 90-365 days
- Delete unused keys: Remove old/test keys
- Monitor usage: Check requests_used regularly
- Encrypt at rest: Use strong SECRET_KEY
- Hash for lookups: Fast validation without decryption
- Log all access: Audit key usage
- Rate limit per key: Prevent abuse
- Enforce expiration: Auto-disable expired keys
- Monitor anomalies: Unusual usage patterns
- Backup keys: Secure encrypted backups
Causes:
- Key not in
Authorization: Bearer KEYheader - Invalid key format
- Expired key
- Inactive key
- IP not in allowlist
Solutions:
- Check header format:
Authorization: Bearer gw_live_... - Verify key hasn't expired
- Check key status with
GET /user/api-keys - Verify your IP if allowlist configured
Cause: Wrong key_hash or key deleted
Solution: Create new key
Cause: Scope doesn't include required permission
Solution: Update key scopes or create new key with broader permissions
-- Most active keys
SELECT key_name, requests_used, max_requests
FROM api_keys_new
WHERE user_id = ?
ORDER BY requests_used DESC;
-- Keys nearing limits
SELECT key_name, requests_used, max_requests,
(requests_used::float / max_requests * 100) as pct_used
FROM api_keys_new
WHERE max_requests IS NOT NULL
AND requests_used::float / max_requests > 0.8;
-- Expiring soon
SELECT key_name, expiration_date,
expiration_date - NOW() as days_remaining
FROM api_keys_new
WHERE expiration_date < NOW() + INTERVAL '30 days';- Authentication - Auth methods
- Rate Limiting - Request limits
- Security - Security guidelines
Last Updated: December 2024 Status: Production Ready
For questions: See Troubleshooting or API Reference
- Role-Based-Access-Control — Roles determine key permissions
- Audit-System — Key operations are audit-logged
- Activity-Logging — Key usage is tracked
- Delta Report — P0-6: Admin endpoint auth coverage audit
Reading Path (start here, in order)
- Conceptual Model
- Stability Definition
- Conceptual Model Features
- Features
- Delta Report
- Features-Acceptance-Criteria
Testing
Security & Access
Billing
Monitoring
Features
Providers
Operations
Data References