ADR-021: User Management & Authentication
Status: Accepted Date: 2025-10-06 Deciders: Development Team, Security Team Related: ADR-020 (GCP Deployment), ADR-004 (FoundationDB), ADR-017 (WebSocket)
Context
The AZ1.AI llm IDE needs a comprehensive user management and authentication system to support:
- Multi-tenancy: Multiple users with isolated workspaces
- Access Control: Role-based permissions (RBAC)
- Authentication: Secure login with JWT tokens
- Authorization: Fine-grained resource access control
- API Keys: For programmatic access
- OAuth Integration: Social login (Google, GitHub)
- Session Management: Track active user sessions
- User Quotas: llm usage limits, storage limits
Current State
- No user authentication system
- Single-user local development mode
- No access control
- No quota tracking
Requirements
- Secure Authentication: Industry-standard auth (JWT, OAuth 2.0)
- Multi-Provider: Support email/password + OAuth providers
- RBAC: Roles (admin, developer, viewer) with permissions
- Session Management: Track active sessions, force logout
- API Keys: For CLI and API access
- User Isolation: Per-user workspaces, sessions, files
- Quota Tracking: llm usage, storage, API calls
- Scalability: Handle 10K+ users
Decision
We will implement a comprehensive user management system using JWT authentication with OAuth 2.0 providers:
Architecture
┌────────────────────────────────────────────────────────────────┐
│ Browser (theia Frontend) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Authentication Service │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │ │
│ │ │ Login │ │ Register │ │ OAuth Popup │ │ │
│ │ │ Form │ │ Form │ │ (Google/GH) │ │ │
│ │ └────────────┘ └────────────┘ └────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ │ HTTP/WebSocket (with JWT) │
└──────────────────────────┼─────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ Authentication Backend │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ UserService │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ register() │ login() │ logout() │ refresh() │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ JWT │ │FoundationDB │ │ OAuth │ │
│ │ Tokens │ │ (Users) │ │ Providers │ │
│ └──────────┘ └──────────────┘ └────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Authorization Middleware │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ RBAC │ Quota Check │ Session Validation │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────┐
│FoundationDB │
│(User Data) │
└─────────────┘
User Database Schema
Users Table:
interface User {
user_id: string; // UUID
email: string; // Unique
password_hash?: string; // bcrypt (null for OAuth-only users)
name: string;
avatar_url?: string;
role: 'admin' | 'developer' | 'viewer';
oauth_providers: Array<{
provider: 'google' | 'github';
provider_user_id: string;
access_token?: string; // Encrypted
}>;
created_at: number; // Timestamp
updated_at: number;
email_verified: boolean;
is_active: boolean;
metadata: Record<string, any>;
}
User Sessions Table:
interface UserSession {
session_id: string; // UUID
user_id: string;
access_token: string; // JWT
refresh_token: string; // JWT (long-lived)
ip_address: string;
user_agent: string;
created_at: number;
expires_at: number;
last_activity_at: number;
is_active: boolean;
}
API Keys Table:
interface APIKey {
key_id: string; // UUID
user_id: string;
key_hash: string; // bcrypt of actual key
key_prefix: string; // First 8 chars for display
name: string; // User-defined name
permissions: string[]; // ['read:files', 'write:files', 'llm:chat']
created_at: number;
expires_at?: number;
last_used_at?: number;
is_active: boolean;
}
User Quotas Table:
interface UserQuota {
user_id: string;
period: 'daily' | 'monthly';
period_start: number;
// Usage
llm_requests: number;
llm_tokens_used: number;
llm_cost_usd: number;
storage_bytes: number;
api_requests: number;
// Limits
llm_requests_limit: number;
llm_tokens_limit: number;
llm_cost_limit_usd: number;
storage_bytes_limit: number;
api_requests_limit: number;
}
Roles & Permissions:
interface Role {
role_name: string; // 'admin', 'developer', 'viewer'
permissions: Permission[];
}
interface Permission {
resource: string; // 'files', 'sessions', 'llm', 'agents'
actions: string[]; // ['read', 'write', 'delete']
}
const ROLES: Record<string, Role> = {
admin: {
role_name: 'admin',
permissions: [
{ resource: '*', actions: ['*'] }, // Full access
]
},
developer: {
role_name: 'developer',
permissions: [
{ resource: 'files', actions: ['read', 'write', 'delete'] },
{ resource: 'sessions', actions: ['read', 'write', 'delete'] },
{ resource: 'llm', actions: ['read', 'write'] },
{ resource: 'agents', actions: ['read', 'write'] },
{ resource: 'users', actions: ['read'] }, // Can only read own profile
]
},
viewer: {
role_name: 'viewer',
permissions: [
{ resource: 'files', actions: ['read'] },
{ resource: 'sessions', actions: ['read'] },
{ resource: 'llm', actions: ['read'] },
{ resource: 'agents', actions: ['read'] },
]
}
};
Implementation
1. User Service
// src/backend/services/user-service.ts
import { hash, compare } from 'bcrypt';
import { sign, verify } from 'jsonwebtoken';
import { randomBytes } from 'crypto';
import { FDBService } from './fdb-service';
export class UserService {
private fdb: FDBService;
private readonly JWT_SECRET = process.env.JWT_SECRET!;
private readonly JWT_EXPIRY = '15m'; // Access token: 15 minutes
private readonly REFRESH_EXPIRY = '7d'; // Refresh token: 7 days
constructor(fdb: FDBService) {
this.fdb = fdb;
}
async register(email: string, password: string, name: string): Promise<User> {
// Check if user exists
const existingUser = await this.fdb.get(`user:email:${email}`);
if (existingUser) {
throw new Error('User already exists');
}
// Hash password
const password_hash = await hash(password, 10);
// Create user
const user: User = {
user_id: this.generateUUID(),
email,
password_hash,
name,
role: 'developer', // Default role
oauth_providers: [],
created_at: Date.now(),
updated_at: Date.now(),
email_verified: false,
is_active: true,
metadata: {}
};
// Save to FDB
await this.fdb.set(`user:${user.user_id}`, user);
await this.fdb.set(`user:email:${email}`, user.user_id);
// Create default quota
await this.createDefaultQuota(user.user_id);
return user;
}
async login(email: string, password: string, ip: string, userAgent: string): Promise<{
user: User;
access_token: string;
refresh_token: string;
}> {
// Get user ID from email
const user_id = await this.fdb.get(`user:email:${email}`);
if (!user_id) {
throw new Error('Invalid credentials');
}
// Get user
const user = await this.fdb.get(`user:${user_id}`);
if (!user || !user.is_active) {
throw new Error('Invalid credentials');
}
// Verify password
const isValid = await compare(password, user.password_hash!);
if (!isValid) {
throw new Error('Invalid credentials');
}
// Generate tokens
const access_token = this.generateAccessToken(user);
const refresh_token = this.generateRefreshToken(user);
// Create session
const session: UserSession = {
session_id: this.generateUUID(),
user_id: user.user_id,
access_token,
refresh_token,
ip_address: ip,
user_agent: userAgent,
created_at: Date.now(),
expires_at: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days
last_activity_at: Date.now(),
is_active: true
};
await this.fdb.set(`session:${session.session_id}`, session);
await this.fdb.set(`user:${user.user_id}:session:${session.session_id}`, true);
return { user, access_token, refresh_token };
}
async loginWithOAuth(
provider: 'google' | 'github',
providerUserId: string,
email: string,
name: string,
avatarUrl: string
): Promise<{ user: User; access_token: string; refresh_token: string }> {
// Check if user exists by OAuth provider
let user_id = await this.fdb.get(`user:oauth:${provider}:${providerUserId}`);
if (!user_id) {
// Check if user exists by email
user_id = await this.fdb.get(`user:email:${email}`);
if (user_id) {
// Link OAuth to existing user
const user = await this.fdb.get(`user:${user_id}`);
user.oauth_providers.push({ provider, provider_user_id: providerUserId });
await this.fdb.set(`user:${user_id}`, user);
await this.fdb.set(`user:oauth:${provider}:${providerUserId}`, user_id);
} else {
// Create new user
const user: User = {
user_id: this.generateUUID(),
email,
name,
avatar_url: avatarUrl,
role: 'developer',
oauth_providers: [{ provider, provider_user_id: providerUserId }],
created_at: Date.now(),
updated_at: Date.now(),
email_verified: true, // OAuth providers verify email
is_active: true,
metadata: {}
};
await this.fdb.set(`user:${user.user_id}`, user);
await this.fdb.set(`user:email:${email}`, user.user_id);
await this.fdb.set(`user:oauth:${provider}:${providerUserId}`, user.user_id);
await this.createDefaultQuota(user.user_id);
user_id = user.user_id;
}
}
const user = await this.fdb.get(`user:${user_id}`);
// Generate tokens
const access_token = this.generateAccessToken(user);
const refresh_token = this.generateRefreshToken(user);
return { user, access_token, refresh_token };
}
async logout(session_id: string): Promise<void> {
const session = await this.fdb.get(`session:${session_id}`);
if (session) {
session.is_active = false;
await this.fdb.set(`session:${session_id}`, session);
await this.fdb.delete(`user:${session.user_id}:session:${session_id}`);
}
}
async refreshAccessToken(refresh_token: string): Promise<string> {
try {
const payload = verify(refresh_token, this.JWT_SECRET) as any;
const user = await this.fdb.get(`user:${payload.user_id}`);
if (!user || !user.is_active) {
throw new Error('Invalid refresh token');
}
return this.generateAccessToken(user);
} catch (error) {
throw new Error('Invalid refresh token');
}
}
async verifyAccessToken(access_token: string): Promise<User> {
try {
const payload = verify(access_token, this.JWT_SECRET) as any;
const user = await this.fdb.get(`user:${payload.user_id}`);
if (!user || !user.is_active) {
throw new Error('Invalid access token');
}
return user;
} catch (error) {
throw new Error('Invalid access token');
}
}
async createAPIKey(user_id: string, name: string, permissions: string[]): Promise<{
api_key: string;
key_id: string;
}> {
// Generate random API key
const api_key = `ak_${randomBytes(32).toString('hex')}`;
const key_hash = await hash(api_key, 10);
const apiKey: APIKey = {
key_id: this.generateUUID(),
user_id,
key_hash,
key_prefix: api_key.substring(0, 8),
name,
permissions,
created_at: Date.now(),
is_active: true
};
await this.fdb.set(`apikey:${apiKey.key_id}`, apiKey);
await this.fdb.set(`user:${user_id}:apikey:${apiKey.key_id}`, true);
return { api_key, key_id: apiKey.key_id };
}
async verifyAPIKey(api_key: string): Promise<{ user: User; permissions: string[] }> {
// Extract key prefix
const key_prefix = api_key.substring(0, 8);
// Find API keys with matching prefix
const apiKeys = await this.fdb.scan(`apikey:`);
for (const apiKey of apiKeys) {
if (apiKey.key_prefix === key_prefix && apiKey.is_active) {
const isValid = await compare(api_key, apiKey.key_hash);
if (isValid) {
// Update last used
apiKey.last_used_at = Date.now();
await this.fdb.set(`apikey:${apiKey.key_id}`, apiKey);
const user = await this.fdb.get(`user:${apiKey.user_id}`);
return { user, permissions: apiKey.permissions };
}
}
}
throw new Error('Invalid API key');
}
async checkQuota(user_id: string, resource: 'llm' | 'storage' | 'api'): Promise<boolean> {
const quota = await this.fdb.get(`quota:${user_id}:monthly`);
if (!quota) return false;
switch (resource) {
case 'llm':
return quota.llm_requests < quota.llm_requests_limit;
case 'storage':
return quota.storage_bytes < quota.storage_bytes_limit;
case 'api':
return quota.api_requests < quota.api_requests_limit;
default:
return false;
}
}
async incrementQuota(
user_id: string,
resource: 'llm' | 'storage' | 'api',
amount: number
): Promise<void> {
const quota = await this.fdb.get(`quota:${user_id}:monthly`);
if (!quota) return;
switch (resource) {
case 'llm':
quota.llm_requests += 1;
quota.llm_tokens_used += amount;
quota.llm_cost_usd += amount * 0.00001; // Example cost calculation
break;
case 'storage':
quota.storage_bytes += amount;
break;
case 'api':
quota.api_requests += 1;
break;
}
await this.fdb.set(`quota:${user_id}:monthly`, quota);
}
async hasPermission(user: User, resource: string, action: string): Promise<boolean> {
const role = ROLES[user.role];
if (!role) return false;
for (const perm of role.permissions) {
if (perm.resource === '*' || perm.resource === resource) {
if (perm.actions.includes('*') || perm.actions.includes(action)) {
return true;
}
}
}
return false;
}
private generateAccessToken(user: User): string {
return sign(
{
user_id: user.user_id,
email: user.email,
role: user.role
},
this.JWT_SECRET,
{ expiresIn: this.JWT_EXPIRY }
);
}
private generateRefreshToken(user: User): string {
return sign(
{
user_id: user.user_id,
email: user.email,
type: 'refresh'
},
this.JWT_SECRET,
{ expiresIn: this.REFRESH_EXPIRY }
);
}
private generateUUID(): string {
return randomBytes(16).toString('hex');
}
private async createDefaultQuota(user_id: string): Promise<void> {
const quota: UserQuota = {
user_id,
period: 'monthly',
period_start: Date.now(),
llm_requests: 0,
llm_tokens_used: 0,
llm_cost_usd: 0,
storage_bytes: 0,
api_requests: 0,
llm_requests_limit: 10000, // 10K requests/month
llm_tokens_limit: 10000000, // 10M tokens/month
llm_cost_limit_usd: 100, // $100/month
storage_bytes_limit: 10 * 1024 * 1024 * 1024, // 10GB
api_requests_limit: 100000 // 100K API requests/month
};
await this.fdb.set(`quota:${user_id}:monthly`, quota);
}
}
2. Authentication Middleware
// src/backend/middleware/auth-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user-service';
export class AuthMiddleware {
constructor(private userService: UserService) {}
async authenticate(req: Request, res: Response, next: NextFunction) {
try {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'No authorization header' });
}
if (authHeader.startsWith('Bearer ')) {
// JWT token
const token = authHeader.substring(7);
const user = await this.userService.verifyAccessToken(token);
req.user = user;
} else if (authHeader.startsWith('APIKey ')) {
// API key
const apiKey = authHeader.substring(7);
const { user, permissions } = await this.userService.verifyAPIKey(apiKey);
req.user = user;
req.apiKeyPermissions = permissions;
} else {
return res.status(401).json({ error: 'Invalid authorization format' });
}
next();
} catch (error: any) {
res.status(401).json({ error: error.message });
}
}
requirePermission(resource: string, action: string) {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const hasPermission = await this.userService.hasPermission(
req.user,
resource,
action
);
if (!hasPermission) {
return res.status(403).json({ error: 'Permission denied' });
}
next();
};
}
requireQuota(resource: 'llm' | 'storage' | 'api') {
return async (req: Request, res: Response, next: NextFunction) {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const hasQuota = await this.userService.checkQuota(req.user.user_id, resource);
if (!hasQuota) {
return res.status(429).json({ error: `${resource} quota exceeded` });
}
next();
};
}
}
3. OAuth Integration (Google)
// src/backend/oauth/google-oauth.ts
import { OAuth2Client } from 'google-auth-library';
export class GoogleOAuth {
private client: OAuth2Client;
constructor() {
this.client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URI
);
}
getAuthUrl(): string {
return this.client.generateAuthUrl({
access_type: 'offline',
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email'
]
});
}
async verifyCode(code: string): Promise<{
email: string;
name: string;
picture: string;
provider_user_id: string;
}> {
const { tokens } = await this.client.getToken(code);
this.client.setCredentials(tokens);
const ticket = await this.client.verifyIdToken({
idToken: tokens.id_token!,
audience: process.env.GOOGLE_CLIENT_ID
});
const payload = ticket.getPayload()!;
return {
email: payload.email!,
name: payload.name!,
picture: payload.picture!,
provider_user_id: payload.sub!
};
}
}
Rationale
Why JWT?
Stateless Authentication:
- ✅ No server-side session storage
- ✅ Scales horizontally (no shared state)
- ✅ Works with Cloud Run (stateless containers)
Standard Protocol:
- ✅ Industry standard (RFC 7519)
- ✅ Wide library support
- ✅ Self-contained (includes claims)
Why Short-Lived Access Tokens?
Security:
- ✅ Limits exposure if token is compromised
- ✅ Forces periodic re-authentication
- ✅ Easier to revoke (via refresh token)
Best Practice:
- ✅ 15-minute access token (standard)
- ✅ 7-day refresh token (acceptable)
- ✅ Can force logout by revoking refresh token
Why FoundationDB for Users?
Performance:
- ✅ Fast reads (< 5ms)
- ✅ ACID transactions
- ✅ Horizontal scaling
Features:
- ✅ Secondary indexes (email, OAuth lookup)
- ✅ Atomic quota updates
- ✅ Multi-tenant isolation
Why OAuth 2.0?
User Experience:
- ✅ No password management for users
- ✅ Trusted providers (Google, GitHub)
- ✅ Faster signup/login
Security:
- ✅ Reduces password breach risk
- ✅ Provider handles 2FA
- ✅ Email verification automatic
Alternatives Considered
Alternative 1: Session-Based Auth
Pros:
- Simple (server-side sessions)
- Easy to revoke
Cons:
- ❌ Requires shared state (Redis)
- ❌ Doesn't scale as well
- ❌ More server-side complexity
Rejected: JWT is more scalable
Alternative 2: Firebase Auth
Pros:
- Fully managed
- Easy integration
- Good DX
Cons:
- ❌ Vendor lock-in
- ❌ Cost at scale
- ❌ Less control
Rejected: Want self-hosted solution
Alternative 3: Auth0
Pros:
- Enterprise features
- Easy to use
- OAuth built-in
Cons:
- ❌ Expensive ($240/month+)
- ❌ Vendor lock-in
- ❌ Overkill for MVP
Rejected: Too expensive for startup
Alternative 4: PostgreSQL for Users
Pros:
- SQL queries
- Relational data
- Wide support
Cons:
- ❌ Slower than FDB
- ❌ Less scalable
- ❌ More complex joins
Rejected: FDB is faster and scales better
Consequences
Positive
✅ Secure: Industry-standard JWT + OAuth 2.0 ✅ Scalable: Stateless auth, horizontal scaling ✅ User-Friendly: Social login, no passwords ✅ Flexible: RBAC, fine-grained permissions ✅ Trackable: Quotas, usage monitoring ✅ Multi-Tenant: Per-user isolation
Negative
❌ Complexity: More code to maintain ❌ Token Management: Refresh token rotation ❌ OAuth Setup: Requires provider credentials ❌ Quota Tracking: Performance overhead
Mitigation
Complexity:
- Use battle-tested libraries (jsonwebtoken, google-auth-library)
- Comprehensive tests
- Document thoroughly
Token Management:
- Automatic refresh token rotation
- Clear expiry handling
- Graceful error messages
OAuth Setup:
- Provide clear setup instructions
- Support multiple providers
- Fallback to email/password
Quota Tracking:
- Background job for quota resets
- Cache quota checks (Redis)
- Optimize FDB queries
Implementation Plan
Phase 1: Core Authentication ✅
- User service implementation
- JWT token generation
- Password hashing (bcrypt)
- Login/register endpoints
Phase 2: OAuth Integration 🔲
- Google OAuth setup
- GitHub OAuth setup
- OAuth callback handlers
- Account linking
Phase 3: Authorization 🔲
- RBAC implementation
- Permission checking middleware
- API key management
- Resource ownership checks
Phase 4: Quotas 🔲
- Quota schema in FDB
- Quota checking middleware
- Quota increment on llm/API usage
- Quota reset job (monthly)
Phase 5: Session Management 🔲
- Active session tracking
- Force logout functionality
- Session expiry cleanup
- Multi-device management
Phase 6: UI Integration 🔲
- Login page
- Registration page
- OAuth buttons
- User profile page
- API key management UI
Success Metrics
Authentication:
- < 500ms login time
- > 90% OAuth adoption
- < 1% auth errors
Security:
- Zero password breaches
- Zero JWT compromises
- Pass security audit
Performance:
- < 10ms permission checks
- < 5ms quota checks
- 10K+ users supported
Related Decisions
- ADR-020: GCP Deployment - Cloud infrastructure
- ADR-004: FoundationDB - User data storage
- ADR-017: WebSocket Backend - JWT in WebSocket
References
JWT:
OAuth 2.0:
Security:
Status: ✅ Accepted Next Review: 2025-11-06 (1 month) Last Updated: 2025-10-06