From 1b835c3a3eaf40c37aba4f69f2e96832c10670b0 Mon Sep 17 00:00:00 2001 From: Bob Roberts Date: Thu, 30 Oct 2025 21:07:53 -0500 Subject: [PATCH] Add simple email/password authentication to frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 21 +++ AUTH_SETUP.md | 190 +++++++++++++++++++++ README.md | 5 +- docs/PRODUCTION_DEPLOYMENT.md | 51 +++++- frontend/src/App.tsx | 28 ++- frontend/src/components/ProtectedRoute.tsx | 31 ++++ frontend/src/pages/Login.tsx | 99 +++++++++++ mcp_server/auth.py | 138 +++++++++++++++ mcp_server/server.py | 63 +++++++ requirements.txt | 5 + 10 files changed, 621 insertions(+), 10 deletions(-) create mode 100644 AUTH_SETUP.md create mode 100644 frontend/src/components/ProtectedRoute.tsx create mode 100644 frontend/src/pages/Login.tsx create mode 100644 mcp_server/auth.py diff --git a/.env.example b/.env.example index d45bddb..1d06d44 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,25 @@ +# ============================================================================ +# Authentication (Optional - Leave empty to disable auth) +# ============================================================================ + +# Login email (required to enable auth) +CLAUDE_OS_EMAIL=admin@example.com + +# Option 1: Plain password (development only - auto-hashed) +CLAUDE_OS_PASSWORD=your_secure_password_here + +# Option 2: Pre-hashed password (production - recommended) +# Generate: python3 -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('your_password'))" +# CLAUDE_OS_PASSWORD_HASH=$2b$12$xyz... + +# JWT secret key (auto-generated if not set) +# Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))" +# CLAUDE_OS_SECRET_KEY=your-super-secret-key-min-32-chars + +# ============================================================================ # Ollama Configuration +# ============================================================================ + OLLAMA_HOST=http://localhost:11434 OLLAMA_MODEL=llama3.1:latest OLLAMA_EMBED_MODEL=nomic-embed-text diff --git a/AUTH_SETUP.md b/AUTH_SETUP.md new file mode 100644 index 0000000..abda42f --- /dev/null +++ b/AUTH_SETUP.md @@ -0,0 +1,190 @@ +# Claude OS Authentication Setup + +Simple email/password authentication to keep strangers out of your Claude OS frontend. + +## Features + +✅ **Optional Authentication** - Disabled by default, enable when needed +✅ **Environment-based** - No database required for user management +✅ **JWT Tokens** - 7-day token expiration +✅ **Secure** - Bcrypt password hashing +✅ **Simple** - Single user account via environment variables + +## Quick Start + +### 1. Enable Authentication + +Set environment variables in your `.env` file or server environment: + +```bash +# Required: Email for login +CLAUDE_OS_EMAIL=admin@example.com + +# Option 1: Plain password (development only - will be hashed automatically) +CLAUDE_OS_PASSWORD=your_secure_password_here + +# Option 2: Pre-hashed password (recommended for production) +CLAUDE_OS_PASSWORD_HASH=$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5hhA82jdg8jpu + +# Optional: Custom secret key for JWT (auto-generated if not set) +CLAUDE_OS_SECRET_KEY=your-super-secret-key-min-32-chars +``` + +### 2. Generate Password Hash (Production) + +For production, use a pre-hashed password instead of plain text: + +```bash +# Install dependencies first +cd ~/Projects/claude-os +source venv/bin/activate +pip install -r requirements.txt + +# Generate hash +python3 -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('your_password_here'))" +``` + +Copy the output and use it as `CLAUDE_OS_PASSWORD_HASH` in your `.env` file. + +### 3. Restart Claude OS + +```bash +# Local (Mac) +./stop.sh +./start.sh + +# Production Server +sudo systemctl restart claude-os +``` + +### 4. Login + +Visit your Claude OS frontend: +- Local: http://localhost:5173/login +- Production: https://your-domain.com/login + +Use the email and password you configured. + +## Production Deployment + +### Server Environment Variables + +Add to `/opt/claude-os/.env`: + +```bash +CLAUDE_OS_EMAIL=admin@pistn.com +CLAUDE_OS_PASSWORD_HASH=$2b$12$xyz... # Generated hash +CLAUDE_OS_SECRET_KEY=your-32-char-secret +``` + +Restart the service: + +```bash +sudo systemctl restart claude-os +``` + +## Disable Authentication + +To disable authentication (open access): + +```bash +# Remove or comment out these variables in .env: +# CLAUDE_OS_EMAIL=... +# CLAUDE_OS_PASSWORD=... +# CLAUDE_OS_PASSWORD_HASH=... +``` + +Restart Claude OS. The frontend will allow access without login. + +## How It Works + +### Backend (FastAPI) + +- **`/api/auth/login`** - Login endpoint, returns JWT token +- **`/api/auth/me`** - Get current user info +- **`/api/auth/status`** - Check if authentication is enabled + +### Frontend (React) + +- **Login Page** - Beautiful gradient login form +- **Auth Context** - Manages authentication state +- **Protected Routes** - Automatically redirects to login if not authenticated +- **Token Storage** - JWT stored in localStorage (7-day expiration) + +### Security Features + +✅ **Bcrypt hashing** - Industry-standard password encryption +✅ **JWT tokens** - Stateless authentication +✅ **Automatic expiration** - Tokens expire after 7 days +✅ **HTTPS recommended** - Use SSL in production +✅ **No database needed** - Single user, environment-based + +## Troubleshooting + +### "Authentication is not configured" + +You haven't set the `CLAUDE_OS_EMAIL` environment variable. Set it and restart. + +### "Incorrect email or password" + +Check your `.env` file: +- Email matches exactly (case-sensitive) +- If using `CLAUDE_OS_PASSWORD`, make sure it's correct +- If using `CLAUDE_OS_PASSWORD_HASH`, regenerate the hash + +### Login page doesn't appear + +Make sure your Claude OS frontend is running: + +```bash +# Local development +cd frontend +npm run dev + +# Production (should be served by Nginx) +curl http://localhost:5173 +``` + +### Token expired + +Tokens last 7 days. Just login again to get a new token. + +## Multiple Users + +This system is designed for single-user access. For multiple users: + +1. **Simple approach**: Share one account with your team +2. **Advanced approach**: Extend the auth system to use a database (requires custom implementation) + +For most teams deploying Claude OS internally, a single shared account is sufficient since it's: +- Behind your firewall +- For trusted team members only +- Just to keep strangers out (not enterprise-grade auth) + +## Security Best Practices + +### Development (Local) + +```bash +CLAUDE_OS_EMAIL=dev@localhost +CLAUDE_OS_PASSWORD=dev123 +``` + +Fine for local development. Password is hashed automatically. + +### Production (Server) + +```bash +CLAUDE_OS_EMAIL=admin@yourcompany.com +CLAUDE_OS_PASSWORD_HASH=$2b$12$xyz... # Pre-hashed +CLAUDE_OS_SECRET_KEY=randomly-generated-32-char-secret +``` + +✅ **Use hashed passwords** +✅ **Use strong secret keys** +✅ **Use HTTPS/SSL** +✅ **Rotate passwords periodically** + +--- + +**Questions?** Check the main Claude OS README or open an issue on GitHub. diff --git a/README.md b/README.md index b2c568a..c022ea8 100644 --- a/README.md +++ b/README.md @@ -371,12 +371,14 @@ your-project/ Agent-OS requires AI for advanced features: **Option 1: Ollama (Local, Free)** + - Requires 8GB+ RAM - Fully private, no data leaves your machine - Install: `https://ollama.ai` - Then: `ollama pull llama2` **Option 2: OpenAI API (Cloud, Paid)** + - Works on any machine - ~$0.02 per request - Requires API key (set during `./install.sh`) @@ -829,11 +831,12 @@ Claude OS optionally integrates with [Agent-OS](https://github.com/builder-metho - **Project**: Agent-OS - Spec-driven development workflow system - **Author**: Builder Methods (CasJam Media LLC) - **License**: MIT -- **Repository**: https://github.com/builder-methods/agent-os +- **Repository**: Agent-OS provides 8 specialized agents for structured feature planning and implementation. We're grateful to Builder Methods for creating such powerful tools and for licensing them under MIT, making this integration possible. If you find Agent-OS valuable, consider: + - ⭐ Starring their repository - 📣 Sharing it with other developers - 🤝 Contributing to their project diff --git a/docs/PRODUCTION_DEPLOYMENT.md b/docs/PRODUCTION_DEPLOYMENT.md index 1469299..b0ee15a 100644 --- a/docs/PRODUCTION_DEPLOYMENT.md +++ b/docs/PRODUCTION_DEPLOYMENT.md @@ -153,7 +153,54 @@ curl "http://localhost:8051/api/kb/list" curl "http://localhost:8051/api/kb/Pistn-project_index/stats" ``` -### 5. Update PISTN Rails App +### 5. Setup Authentication (Recommended for Production) + +Protect the Claude OS frontend from strangers with simple email/password authentication. + +**Add to `/opt/claude-os/.env`:** + +```bash +# Authentication +CLAUDE_OS_EMAIL=admin@pistn.com +CLAUDE_OS_PASSWORD_HASH= +CLAUDE_OS_SECRET_KEY= +``` + +**Generate password hash:** + +```bash +# SSH into server +ssh deploy@staging.pistn.com + +# Generate hash +cd /opt/claude-os +source venv/bin/activate +python3 -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('your_secure_password'))" + +# Copy the output and add to .env as CLAUDE_OS_PASSWORD_HASH +``` + +**Generate secret key:** + +```bash +python3 -c "import secrets; print(secrets.token_urlsafe(32))" +``` + +**Restart Claude OS:** + +```bash +sudo systemctl restart claude-os +``` + +**Test login:** + +Visit `https://staging.pistn.com/login` and use your configured email/password. + +**Note:** If you don't configure these variables, authentication is disabled and the frontend is open access. + +See [`AUTH_SETUP.md`](/AUTH_SETUP.md) for detailed authentication documentation. + +### 6. Update PISTN Rails App **Add to `/var/www/pistn/shared/.env`:** ```bash @@ -211,7 +258,7 @@ end cap production deploy ``` -### 6. Test Integration +### 7. Test Integration ```bash # Test Claude OS directly diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9dc3e1d..c741dcd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,10 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { AuthProvider } from './lib/auth'; import Welcome from './pages/Welcome'; import MainApp from './pages/MainApp'; +import Login from './pages/Login'; +import ProtectedRoute from './components/ProtectedRoute'; const queryClient = new QueryClient({ defaultOptions: { @@ -15,13 +18,24 @@ const queryClient = new QueryClient({ function App() { return ( - - - } /> - } /> - } /> - - + + + + } /> + + + + } /> + + + + } /> + } /> + + + ); } diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..4eea5fe --- /dev/null +++ b/frontend/src/components/ProtectedRoute.tsx @@ -0,0 +1,31 @@ +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../lib/auth'; + +export default function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isAuthEnabled, isLoading } = useAuth(); + + // Show loading state while checking auth + if (isLoading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + // If auth is disabled, allow access + if (!isAuthEnabled) { + return <>{children}; + } + + // If auth is enabled but user not authenticated, redirect to login + if (!isAuthenticated) { + return ; + } + + // User is authenticated, show the protected content + return <>{children}; +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..2730762 --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../lib/auth'; + +export default function Login() { + const navigate = useNavigate(); + const { login } = useAuth(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + await login(email, password); + navigate('/app'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Login failed'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

