API Key Management

Comprehensive API for managing API keys for service-to-service authentication, enabling secure backend access without user JWTs.

Updated Dec 16, 2025
Edit on GitHub
api-keys service-auth security backend

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 provided
  • 401 Unauthorized: Invalid or missing JWT
  • 403 Forbidden: User is not an owner or admin, or organization not active
  • 404 Not Found: Organization or service not found
  • 500 Internal Server Error: Database error

Important Notes:

  • Store the key securely: The key field 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_at to 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 JWT
  • 403 Forbidden: User is not a member, or organization not active
  • 404 Not Found: Organization or service not found
  • 500 Internal Server Error: Database error

Notes:

  • All members can view API keys (but not the actual key value)
  • last_used_at shows when the key was last used (null if never used)
  • expires_at is null for keys with no expiration
  • Expired keys are still returned (check expires_at on client side)
  • Use last_used_at to identify inactive keys for cleanup
  • Pagination supported via limit and offset

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 JWT
  • 403 Forbidden: User is not a member, or organization not active
  • 404 Not Found: Organization, service, or API key not found
  • 500 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_at to detect expired keys
  • Check last_used_at to 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 JWT
  • 403 Forbidden: User is not an owner or admin, or organization not active
  • 404 Not Found: Organization, service, or API key not found
  • 500 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 (requires read:users)
  • GET /api/service/users/:user_id - Get user details (requires read:users)
  • POST /api/service/users - Create user (requires write:users)
  • PATCH /api/service/users/:user_id - Update user (requires write:users)
  • GET /api/service/subscriptions - List subscriptions (requires read:subscriptions)
  • GET /api/service/subscriptions/:user_id - Get user subscription (requires read:subscriptions)
  • POST /api/service/subscriptions - Create subscription (requires write:subscriptions)
  • PATCH /api/service/subscriptions/:user_id - Update subscription (requires write:subscriptions)
  • GET /api/service/analytics - Get analytics (requires read:analytics)
  • GET /api/service/info - Get service info (requires read:service)
  • PATCH /api/service/info - Update service (requires write: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_at updated 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_at for inactive keys
  • Audit logs regularly for security events

Key Rotation:

  1. Create new API key with same permissions
  2. Update service configuration with new key
  3. Test new key in non-production environment
  4. Deploy service with new key
  5. Monitor for errors
  6. Delete old key after confirmation

Compromise Response:

  1. Delete compromised key immediately
  2. Create new key with different prefix
  3. Review audit logs for unauthorized access
  4. Notify security team
  5. Investigate scope of compromise
  6. 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']}")