78.1 Auth Sync Implementation

Documentation for Sessions

78.1 Authentication Sync Implementation

Session: 78 Focus: Chrome Extension Auth Sync from Main App Status: Completed


Problem Statement

The Chrome extension was showing "Not Authenticated" because:

  1. The main app (localhost:5200) stores auth tokens in localStorage
  2. The extension expected tokens in chrome.storage.local
  3. No mechanism existed to sync tokens between these storage systems

The extension's content script would send null tokens to the embedded panel, resulting in unauthenticated requests.


Solution Architecture

Overview

┌─────────────────────────────────────────────────────────────────────┐
│  Main App (localhost:5200)                                          │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ User logs in → Token stored in localStorage                  │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘
                                  ↓
                    Background Script detects main app tab
                                  ↓
┌─────────────────────────────────────────────────────────────────────┐
│  Service Worker (background/service-worker.js)                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ Injects syncAuthFromMainApp() function into page            │    │
│  │ Receives AUTH_SYNC message with token                        │    │
│  │ Stores token in chrome.storage.local                         │    │
│  │ Broadcasts AUTH_UPDATED to all tabs                          │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘
                                  ↓
┌─────────────────────────────────────────────────────────────────────┐
│  Content Script (content/inject.js)                                 │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ Listens for AUTH_UPDATED message                             │    │
│  │ Reads token from chrome.storage.local                        │    │
│  │ Sends AI_AUTH_INIT postMessage to embedded panel iframe      │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘
                                  ↓
┌─────────────────────────────────────────────────────────────────────┐
│  Panel (localhost:5300/panel)                                       │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ Receives AI_AUTH_INIT via postMessage                        │    │
│  │ Shows "Authenticated" status                                 │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

Implementation Details

1. Background Script: Auth Sync Detection

The service worker monitors tab updates to detect when the user is on the main app:

// apps/gograb-ai-extension/background/service-worker.js

// Session 78: Auth sync from main app
const MAIN_APP_ORIGINS = [
  'http://localhost:5200',
  'https://gograbspace.com',
  'https://app.gograbspace.com'
];

// Session 78: Listen for tab updates to sync auth from main app
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete' && tab.url) {
    const isMainApp = MAIN_APP_ORIGINS.some(origin => tab.url.startsWith(origin));
    if (isMainApp) {
      try {
        await chrome.scripting.executeScript({
          target: { tabId },
          func: syncAuthFromMainApp
        });
        console.log('[GoGrab AI] Auth sync script injected into main app tab');
      } catch (e) {
        console.log('[GoGrab AI] Could not inject auth sync script:', e.message);
      }
    }
  }
});

2. Auth Extraction Function

This function is injected into main app pages to extract auth from localStorage:

// Session 78: Function injected into main app page to read auth
function syncAuthFromMainApp() {
  // Try multiple possible localStorage keys for auth
  const token = localStorage.getItem('authToken') ||
                localStorage.getItem('token') ||
                localStorage.getItem('access_token') ||
                localStorage.getItem('gograb_auth_token');
  const userId = localStorage.getItem('userId') ||
                 localStorage.getItem('user_id') ||
                 localStorage.getItem('gograb_user_id');
  const userName = localStorage.getItem('userName') ||
                   localStorage.getItem('user_name') ||
                   localStorage.getItem('gograb_user_name');
  const userEmail = localStorage.getItem('userEmail') ||
                    localStorage.getItem('user_email') ||
                    localStorage.getItem('gograb_user_email');

  if (token) {
    chrome.runtime.sendMessage({
      type: 'AUTH_SYNC',
      payload: { token, userId, userName, userEmail }
    });
  }
}

3. Message Handlers in Service Worker

Handle auth-related messages from content scripts and popup:

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.type) {
    // Session 78: Auth sync from main app
    case 'AUTH_SYNC':
      if (message.payload) {
        chrome.storage.local.set({
          authToken: message.payload.token,
          userId: message.payload.userId,
          userName: message.payload.userName,
          userEmail: message.payload.userEmail,
          authSyncedAt: Date.now()
        }, () => {
          console.log('[GoGrab AI] Auth synced from main app');
          // Notify all tabs that auth is updated
          broadcastToTabs({ type: 'AUTH_UPDATED' });
          sendResponse({ success: true });
        });
      } else {
        sendResponse({ success: false, error: 'No auth payload' });
      }
      return true; // Keep channel open for async response

    // Session 78: Get current auth status
    case 'GET_AUTH_STATUS':
      chrome.storage.local.get(['authToken', 'userId', 'userName', 'userEmail', 'authSyncedAt'], (result) => {
        sendResponse({
          authenticated: !!result.authToken,
          userId: result.userId,
          userName: result.userName,
          userEmail: result.userEmail,
          syncedAt: result.authSyncedAt
        });
      });
      return true;

    // Session 78: Clear auth (logout)
    case 'CLEAR_AUTH':
      chrome.storage.local.remove(['authToken', 'userId', 'userName', 'userEmail', 'authSyncedAt'], () => {
        console.log('[GoGrab AI] Auth cleared');
        broadcastToTabs({ type: 'AUTH_UPDATED' });
        sendResponse({ success: true });
      });
      return true;

    // Session 78: Request auth sync (manually trigger sync)
    case 'REQUEST_AUTH_SYNC':
      requestAuthSyncFromMainApp().then((success) => {
        sendResponse({ success });
      });
      return true;
    
    // ... other handlers
  }
});

4. Content Script: Auth to Panel Bridge

The content script listens for auth updates and forwards them to the embedded panel:

// apps/gograb-ai-extension/content/inject.js