Claude OS

+

AI-Assisted Development with Persistent Context

+
+ +
+

Sign In

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + required + className="w-full px-4 py-2 rounded-lg bg-white/10 border border-white/20 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent" + placeholder="admin@example.com" + disabled={isLoading} + /> +
+ +
+ + setPassword(e.target.value)} + required + className="w-full px-4 py-2 rounded-lg bg-white/10 border border-white/20 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent" + placeholder="••••••••" + disabled={isLoading} + /> +
+ + +
+ +
+

Authentication keeps strangers out.

+

Contact your administrator for access.

+
+
+ +
+

Powered by Claude OS • Secure & Private

+
+
+
+ ); +} diff --git a/mcp_server/auth.py b/mcp_server/auth.py new file mode 100644 index 0000000..4b7695f --- /dev/null +++ b/mcp_server/auth.py @@ -0,0 +1,138 @@ +""" +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= + + 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 diff --git a/mcp_server/server.py b/mcp_server/server.py index 96ad7de..43221db 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -1566,6 +1566,69 @@ async def handle_mcp_request(request: Request, kb_filter: Optional[str] = None) }) +# ============================================================================ +# Authentication Endpoints +# ============================================================================ + +from mcp_server.auth import ( + authenticate_user, + create_access_token, + is_auth_enabled, + get_current_user +) +from datetime import timedelta + + +class LoginRequest(BaseModel): + email: str + password: str + + +@app.post("/api/auth/login") +async def login(request: LoginRequest): + """Login endpoint - returns JWT token if credentials are valid.""" + if not is_auth_enabled(): + raise HTTPException( + status_code=501, + detail="Authentication is not configured. Set CLAUDE_OS_EMAIL and CLAUDE_OS_PASSWORD in environment." + ) + + if not authenticate_user(request.email, request.password): + raise HTTPException( + status_code=401, + detail="Incorrect email or password" + ) + + access_token = create_access_token( + data={"sub": request.email}, + expires_delta=timedelta(days=7) + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "email": request.email + } + + +@app.get("/api/auth/me") +async def get_me(current_user: dict = Depends(get_current_user)): + """Get current user info.""" + return current_user + + +@app.get("/api/auth/status") +async def auth_status(): + """Check if authentication is enabled.""" + return { + "auth_enabled": is_auth_enabled() + } + + +# ============================================================================ +# Health Check +# ============================================================================ + @app.get("/health") async def health_check(): """ diff --git a/requirements.txt b/requirements.txt index bc73f29..256cb15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,11 @@ fastapi>=0.104.0 uvicorn>=0.24.0 pydantic>=2.0.0 +# Authentication +passlib[bcrypt]>=1.7.4 +python-jose[cryptography]>=3.3.0 +python-multipart>=0.0.6 + # Utilities python-dotenv>=1.0.0 requests>=2.31.0