Implemented optional authentication to keep strangers out of the Claude OS frontend when deployed to production servers. Backend (FastAPI): - mcp_server/auth.py: JWT-based authentication * Bcrypt password hashing * Environment-based user credentials (no database needed) * 7-day token expiration * Optional - disabled by default - mcp_server/server.py: Auth endpoints * POST /api/auth/login - Returns JWT token * GET /api/auth/me - Get current user * GET /api/auth/status - Check if auth enabled - requirements.txt: Added dependencies * passlib[bcrypt] for password hashing * python-jose[cryptography] for JWT * python-multipart for form data Frontend (React): - frontend/src/lib/auth.tsx: Auth context and hooks * Manages authentication state * Automatic token validation * localStorage for token persistence - frontend/src/pages/Login.tsx: Beautiful login page * Gradient design matching Claude OS branding * Clear error messages * Loading states - frontend/src/components/ProtectedRoute.tsx: Route protection * Automatically redirects to login if not authenticated * Shows loading state during auth check * Bypasses auth if disabled - frontend/src/App.tsx: Integrated auth * Wrapped app in AuthProvider * Protected routes for Welcome and MainApp * Login route for authentication Documentation: - AUTH_SETUP.md: Complete authentication guide * Quick start instructions * Password hash generation * Production deployment steps * Troubleshooting guide - .env.example: Added auth configuration * CLAUDE_OS_EMAIL (login email) * CLAUDE_OS_PASSWORD (dev - plain text) * CLAUDE_OS_PASSWORD_HASH (production - bcrypt hash) * CLAUDE_OS_SECRET_KEY (JWT secret) - docs/PRODUCTION_DEPLOYMENT.md: Added Step 5 * Authentication setup for production * Hash and secret generation commands * Testing instructions Features: ✅ Optional authentication (disabled by default) ✅ Single user via environment variables ✅ Secure bcrypt hashing ✅ JWT tokens with 7-day expiration ✅ Beautiful login UI ✅ No database required for user management Usage: 1. Set CLAUDE_OS_EMAIL and CLAUDE_OS_PASSWORD in .env 2. Restart Claude OS 3. Visit /login to authenticate 4. Token valid for 7 days To disable auth: Remove CLAUDE_OS_EMAIL from .env 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
138 lines
4 KiB
Python
138 lines
4 KiB
Python
"""
|
|
Simple authentication for Claude OS.
|
|
Uses environment variables for credentials - no database needed.
|
|
"""
|
|
|
|
import os
|
|
import secrets
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional
|
|
from fastapi import HTTPException, Security, Depends
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
import jwt
|
|
from passlib.context import CryptContext
|
|
|
|
# Password hashing
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
|
|
# JWT settings
|
|
SECRET_KEY = os.getenv("CLAUDE_OS_SECRET_KEY", secrets.token_urlsafe(32))
|
|
ALGORITHM = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
|
|
|
|
# Security
|
|
security = HTTPBearer()
|
|
|
|
|
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
"""Verify a password against its hash."""
|
|
return pwd_context.verify(plain_password, hashed_password)
|
|
|
|
|
|
def get_password_hash(password: str) -> str:
|
|
"""Hash a password."""
|
|
return pwd_context.hash(password)
|
|
|
|
|
|
def get_user_credentials() -> dict:
|
|
"""
|
|
Get user credentials from environment variables.
|
|
|
|
Set in .env or environment:
|
|
CLAUDE_OS_EMAIL=admin@example.com
|
|
CLAUDE_OS_PASSWORD_HASH=<bcrypt hash>
|
|
|
|
To generate a password hash:
|
|
python3 -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('your_password'))"
|
|
"""
|
|
email = os.getenv("CLAUDE_OS_EMAIL")
|
|
password_hash = os.getenv("CLAUDE_OS_PASSWORD_HASH")
|
|
|
|
# For development, allow plain password (will be hashed on the fly)
|
|
password_plain = os.getenv("CLAUDE_OS_PASSWORD")
|
|
|
|
if not email:
|
|
# Default to disabled auth if not configured
|
|
return None
|
|
|
|
if password_hash:
|
|
return {"email": email, "password_hash": password_hash}
|
|
elif password_plain:
|
|
# Hash the plain password (for development only)
|
|
return {"email": email, "password_hash": get_password_hash(password_plain)}
|
|
else:
|
|
return None
|
|
|
|
|
|
def authenticate_user(email: str, password: str) -> bool:
|
|
"""Authenticate a user by email and password."""
|
|
creds = get_user_credentials()
|
|
|
|
if not creds:
|
|
# Auth disabled
|
|
return False
|
|
|
|
if email != creds["email"]:
|
|
return False
|
|
|
|
return verify_password(password, creds["password_hash"])
|
|
|
|
|
|
def create_access_token(data: dict, 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)
|
|
|
|
to_encode.update({"exp": expire})
|
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
return encoded_jwt
|
|
|
|
|
|
def decode_access_token(token: str) -> Optional[dict]:
|
|
"""Decode and validate a JWT access token."""
|
|
try:
|
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
return payload
|
|
except jwt.PyJWTError:
|
|
return None
|
|
|
|
|
|
def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security)) -> dict:
|
|
"""
|
|
Dependency to get the current authenticated user.
|
|
Use in FastAPI routes: current_user = Depends(get_current_user)
|
|
"""
|
|
creds = get_user_credentials()
|
|
|
|
# If auth not configured, allow all requests
|
|
if not creds:
|
|
return {"email": "guest", "auth_disabled": True}
|
|
|
|
token = credentials.credentials
|
|
payload = decode_access_token(token)
|
|
|
|
if payload is None:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="Invalid authentication credentials",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
email = payload.get("sub")
|
|
if email is None:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="Invalid authentication credentials",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
return {"email": email}
|
|
|
|
|
|
def is_auth_enabled() -> bool:
|
|
"""Check if authentication is enabled."""
|
|
return get_user_credentials() is not None
|