================================================================================ UNIFIED AUTH SYSTEM: Complete Implementation Guide ================================================================================ PROJECT: NewApproach DATE: 2026-01-10 STATUS: Production-ready, drop-in authentication system ================================================================================ OVERVIEW ================================================================================ This is a COMPLETE, STANDALONE authentication system that handles ALL token acquisition, storage, and retrieval across 4 authentication scenarios: 1. PocketBase User (OAuth via Microsoft) - "pb-user" 2. PocketBase Service Account (email/password) - "pb-agent" 3. Microsoft Graph Delegated (on behalf of user) - "graph-user" 4. Microsoft Graph Service Principal (app-only) - "graph-agent" The system is modular and can be dropped into ANY project. It works in BOTH browser (frontend) and backend (Node.js) environments. ================================================================================ ARCHITECTURE ================================================================================ TWO-TIER DESIGN: FRONTEND (Browser) ├─ auth/auth.unified.ts │ ├─ Universal login popup (username/password + OAuth buttons) │ ├─ Token acquisition from external auth providers │ ├─ localStorage-based token persistence │ ├─ Token refresh and expiration tracking │ └─ User info extraction │ └─ Usage: Auth.getToken('pb-user') → automatically prompts if needed BACKEND (Server) ├─ src/backend/auth.ts │ ├─ Service principal authentication (client credentials flow) │ ├─ PocketBase service account auth │ ├─ Token caching with expiration │ ├─ Hono middleware for token injection │ └─ API endpoints for token operations │ └─ Usage: await backendAuth.getGraphServicePrincipalToken() ================================================================================ TOKEN ACQUISITION FLOW: DETAILED BREAKDOWN ================================================================================ SCENARIO 1: PocketBase User Login (Primary Flow) ═════════════════════════════════════════════════ WHEN: User visits app and is not authenticated WHERE: auth/auth.unified.ts → acquirePocketBaseUser() HOW: 1. User clicks "Sign in with Microsoft" 2. Code calls: pb.collection('users').authWithOAuth2({ provider: 'microsoft' }) 3. PocketBase redirects to Microsoft OAuth consent page 4. User consents and is redirected back 5. OAuth provider returns: - PocketBase auth token (for pb-user) - Microsoft access token (for graph-user, if configured in PocketBase) 6. Tokens are extracted and stored in localStorage STORAGE: - pb-user token → localStorage['auth:pb-user'] - graph-user token → localStorage['auth:graph-user'] (if returned) RETRIEVAL: - Frontend calls: const token = await Auth.getToken('pb-user') - Automatically uses cached token if valid - Automatically refreshes if expired CRITICAL: This is a UNIFIED login that gets TWO tokens at once. User only logs in once, both pb-user and graph-user are acquired. ──────────────────────────────────────────────────────────────────────────── SCENARIO 2: PocketBase Service Account (Agent) ═══════════════════════════════════════════════ WHEN: Backend or agent code needs PocketBase access WHERE: src/backend/auth.ts → getPocketBaseServiceToken() HOW: 1. Code calls: backendAuth.getPocketBaseServiceToken() 2. Checks environment variables: - POCKETBASE_URL - POCKETBASE_SERVICE_EMAIL - POCKETBASE_SERVICE_PASSWORD 3. Makes POST request to PocketBase API: POST /api/collections/users/auth-with-password Body: { identity: email, password: password } 4. PocketBase returns auth token 5. Token is cached locally (7 day expiration) STORAGE: - In-memory cache in BackendAuthManager - Token expires after 7 days (typical PocketBase duration) RETRIEVAL: - Backend code: const token = await backendAuth.getPocketBaseServiceToken() - Automatically uses cached token if valid - Automatically re-authenticates if expired USE CASE: Backend calling PocketBase to create records, process data, etc. ──────────────────────────────────────────────────────────────────────────── SCENARIO 3: Microsoft Graph Delegated (User Token) ═══════════════════════════════════════════════════ WHEN: User's apps need access to Microsoft Graph (email, files, etc.) WHERE: Acquired via OAuth, stored by frontend HOW: 1. Already acquired during OAuth login (Scenario 1) 2. Returned in authData.meta by PocketBase 3. Frontend extracts: extractGraphToken(authData.meta) 4. Stored in localStorage['auth:graph-user'] 5. Can be refreshed by calling Auth.refreshToken('graph-user') STORAGE: - localStorage['auth:graph-user'] RETRIEVAL: - Frontend: const token = await Auth.getToken('graph-user') - Backend: Frontend sends token in header 'x-graph-token' USE CASE: App accessing user's OneDrive, Outlook, Teams, etc. ──────────────────────────────────────────────────────────────────────────── SCENARIO 4: Microsoft Graph Service Principal (App-Only) ═════════════════════════════════════════════════════════ WHEN: Backend needs app-only access to Graph (no user context) WHERE: src/backend/auth.ts → getGraphServicePrincipalToken() HOW: 1. Code calls: backendAuth.getGraphServicePrincipalToken() 2. Checks environment variables: - GRAPH_TENANT_ID - GRAPH_CLIENT_ID - GRAPH_CLIENT_SECRET 3. Makes POST request to Azure AD token endpoint: POST https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token Body: { client_id, client_secret, scope, grant_type: 'client_credentials' } 4. Azure AD returns access token 5. Token is cached locally (typically 1 hour) STORAGE: - In-memory cache in BackendAuthManager - Token expires after ~1 hour (configurable by Azure) RETRIEVAL: - Backend code: const token = await backendAuth.getGraphServicePrincipalToken() - Automatically uses cached token if valid - Automatically re-authenticates if expired USE CASE: App reading all users' files, sending emails on behalf of org, etc. ================================================================================ FILE STRUCTURE & LOCATIONS ================================================================================ /NewApproach/ ├── auth/ │ ├── auth.unified.ts ← FRONTEND: Main auth module (THIS IS THE CORE) │ ├── auth-test.html ← Testing interface │ └── AUTH_ANALYSIS.md ← Detailed analysis (previous) │ ├── src/ │ ├── backend/ │ │ ├── auth.ts ← BACKEND: Service auth (THIS IS THE CORE) │ │ └── server.ts ← Hono server (uses auth middleware) │ │ │ └── frontend/ │ └── (integrate auth.unified.ts here) │ └── public/ ├── index.html ← Should include auth initialization └── app.js ← Application code using tokens ================================================================================ ENVIRONMENT VARIABLES REQUIRED ================================================================================ FRONTEND (Browser - Usually from PocketBase config): ────────────────────────────────────────────────── Optional, but used by Auth.init(): POCKETBASE_URL PocketBase instance URL (default: http://127.0.0.1:8090) GRAPH_TENANT_ID Azure AD tenant for Graph scopes (optional) BACKEND (Server): ──────────────── CRITICAL - Must be set for service authentication: PocketBase Service Account: POCKETBASE_URL http://localhost:8090 POCKETBASE_SERVICE_EMAIL agent@example.com POCKETBASE_SERVICE_PASSWORD agent_password Microsoft Graph Service Principal: GRAPH_TENANT_ID {tenant-id} GRAPH_CLIENT_ID {client-id} GRAPH_CLIENT_SECRET {client-secret} All should be set in .env file or environment. ================================================================================ USAGE EXAMPLES ================================================================================ FRONTEND: Getting Tokens ════════════════════════ // 1. Initialize auth module (usually in app startup) await Auth.init('pb+graph', { pbUrl: 'http://localhost:8090' }); // 2. Get user token (automatically prompts for login if needed) const pbUserToken = await Auth.getToken('pb-user'); // First call: Shows login popup → User signs in → Token returned // Subsequent calls: Returns cached token // 3. Get Graph token (returned from same OAuth flow) const graphUserToken = await Auth.getToken('graph-user'); // Usually already available from Auth.getToken('pb-user') // If not, can be refreshed separately // 4. Use token in API calls fetch('/api/data', { headers: { 'Authorization': `Bearer ${pbUserToken}`, 'X-Graph-Token': graphUserToken } }); // 5. Get user info const { displayName, email } = Auth.getUserInfo(); console.log(`Logged in as: ${displayName} (${email})`); // 6. Clear tokens on logout Auth.clearAllTokens(); ──────────────────────────────────────────────────────────────────────────── BACKEND: Service Authentication ════════════════════════════════ import backendAuth, { serviceTokenMiddleware, createTokenEndpoint } from './auth.ts'; import { Hono } from 'hono'; const app = new Hono(); // 1. Initialize auth with config (loads from env vars) backendAuth.init({ pbUrl: process.env.POCKETBASE_URL }); // 2. Register middleware to inject tokens app.use('/*', serviceTokenMiddleware()); // 3. Get service tokens when needed app.get('/api/process', async (c) => { // Option A: Manual acquisition const pbToken = await backendAuth.getPocketBaseServiceToken(); const graphToken = await backendAuth.getGraphServicePrincipalToken(); // Option B: From middleware (if registered) const pbToken2 = c.get('pbServiceToken'); const graphToken2 = c.get('graphServiceToken'); // Use tokens const response = await fetch('https://graph.microsoft.com/v1.0/me', { headers: { 'Authorization': `Bearer ${graphToken}` } }); return c.json({ success: true }); }); // 4. Register token endpoints (optional, for frontend to fetch) createTokenEndpoint(app); // GET /api/auth/service-tokens createRefreshEndpoint(app); // POST /api/auth/refresh-tokens ──────────────────────────────────────────────────────────────────────────── INTEGRATION EXAMPLE: Full App ══════════════════════════════ // public/index.html // Backend routes app.get('/api/data', async (c) => { // Get Graph service token from cache const graphToken = await backendAuth.getGraphServicePrincipalToken(); // Fetch data from Microsoft Graph const result = await fetch('https://graph.microsoft.com/v1.0/drives', { headers: { 'Authorization': `Bearer ${graphToken}` } }).then(r => r.json()); return c.json(result); }); ================================================================================ SECURITY CONSIDERATIONS ================================================================================ FRONTEND TOKEN STORAGE ────────────────────── Current: localStorage (plaintext) ✓ Works for development and many scenarios ✗ Vulnerable to XSS attacks ✗ Not suitable for highly sensitive credentials IMPROVEMENTS FOR PRODUCTION: 1. Use HTTP-only cookies (backend sets them) 2. Implement CSRF protection 3. Validate tokens server-side 4. Implement token refresh rotation (new refresh token each time) 5. Use Content Security Policy headers ──────────────────────────────────────────────────────────────────────────── BACKEND CREDENTIALS ─────────────────── CRITICAL: Service credentials (GRAPH_CLIENT_SECRET, etc.) should NEVER be: ✗ Committed to git ✗ Logged or printed ✗ Exposed to frontend ✗ Stored in localStorage DO: ✓ Use environment variables only ✓ Use .env.local (not committed) ✓ Use secrets manager in production (Azure Key Vault, etc.) ✓ Rotate credentials regularly ✓ Monitor token usage and access logs ──────────────────────────────────────────────────────────────────────────── FRONTEND/BACKEND COMMUNICATION ─────────────────────────────── Frontend should NOT send credentials to backend. Instead: ✓ Frontend gets user token from OAuth ✓ Frontend sends token in headers (Authorization header) ✓ Backend verifies token validity ✓ Backend uses ITS OWN service principal for backend-to-backend calls Example: Frontend has: graph-user token (on behalf of user) Backend has: graph-agent token (service principal) Both can call Microsoft Graph, with different permissions ================================================================================ TOKEN REFRESH & EXPIRATION ================================================================================ PocketBase Tokens: - Expiration: Typically 7 days - Refresh: Auth.refreshToken('pb-user') or automatic on getToken() - Strategy: Stored in localStorage, cached in memory Graph Delegated Tokens: - Expiration: Typically 1 hour - Refresh: Auth.refreshToken('graph-user') - Strategy: Can refresh via PocketBase authRefresh() Graph Service Principal: - Expiration: Typically 1 hour - Refresh: Automatic via client credentials flow - Strategy: Cached in backend, auto-refreshes if expired ──────────────────────────────────────────────────────────────────────────── AUTO-REFRESH IMPLEMENTATION When you call Auth.getToken(type): 1. Checks if token exists and is not expired 2. If expired: calls Auth.refreshToken(type) 3. If refresh fails: calls Auth.acquireToken(type) to re-login 4. Returns valid token This means: - Tokens are automatically refreshed before use - No manual refresh logic needed - User rarely sees "login again" prompts ================================================================================ COMMON WORKFLOWS ================================================================================ WORKFLOW 1: User Logs In, Gets Both Tokens ═══════════════════════════════════════════ Step 1: Page loads, Auth.init() is called Step 2: User not authenticated, clicks "Sign in" Step 3: Auth.getToken('pb-user') is called Step 4: System shows "Sign in with Microsoft" button Step 5: User clicks, Microsoft OAuth popup appears Step 6: User consents Step 7: OAuth returns PB token + Graph token (in PocketBase meta) Step 8: Both stored in localStorage Step 9: User is logged in, can make API calls with either token Step 10: Future requests use cached tokens, auto-refresh as needed ──────────────────────────────────────────────────────────────────────────── WORKFLOW 2: Backend Processes Data with Service Account ════════════════════════════════════════════════════════ Step 1: Backend init() is called, loads credentials from env Step 2: Route receives request Step 3: Code calls backendAuth.getPocketBaseServiceToken() Step 4: System checks cache: - If valid: Returns cached token - If expired or missing: Authenticates with service account Step 5: Backend makes request with service token Step 6: PocketBase validates token and returns data Step 7: Backend processes and returns to frontend ──────────────────────────────────────────────────────────────────────────── WORKFLOW 3: Frontend Uses Microsoft Graph ═══════════════════════════════════════════ Step 1: User has graph-user token from initial OAuth Step 2: Frontend code calls: const token = await Auth.getToken('graph-user') Step 3: System returns cached token (if still valid) Step 4: Frontend makes fetch to Microsoft Graph: fetch('https://graph.microsoft.com/v1.0/me', { headers: { 'Authorization': `Bearer ${token}` } }) Step 5: Graph API validates token and returns user data Step 6: Frontend displays results ──────────────────────────────────────────────────────────────────────────── WORKFLOW 4: Backend Calls Microsoft Graph (Service Principal) ══════════════════════════════════════════════════════════════ Step 1: Backend init() is called, loads Graph credentials from env Step 2: Route needs to call Microsoft Graph Step 3: Code calls: const token = await backendAuth.getGraphServicePrincipalToken() Step 4: System authenticates with service principal via client credentials flow Step 5: Token is cached (1 hour typical) Step 6: Backend makes request: fetch('https://graph.microsoft.com/v1.0/drives', { headers: { 'Authorization': `Bearer ${token}` } }) Step 7: Graph API validates service principal token Step 8: Returns data (with service principal permissions, not user's) ================================================================================ TESTING ================================================================================ Manual Testing: auth-test.html ────────────────────────────── - Open in browser - Click "Test Module Load" → Verifies Auth is available - Click "Test Token Storage" → Verifies localStorage works - Click "Test Configuration" → Verifies Auth.configure exists - Click "Clear Tokens" → Tests token clearing Integration Testing: ──────────────────── 1. Start backend: bun run dev 2. Open public/index.html in browser 3. Should see "Sign in with Microsoft" button 4. Click it, authenticate 5. Check localStorage → tokens should be stored 6. Make API call → should include Authorization headers 7. Backend should receive and validate tokens Unit Testing (TODO): ───────────────── - Test token parsing and storage - Test expiration tracking - Test refresh logic - Test error handling - Test popup UI ================================================================================ TROUBLESHOOTING ================================================================================ ISSUE: "PocketBase SDK not loaded" ────────────────────────────────── Solution: Include PocketBase script before auth.ts: ──────────────────────────────────────────────────────────────────────────── ISSUE: Graph token is undefined after OAuth ───────────────────────────────────────────── Solution: PocketBase may not be configured to return Graph token Check PocketBase settings → OAuth2 → Configure Microsoft with proper scopes Scopes needed: - offline_access (for refresh tokens) - User.Read - Files.Read - Sites.Read.All ──────────────────────────────────────────────────────────────────────────── ISSUE: Service principal token acquisition fails ────────────────────────────────────────────────── Solution: Check environment variables are set echo $GRAPH_TENANT_ID echo $GRAPH_CLIENT_ID echo $GRAPH_CLIENT_SECRET (DON'T print in production!) Also check: - Credentials are correct - Service principal has required permissions - Network connectivity to Azure AD ──────────────────────────────────────────────────────────────────────────── ISSUE: Tokens not persisting across page reloads ──────────────────────────────────────────────── Solution: Check localStorage is enabled and available localStorage.setItem('test', '1') localStorage.getItem('test') Also check browser DevTools → Application → Local Storage ════════════════════════════════════════════════════════════════════════════ DEPLOYMENT & PRODUCTION ════════════════════════════════════════════════════════════════════════════ FRONTEND DEPLOYMENT: 1. Compile TypeScript: bun run build 2. Output goes to dist/ 3. Serve from static file server 4. Ensure PocketBase script is loaded before auth module BACKEND DEPLOYMENT: 1. Set all environment variables: - POCKETBASE_URL - POCKETBASE_SERVICE_EMAIL - POCKETBASE_SERVICE_PASSWORD - GRAPH_TENANT_ID - GRAPH_CLIENT_ID - GRAPH_CLIENT_SECRET 2. Deploy server (e.g., to Docker, Vercel, etc.) 3. Test token endpoints MONITORING: 1. Log all token acquisitions and errors 2. Monitor token refresh failures 3. Alert on repeated auth failures 4. Track token expiration and refresh rates ════════════════════════════════════════════════════════════════════════════ END OF UNIFIED AUTH SYSTEM DOCUMENTATION ════════════════════════════════════════════════════════════════════════════