Skip to content

Developer API Key System #243

Description

@Entropy-rgb

Problem Statement

REST API exists but uses cookie-based auth only. No API key system for external developers to integrate programmatically. Developers cannot build applications on top of BoloDB without browser-based authentication.

Proposed Solution

Implement a BoloDB API key system with key generation, scoping, rate limiting, and usage tracking.

Acceptance Criteria

  • API key generation in Settings
  • Multiple keys per user (named, e.g., "dev", "staging")
  • Bearer token authentication support
  • Key scoping (read-only, read-write, admin)
  • Rate limiting per key
  • Usage tracking (calls, tokens, cost)
  • Key revocation
  • API documentation (OpenAPI/Swagger)
  • SDK examples (Python, JavaScript)

Technical Approach

Backend Changes

1. API key model (backend/app/pgdatabase/models.py):

class ApiKey(Base):
    __tablename__ = "api_keys"
    id = Column(UUID, primary_key=True)
    user_id = Column(UUID, ForeignKey("users.id"))
    name = Column(String, nullable=False)
    key_hash = Column(String, nullable=False, unique=True)
    key_prefix = Column(String, nullable=False)  # First 8 chars for identification
    scope = Column(String, default="read")  # read, write, admin
    is_active = Column(Boolean, default=True)
    last_used = Column(DateTime)
    usage_count = Column(Integer, default=0)
    rate_limit = Column(Integer, default=1000)  # requests per day
    created_at = Column(DateTime, default=func.now())
    expires_at = Column(DateTime)

2. Key generation service (backend/app/services/api_keys.py):

import secrets
import hashlib

class ApiKeyService:
    def __init__(self, db):
        self.db = db
    
    async def generate_key(self, user_id: str, name: str, scope: str = "read") -> dict:
        """Generate a new API key."""
        # Generate secure random key
        raw_key = secrets.token_urlsafe(32)
        key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
        key_prefix = raw_key[:8]
        
        api_key = ApiKey(
            user_id=user_id,
            name=name,
            key_hash=key_hash,
            key_prefix=key_prefix,
            scope=scope
        )
        self.db.add(api_key)
        await self.db.commit()
        
        # Return raw key only once
        return {
            "key": raw_key,
            "id": api_key.id,
            "name": name,
            "scope": scope,
            "prefix": key_prefix
        }
    
    async def validate_key(self, raw_key: str) -> dict:
        """Validate an API key and return user info."""
        key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
        
        api_key = self.db.query(ApiKey).filter(
            ApiKey.key_hash == key_hash,
            ApiKey.is_active == True
        ).first()
        
        if not api_key:
            return None
        
        # Check expiration
        if api_key.expires_at and api_key.expires_at < datetime.utcnow():
            return None
        
        # Update usage
        api_key.last_used = datetime.utcnow()
        api_key.usage_count += 1
        await self.db.commit()
        
        return {
            "user_id": api_key.user_id,
            "scope": api_key.scope,
            "key_id": api_key.id
        }
    
    async def revoke_key(self, key_id: str):
        """Revoke an API key."""
        api_key = self.db.query(ApiKey).get(key_id)
        if api_key:
            api_key.is_active = False
            await self.db.commit()

3. API key authentication dependency (backend/app/dependencies.py):

async def get_api_key_user(request: Request, db = Depends(get_db)):
    """Authenticate via API key (Bearer token or X-API-Key header)."""
    auth_header = request.headers.get("Authorization", "")
    api_key_header = request.headers.get("X-API-Key", "")
    
    raw_key = None
    if auth_header.startswith("Bearer "):
        raw_key = auth_header[7:]
    elif api_key_header:
        raw_key = api_key_header
    
    if not raw_key:
        return None  # Fall back to cookie auth
    
    service = ApiKeyService(db)
    return await service.validate_key(raw_key)

4. API routes (backend/app/routes/api_keys.py):

  • GET /api/keys - List user's API keys
  • POST /api/keys - Generate new key
  • DELETE /api/keys/{id} - Revoke key
  • GET /api/keys/{id}/usage - Get usage stats

5. Rate limiting - Implement per-key rate limiting

Frontend Changes

1. API key manager (frontend/src/routes/settings/api-keys/+page.svelte):

<script>
    let keys = [];
    let newKeyName = '';
    let newKeyScope = 'read';
    let generatedKey = null;
    
    async function generateKey() {
        const response = await apiCall('/api/keys', {
            method: 'POST',
            body: JSON.stringify({ name: newKeyName, scope: newKeyScope })
        });
        generatedKey = response.key;
        keys = [...keys, response];
    }
    
    async function revokeKey(id) {
        await apiCall(`/api/keys/${id}`, { method: 'DELETE' });
        keys = keys.filter(k => k.id !== id);
    }
</script>

<div class="api-keys">
    <h1>API Keys</h1>
    
    {#if generatedKey}
        <div class="alert">
            <strong>Your API Key:</strong>
            <code>{generatedKey}</code>
            <p>Copy this key now. It won't be shown again.</p>
            <button on:click={() => navigator.clipboard.writeText(generatedKey)}>Copy</button>
        </div>
    {/if}
    
    <div class="generate-form">
        <input bind:value={newKeyName} placeholder="Key name (e.g., 'dev', 'staging')" />
        <select bind:value={newKeyScope}>
            <option value="read">Read Only</option>
            <option value="read">Read/Write</option>
            <option value="admin">Admin</option>
        </select>
        <button on:click={generateKey}>Generate Key</button>
    </div>
    
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Prefix</th>
                <th>Scope</th>
                <th>Last Used</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            {#each keys as key}
                <tr>
                    <td>{key.name}</td>
                    <td><code>{key.prefix}...</code></td>
                    <td>{key.scope}</td>
                    <td>{key.last_used || 'Never'}</td>
                    <td>
                        <button on:click={() => revokeKey(key.id)}>Revoke</button>
                    </td>
                </tr>
            {/each}
        </tbody>
    </table>
</div>

2. Usage dashboard - Charts showing API usage over time

3. Quick start guide - Code examples for Python/JavaScript

Key Files

  • backend/app/pgdatabase/models.py - ApiKey model
  • backend/app/services/api_keys.py - Key service (new)
  • backend/app/dependencies.py - Auth dependency
  • backend/app/routes/api_keys.py - API routes (new)
  • frontend/src/routes/settings/api-keys/+page.svelte - Key manager

Related Issues

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions