claude-os/mcp_server/auth.py
Bob Roberts 1b835c3a3e Add simple email/password authentication to frontend
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>
2025-10-30 21:07:53 -05:00

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