Files
Job-Info/TOKEN_FLOW.md
T
aewing 42ff3e8f33
Build & Deploy / Test & Lint (push) Has been cancelled
Build & Deploy / Security Scan (push) Has been cancelled
Code Quality / Code Quality Checks (push) Has been cancelled
Code Quality / Dependency Vulnerabilities (push) Has been cancelled
Code Quality / Performance Testing (push) Has been cancelled
Build & Deploy / Build Docker Images (push) Has been cancelled
Build & Deploy / Deploy to Kubernetes (push) Has been cancelled
Add Mode6Test: copy of Mode5Test running on port 3000
2026-01-20 13:43:29 +00:00

560 lines
20 KiB
Markdown

# 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=<your-microsoft-graph-token>
MS_GRAPH_TOKEN=<alternative-name>
# 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