Add Mode6Test: copy of Mode5Test running on port 3000
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
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
This commit is contained in:
+559
@@ -0,0 +1,559 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user