Skip to content
arminrad edited this page Mar 16, 2026 · 2 revisions

API Key Management

Secure API key creation, encryption, rotation, and management


Overview

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

Quick Start

Create API 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!

List Keys

curl http://localhost:8000/user/api-keys \
  -H "Authorization: Bearer YOUR_KEY"

Update 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
  }'

Delete Key

curl -X DELETE http://localhost:8000/user/api-keys/1 \
  -H "Authorization: Bearer YOUR_KEY"

Key Format

Environment Prefixes

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...

Structure

gw_live_<32_random_chars>
│   │    │
│   │    └─ URL-safe random string
│   └────── Environment tag
└────────── Prefix

API Endpoints

1. Create API Key

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)

2. List API Keys

GET /user/api-keys

Returns all keys for authenticated user (encrypted keys hidden).

3. Get Specific Key

GET /user/api-keys/{key_id}

4. Update API Key

PUT /user/api-keys/{key_id}

Updatable Fields:

  • key_name
  • scope_permissions
  • max_requests
  • ip_allowlist
  • domain_referrers
  • is_active

Cannot update: environment_tag, expiration_date

5. Delete API Key

DELETE /user/api-keys/{key_id}

Soft delete - key becomes inactive.

6. Rotate API Key

POST /user/api-keys/{key_id}/rotate

Generates new key, invalidates old one.


Security Features

Encryption at Rest

Algorithm: Fernet (AES-128)

# Key storage
encrypted_key = fernet.encrypt(api_key.encode())

# Validation
decrypted_key = fernet.decrypt(encrypted_key)

Environment Variable: SECRET_KEY

Hashing for Validation

Algorithm: HMAC-SHA256

key_hash = hmac.new(
    secret_key.encode(),
    api_key.encode(),
    hashlib.sha256
).hexdigest()

Fast lookup without decryption.

Last 4 Characters

Stored for easy identification:

last4 = api_key[-4:]  # Display as "...abc123"

Permission Scopes

Default Permissions

{
  "read": ["*"],
  "write": ["*"],
  "admin": ["*"]
}

Custom Scopes

{
  "read": ["models", "balance", "usage"],
  "write": ["chat", "images"],
  "admin": []
}

Scope Checking

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", [])

Advanced Features

IP Allowlist

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.

Domain Referrers

Restrict to specific domains:

{
  "domain_referrers": [
    "app.example.com",
    "*.example.com",
    "https://secure.example.com"
  ]
}

Checks HTTP Referer header.

Request Limits

Per-key monthly limits:

{
  "max_requests": 100000,
  "requests_used": 5234,
  "requests_remaining": 94766
}

Resets monthly based on creation date.

Expiration

Auto-disable after expiration:

{
  "expires_at": "2025-12-15T00:00:00Z",
  "is_expired": false,
  "days_until_expiry": 365
}

Database Schema

api_keys_new Table

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);

Implementation

Location

  • DB Layer: src/db/api_keys.py
  • Routes: src/routes/api_keys.py
  • Security: src/security/security.py
  • Crypto: src/utils/crypto.py

Key Generation

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_part

Validation Flow

1. 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

Best Practices

For Users

  1. Name keys descriptively: "Production Web App", "Mobile App iOS"
  2. Use environment tags: Separate live/test/staging
  3. Set expiration dates: Force rotation annually
  4. Limit scopes: Grant minimum necessary permissions
  5. Rotate regularly: Every 90-365 days
  6. Delete unused keys: Remove old/test keys
  7. Monitor usage: Check requests_used regularly

For Admins

  1. Encrypt at rest: Use strong SECRET_KEY
  2. Hash for lookups: Fast validation without decryption
  3. Log all access: Audit key usage
  4. Rate limit per key: Prevent abuse
  5. Enforce expiration: Auto-disable expired keys
  6. Monitor anomalies: Unusual usage patterns
  7. Backup keys: Secure encrypted backups

Troubleshooting

Unauthorized (401) Error

Causes:

  • Key not in Authorization: Bearer KEY header
  • Invalid key format
  • Expired key
  • Inactive key
  • IP not in allowlist

Solutions:

  1. Check header format: Authorization: Bearer gw_live_...
  2. Verify key hasn't expired
  3. Check key status with GET /user/api-keys
  4. Verify your IP if allowlist configured

Key Not Found

Cause: Wrong key_hash or key deleted

Solution: Create new key

Permission Denied

Cause: Scope doesn't include required permission

Solution: Update key scopes or create new key with broader permissions


Monitoring

Track Key Usage

-- 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';

Related Documentation


Last Updated: December 2024 Status: Production Ready

For questions: See Troubleshooting or API Reference


Related

Clone this wiki locally