Files
Job-Info/OAUTH2_REFERENCE_GUIDE.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

14 KiB

/**

  • REFERENCE TOOL: OAuth2 + Microsoft Graph Implementation Guide
  • This document provides exact patterns and code structures to follow when
  • building OAuth2 and Microsoft Graph integration. */

// ============================================================================ // PART 1: BACKEND OAUTH ROUTES (Hono + TypeScript) // ============================================================================

/**

  • Route: POST /api/auth/authorize
  • Purpose: Exchange authorization code for tokens using service worker credentials
  • CRITICAL: This route uses CLIENT_SECRET on backend only. Never expose to frontend.
  • Request body:
  • {
  • code: string; // Authorization code from Microsoft
  • state: string; // State value for CSRF protection
  • codeVerifier: string; // PKCE code verifier (for SPA security)
  • }
  • Response:
  • {
  • accessToken: string; // Short-lived token for Graph API (1 hour)
  • refreshToken: string; // Long-lived refresh token (24 hours for SPA)
  • expiresIn: number; // Token expiration in seconds
  • user: { // User info from Microsoft
  • id: string;
    
  • displayName: string;
    
  • mail: string;
    
  • }
  • }
  • Error responses:
  • {
  • error: "invalid_code" | "expired_code" | "invalid_request";
  • message: string;
  • } */

// Pseudo-code (actual implementation will be TypeScript): async function authorizeRoute(c: Context) { // 1. EXTRACT & VALIDATE request const { code, state, codeVerifier } = c.req.json();

// VALIDATION CHECKLIST: if (!code || !state || !codeVerifier) { return c.json({ error: 'Missing required parameters' }, 400); }

// Verify state matches session state (CSRF protection) // This requires storing state in session/Redis during login initiation

// 2. EXCHANGE CODE for tokens using CLIENT_SECRET const tokenResponse = await fetch('https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: process.env.MICROSOFT_CLIENT_ID, client_secret: process.env.MICROSOFT_CLIENT_SECRET, // BACKEND ONLY code: code, redirect_uri: process.env.MICROSOFT_REDIRECT_URI, grant_type: 'authorization_code', code_verifier: codeVerifier, // Required for PKCE }).toString(), });

if (!tokenResponse.ok) { const error = await tokenResponse.json(); console.error('Microsoft token error:', error); return c.json({ error: error.error, message: error.error_description }, 400); }

const tokens = await tokenResponse.json(); // tokens contains: { access_token, refresh_token, expires_in, token_type, ... }

// 3. GET USER INFO from Microsoft Graph const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', { headers: { Authorization: Bearer ${tokens.access_token} }, });

if (!userResponse.ok) { return c.json({ error: 'Failed to fetch user info' }, 500); }

const userInfo = await userResponse.json(); // userInfo contains: { id, displayName, mail, ... }

// 4. STORE REFRESH TOKEN in PocketBase (secured backend) // Pattern: // - Create/update user record in PocketBase with refresh_token field // - Key field: Microsoft ID or email // - Store encrypted refresh token // - NEVER return refresh token to frontend

