Skip to main content

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

  1. Secure Authentication: Industry-standard auth (JWT, OAuth 2.0)
  2. Multi-Provider: Support email/password + OAuth providers
  3. RBAC: Roles (admin, developer, viewer) with permissions
  4. Session Management: Track active sessions, force logout
  5. API Keys: For CLI and API access
  6. User Isolation: Per-user workspaces, sessions, files
  7. Quota Tracking: llm usage, storage, API calls
  8. 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


References

JWT:

OAuth 2.0:

Security:


Status: ✅ Accepted Next Review: 2025-11-06 (1 month) Last Updated: 2025-10-06