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
560 lines
20 KiB
Markdown
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
|
|
|