API Key Authentication
Overview
ASCEND provides API key authentication for programmatic access via the SDK, CI/CD pipelines, and automated integrations. API keys implement enterprise-grade security with SHA-256 hashing, constant-time comparison, per-key rate limiting, and comprehensive usage tracking.
Why It Matters
API key authentication enables:
- SDK Integration: Programmatic access for your applications
- CI/CD Automation: Automated deployments and testing
- Service-to-Service: Backend system integration
- Audit Compliance: Complete tracking of programmatic access
- Granular Control: Per-key permissions and rate limits
Architecture
Authentication Flow
+------------------+ +------------------+ +------------------+
| SDK Client | | ASCEND API | | Database |
| (Application) | | (FastAPI) | | (PostgreSQL) |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
| 1. Request with | |
| API Key | |
+----------------------->| |
| | |
| | 2. Extract prefix |
| | (32 chars) |
| | |
| | 3. Lookup by prefix |
| | (RLS-bypass func) |
| +----------------------->|
| | |
| | 4. Return candidates |
| |<-----------------------+
| | |
| | 5. Hash comparison |
| | (constant-time) |
| | |
| | 6. Set RLS context |
| +----------------------->|
| | |
| | 7. Check rate limit |
| +----------------------->|
| | |
| | 8. Update usage |
| +----------------------->|
| | |
| 9. Response | |
|<-----------------------+ |
Security Features
| Feature | Implementation | Purpose |
|---|---|---|
| SHA-256 Hashing | Salted hash storage | Never stores plaintext keys |
| Constant-Time Compare | secrets.compare_digest() | Prevents timing attacks |
| 32-Character Prefix | Collision-resistant lookup | Efficient and secure search |
| Per-Key Rate Limiting | Sliding window algorithm | Prevents abuse |
| Usage Tracking | Per-request logging | Audit trail and analytics |
| RLS Integration | Organization context | Multi-tenant isolation |
API Key Format
Structure
owkai_{role}_{random_string}
Examples:
- owkai_admin_abc123def456ghi789jkl012mno345pqr
- owkai_super_admin_xyz789abc123def456ghi012jkl345
- owkai_user_mno345pqr678stu901vwx234yz567abc
| Component | Length | Description |
|---|---|---|
| Prefix | 6+ chars | owkai_ |
| Role | Variable | admin_, super_admin_, user_ |
| Random | 32+ chars | Cryptographically random |
| Total | 50+ chars | Full API key |
Lookup Prefix
For database lookup, ASCEND uses the first 32 characters as the prefix:
# SEC-096: 32-char prefix for collision prevention
# Role prefix "owkai_super_admin_" = 18 chars
# Plus 14 random chars = 32 total
prefix = provided_key[:32]
Configuration
Environment Variables
# API Key Configuration
API_KEY_PREFIX_LENGTH=32
API_KEY_HASH_ALGORITHM=sha256
API_KEY_DEFAULT_RATE_LIMIT=1000/hour
# Rate Limiting
RATE_LIMIT_WINDOW_SECONDS=3600
RATE_LIMIT_MAX_REQUESTS=1000
Database Schema
-- API Keys table
CREATE TABLE api_keys (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
organization_id INTEGER NOT NULL REFERENCES organizations(id),
name VARCHAR(255) NOT NULL,
key_prefix VARCHAR(32) NOT NULL,
key_hash VARCHAR(255) NOT NULL,
salt VARCHAR(64) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
expires_at TIMESTAMP,
last_used_at TIMESTAMP,
usage_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER REFERENCES users(id),
UNIQUE(key_prefix)
);
-- Rate limits table
CREATE TABLE api_key_rate_limits (
id SERIAL PRIMARY KEY,
api_key_id INTEGER NOT NULL REFERENCES api_keys(id),
max_requests INTEGER NOT NULL DEFAULT 1000,
window_seconds INTEGER NOT NULL DEFAULT 3600,
current_window_start TIMESTAMP,
current_window_count INTEGER DEFAULT 0
);
-- Usage logs table
CREATE TABLE api_key_usage_logs (
id SERIAL PRIMARY KEY,
api_key_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
endpoint VARCHAR(500) NOT NULL,
http_method VARCHAR(10) NOT NULL,
http_status INTEGER NOT NULL,
ip_address VARCHAR(45),
user_agent VARCHAR(500),
request_id VARCHAR(255),
response_time_ms INTEGER,
request_metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
RLS-Bypass Function
-- SEC-RLS-002: Authentication bootstrap function
-- Uses SECURITY DEFINER to bypass RLS during key lookup
CREATE OR REPLACE FUNCTION auth_lookup_api_key(prefix VARCHAR(32))
RETURNS TABLE (
id INTEGER,
key_hash VARCHAR(255),
salt VARCHAR(64),
user_id INTEGER,
organization_id INTEGER,
expires_at TIMESTAMP,
is_active BOOLEAN
)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
RETURN QUERY
SELECT
ak.id,
ak.key_hash,
ak.salt,
ak.user_id,
ak.organization_id,
ak.expires_at,
ak.is_active
FROM api_keys ak
WHERE ak.key_prefix = prefix
AND ak.is_active = TRUE;
END;
$$;
-- Restrict function execution
REVOKE ALL ON FUNCTION auth_lookup_api_key FROM PUBLIC;
GRANT EXECUTE ON FUNCTION auth_lookup_api_key TO api_user;
Usage
Supported Headers
# Option 1: Bearer token (recommended)
Authorization: Bearer owkai_admin_xxxxxxxxxxxxx...
# Option 2: X-API-Key header (SDK standard)
X-API-Key: owkai_admin_xxxxxxxxxxxxx...
SDK Usage (Python)
from ascend import AscendClient
# Initialize with API key
client = AscendClient(
api_key="owkai_admin_xxxxxxxxxxxxx...",
base_url="https://api.ascend.io/v1"
)
# Make authenticated requests
agents = client.agents.list()
SDK Usage (Node.js)
import { AscendClient } from '@ascend/sdk';
// Initialize with API key
const client = new AscendClient({
apiKey: 'owkai_admin_xxxxxxxxxxxxx...',
baseUrl: 'https://api.ascend.io/v1'
});
// Make authenticated requests
const agents = await client.agents.list();
cURL Usage
# Using Bearer token
curl -X GET https://api.ascend.io/v1/agents \
-H "Authorization: Bearer owkai_admin_xxxxxxxxxxxxx..."
# Using X-API-Key header
curl -X GET https://api.ascend.io/v1/agents \
-H "X-API-Key: owkai_admin_xxxxxxxxxxxxx..."
API Key Management
Create API Key
curl -X POST https://api.ascend.io/v1/api-keys \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Production SDK Key",
"expires_in_days": 90,
"rate_limit": {
"max_requests": 10000,
"window_seconds": 3600
}
}'
# Response
{
"id": 123,
"name": "Production SDK Key",
"key": "owkai_admin_abc123def456ghi789jkl012mno345pqr",
"key_prefix": "owkai_admin_abc123def456ghi7",
"expires_at": "2026-04-20T00:00:00Z",
"rate_limit": {
"max_requests": 10000,
"window_seconds": 3600
},
"created_at": "2026-01-20T10:00:00Z"
}
Important: The full API key is only shown once at creation. Store it securely.
List API Keys
curl -X GET https://api.ascend.io/v1/api-keys \
-H "Authorization: Bearer $JWT_TOKEN"
# Response
{
"api_keys": [
{
"id": 123,
"name": "Production SDK Key",
"key_prefix": "owkai_admin_abc123def456ghi7",
"is_active": true,
"expires_at": "2026-04-20T00:00:00Z",
"last_used_at": "2026-01-20T09:45:00Z",
"usage_count": 1523,
"created_at": "2026-01-20T10:00:00Z"
}
]
}
Revoke API Key
curl -X DELETE https://api.ascend.io/v1/api-keys/123 \
-H "Authorization: Bearer $JWT_TOKEN"
# Response
{
"message": "API key revoked successfully",
"key_id": 123,
"revoked_at": "2026-01-20T10:30:00Z"
}
View Usage Statistics
curl -X GET https://api.ascend.io/v1/api-keys/123/usage \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"start_date": "2026-01-01",
"end_date": "2026-01-20"
}'
# Response
{
"key_id": 123,
"period": {
"start": "2026-01-01T00:00:00Z",
"end": "2026-01-20T23:59:59Z"
},
"total_requests": 15234,
"successful_requests": 15201,
"failed_requests": 33,
"rate_limit_hits": 5,
"top_endpoints": [
{"endpoint": "/v1/agents", "count": 5432},
{"endpoint": "/v1/actions/evaluate", "count": 4521}
],
"requests_by_day": [
{"date": "2026-01-20", "count": 856},
{"date": "2026-01-19", "count": 921}
]
}
Rate Limiting
Algorithm: Sliding Window
async def check_rate_limit(api_key_id: int, db: Session) -> tuple[bool, int]:
"""
Check if API key is within rate limit.
Algorithm: Sliding window
- Count requests in current window
- Reset window if expired
- Return (allowed: bool, retry_after: int seconds)
"""
rate_limit = db.query(ApiKeyRateLimit).filter(
ApiKeyRateLimit.api_key_id == api_key_id
).first()
if not rate_limit:
return True, 0 # No rate limit configured
now = datetime.now(UTC)
# Reset window if expired
if not rate_limit.current_window_start or \
(now - rate_limit.current_window_start).total_seconds() >= rate_limit.window_seconds:
rate_limit.current_window_start = now
rate_limit.current_window_count = 0
db.commit()
# Check limit
if rate_limit.current_window_count >= rate_limit.max_requests:
window_end = rate_limit.current_window_start + timedelta(seconds=rate_limit.window_seconds)
retry_after = int((window_end - now).total_seconds())
return False, max(retry_after, 1)
# Increment count
rate_limit.current_window_count += 1
db.commit()
return True, 0
Rate Limit Tiers
| Tier | Max Requests | Window | Use Case |
|---|---|---|---|
| Free | 100 | 1 hour | Development/testing |
| Standard | 1,000 | 1 hour | Small production |
| Professional | 10,000 | 1 hour | Medium production |
| Enterprise | 100,000 | 1 hour | Large production |
| Unlimited | No limit | - | Custom agreement |
Rate Limit Headers
# Successful request
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 872
X-RateLimit-Reset: 1705770000
# Rate limited request
HTTP/1.1 429 Too Many Requests
Retry-After: 1823
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705770000
Security Implementation
Hash Generation (Key Creation)
import hashlib
import secrets
def create_api_key(user_id: int, org_id: int, name: str) -> tuple[str, str, str]:
"""
Create a new API key with secure hash storage.
Returns: (full_key, prefix, salt)
"""
# Generate random key
role_prefix = "owkai_admin_"
random_part = secrets.token_urlsafe(32)
full_key = f"{role_prefix}{random_part}"
# Generate salt
salt = secrets.token_hex(32)
# Hash the key
key_hash = hashlib.sha256((full_key + salt).encode()).hexdigest()
# Extract prefix for lookup
prefix = full_key[:32]
return full_key, prefix, salt, key_hash
Hash Verification (Authentication)
import secrets
import hashlib
async def verify_api_key(provided_key: str, db: Session) -> dict:
"""
Verify API key with constant-time comparison.
"""
# Extract prefix
prefix = provided_key[:32]
# Lookup candidates by prefix (RLS-bypass function)
results = db.execute(
text("SELECT * FROM auth_lookup_api_key(:prefix)"),
{"prefix": prefix}
).fetchall()
if not results:
raise HTTPException(status_code=401, detail="Invalid API key")
# Find matching key (constant-time comparison)
for candidate in results:
candidate_hash = hashlib.sha256(
(provided_key + candidate.salt).encode()
).hexdigest()
# Constant-time comparison prevents timing attacks
if secrets.compare_digest(candidate_hash, candidate.key_hash):
return {
"user_id": candidate.user_id,
"organization_id": candidate.organization_id,
"api_key_id": candidate.id
}
raise HTTPException(status_code=401, detail="Invalid API key")
Fail-Secure Behavior
| Scenario | Response | HTTP Status |
|---|---|---|
| Missing API key | "API key required" | 401 |
| Invalid key format | "Invalid API key format" | 401 |
| Key not found | "Invalid API key" | 401 |
| Hash mismatch | "Invalid API key" | 401 |
| Key expired | "API key expired" | 401 |
| Key revoked | "API key revoked" | 401 |
| Rate limit exceeded | "Rate limit exceeded" | 429 |
| Database unavailable | "Authentication service error" | 500 |
Compliance Mapping
| Framework | Control | Implementation |
|---|---|---|
| SOC 2 | CC6.1 | SHA-256 hashing, constant-time comparison |
| HIPAA | 164.312(d) | Unique key-based authentication |
| PCI-DSS | Req 8.2 | Strong cryptographic key storage |
| PCI-DSS | Req 8.6 | Unique key identification |
| NIST 800-53 | IA-5 | Authenticator management |
| NIST 800-53 | IA-5(1) | Password-based authentication |
Verification
Test API Key Authentication
# Test with valid key
curl -X GET https://api.ascend.io/v1/auth/me \
-H "X-API-Key: owkai_admin_xxxxxxxxxxxxx..."
# Expected response
{
"user_id": 123,
"email": "user@example.com",
"organization_id": 1,
"role": "admin",
"auth_method": "api_key",
"api_key_id": 456
}
Test Rate Limiting
# Make requests until rate limited
for i in {1..1001}; do
curl -s -o /dev/null -w "%{http_code}\n" \
-X GET https://api.ascend.io/v1/agents \
-H "X-API-Key: $API_KEY"
done
# Should see 429 responses after limit exceeded
Next Steps
- Authorization Overview - RBAC configuration
- RBAC Reference - Role hierarchy details
- Permissions Reference - All 23+ permissions