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

20 KiB

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):

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)

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)

// 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

// Browser console (any page after login)
localStorage.getItem('graphAccessToken')
// Output: "eyJ0eXAiOiJKV1QiLCJhbGc..."

Verify Token Refresh

// 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

# 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)

# 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