Implement complete Bearer Token and API key authentication system

- Create comprehensive authentication backend with JWT and API key support
- Add database models for users, API keys, and tokens with proper security
- Implement authentication middleware and API endpoints
- Build complete frontend authentication UI with:
  - LoginForm component with JWT authentication
  - APIKeyManager for creating and managing API keys
  - AuthDashboard for comprehensive auth management
  - AuthContext for state management and authenticated requests
- Initialize database with default admin user (admin/admin123)
- Add proper token refresh, validation, and blacklisting
- Implement scope-based API key authorization system

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-07-10 20:52:58 +10:00
parent d19c9d596f
commit 7af5b47477
10 changed files with 2535 additions and 73 deletions

View File

@@ -0,0 +1,199 @@
"""
Authentication dependencies for FastAPI endpoints.
Provides dependency injection for authentication and authorization.
"""
from typing import Optional, Dict, Any, List
from fastapi import Depends, HTTPException, status, Request, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import AuthManager, AuthenticationError
security = HTTPBearer(auto_error=False)
def get_api_key_from_header(x_api_key: Optional[str] = Header(None)) -> Optional[str]:
"""Extract API key from X-API-Key header."""
return x_api_key
def get_current_user_context(
request: Request,
db: Session = Depends(get_db),
authorization: Optional[HTTPAuthorizationCredentials] = Depends(security),
api_key: Optional[str] = Depends(get_api_key_from_header),
) -> Dict[str, Any]:
"""
Get current authenticated user context.
Supports both JWT Bearer tokens and API key authentication.
"""
try:
user_context = AuthManager.authenticate_request(
session=db,
authorization=authorization,
api_key=api_key
)
# Add request metadata
user_context["request_ip"] = request.client.host if request.client else None
user_context["user_agent"] = request.headers.get("user-agent")
return user_context
except AuthenticationError as e:
raise HTTPException(
status_code=e.status_code,
detail=e.message,
headers={"WWW-Authenticate": "Bearer"},
)
def get_current_user(
user_context: Dict[str, Any] = Depends(get_current_user_context),
) -> Dict[str, Any]:
"""Get current authenticated user (alias for get_current_user_context)."""
return user_context
def get_current_active_user(
user_context: Dict[str, Any] = Depends(get_current_user_context),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""Get current authenticated and active user."""
from app.models.auth import User
user = db.query(User).filter(User.id == user_context["user_id"]).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return user_context
def get_current_superuser(
user_context: Dict[str, Any] = Depends(get_current_active_user),
) -> Dict[str, Any]:
"""Get current authenticated superuser."""
if not user_context.get("is_superuser", False):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return user_context
def require_scope(required_scope: str):
"""
Dependency factory to require specific scope for an endpoint.
Usage:
@app.get("/admin/users", dependencies=[Depends(require_scope("admin"))])
async def get_users():
...
"""
def scope_dependency(
user_context: Dict[str, Any] = Depends(get_current_active_user)
) -> Dict[str, Any]:
from app.core.security import APIKeyManager
user_scopes = user_context.get("scopes", [])
if not APIKeyManager.check_scope_permission(user_scopes, required_scope):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Insufficient permissions. Required scope: {required_scope}"
)
return user_context
return scope_dependency
def require_scopes(required_scopes: List[str]):
"""
Dependency factory to require multiple scopes for an endpoint.
User must have ALL specified scopes.
"""
def scopes_dependency(
user_context: Dict[str, Any] = Depends(get_current_active_user)
) -> Dict[str, Any]:
from app.core.security import APIKeyManager
user_scopes = user_context.get("scopes", [])
for scope in required_scopes:
if not APIKeyManager.check_scope_permission(user_scopes, scope):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Insufficient permissions. Required scopes: {', '.join(required_scopes)}"
)
return user_context
return scopes_dependency
def require_any_scope(required_scopes: List[str]):
"""
Dependency factory to require at least one of the specified scopes.
User must have ANY of the specified scopes.
"""
def any_scope_dependency(
user_context: Dict[str, Any] = Depends(get_current_active_user)
) -> Dict[str, Any]:
from app.core.security import APIKeyManager
user_scopes = user_context.get("scopes", [])
for scope in required_scopes:
if APIKeyManager.check_scope_permission(user_scopes, scope):
return user_context
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Insufficient permissions. Required one of: {', '.join(required_scopes)}"
)
return any_scope_dependency
# Optional authentication (won't raise error if not authenticated)
def get_optional_user_context(
db: Session = Depends(get_db),
authorization: Optional[HTTPAuthorizationCredentials] = Depends(security),
api_key: Optional[str] = Depends(get_api_key_from_header),
) -> Optional[Dict[str, Any]]:
"""
Get current user context if authenticated, None otherwise.
Useful for endpoints that work with or without authentication.
"""
try:
return AuthManager.authenticate_request(
session=db,
authorization=authorization,
api_key=api_key
)
except AuthenticationError:
return None
# Common scope dependencies for convenience
require_admin = require_scope("admin")
require_agents_read = require_scope("agents:read")
require_agents_write = require_scope("agents:write")
require_workflows_read = require_scope("workflows:read")
require_workflows_write = require_scope("workflows:write")
require_tasks_read = require_scope("tasks:read")
require_tasks_write = require_scope("tasks:write")
require_metrics_read = require_scope("metrics:read")
require_system_read = require_scope("system:read")
require_system_write = require_scope("system:write")

127
backend/app/core/init_db.py Normal file
View File

@@ -0,0 +1,127 @@
"""
Database initialization script for Hive platform.
Creates all tables and sets up initial data.
"""
import logging
from sqlalchemy.orm import Session
from app.core.database import engine, SessionLocal
from app.models.auth import Base as AuthBase, User, API_SCOPES
from app.models.auth import APIKey
# Import other model bases here as they're created
# from app.models.workflows import Base as WorkflowsBase
# from app.models.agents import Base as AgentsBase
def create_tables():
"""Create all database tables."""
try:
# Create auth tables
AuthBase.metadata.create_all(bind=engine)
# Add other model bases here
# WorkflowsBase.metadata.create_all(bind=engine)
# AgentsBase.metadata.create_all(bind=engine)
logging.info("Database tables created successfully")
return True
except Exception as e:
logging.error(f"Failed to create database tables: {e}")
return False
def create_initial_user(db: Session):
"""Create initial admin user if none exists."""
try:
# Check if any users exist
user_count = db.query(User).count()
if user_count > 0:
logging.info("Users already exist, skipping initial user creation")
return True
# Create initial admin user
admin_user = User(
username="admin",
email="admin@hive.local",
full_name="Hive Administrator",
hashed_password=User.hash_password("admin123"), # Change this!
is_active=True,
is_superuser=True,
is_verified=True
)
db.add(admin_user)
db.commit()
db.refresh(admin_user)
logging.info("Initial admin user created: admin/admin123")
logging.warning("SECURITY: Please change the default admin password!")
# Create initial API key for the admin user
from app.core.security import APIKeyManager
plain_key, hashed_key, prefix = APIKeyManager.generate_api_key()
admin_api_key = APIKey(
user_id=admin_user.id,
name="Default Admin API Key",
key_hash=hashed_key,
key_prefix=prefix,
is_active=True
)
admin_api_key.set_scopes(["admin"])
db.add(admin_api_key)
db.commit()
logging.info(f"Initial admin API key created: {plain_key}")
logging.warning("SECURITY: Save this API key securely, it won't be shown again!")
return True
except Exception as e:
logging.error(f"Failed to create initial user: {e}")
db.rollback()
return False
def initialize_database():
"""Initialize the complete database."""
logging.info("Starting database initialization...")
# Create tables
if not create_tables():
return False
# Create initial data
db = SessionLocal()
try:
# Create initial admin user
if not create_initial_user(db):
return False
logging.info("Database initialization completed successfully")
return True
except Exception as e:
logging.error(f"Database initialization failed: {e}")
return False
finally:
db.close()
if __name__ == "__main__":
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Initialize database
success = initialize_database()
if success:
print("✅ Database initialization completed successfully")
print("🔑 Default admin credentials: admin/admin123")
print("⚠️ SECURITY: Please change the default password immediately!")
else:
print("❌ Database initialization failed")
exit(1)

View File

@@ -0,0 +1,289 @@
"""
Security utilities for JWT token generation, validation, and API key management.
"""
import os
import uuid
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
import jwt
from fastapi import HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Request
from sqlalchemy.orm import Session
# JWT Configuration
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-this-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "30"))
# Security scheme
security = HTTPBearer(auto_error=False)
class TokenManager:
"""Manages JWT token creation, validation, and refresh."""
@staticmethod
def create_access_token(
data: Dict[str, Any],
expires_delta: Optional[timedelta] = None
) -> str:
"""Create a JWT access token."""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
# Add standard claims
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": "access",
"jti": str(uuid.uuid4()), # JWT ID for blacklisting
})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@staticmethod
def create_refresh_token(
user_id: int,
expires_delta: Optional[timedelta] = None
) -> str:
"""Create a JWT refresh token."""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
to_encode = {
"sub": str(user_id),
"exp": expire,
"iat": datetime.utcnow(),
"type": "refresh",
"jti": str(uuid.uuid4()),
}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@staticmethod
def verify_token(token: str) -> Dict[str, Any]:
"""Verify and decode a JWT token."""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
except jwt.JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
@staticmethod
def extract_user_id(token: str) -> int:
"""Extract user ID from a valid token."""
payload = TokenManager.verify_token(token)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token missing user information"
)
return int(user_id)
@staticmethod
def get_token_claims(token: str) -> Dict[str, Any]:
"""Get all claims from a token without verification (for expired tokens)."""
try:
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_exp": False})
except jwt.JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token format"
)
class APIKeyManager:
"""Manages API key generation, validation, and permissions."""
@staticmethod
def generate_api_key() -> tuple[str, str, str]:
"""
Generate a new API key.
Returns: (plain_key, hashed_key, prefix)
"""
from app.models.auth import APIKey
plain_key, hashed_key = APIKey.generate_api_key()
prefix = plain_key[:8] # First 8 characters for identification
return plain_key, hashed_key, prefix
@staticmethod
def validate_api_key(session: Session, api_key: str) -> Optional[Dict[str, Any]]:
"""
Validate an API key and return user/key information.
Returns None if invalid.
"""
from app.models.auth import APIKey, User
# Find API key by trying to match the hash
api_keys = session.query(APIKey).filter(APIKey.is_active == True).all()
for key_record in api_keys:
if APIKey.verify_api_key(api_key, key_record.key_hash):
if not key_record.is_valid():
return None
# Get user information
user = session.query(User).filter(User.id == key_record.user_id).first()
if not user or not user.is_active:
return None
# Record usage
key_record.record_usage()
session.commit()
return {
"user_id": user.id,
"username": user.username,
"api_key_id": key_record.id,
"scopes": key_record.get_scopes(),
"is_superuser": user.is_superuser,
}
return None
@staticmethod
def check_scope_permission(user_scopes: List[str], required_scope: str) -> bool:
"""Check if user has required scope permission."""
# Admin users have all permissions
if "admin" in user_scopes:
return True
# Check for specific scope
if required_scope in user_scopes:
return True
# Check for wildcard permissions (e.g., "workflows:*" covers "workflows:read")
scope_parts = required_scope.split(":")
if len(scope_parts) >= 2:
wildcard_scope = f"{scope_parts[0]}:*"
if wildcard_scope in user_scopes:
return True
return False
class AuthenticationError(Exception):
"""Custom exception for authentication errors."""
def __init__(self, message: str, status_code: int = status.HTTP_401_UNAUTHORIZED):
self.message = message
self.status_code = status_code
super().__init__(self.message)
class AuthManager:
"""Main authentication manager combining JWT and API key auth."""
@staticmethod
def authenticate_request(
session: Session,
authorization: Optional[HTTPAuthorizationCredentials] = None,
api_key: Optional[str] = None
) -> Dict[str, Any]:
"""
Authenticate a request using either Bearer token or API key.
Returns user context information.
"""
# Try API key authentication first
if api_key:
user_context = APIKeyManager.validate_api_key(session, api_key)
if user_context:
user_context["auth_type"] = "api_key"
return user_context
else:
raise AuthenticationError("Invalid API key")
# Try JWT Bearer token authentication
if authorization and authorization.scheme.lower() == "bearer":
try:
payload = TokenManager.verify_token(authorization.credentials)
# Check if token is blacklisted
from app.models.auth import TokenBlacklist
jti = payload.get("jti")
if jti and TokenBlacklist.is_token_blacklisted(session, jti):
raise AuthenticationError("Token has been revoked")
# Get user information
user_id = int(payload.get("sub"))
from app.models.auth import User
user = session.query(User).filter(User.id == user_id).first()
if not user or not user.is_active:
raise AuthenticationError("User not found or inactive")
return {
"user_id": user.id,
"username": user.username,
"scopes": ["admin"] if user.is_superuser else [],
"is_superuser": user.is_superuser,
"auth_type": "jwt",
"token_jti": jti,
}
except HTTPException as e:
raise AuthenticationError(e.detail, e.status_code)
raise AuthenticationError("No valid authentication provided")
@staticmethod
def require_scope(required_scope: str):
"""Decorator to require specific scope for an endpoint."""
def decorator(func):
def wrapper(*args, **kwargs):
# This will be implemented in the dependency injection system
return func(*args, **kwargs)
return wrapper
return decorator
def create_token_response(user_id: int, user_data: Dict[str, Any]) -> Dict[str, Any]:
"""Create a complete token response with access and refresh tokens."""
# Create access token with user data
access_token_data = {
"sub": str(user_id),
"username": user_data.get("username"),
"scopes": user_data.get("scopes", []),
}
access_token = TokenManager.create_access_token(access_token_data)
refresh_token = TokenManager.create_refresh_token(user_id)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60, # seconds
"user": user_data,
}
def get_password_hash(password: str) -> str:
"""Hash a password for storage."""
from app.models.auth import User
return User.hash_password(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash."""
from app.models.auth import pwd_context
return pwd_context.verify(plain_password, hashed_password)