Sasha Claude OAuth Integration Plan
WARNING: REJECTED DESIGN - RESEARCH ONLY
This document represents an overly complex OAuth approach that was REJECTED in favor of simple API key authentication.
DO NOT IMPLEMENT THIS DESIGN
See claude-code-simple-integration-plan.md for the actual implementation approach.
This document is kept for research purposes only to show why simpler solutions are better.
Purpose: Integration strategy for Claude Code authentication within Docker container environment [REJECTED]
Created: 2025-01-09
Status: REJECTED - DO NOT USE
Replacement: Simple API Key Integration Plan
Overview
This document outlines the integration of Claude Code CLI authentication within Sasha's Docker container environment. Claude Code requires OAuth authentication (browser-based) or API key authentication, which presents unique challenges in containerized environments.
Authentication Challenge
The Problem
- Claude Code CLI requires authentication to function
- Default authentication is OAuth (opens browser)
- Docker containers don't have browser access
- Users need their own Claude Code subscription
- Credentials must be securely stored and isolated per user
Authentication Methods Available
- OAuth Flow (Claude Pro/Max subscribers)
- API Key (Console.anthropic.com)
- Enterprise (AWS Bedrock, Google Vertex)
Architecture Solution: OAuth Bridge Pattern
βββββββββββββββββββββββ
β User's Browser β
β (Sasha Web UI) β
ββββββββββββ¬βββββββββββ
β 1. Initiate Auth
βΌ
βββββββββββββββββββββββ
β Sasha Server β
β (Node.js) β
β βββββββββββββββββ β
β β OAuth Bridge β ββββ Handles OAuth flow
β βββββββββββββββββ β
ββββββββββββ¬βββββββββββ
β 2. Store Credentials
βΌ
βββββββββββββββββββββββ
β Docker Container β
β βββββββββββββββββ β
β β Claude Code β ββββ Uses stored credentials
β β ~/.claude/ β β
β βββββββββββββββββ β
βββββββββββββββββββββββ
Implementation Components
1. Database Schema for Credentials
File: server/database/init.sql
CREATE TABLE IF NOT EXISTS claude_credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
auth_type TEXT NOT NULL, -- 'oauth' or 'api_key'
credentials_encrypted TEXT, -- Encrypted OAuth tokens or API key
oauth_refresh_token TEXT,
oauth_expires_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
2. Claude Authentication UI Component
File: src/components/ClaudeAuthSetup.jsx (New)
const ClaudeAuthSetup = ({ onComplete }) => {
const [authMethod, setAuthMethod] = useState(null);
const [apiKey, setApiKey] = useState('');
const [oauthPending, setOauthPending] = useState(false);
// Option 1: OAuth Flow
const handleOAuthSetup = async () => {
const response = await fetch('/api/claude/auth/oauth/initiate', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
const { authUrl, state } = await response.json();
// Open OAuth in new window
const authWindow = window.open(authUrl, 'claude-auth',
'width=600,height=700');
setOauthPending(true);
// Poll for completion
const pollInterval = setInterval(async () => {
const status = await fetch(`/api/claude/auth/oauth/status/${state}`);
if (status.ok) {
const { completed } = await status.json();
if (completed) {
clearInterval(pollInterval);
authWindow.close();
setOauthPending(false);
onComplete();
}
}
}, 1000);
};
// Option 2: API Key
const handleApiKeySetup = async () => {
const response = await fetch('/api/claude/auth/api-key', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ apiKey })
});
if (response.ok) {
onComplete();
} else {
alert('Invalid API key. Please check and try again.');
}
};
return (
<div className="claude-auth-setup">
<h2>Connect Claude Code</h2>
<p>Sasha requires Claude Code to operate. Choose your authentication method:</p>
{!authMethod && (
<div className="auth-options">
<button
onClick={() => setAuthMethod('oauth')}
className="auth-option oauth"
>
<h3>Sign in with Claude Account</h3>
<p>For Claude Pro/Max subscribers</p>
<span className="recommended">Recommended</span>
</button>
<button
onClick={() => setAuthMethod('api_key')}
className="auth-option api-key"
>
<h3>Use API Key</h3>
<p>From console.anthropic.com</p>
</button>
</div>
)}
{authMethod === 'oauth' && (
<div className="oauth-flow">
<p>You'll be redirected to Anthropic to sign in.</p>
<p>Make sure pop-ups are enabled for this site.</p>
<button
onClick={handleOAuthSetup}
disabled={oauthPending}
className="primary"
>
{oauthPending ? 'Waiting for authentication...' : 'Open Claude Sign In'}
</button>
{oauthPending && (
<p className="hint">Complete sign-in in the popup window...</p>
)}
</div>
)}
{authMethod === 'api_key' && (
<div className="api-key-flow">
<p>Enter your API key from console.anthropic.com</p>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-ant-..."
className="api-key-input"
/>
<button
onClick={handleApiKeySetup}
disabled={!apiKey}
className="primary"
>
Connect with API Key
</button>
<a
href="https://console.anthropic.com/settings/keys"
target="_blank"
className="help-link"
>
Get your API key β
</a>
</div>
)}
</div>
);
};
3. OAuth Bridge Service
File: server/services/claude-auth-bridge.js (New)
import crypto from 'crypto';
import { encrypt, decrypt } from '../utils/encryption.js';
export class ClaudeAuthBridge {
constructor() {
this.pendingAuth = new Map(); // Track OAuth flows
this.oauth_callback_url = process.env.OAUTH_CALLBACK_URL ||
'http://localhost:3001/api/claude/auth/oauth/callback';
}
// Start OAuth flow
async initiateOAuth(userId) {
// Generate state token for security
const state = crypto.randomBytes(32).toString('hex');
this.pendingAuth.set(state, { userId, timestamp: Date.now() });
// Clean up old pending auth (> 10 minutes)
this.cleanupPendingAuth();
// Build OAuth URL
const authUrl = this.buildOAuthUrl(state);
return { authUrl, state };
}
buildOAuthUrl(state) {
const params = new URLSearchParams({
client_id: process.env.CLAUDE_CLIENT_ID || 'claude-code-cli',
redirect_uri: this.oauth_callback_url,
response_type: 'code',
scope: 'claude-code',
state: state
});
return `https://console.anthropic.com/oauth/authorize?${params}`;
}
// Handle OAuth callback
async handleOAuthCallback(code, state) {
const pending = this.pendingAuth.get(state);
if (!pending) {
throw new Error('Invalid or expired OAuth state');
}
const { userId } = pending;
try {
// Exchange code for tokens
const tokens = await this.exchangeCodeForTokens(code);
// Store encrypted credentials
await this.storeCredentials(userId, 'oauth', tokens);
// Configure Claude in container
await this.configureClaudeOAuth(userId, tokens);
this.pendingAuth.delete(state);
return { success: true, userId };
} catch (error) {
this.pendingAuth.delete(state);
throw error;
}
}
// Exchange OAuth code for tokens
async exchangeCodeForTokens(code) {
const response = await fetch('https://console.anthropic.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: code,
client_id: process.env.CLAUDE_CLIENT_ID,
client_secret: process.env.CLAUDE_CLIENT_SECRET,
redirect_uri: this.oauth_callback_url
})
});
if (!response.ok) {
throw new Error('Failed to exchange OAuth code');
}
return response.json();
}
// Alternative: API Key authentication
async setupApiKey(userId, apiKey) {
// Validate API key
const valid = await this.validateApiKey(apiKey);
if (!valid) {
throw new Error('Invalid API key');
}
// Store encrypted
await this.storeCredentials(userId, 'api_key', { apiKey });
// Configure Claude in container
await this.configureClaudeApiKey(userId, apiKey);
return { success: true };
}
// Validate API key with Claude
async validateApiKey(apiKey) {
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
},
body: JSON.stringify({
model: 'claude-3-haiku-20240307',
max_tokens: 10,
messages: [{ role: 'user', content: 'test' }]
})
});
// If we get 401, key is invalid
// If we get 200 or 400 (bad request), key is valid
return response.status !== 401;
} catch (error) {
console.error('API key validation error:', error);
return false;
}
}
// Store credentials in database (encrypted)
async storeCredentials(userId, authType, credentials) {
const encrypted = encrypt(JSON.stringify(credentials));
await db.upsertClaudeCredentials(userId, {
auth_type: authType,
credentials_encrypted: encrypted,
oauth_refresh_token: credentials.refresh_token || null,
oauth_expires_at: credentials.expires_at || null
});
}
// Configure Claude Code in container with OAuth
async configureClaudeOAuth(userId, tokens) {
const userClaudeDir = `/workspace/users/${userId}/.claude`;
// Create user-specific Claude config directory
await fs.mkdir(userClaudeDir, { recursive: true });
// Write OAuth credentials
const credentials = {
oauth_token: tokens.access_token,
oauth_refresh_token: tokens.refresh_token,
oauth_expires_at: tokens.expires_at,
auth_type: 'oauth'
};
await fs.writeFile(
`${userClaudeDir}/credentials.json`,
JSON.stringify(credentials, null, 2)
);
// Set permissions
await fs.chmod(`${userClaudeDir}/credentials.json`, 0o600);
}
// Configure Claude Code with API key
async configureClaudeApiKey(userId, apiKey) {
const userClaudeDir = `/workspace/users/${userId}/.claude`;
await fs.mkdir(userClaudeDir, { recursive: true });
// Write config for API key auth
const config = {
auth_method: 'api_key',
api_key: apiKey
};
await fs.writeFile(
`${userClaudeDir}/config.json`,
JSON.stringify(config, null, 2)
);
// Set permissions
await fs.chmod(`${userClaudeDir}/config.json`, 0o600);
// Also set environment variable for user's session
process.env[`ANTHROPIC_API_KEY_${userId}`] = apiKey;
}
// Get user's auth status
async getUserAuthStatus(userId) {
const credentials = await db.getClaudeCredentials(userId);
if (!credentials) {
return { authenticated: false };
}
// Check if OAuth token expired
if (credentials.auth_type === 'oauth' && credentials.oauth_expires_at) {
const expired = new Date(credentials.oauth_expires_at) < new Date();
if (expired) {
// TODO: Implement token refresh
return { authenticated: false, needsRefresh: true };
}
}
return {
authenticated: true,
authType: credentials.auth_type
};
}
// Cleanup old pending auth
cleanupPendingAuth() {
const tenMinutesAgo = Date.now() - (10 * 60 * 1000);
for (const [state, data] of this.pendingAuth.entries()) {
if (data.timestamp < tenMinutesAgo) {
this.pendingAuth.delete(state);
}
}
}
}
4. Claude Executor Service
File: server/services/claude-executor.js (New)
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import { decrypt } from '../utils/encryption.js';
export class ClaudeExecutor extends EventEmitter {
constructor() {
super();
this.activeSessions = new Map();
}
// Execute Claude command for user
async executeForUser(userId, projectId, prompt) {
// Get user's credentials
const credentials = await this.getUserCredentials(userId);
if (!credentials) {
throw new Error('User not authenticated with Claude');
}
// Set up environment
const env = await this.setupEnvironment(userId, projectId, credentials);
// Execute Claude
return this.runClaude(env, prompt);
}
// Get and decrypt user credentials
async getUserCredentials(userId) {
const encryptedCreds = await db.getClaudeCredentials(userId);
if (!encryptedCreds) return null;
const credentials = JSON.parse(
decrypt(encryptedCreds.credentials_encrypted)
);
return {
type: encryptedCreds.auth_type,
...credentials
};
}
// Set up environment for Claude execution
async setupEnvironment(userId, projectId, credentials) {
const userClaudeDir = `/workspace/users/${userId}/.claude`;
const projectPath = `/workspace/projects/${projectId}`;
const env = {
...process.env,
CLAUDE_HOME: userClaudeDir,
CLAUDE_PROJECT_ROOT: projectPath,
HOME: `/workspace/users/${userId}` // User-specific home
};
// Add API key to environment if using API key auth
if (credentials.type === 'api_key') {
env.ANTHROPIC_API_KEY = credentials.apiKey;
}
return env;
}
// Run Claude Code CLI
runClaude(env, prompt) {
return new Promise((resolve, reject) => {
const claude = spawn('claude', ['--no-interactive', '-p', prompt], {
env,
cwd: env.CLAUDE_PROJECT_ROOT
});
let output = '';
let error = '';
claude.stdout.on('data', (data) => {
output += data.toString();
this.emit('output', data.toString());
});
claude.stderr.on('data', (data) => {
error += data.toString();
this.emit('error', data.toString());
});
claude.on('close', (code) => {
if (code === 0) {
resolve(output);
} else {
reject(new Error(`Claude exited with code ${code}: ${error}`));
}
});
// Timeout after 10 minutes
setTimeout(() => {
claude.kill();
reject(new Error('Claude execution timeout'));
}, 10 * 60 * 1000);
});
}
// Create interactive session for streaming
async createInteractiveSession(userId, projectId) {
// Get credentials and environment
const credentials = await this.getUserCredentials(userId);
if (!credentials) {
throw new Error('User not authenticated with Claude');
}
const env = await this.setupEnvironment(userId, projectId, credentials);
// Spawn interactive Claude process
const claude = spawn('claude', ['chat', '--stream'], {
env,
cwd: env.CLAUDE_PROJECT_ROOT
});
const sessionId = `${userId}-${projectId}-${Date.now()}`;
const session = {
process: claude,
userId,
projectId,
sessionId,
sendPrompt: (prompt) => {
claude.stdin.write(prompt + '\n');
},
close: () => {
claude.stdin.end();
claude.kill();
this.activeSessions.delete(sessionId);
}
};
this.activeSessions.set(sessionId, session);
// Handle process events
claude.stdout.on('data', (data) => {
this.emit('session-output', {
sessionId,
data: data.toString()
});
});
claude.stderr.on('data', (data) => {
this.emit('session-error', {
sessionId,
error: data.toString()
});
});
claude.on('close', (code) => {
this.emit('session-closed', {
sessionId,
code
});
this.activeSessions.delete(sessionId);
});
return session;
}
// Get active session
getSession(sessionId) {
return this.activeSessions.get(sessionId);
}
// Clean up sessions for user
cleanupUserSessions(userId) {
for (const [sessionId, session] of this.activeSessions) {
if (session.userId === userId) {
session.close();
}
}
}
}
5. API Routes for Authentication
File: server/routes/claude-auth.js (New)
import express from 'express';
import { ClaudeAuthBridge } from '../services/claude-auth-bridge.js';
import { authenticateToken } from '../middleware/auth.js';
const router = express.Router();
const authBridge = new ClaudeAuthBridge();
// Initiate OAuth flow
router.post('/oauth/initiate', authenticateToken, async (req, res) => {
try {
const { authUrl, state } = await authBridge.initiateOAuth(req.user.id);
res.json({ authUrl, state });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// OAuth callback (from Anthropic)
router.get('/oauth/callback', async (req, res) => {
try {
const { code, state } = req.query;
if (!code || !state) {
throw new Error('Missing OAuth parameters');
}
await authBridge.handleOAuthCallback(code, state);
// Close the popup window
res.send(`
<html>
<body>
<h1>Authentication Successful!</h1>
<p>You can close this window.</p>
<script>
window.close();
</script>
</body>
</html>
`);
} catch (error) {
res.status(400).send(`
<html>
<body>
<h1>Authentication Failed</h1>
<p>${error.message}</p>
<p>Please close this window and try again.</p>
</body>
</html>
`);
}
});
// Check OAuth status
router.get('/oauth/status/:state', authenticateToken, async (req, res) => {
const status = await authBridge.getUserAuthStatus(req.user.id);
res.json({ completed: status.authenticated });
});
// API key authentication
router.post('/api-key', authenticateToken, async (req, res) => {
try {
const { apiKey } = req.body;
if (!apiKey) {
return res.status(400).json({ error: 'API key required' });
}
await authBridge.setupApiKey(req.user.id, apiKey);
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Get auth status
router.get('/status', authenticateToken, async (req, res) => {
const status = await authBridge.getUserAuthStatus(req.user.id);
res.json(status);
});
export default router;
6. Docker Configuration
File: Dockerfile
FROM node:20-alpine
# Install dependencies for Claude Code
RUN apk add --no-cache \
python3 \
py3-pip \
git \
bash
# Install Claude Code CLI
RUN npm install -g @anthropic/claude-code-cli
# Create workspace directories
RUN mkdir -p /workspace/projects
RUN mkdir -p /workspace/users
RUN mkdir -p /workspace/.claude
# Set up app
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Environment
ENV CLAUDE_HOME=/workspace/.claude
ENV CLAUDE_PROJECTS=/workspace/projects
ENV NODE_ENV=production
EXPOSE 3001
CMD ["node", "server.js"]
File: docker-compose.yml
version: '3.8'
services:
sasha:
build: .
ports:
- "3001:3001"
volumes:
# Persist user data
- sasha-projects:/workspace/projects
- sasha-users:/workspace/users
- sasha-claude:/workspace/.claude
environment:
- NODE_ENV=production
- DB_PATH=/data/sasha.db
- OAUTH_CALLBACK_URL=${OAUTH_CALLBACK_URL}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
volumes:
- sasha-data:/data
restart: unless-stopped
volumes:
sasha-projects:
sasha-users:
sasha-claude:
sasha-data:
Integration Flow
First-Time Setup
Research Execution
Security Considerations
Credential Storage
- All credentials encrypted at rest
- User-specific encryption keys
- Credentials isolated per user
- No credentials in environment variables visible to other users
Container Isolation
- User-specific directories
- File permissions (600) on credential files
- Process isolation per user
- Resource limits per session
OAuth Security
- State parameter for CSRF protection
- Timeout on pending auth (10 minutes)
- Secure callback URL validation
- Token refresh implementation needed
API Key Security
- Validated before storage
- Never logged or exposed
- Encrypted in database
- Injected only at execution time
Testing Strategy
Local Development Testing
- Mock Mode: Use mock Claude responses without real authentication
- API Key Testing: Use development API key
- OAuth Testing: Set up localhost callback URL
Integration Testing
// Test authentication flow
describe('Claude Authentication', () => {
it('should handle OAuth flow', async () => {
// Test OAuth initiation
// Test callback handling
// Test credential storage
});
it('should validate API keys', async () => {
// Test valid key
// Test invalid key
// Test key storage
});
it('should execute Claude commands', async () => {
// Test with OAuth credentials
// Test with API key
// Test error handling
});
});
Deployment Considerations
Environment Variables
# OAuth Configuration
OAUTH_CALLBACK_URL=https://yourdomain.com/api/claude/auth/oauth/callback
CLAUDE_CLIENT_ID=your-client-id
CLAUDE_CLIENT_SECRET=your-client-secret
# Security
ENCRYPTION_KEY=your-256-bit-encryption-key
# Optional: Default to API key mode
DEFAULT_AUTH_METHOD=api_key
Production Setup
- Configure OAuth callback URL with Anthropic
- Set up SSL/TLS for secure OAuth flow
- Configure persistent volumes for credentials
- Set up credential backup strategy
- Implement token refresh for OAuth
- Add monitoring for auth failures
Error Handling
Common Errors and Solutions
| Error | Cause | Solution |
|---|---|---|
| OAuth popup blocked | Browser blocks popups | Show instruction to enable |
| Invalid API key | Wrong or expired key | Prompt for new key |
| OAuth timeout | User took too long | Retry authentication |
| Container permission denied | Incorrect file permissions | Fix in Dockerfile |
| Claude not found | CLI not installed | Rebuild container |
| Rate limit exceeded | Too many API calls | Implement rate limiting |
Related Resources
- Claude Code Documentation
- OAuth 2.0 Specification
- Docker Security Best Practices
- Organization Setup UI Plan
Success Metrics
- Authentication success rate > 95%
- OAuth flow completion < 30 seconds
- API key validation < 2 seconds
- Credential persistence 100%
- Zero credential leaks
- Session stability > 99%
Next Steps
- Implement OAuth token refresh mechanism
- Add multi-factor authentication support
- Create credential migration tools
- Add authentication analytics
- Implement credential rotation policy
- Add support for enterprise SSO