KEY INSIGHT
API keys are the primary authentication mechanism for programmatic access—treat them like passwords, implement rotation, and provide fine-grained access controls.
API key management in multi-tenant SaaS requires balancing security with developer experience. Keys should be easy to create and rotate but difficult to compromise and easy to revoke.
Key scopes limit what an API key can access. A key for development might only access test models; a production key might access all models but have IP restrictions.
```python
from sqlalchemy import Column, String, Integer, Boolean, DateTime, JSON
from datetime import datetime, timedelta
class ApiKey(Base):
__tablename__ = "api_keys"
id = Column(String(50), primary_key=True)
workspace_id = Column(String(36), ForeignKey("workspaces.id"), nullable=False)
name = Column(String(255), nullable=False)
key_hash = Column(String(64), nullable=False)
# Scoping
allowed_models = Column(JSON, nullable=True) # None = all models
allowed_ips = Column(JSON, nullable=True) # None = any IP
max_requests_per_day = Column(Integer, nullable=True)
# Status
is_active = Column(Boolean, default=True)
expires_at = Column(DateTime, nullable=True)
last_used_at = Column(DateTime, nullable=True)
# Rate tracking
total_usage_kobo = Column(Integer, default=0)
total_tokens = Column(Integer, default=0)
workspace = relationship("Workspace", back_populates="api_keys")
class ApiKeyManager:
def __init__(self, db: Session):
self.db = db
self.token_tracker = TokenTracker()
def create_key(
self,
workspace_id: str,
name: str,
allowed_models: list[str] | None = None,
allowed_ips: list[str] | None = None,
expires_in_days: int | None = 365
) -> tuple[str, ApiKey]:
"""Create a new API key. Returns (full_key, database_record).
The full_key is shown ONLY once—after this, only the hash exists.
"""
key_id = f"sk_{secrets.token_urlsafe(12)}"
secret = secrets.token_urlsafe(32)
key_hash = hashlib.sha256(secret.encode()).hexdigest()
expires_at = None
if expires_in_days:
expires_at = datetime.utcnow() + timedelta(days=expires_in_days)
api_key = ApiKey(
id=key_id,
workspace_id=workspace_id,
name=name,
key_hash=key_hash,
allowed_models=allowed_models,
allowed_ips=allowed_ips,
expires_at=expires_at
)
self.db.add(api_key)
self.db.commit()
# Return full key for user to copy
return f"{key_id}_{secret}", api_key
def validate_key(
self,
full_key: str,
client_ip: str | None = None
) -> ApiKey:
"""Validate API key and check scope restrictions."""
key_id, secret = full_key.split("_", 1)
secret_hash = hashlib.sha256(secret.encode()).hexdigest()
api_key = self.db.query(ApiKey).filter_by(
id=key_id,
key_hash=secret_hash
).first()
if not api_key or not api_key.is_active:
raise ValueError("Invalid or inactive API key")
if api_key.expires_at and api_key.expires_at < datetime.utcnow():
raise ValueError("API key has expired")
if api_key.allowed_ips and client_ip not in api_key.allowed_ips:
raise ValueError(f"IP {client_ip} is not allowed for this key")
# Update last used timestamp
api_key.last_used_at = datetime.utcnow()
self.db.commit()
return api_key
```
A failure scenario: keys created for testing in development environments getting used in production. If allowed_models isn't set, any model is accessible. Consider requiring explicit model whitelisting for production workspaces.