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>
This commit is contained in:
parent
0973601fec
commit
1b835c3a3e
10 changed files with 621 additions and 10 deletions
21
.env.example
21
.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
|
||||
|
|
|
|||
190
AUTH_SETUP.md
Normal file
190
AUTH_SETUP.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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**: <https://github.com/builder-methods/agent-os>
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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=<generated_hash>
|
||||
CLAUDE_OS_SECRET_KEY=<random_32_char_secret>
|
||||
```
|
||||
|
||||
**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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Welcome />} />
|
||||
<Route path="/app" element={<MainApp />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<Welcome />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/app" element={
|
||||
<ProtectedRoute>
|
||||
<MainApp />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
31
frontend/src/components/ProtectedRoute.tsx
Normal file
31
frontend/src/components/ProtectedRoute.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 flex items-center justify-center">
|
||||
<div className="text-white text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto mb-4"></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If auth is disabled, allow access
|
||||
if (!isAuthEnabled) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// If auth is enabled but user not authenticated, redirect to login
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
// User is authenticated, show the protected content
|
||||
return <>{children}</>;
|
||||
}
|
||||
99
frontend/src/pages/Login.tsx
Normal file
99
frontend/src/pages/Login.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-white mb-2">Claude OS</h1>
|
||||
<p className="text-slate-300">AI-Assisted Development with Persistent Context</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/10 backdrop-blur-lg rounded-lg p-8 shadow-2xl border border-white/20">
|
||||
<h2 className="text-2xl font-semibold text-white mb-6">Sign In</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 rounded bg-red-500/20 border border-red-500/50 text-red-200 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-slate-200 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-slate-200 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full bg-purple-600 hover:bg-purple-700 disabled:bg-purple-800 disabled:cursor-not-allowed text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-slate-400">
|
||||
<p>Authentication keeps strangers out.</p>
|
||||
<p className="mt-2">Contact your administrator for access.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center text-xs text-slate-400">
|
||||
<p>Powered by Claude OS • Secure & Private</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
138
mcp_server/auth.py
Normal file
138
mcp_server/auth.py
Normal file
|
|
@ -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=<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
|
||||
|
|
@ -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():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue