API Key Management
Comprehensive API for managing API keys for service-to-service authentication, enabling backend services to securely access the AuthOS API without user JWTs.
Overview
API keys enable service-to-service authentication, allowing backend services to authenticate with the AuthOS API using the X-Api-Key header instead of user JWTs. Each API key:
- Belongs to a specific service: Scoped to a single service within an organization
- Has granular permissions: Fine-grained access control (read/write for users, subscriptions, analytics)
- Can expire: Optional expiration date for security
- Is hashed securely: SHA256 hashing with constant-time verification
- Tracks usage: Last used timestamp for monitoring
- Is shown once: Full key revealed only during creation
API keys are ideal for:
- Backend services that need to verify user subscriptions
- Internal admin tools that manage users programmatically
- Analytics dashboards pulling service metrics
- Automated scripts for user provisioning
Data Models
API Key Model
{
"id": "uuid",
"service_id": "uuid",
"name": "Production Backend Server",
"prefix": "sk_a1b2c3",
"key_hash": "sha256-hash",
"permissions": ["read:users", "read:subscriptions", "read:analytics"],
"last_used_at": "datetime",
"expires_at": "datetime",
"created_at": "datetime",
"created_by": "uuid"
}
API Key Response (without key)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"service_id": "service-uuid",
"name": "Production Backend Server",
"prefix": "sk_a1b2c3",
"permissions": ["read:users", "read:subscriptions", "read:analytics"],
"last_used_at": "2025-01-20T14:22:00Z",
"expires_at": "2025-04-15T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z",
"created_by": "user-uuid"
}
API Key Create Response (with full key)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"service_id": "service-uuid",
"name": "Production Backend Server",
"prefix": "sk_a1b2c3",
"permissions": ["read:users", "read:subscriptions", "read:analytics"],
"expires_at": "2025-04-15T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z",
"created_by": "user-uuid",
"key": "sk_a1b2c3_d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2"
}
Valid Permissions
API keys support the following granular permissions:
| Permission | Description |
|---|---|
read:users |
List and retrieve user information for users who have authenticated with the service |
write:users |
Create and manage users |
delete:users |
Delete users |
read:subscriptions |
List and retrieve subscription information |
write:subscriptions |
Create and update subscriptions |
delete:subscriptions |
Delete subscriptions |
read:analytics |
Access service analytics and metrics |
read:service |
View service configuration |
write:service |
Modify service configuration |
Permission Best Practices:
- Principle of Least Privilege: Grant only the permissions required for the specific use case
- Read-Only Keys: Use read-only permissions (
read:*) for analytics dashboards and monitoring - Separate Keys: Create different keys for different backend services
- Time-Limited: Set expiration dates for keys used in CI/CD or temporary integrations
Endpoints Summary
| Method | Path | Description | Permissions |
|---|---|---|---|
| POST | /api/organizations/:org_slug/services/:service_slug/api-keys |
Create API key | Owner/Admin |
| GET | /api/organizations/:org_slug/services/:service_slug/api-keys |
List API keys | Owner/Admin/Member |
| GET | /api/organizations/:org_slug/services/:service_slug/api-keys/:api_key_id |
Get API key details | Owner/Admin/Member |
| DELETE | /api/organizations/:org_slug/services/:service_slug/api-keys/:api_key_id |
Delete API key | Owner/Admin |
API Key Operations
POST /api/organizations/:org_slug/services/:service_slug/api-keys
Create a new API key for a service.
Permissions: Owner or Admin
Headers:
| Header | Value |
|---|---|
Authorization |
Bearer {jwt} |
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
service_slug |
string | Service slug |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Descriptive name for the API key (e.g., “Production Backend Server”) |
permissions |
string[] | Yes | Array of permissions (at least one required) |
expires_in_days |
integer | No | Number of days until expiration (e.g., 90 for 3 months) |
Example Request:
curl -X POST https://sso.example.com/api/organizations/acme-corp/services/main-app/api-keys \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"name": "Production Backend Server",
"permissions": ["read:users", "read:subscriptions", "read:analytics"],
"expires_in_days": 90
}'
Example Response (201 Created):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"service_id": "service-uuid",
"name": "Production Backend Server",
"prefix": "sk_a1b2c3",
"permissions": ["read:users", "read:subscriptions", "read:analytics"],
"expires_at": "2025-04-15T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z",
"created_by": "user-uuid",
"key": "sk_a1b2c3_d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2"
}
Error Responses:
400 Bad Request: Invalid permissions, empty name, or no permissions provided401 Unauthorized: Invalid or missing JWT403 Forbidden: User is not an owner or admin, or organization not active404 Not Found: Organization or service not found500 Internal Server Error: Database error
Important Notes:
- Store the key securely: The
keyfield contains the full API key and is only returned once - Cannot be retrieved: If you lose the key, you must create a new one
- Key format:
sk_{prefix}_{random_component}(e.g.,sk_a1b2c3_d4e5f6g7...) - SHA256 hashed: Only the hash is stored in the database
- Constant-time verification: Prevents timing attacks
- Audit logged: Key creation is logged with permissions and expiration
- Organization must be active: Inactive organizations cannot create API keys
Security Best Practices:
- Store keys in environment variables or secret managers (never in code)
- Use different keys for development, staging, and production
- Set expiration dates for temporary or CI/CD keys
- Rotate keys regularly (e.g., every 90 days)
- Delete unused keys immediately
- Monitor
last_used_atto detect unused keys
Example: Store Key Securely
# Environment variable (recommended)
export SSO_API_KEY="sk_a1b2c3_d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2"
# Use in application
curl -X GET https://sso.example.com/api/service/users \
-H "X-Api-Key: $SSO_API_KEY"
GET /api/organizations/:org_slug/services/:service_slug/api-keys
List all API keys for a service.
Permissions: Owner, Admin, or Member
Headers:
| Header | Value |
|---|---|
Authorization |
Bearer {jwt} |
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
service_slug |
string | Service slug |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
limit |
integer | No | Maximum number of keys to return (default: 50, max: 100) |
offset |
integer | No | Number of keys to skip for pagination (default: 0) |
Example Request:
curl -X GET "https://sso.example.com/api/organizations/acme-corp/services/main-app/api-keys?limit=50&offset=0" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
Example Response (200 OK):
{
"api_keys": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"service_id": "service-uuid",
"name": "Production Backend Server",
"prefix": "sk_a1b2c3",
"permissions": ["read:users", "read:subscriptions"],
"last_used_at": "2025-01-20T14:22:00Z",
"expires_at": "2025-04-15T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z",
"created_by": "user-uuid"
},
{
"id": "another-key-uuid",
"service_id": "service-uuid",
"name": "Analytics Dashboard",
"prefix": "sk_d4e5f6",
"permissions": ["read:analytics", "read:service"],
"last_used_at": "2025-01-21T09:15:00Z",
"expires_at": null,
"created_at": "2025-01-16T11:00:00Z",
"created_by": "another-user-uuid"
},
{
"id": "expired-key-uuid",
"service_id": "service-uuid",
"name": "Deprecated Integration",
"prefix": "sk_g7h8i9",
"permissions": ["read:users"],
"last_used_at": "2024-12-01T10:00:00Z",
"expires_at": "2025-01-01T00:00:00Z",
"created_at": "2024-10-01T10:00:00Z",
"created_by": "user-uuid"
}
],
"total": 3
}
Error Responses:
401 Unauthorized: Invalid or missing JWT403 Forbidden: User is not a member, or organization not active404 Not Found: Organization or service not found500 Internal Server Error: Database error
Notes:
- All members can view API keys (but not the actual key value)
last_used_atshows when the key was last used (null if never used)expires_atis null for keys with no expiration- Expired keys are still returned (check
expires_aton client side) - Use
last_used_atto identify inactive keys for cleanup - Pagination supported via
limitandoffset
Example: Identify Unused Keys
// Filter keys not used in the last 90 days
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
const unusedKeys = apiKeys.filter(key =>
!key.last_used_at || new Date(key.last_used_at) < ninetyDaysAgo
);
GET /api/organizations/:org_slug/services/:service_slug/api-keys/:api_key_id
Get details for a specific API key.
Permissions: Owner, Admin, or Member
Headers:
| Header | Value |
|---|---|
Authorization |
Bearer {jwt} |
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
service_slug |
string | Service slug |
api_key_id |
string | API key ID (UUID) |
Example Request:
curl -X GET https://sso.example.com/api/organizations/acme-corp/services/main-app/api-keys/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
Example Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"service_id": "service-uuid",
"name": "Production Backend Server",
"prefix": "sk_a1b2c3",
"permissions": ["read:users", "read:subscriptions", "read:analytics"],
"last_used_at": "2025-01-20T14:22:00Z",
"expires_at": "2025-04-15T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z",
"created_by": "user-uuid"
}
Error Responses:
401 Unauthorized: Invalid or missing JWT403 Forbidden: User is not a member, or organization not active404 Not Found: Organization, service, or API key not found500 Internal Server Error: Database error
Notes:
- Full key value is never returned (only available during creation)
- Use this endpoint to check key details and permissions
- Verify
expires_atto detect expired keys - Check
last_used_atto monitor key activity
DELETE /api/organizations/:org_slug/services/:service_slug/api-keys/:api_key_id
Delete an API key immediately.
Permissions: Owner or Admin
Headers:
| Header | Value |
|---|---|
Authorization |
Bearer {jwt} |
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
org_slug |
string | Organization slug |
service_slug |
string | Service slug |
api_key_id |
string | API key ID (UUID) |
Example Request:
curl -X DELETE https://sso.example.com/api/organizations/acme-corp/services/main-app/api-keys/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
Example Response (204 No Content):
No response body.
Error Responses:
401 Unauthorized: Invalid or missing JWT403 Forbidden: User is not an owner or admin, or organization not active404 Not Found: Organization, service, or API key not found500 Internal Server Error: Database error
Important Warnings:
- Immediate revocation: Services using this key will lose access immediately
- Cannot be undone: Deleted keys cannot be recovered
- No grace period: Active requests using the key will fail instantly
- Audit logged: Deletion is logged with key name and actor
Best Practices:
- Notify relevant teams before deleting keys used in production
- Update services to use new keys before deleting old ones
- Consider setting expiration dates instead of manual deletion for temporary keys
- Delete keys immediately if compromised
Example: Key Rotation Workflow
# 1. Create new API key
NEW_KEY=$(curl -X POST .../api-keys -d '{"name":"New Key","permissions":[...]}' | jq -r '.key')
# 2. Update service configuration with new key
export SSO_API_KEY="$NEW_KEY"
# 3. Test new key
curl -X GET .../api/service/users -H "X-Api-Key: $SSO_API_KEY"
# 4. Delete old key
curl -X DELETE .../api-keys/old-key-uuid
# 5. Store new key securely
echo "$NEW_KEY" | vault write secret/sso-api-key value=-
Using API Keys
Authentication
API keys authenticate via the X-Api-Key header:
curl -X GET https://sso.example.com/api/service/users \
-H "X-Api-Key: sk_a1b2c3_d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2"
Available Endpoints
API keys can access service-scoped endpoints under /api/service/*:
GET /api/service/users- List users (requiresread:users)GET /api/service/users/:user_id- Get user details (requiresread:users)POST /api/service/users- Create user (requireswrite:users)PATCH /api/service/users/:user_id- Update user (requireswrite:users)GET /api/service/subscriptions- List subscriptions (requiresread:subscriptions)GET /api/service/subscriptions/:user_id- Get user subscription (requiresread:subscriptions)POST /api/service/subscriptions- Create subscription (requireswrite:subscriptions)PATCH /api/service/subscriptions/:user_id- Update subscription (requireswrite:subscriptions)GET /api/service/analytics- Get analytics (requiresread:analytics)GET /api/service/info- Get service info (requiresread:service)PATCH /api/service/info- Update service (requireswrite:service)
See the Service API Reference for detailed endpoint documentation.
Permission Enforcement
Each endpoint checks for required permissions:
// Error when lacking permissions
{
"error": "Insufficient permissions. Required: write:users",
"error_code": "FORBIDDEN",
"timestamp": "2025-01-15T10:30:00Z"
}
Key Expiration
Expired keys are rejected with an error:
{
"error": "API key has expired",
"error_code": "UNAUTHORIZED",
"timestamp": "2025-01-15T10:30:00Z"
}
Security Considerations
Key Format:
- Format:
sk_{prefix}_{random_component} - Prefix is stored for identification (e.g.,
sk_a1b2c3) - Random component is 64 characters (high entropy)
- Total length: ~74 characters
Storage Security:
- Keys are hashed using SHA256 before storage
- Constant-time comparison prevents timing attacks
- Original key is never stored or logged
- Key shown only once during creation
Permission Model:
- Keys are scoped to a single service (cannot access other services)
- Granular permissions (read vs. write, per resource type)
- No privilege escalation (keys cannot modify their own permissions)
- Service-level isolation (cannot access organization management endpoints)
Expiration:
- Optional expiration date for time-limited access
- Expired keys are rejected immediately
- No automatic cleanup (expired keys remain in database for audit)
- Set expiration for temporary integrations and CI/CD
Monitoring:
last_used_atupdated on each request- Audit logs track key creation and deletion
- Monitor for unused keys and suspicious activity
- Alert on keys used from unexpected IP addresses
Best Practices:
- Never commit keys to version control
- Use environment variables or secret managers
- Rotate keys every 90 days
- Delete unused keys immediately
- Use separate keys per environment (dev/staging/prod)
- Grant minimum required permissions
- Set expiration dates for temporary keys
- Monitor
last_used_atfor inactive keys - Audit logs regularly for security events
Key Rotation:
- Create new API key with same permissions
- Update service configuration with new key
- Test new key in non-production environment
- Deploy service with new key
- Monitor for errors
- Delete old key after confirmation
Compromise Response:
- Delete compromised key immediately
- Create new key with different prefix
- Review audit logs for unauthorized access
- Notify security team
- Investigate scope of compromise
- Update incident response documentation
Rate Limiting
API key operations are rate limited:
- Create API Key: 20 per hour per user
- List API Keys: 100 per hour per user
- Delete API Key: 30 per hour per user
Service API endpoints (accessed with API keys) have separate rate limits:
- Read Operations: 1000 requests per hour per key
- Write Operations: 500 requests per hour per key
Rate limit headers are included in responses:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1642348800
Examples
Create Read-Only Analytics Key
curl -X POST https://sso.example.com/api/organizations/acme-corp/services/main-app/api-keys \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"name": "Analytics Dashboard (Read-Only)",
"permissions": ["read:analytics", "read:service"]
}'
Create Full-Access Backend Key
curl -X POST https://sso.example.com/api/organizations/acme-corp/services/main-app/api-keys \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"name": "Backend Server (Full Access)",
"permissions": [
"read:users",
"write:users",
"read:subscriptions",
"write:subscriptions",
"read:analytics"
]
}'
Create Temporary CI/CD Key
curl -X POST https://sso.example.com/api/organizations/acme-corp/services/main-app/api-keys \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"name": "CI/CD Pipeline - Sprint 42",
"permissions": ["read:users", "read:subscriptions"],
"expires_in_days": 30
}'
Node.js Service Integration
const SSO_API_KEY = process.env.SSO_API_KEY;
const SSO_API_BASE = 'https://sso.example.com';
async function getUserSubscription(userId) {
const response = await fetch(
`${SSO_API_BASE}/api/service/subscriptions/${userId}`,
{
headers: {
'X-Api-Key': SSO_API_KEY
}
}
);
if (!response.ok) {
throw new Error(`AuthOS API error: ${response.status}`);
}
return response.json();
}
// Usage
const subscription = await getUserSubscription('user-uuid');
console.log(`User plan: ${subscription.plan_name}`);
Python Service Integration
import os
import requests
SSO_API_KEY = os.environ['SSO_API_KEY']
SSO_API_BASE = 'https://sso.example.com'
def get_user_subscription(user_id):
response = requests.get(
f'{SSO_API_BASE}/api/service/subscriptions/{user_id}',
headers={'X-Api-Key': SSO_API_KEY}
)
response.raise_for_status()
return response.json()
# Usage
subscription = get_user_subscription('user-uuid')
print(f"User plan: {subscription['plan_name']}")