await pb.collection('users').upsert({ microsoftId: userInfo.id, email: userInfo.mail, displayName: userInfo.displayName, refreshToken: tokens.refresh_token, // Store securely refreshTokenExpiry: Date.now() + (24 * 60 * 60 * 1000), // 24 hours });

// 5. RETURN ONLY ACCESS TOKEN to frontend return c.json({ accessToken: tokens.access_token, expiresIn: tokens.expires_in, // Usually 3600 seconds (1 hour) user: { id: userInfo.id, displayName: userInfo.displayName, mail: userInfo.mail, }, }); }

// ============================================================================ // PART 2: TOKEN REFRESH ROUTE // ============================================================================

/**

  • Route: POST /api/auth/refresh
  • Purpose: Get new access token using refresh token from PocketBase
  • Headers required:
    • Authorization: Bearer {userId} (user ID from frontend, used to lookup refresh token)
  • Response:
  • {
  • accessToken: string;
  • expiresIn: number;
  • } */

async function refreshRoute(c: Context) { // 1. EXTRACT user ID from auth header const auth = c.req.header('Authorization'); if (!auth || !auth.startsWith('Bearer ')) { return c.json({ error: 'Unauthorized' }, 401); } const userId = auth.replace('Bearer ', '');

// 2. RETRIEVE refresh token from PocketBase const user = await pb.collection('users').getOne(userId); if (!user || !user.refreshToken) { return c.json({ error: 'No refresh token found' }, 401); }

// 3. EXCHANGE refresh token for new access token const tokenResponse = await fetch('https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: process.env.MICROSOFT_CLIENT_ID, client_secret: process.env.MICROSOFT_CLIENT_SECRET, refresh_token: user.refreshToken, grant_type: 'refresh_token', scope: 'https://graph.microsoft.com/.default', }).toString(), });

if (!tokenResponse.ok) { const error = await tokenResponse.json(); // If refresh token invalid, user must re-authenticate return c.json({ error: 'Token refresh failed', needsReauth: true }, 401); }

const tokens = await tokenResponse.json();

// 4. UPDATE refresh token in PocketBase (tokens may have new refresh token) if (tokens.refresh_token) { await pb.collection('users').update(userId, { refreshToken: tokens.refresh_token, }); }

// 5. RETURN new access token (never refresh token) return c.json({ accessToken: tokens.access_token, expiresIn: tokens.expires_in, }); }

// ============================================================================ // PART 3: FRONTEND OAUTH FLOW (Svelte + PKCE) // ============================================================================

/**

  • PKCE Pattern (Proof Key for Code Exchange)
  • Why: SPAs can't securely store client secrets, so PKCE adds extra layer.
  • Flow:
    1. Generate random code_verifier (43-128 chars)
    1. Create code_challenge = BASE64_URL(SHA256(code_verifier))
    1. Send code_challenge to Microsoft
    1. Microsoft verifies code_verifier when exchanging code
  • This prevents authorization code interception attacks. */

// Generate PKCE code verifier and challenge function generatePKCE() { // Step 1: Generate random string 43-128 chars, base64url encoded const codeVerifier = Array.from(crypto.getRandomValues(new Uint8Array(32))) .map(b => String.fromCharCode(b)) .join(''); // Actually encode as base64url: const encoded = btoa(codeVerifier) .replace(/+/g, '-') .replace(///g, '_') .replace(/=/g, '');

// Step 2: Create code challenge = SHA256(verifier) -> base64url const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(encoded)); const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(hash))) .replace(/+/g, '-') .replace(///g, '_') .replace(/=/g, '');

return { codeVerifier: encoded, codeChallenge }; }

// Initiate login function initiateLogin() { const { codeVerifier, codeChallenge } = generatePKCE();

// STORE code_verifier in sessionStorage (lost on page refresh, safe) sessionStorage.setItem('oauth_code_verifier', codeVerifier);

// Generate state for CSRF protection const state = crypto.getRandomValues(new Uint8Array(16)).toString(); sessionStorage.setItem('oauth_state', state);

// Redirect to Microsoft login const params = new URLSearchParams({ client_id: process.env.PUBLIC_MICROSOFT_CLIENT_ID, response_type: 'code', scope: 'https://graph.microsoft.com/User.Read https://graph.microsoft.com/Files.Read.All', redirect_uri: 'http://localhost:5173/auth/callback', code_challenge: codeChallenge, code_challenge_method: 'S256', state: state, prompt: 'select_account', // Let user choose account });

window.location.href = https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?${params.toString()}; }

// Handle callback (in /auth/callback route) async function handleOAuthCallback() { // Step 1: Extract code and state from URL const params = new URLSearchParams(window.location.search); const code = params.get('code'); const returnedState = params.get('state');

if (!code || !returnedState) { throw new Error('Missing code or state in callback'); }

// Step 2: Verify state (CSRF protection) const storedState = sessionStorage.getItem('oauth_state'); if (returnedState !== storedState) { throw new Error('State mismatch - possible CSRF attack'); }

// Step 3: Retrieve code verifier const codeVerifier = sessionStorage.getItem('oauth_code_verifier'); if (!codeVerifier) { throw new Error('Code verifier not found'); }

// Step 4: Send to backend to exchange code for tokens const response = await fetch('/api/auth/authorize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, state: returnedState, codeVerifier }), });

if (!response.ok) { const error = await response.json(); throw new Error(error.message); }

const { accessToken, expiresIn, user } = await response.json();

// Step 5: Store in Svelte stores and sessionStorage authStore.set({ user, accessToken, expiresIn }); sessionStorage.setItem('ms_access_token', accessToken); sessionStorage.setItem('ms_token_expiry', (Date.now() + expiresIn * 1000).toString());

// Clear OAuth temporary values sessionStorage.removeItem('oauth_code_verifier'); sessionStorage.removeItem('oauth_state');

// Redirect to home goto('/'); }

// ============================================================================ // PART 4: SERVICE WORKER TOKEN REFRESH LOGIC // ============================================================================

/**

  • Service Worker: Background token refresh
  • Pattern: Service Worker periodically checks token expiry and refreshes
  • before expiration, updating the frontend via postMessage.
  • Why: Ensures frontend always has valid token without user interaction */

// In service-worker.ts let tokenExpiry = 0; let userId = '';

// Main loop: Check and refresh every minute setInterval(async () => { if (!userId || Date.now() < tokenExpiry - 5 * 60 * 1000) { // Token still valid (5 minute buffer), skip return; }

try { const response = await fetch('/api/auth/refresh', { method: 'POST', headers: { Authorization: Bearer ${userId} }, });

if (!response.ok) {
  // Refresh failed, notify frontend
  const clients = await self.clients.matchAll();
  clients.forEach(client => {
    client.postMessage({
      type: 'TOKEN_REFRESH_FAILED',
      needsReauth: true,
    });
  });
  return;
}

const { accessToken, expiresIn } = await response.json();

// Update token expiry
tokenExpiry = Date.now() + expiresIn * 1000;

// Notify all clients of new token
const clients = await self.clients.matchAll();
clients.forEach(client => {
  client.postMessage({
    type: 'TOKEN_REFRESHED',
    accessToken,
    expiresIn,
  });
});

} catch (error) { console.error('Token refresh error:', error); } }, 60 * 1000); // Every 60 seconds

