Skip to main content

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

FeatureImplementationPurpose
SHA-256 HashingSalted hash storageNever stores plaintext keys
Constant-Time Comparesecrets.compare_digest()Prevents timing attacks
32-Character PrefixCollision-resistant lookupEfficient and secure search
Per-Key Rate LimitingSliding window algorithmPrevents abuse
Usage TrackingPer-request loggingAudit trail and analytics
RLS IntegrationOrganization contextMulti-tenant isolation

API Key Format

Structure

owkai_{role}_{random_string}

Examples:
- owkai_admin_abc123def456ghi789jkl012mno345pqr
- owkai_super_admin_xyz789abc123def456ghi012jkl345
- owkai_user_mno345pqr678stu901vwx234yz567abc
ComponentLengthDescription
Prefix6+ charsowkai_
RoleVariableadmin_, super_admin_, user_
Random32+ charsCryptographically random
Total50+ charsFull 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

TierMax RequestsWindowUse Case
Free1001 hourDevelopment/testing
Standard1,0001 hourSmall production
Professional10,0001 hourMedium production
Enterprise100,0001 hourLarge production
UnlimitedNo 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

ScenarioResponseHTTP 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

FrameworkControlImplementation
SOC 2CC6.1SHA-256 hashing, constant-time comparison
HIPAA164.312(d)Unique key-based authentication
PCI-DSSReq 8.2Strong cryptographic key storage
PCI-DSSReq 8.6Unique key identification
NIST 800-53IA-5Authenticator management
NIST 800-53IA-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