// Bridge Protocol Constants (Session 75.7)
const BRIDGE_MESSAGES = {
  // Auth
  AI_AUTH_INIT: 'AI_AUTH_INIT',
  AI_AUTH_REFRESH: 'AI_AUTH_REFRESH',
  // ... other messages
};

// Session 78: Listen for auth updates from background
case 'AUTH_UPDATED':
  console.log('[GoGrab AI Extension] Auth updated, refreshing panel auth');
  sendAuthToPanel();
  sendResponse({ success: true });
  break;

/**
 * Send auth to panel if available
 * Session 78: Enhanced with retry logic and auth sync request
 */
function sendAuthToPanel(retryCount = 0) {
  chrome.storage.local.get(['authToken', 'userId', 'userName', 'userEmail'], (result) => {
    if (result.authToken) {
      sendBridgeMessage(BRIDGE_MESSAGES.AI_AUTH_INIT, {
        token: result.authToken,
        userId: result.userId,
        userName: result.userName,
        userEmail: result.userEmail,
        platform: 'chrome-extension',
        instanceId: instanceId,
      });
      console.log('[GoGrab AI Extension] Auth sent to panel');
    } else if (retryCount < 3) {
      // No auth found - request sync from main app and retry
      console.log('[GoGrab AI Extension] No auth found, requesting sync (attempt', retryCount + 1, ')');
      chrome.runtime.sendMessage({ type: 'REQUEST_AUTH_SYNC' }, (response) => {
        if (response?.success) {
          // Wait a bit for the sync to complete then retry
          setTimeout(() => sendAuthToPanel(retryCount + 1), 1000);
        } else {
          console.log('[GoGrab AI Extension] Auth sync failed, no main app tab found');
        }
      });
    } else {
      console.log('[GoGrab AI Extension] No auth available after retries');
    }
  });
}

5. Popup UI: Login and Sync Buttons

Added fallback controls in the popup for manual auth management:

// apps/gograb-ai-extension/popup/popup.js

// Session 78: Auth DOM Elements
const authIndicator = document.getElementById('authIndicator');
const authStatusText = document.getElementById('authStatusText');
const authUserInfo = document.getElementById('authUserInfo');
const authUserName = document.getElementById('authUserName');
const authUserEmail = document.getElementById('authUserEmail');
const loginBtn = document.getElementById('loginBtn');
const syncAuthBtn = document.getElementById('syncAuthBtn');

// Session 78: Main app URLs for login
const MAIN_APP_LOGIN_URL = 'http://localhost:5200/auth/login';

// Session 78: Check authentication status
async function checkAuthStatus() {
  try {
    const status = await chrome.runtime.sendMessage({ type: 'GET_AUTH_STATUS' });
    updateAuthUI(status);
  } catch (err) {
    console.error('[Auth] Failed to get status:', err);
    updateAuthUI({ authenticated: false });
  }
}

// Session 78: Update auth UI based on status
function updateAuthUI(status) {
  if (status.authenticated) {
    authIndicator.classList.add('authenticated');
    authStatusText.textContent = 'Authenticated';
    authStatusText.style.color = '#22c55e';

    // Show user info
    if (status.userName || status.userEmail) {
      authUserInfo.style.display = 'block';
      authUserName.textContent = status.userName || 'User';
      authUserEmail.textContent = status.userEmail ? ` (${status.userEmail})` : '';
    }

    // Hide login, show sync button
    loginBtn.style.display = 'none';
    syncAuthBtn.style.display = 'block';
    syncAuthBtn.textContent = 'Refresh Auth';
  } else {
    authIndicator.classList.remove('authenticated');
    authStatusText.textContent = 'Not Authenticated';
    authStatusText.style.color = '#ef4444';

    // Hide user info
    authUserInfo.style.display = 'none';

    // Show login and sync buttons
    loginBtn.style.display = 'block';
    syncAuthBtn.style.display = 'block';
    syncAuthBtn.textContent = 'Sync Auth';
  }
}

Key Design Decisions

1. Multiple Storage Key Support

The sync function tries multiple localStorage key names to maximize compatibility:

const token = localStorage.getItem('authToken') ||
              localStorage.getItem('token') ||
              localStorage.getItem('access_token') ||
              localStorage.getItem('gograb_auth_token');

Reasoning: Different versions of the main app might use different key names. This ensures backward and forward compatibility.

2. Retry Logic with Exponential Backoff

The content script retries auth sync up to 3 times if no token is found:

if (retryCount < 3) {
  chrome.runtime.sendMessage({ type: 'REQUEST_AUTH_SYNC' }, (response) => {
    if (response?.success) {
      setTimeout(() => sendAuthToPanel(retryCount + 1), 1000);
    }
  });
}

Reasoning: The main app tab might not be immediately available, or the auth sync might take time to complete.

3. Broadcast Auth Updates

When auth is synced, all tabs are notified:

broadcastToTabs({ type: 'AUTH_UPDATED' });

Reasoning: Multiple extension panels might be open across different tabs. All should be updated when auth changes.


Testing

Manual Testing Steps

  1. Fresh Install Test:

    • Install extension
    • Open popup → Should show "Not Authenticated"
    • Open main app → Login
    • Extension should auto-sync → Popup shows "Authenticated"
  2. Sync Button Test:

    • While logged in to main app
    • Click "Sync Auth" in popup
    • Should update auth status
  3. Panel Auth Test:

    • Open any webpage
    • Click extension floating button
    • Panel should show "Authenticated" status

Future Improvements

  1. Token Refresh: Implement automatic token refresh before expiry
  2. Logout Sync: Detect logout in main app and clear extension auth
  3. Multiple Accounts: Support switching between accounts
  4. Token Encryption: Encrypt stored tokens in chrome.storage