# Mode1Test Token Flow ## Overview Mode1Test uses **PocketBase** as the OAuth2 provider for Microsoft authentication. The system manages two types of tokens: - **Graph Access Token** (short-lived, 1 hour) - Stored in localStorage - **Refresh Token** (long-lived, ~30 days) - Managed internally by PocketBase --- ## Architecture Diagram ``` ┌─────────────────────────────────────────────────────────────────┐ │ Mode1Test Token Architecture │ └─────────────────────────────────────────────────────────────────┘ ┌──────────────────────┐ │ Microsoft OAuth2 │ │ (Microsoft Entra) │ └──────────┬───────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ Access Token (1hr) Refresh Token (30d) ├─ For Graph API └─ Stored in pb.authStore │ (PocketBase internal) │ ▼ localStorage 'graphAccessToken' │ ├──────────────┬──────────────┐ │ │ │ ▼ ▼ ▼ Frontend Backend Session (index.html) (/api/...) (Memory) Reads token Receives Persists from store x-graph-token until logout for API calls headers ``` --- ## 1. Sign-In Flow Diagram ``` USER INITIATES LOGIN │ ▼ ┌─────────────────────────────────┐ │ signin.html │ │ Click "Sign in with Microsoft" │ └────────┬────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ pb.authWithOAuth2({ │ │ provider: 'microsoft' │ │ }) │ └────────┬────────────────────────┘ │ ▼ [Browser redirects to Microsoft OAuth] │ ▼ ┌─────────────────────────────────┐ │ User grants permissions │ │ Microsoft validates credentials │ └────────┬────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Microsoft returns to signin.html │ │ With authorization code │ └────────┬────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ PocketBase exchanges code for tokens: │ │ • Access Token (Graph API) │ │ • Refresh Token (for later use) │ │ • User data │ └────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ displayUserInfo(authData) called │ │ • Extract Graph token from authData │ │ • Store in localStorage │ │ • Show authenticated UI │ └────────┬────────────────────────────────┘ │ ▼ USER AUTHENTICATED ✓ Ready to access job files ``` --- ## 2. Token Acquisition (Detailed) ### Frontend (signin.html): ```javascript async function login() { const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft', }); displayUserInfo(authData); } const extractGraphToken = (authData) => { return ( authData?.meta?.graphAccessToken || authData?.meta?.graph_token || authData?.meta?.graphToken || authData?.meta?.accessToken || authData?.meta?.access_token || authData?.meta?.token || authData?.meta?.rawToken || authData?.meta?.authData?.access_token || '' ); }; function displayUserInfo(authData) { const graphToken = extractGraphToken(authData); if (graphToken) { localStorage.setItem('graphAccessToken', graphToken); // ← STORED } } ``` **Location**: `Mode1Test/frontend/signin.html` lines 20-50 --- ## 3. Token Storage Diagram ``` ┌──────────────────────────────────────────────────────────────┐ │ Token Storage Locations │ └──────────────────────────────────────────────────────────────┘ BROWSER CLIENT │ ├─ localStorage │ └─ 'graphAccessToken' │ ├─ Value: "eyJ0eXAiOiJKV1QiLCJhbGc..." │ ├─ Source: Extracted from OAuth response │ ├─ Lifetime: ~1 hour │ └─ Access: fetch() headers in any page │ └─ pb.authStore (PocketBase Internal) ├─ Refresh Token (secure, not exposed) ├─ Auth Token (PocketBase session) ├─ User Model (profile data) ├─ Managed: By PocketBase SDK automatically └─ Persists: Across page reloads BACKEND │ └─ Request Header: x-graph-token ├─ Source: Sent from frontend ├─ Usage: Passed to Microsoft Graph API └─ Not Stored: Used immediately, discarded after request ``` --- ## 4. API Call Flow (How Tokens Are Used) ``` USER ACTION (index.html) │ ▼ READ TOKEN FROM localStorage │ ├─ Token found? │ ├─ YES → Continue │ └─ NO → Redirect to signin.html │ ▼ MAKE API REQUEST │ fetch('/api/job-files?link=...', { headers: { 'x-graph-token': token ← Token sent here } }) │ ▼ BACKEND RECEIVES (server.ts) │ const token = c.req.header('x-graph-token') │ ▼ CALL MICROSOFT GRAPH API │ fetch('https://graph.microsoft.com/v1.0/...', { headers: { Authorization: `Bearer ${token}` } }) │ ├─ Success (200) ──┐ │ ▼ │ RETURN FILES TO FRONTEND │ │ │ ▼ │ DISPLAY IN UI │ └─ Token Expired (401) │ ▼ RETURN 401 TO FRONTEND │ ▼ FRONTEND DETECTS 401 │ ▼ REDIRECT TO signin.html?reauth=1 │ ▼ TRIGGER TOKEN REFRESH (see section 5) ``` --- ## 5. Token Refresh Flow Diagram ``` TOKEN EXPIRED (401 from Graph API) │ ▼ ┌──────────────────────────────────────────┐ │ Frontend receives 401 error │ │ (from /api/job-files endpoint) │ └────────┬─────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Set flag & redirect: │ │ localStorage.setItem( │ │ 'graphReauthAttempted', 'true' │ │ ) │ │ window.location.href = │ │ 'signin.html?reauth=1' │ └────────┬─────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ signin.html (with reauth=1 param) │ │ │ │ Check if already authenticated: │ │ if (pb.authStore.isValid) { │ └────────┬─────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Call authRefresh(): │ │ await pb.collection('users') │ │ .authRefresh() │ │ │ │ PocketBase internally: │ │ • Uses stored refresh token │ │ • Exchanges with Microsoft │ │ • Gets new access token │ └────────┬─────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Extract new Graph token: │ │ const newGraphToken = │ │ extractGraphToken(authData) │ │ │ │ authData?.meta?.graphAccessToken ✓ │ └────────┬─────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Update localStorage: │ │ localStorage.setItem( │ │ 'graphAccessToken', newGraphToken │ │ ) │ │ localStorage.removeItem( │ │ 'graphReauthAttempted' │ │ ) │ └────────┬─────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Auto-redirect if reauth param set: │ │ const hasToken = │ │ localStorage.getItem( │ │ 'graphAccessToken' │ │ ) │ │ if (reauth && hasToken) { │ │ window.location.href = 'index.html' │ │ } │ └────────┬─────────────────────────────────┘ │ ▼ BACK TO index.html NEW TOKEN IN localStorage RETRY API CALL ✓ ``` --- ## 6. Token Lifetime & Expiry ``` TIMELINE: Access Token Lifecycle t=0min t=55min t=59min55s t=60min │ │ │ │ ├──────────────┼─────────────┼────────────┤ │ VALID │ VALID │ EXPIRED │ │ (Full) │ (5min │ (401 │ │ │ buffer) │ error) │ │ │ │ │ │ │ ← Frontend │ ← Redirect │ │ │ should │ to │ │ │ refresh │ signin │ │ │ soon │ │ KEY POINTS: • Access Token: 1 hour (from Microsoft) • Refresh Token: ~30 days (auto-managed by PocketBase) • Monitor: No built-in expiry timer in Mode1Test → Errors surface when API call made with expired token → Frontend detects 401 and triggers refresh ``` --- ## 7. Code Reference - Token Extraction **File**: `Mode1Test/frontend/signin.html` (lines 20-50) ```javascript const extractGraphToken = (authData) => { // Tries multiple possible locations where token might be return ( authData?.meta?.graphAccessToken || // Primary location authData?.meta?.graph_token || // Alternate spelling authData?.meta?.graphToken || // Another variant authData?.meta?.accessToken || // Generic name authData?.meta?.access_token || // Underscore version authData?.meta?.token || // Just "token" authData?.meta?.rawToken || // Raw token authData?.meta?.authData?.access_token || // Nested location '' // Fallback: empty string ); }; ``` --- ## 8. Code Reference - Token Usage **File**: `Mode1Test/backend/server.ts` (lines 172-210) ```typescript // Get token from request header or environment const getGraphToken = (c?: any) => { const headerToken = c?.req?.header('x-graph-token') || ''; // From frontend const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || ''; // Fallback const token = headerToken || envToken; console.log('[getGraphToken] Header token:', headerToken ? headerToken.substring(0, 20) + '...' : 'NONE'); console.log('[getGraphToken] Env token:', envToken ? 'SET' : 'NOT SET'); console.log('[getGraphToken] Using:', token ? token.substring(0, 20) + '...' : 'NONE'); return token; }; // Make API call to Microsoft Graph with token const fetchJson = async (url: string, token: string) => { const res = await fetch(url, { headers: { Authorization: `Bearer ${token}`, // Bearer scheme for Graph API Accept: 'application/json', }, }); if (!res.ok) { // 401: Token expired or invalid // 403: Insufficient permissions // 404: Resource not found const text = await res.text(); const err: any = new Error(`Graph ${res.status}: ${text}`); err.status = res.status; throw err; } return res.json(); }; // Example API endpoint that uses token app.get('/api/job-files', async (c) => { const token = getGraphToken(c); // Get token from header if (!token) { return c.json({ error: 'GRAPH_TOKEN missing' }, 500); } const link = c.req.query('link'); // ... uses fetchJson(url, token) to call Graph API }); ``` --- ## 9. Storage Comparison ``` ┌─────────────────────────────────────────────────────────────┐ │ localStorage vs pb.authStore │ ├─────────────────────────────────────────────────────────────┤ │ │ │ localStorage (Visible) │ │ ├─ What: graphAccessToken (Graph API access) │ │ ├─ Where: Browser's persistent storage │ │ ├─ How: localStorage.getItem/setItem │ │ ├─ Clears: Never (until logout or script removes) │ │ ├─ Access: JavaScript can read/write │ │ └─ Risk: XSS attacks can steal token │ │ │ │ pb.authStore (Hidden) │ │ ├─ What: Refresh token + auth token + user data │ │ ├─ Where: PocketBase SDK internal storage │ │ ├─ How: pb.authStore.clear() / pb.authStore.model │ │ ├─ Clears: On logout() or pb.authStore.clear() │ │ ├─ Access: Limited to PocketBase SDK methods │ │ └─ Risk: Lower (not directly accessible) │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 10. Error Handling Flowchart ``` API CALL ATTEMPT │ ▼ TOKEN AVAILABLE? │ ├─ NO → USER NOT AUTHENTICATED │ └─ Redirect to signin.html ✓ │ └─ YES → SEND REQUEST WITH TOKEN │ ▼ RESPONSE STATUS? │ ├─ 200 OK → PROCESS DATA │ └─ Display in UI ✓ │ ├─ 401 UNAUTHORIZED │ (Token expired/invalid) │ └─ TRIGGER TOKEN REFRESH │ └─ Redirect to signin.html?reauth=1 │ └─ PocketBase refreshes token │ └─ Redirect back to index.html │ └─ RETRY API CALL ✓ │ ├─ 403 FORBIDDEN │ (Insufficient permissions) │ └─ SHOW ERROR MESSAGE │ └─ User needs additional permissions │ └─ 5XX SERVER ERROR └─ RETRY LOGIC └─ Exponential backoff (optional) ``` --- ## 11. Testing the Token Flow ### Verify Token in localStorage ```javascript // Browser console (any page after login) localStorage.getItem('graphAccessToken') // Output: "eyJ0eXAiOiJKV1QiLCJhbGc..." ``` ### Verify Token Refresh ```javascript // Browser console (signin.html with reauth=1) const { pb } = window.Auth; const authData = await pb.collection('users').authRefresh(); console.log('New token:', authData?.meta?.graphAccessToken?.substring(0, 30) + '...'); ``` ### Test API Endpoint ```bash # Get token from localStorage first, then: curl -H "x-graph-token: eyJ0eXAi..." \ http://localhost:3000/api/job-files?link=https://czflex.sharepoint.com/... ``` --- ## 12. Environment Configuration **File**: `secrets/.env` (required) ```env # PocketBase connection (if running separate) POCKETBASE_URL=https://pocketbase.ccllc.pro # Microsoft OAuth (fallback Graph token if needed) GRAPH_TOKEN= MS_GRAPH_TOKEN= # Server port PORT=3000 ``` --- ## Summary **Mode1Test Token Flow in 3 Steps:** 1. **Acquisition** (signin.html): - User clicks "Sign in with Microsoft" - PocketBase handles OAuth2 handshake - Extracts Graph token from response - Stores in localStorage 2. **Usage** (index.html → server.ts): - Frontend reads token from localStorage - Sends in `x-graph-token` header - Backend receives and passes to Graph API 3. **Refresh** (When 401 occurs): - Frontend redirects to signin.html?reauth=1 - PocketBase calls authRefresh() - Uses internal refresh token (pb.authStore) - Gets new Graph token from Microsoft - Updates localStorage - Auto-redirects back with fresh token **Key Files**: - `Mode1Test/frontend/signin.html` - Token extraction & storage - `Mode1Test/frontend/auth.js` - Auth helper functions - `Mode1Test/backend/server.ts` - Token usage in API endpoints