// ============================================================================ // PART 5: CONSTANTS & CONFIGURATION // ============================================================================

// Environment variables required (in .env and secrets/.env) const OAUTH_CONFIG = { MICROSOFT_CLIENT_ID: 'your-client-id', MICROSOFT_CLIENT_SECRET: 'your-secret', // Backend only MICROSOFT_TENANT_ID: 'common', // Or specific tenant MICROSOFT_REDIRECT_URI: 'http://localhost:5173/auth/callback', };

// Graph API scopes needed const GRAPH_SCOPES = [ 'https://graph.microsoft.com/User.Read', // Read user profile 'https://graph.microsoft.com/Files.Read.All', // Read files in OneDrive 'offline_access', // Required for refresh token ];

// ============================================================================ // PART 6: ERROR HANDLING CHECKLIST // ============================================================================

/**

  • Common OAuth errors and how to handle them:
  • invalid_code / expired_code
  • → User took too long to complete auth, or code was reused
  • → Solution: Restart login flow
  • invalid_request
  • → Missing required parameters (client_id, redirect_uri, code, etc)
  • → Solution: Check all required params are sent correctly
  • unauthorized_client
  • → Client not registered in Azure or not configured
  • → Solution: Verify app registration in Azure portal
  • access_denied
  • → User declined consent or permissions
  • → Solution: Retry with proper scopes, or user chose to not authorize
  • invalid_scope
  • → Requested scope not allowed for this app
  • → Solution: Check scopes in app registration, match exactly
  • invalid_client
  • → Client secret is wrong
  • → Solution: Verify MICROSOFT_CLIENT_SECRET is correct
  • token_expired
  • → Refresh token expired (24 hours for SPA)
  • → Solution: User must re-authenticate
  • AADSTS_error_codes
  • → Azure-specific errors (look up AADSTS number)
  • → Solution: Check error_description, consult Azure docs */

// ============================================================================ // SUMMARY // ============================================================================

/**

  • GOLDEN RULES for OAuth2 + Microsoft:
    1. NEVER expose CLIENT_SECRET to frontend
    1. ALWAYS use PKCE for SPA (code challenge/verifier)
    1. ALWAYS validate state parameter (CSRF protection)
    1. ALWAYS store refresh token on backend only
    1. STORE access token in memory or sessionStorage, NEVER localStorage
    1. ACCESS token expires in 1 hour, refresh BEFORE expiry
    1. REFRESH token expires in 24 hours, user must re-auth after
    1. Use SERVICE WORKER to refresh tokens in background
    1. SCOPE must include offline_access for refresh token
    1. Test with actual Microsoft account, not demo account */