Skip to content

Learn how to integrate Nube Auth authentication into your browser extension (Chrome, Firefox, Edge).

Overview

This guide covers:

  • Manifest V3 configuration
  • OAuth authentication flow
  • Session management in extensions
  • Background service worker setup
  • Popup UI integration

Prerequisites

  • Chrome/Firefox browser
  • Basic understanding of browser extensions
  • Nube Auth app created in Admin Dashboard
  • OAuth providers configured

Project Structure

extension/
├── manifest.json           # Extension manifest
├── background.js          # Service worker (handles auth)
├── popup.html            # Extension popup UI
├── popup.js              # Popup logic
├── content.js            # Content script (optional)
└── icons/                # Extension icons
    ├── icon16.png
    ├── icon48.png
    └── icon128.png

Manifest Configuration

Create manifest.json:

json
{
  "manifest_version": 3,
  "name": "Your Extension Name",
  "version": "1.0.0",
  "description": "Your extension description",
  
  "permissions": [
    "storage",
    "identity",
    "cookies"
  ],
  
  "host_permissions": [
    "https://api.nubeauth.com/*",
    "https://your-app-domain.com/*"
  ],
  
  "background": {
    "service_worker": "background.js"
  },
  
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

Background Service Worker

Create background.js to handle authentication:

javascript
// background.js

const CONFIG = {
  appId: 'APP0abc123xyz...',
  gatewayUrl: 'https://api.nubeauth.com',
  appDomain: 'https://your-app-domain.com',
};

// Handle login request from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'login') {
    handleLogin(request.provider)
      .then(() => sendResponse({ success: true }))
      .catch((error) => sendResponse({ success: false, error: error.message }));
    return true; // Keep channel open for async response
  }
  
  if (request.action === 'logout') {
    handleLogout()
      .then(() => sendResponse({ success: true }))
      .catch((error) => sendResponse({ success: false, error: error.message }));
    return true;
  }
  
  if (request.action === 'getUser') {
    getUser()
      .then((user) => sendResponse({ success: true, user }))
      .catch((error) => sendResponse({ success: false, error: error.message }));
    return true;
  }
});

// OAuth login flow
async function handleLogin(provider) {
  const authUrl = `${CONFIG.gatewayUrl}/v1/auth/start?` +
    `app_id=${CONFIG.appId}&` +
    `provider=${provider}&` +
    `redirect_uri=${encodeURIComponent(CONFIG.appDomain + '/auth/callback')}`;

  return new Promise((resolve, reject) => {
    chrome.tabs.create({ url: authUrl, active: true }, (tab) => {
      // Listen for the callback
      const tabId = tab.id;
      
      chrome.tabs.onUpdated.addListener(function listener(updatedTabId, changeInfo) {
        if (updatedTabId === tabId && changeInfo.url) {
          const url = new URL(changeInfo.url);
          
          // Check if this is the callback URL
          if (url.origin === CONFIG.appDomain && url.pathname === '/auth/callback') {
            // Extract session from URL or cookie
            chrome.cookies.get({
              url: CONFIG.appDomain,
              name: 'pp_app_session'
            }, async (cookie) => {
              if (cookie) {
                // Store session
                await chrome.storage.local.set({ 
                  session: cookie.value,
                  sessionExpiry: Date.now() + (365 * 24 * 60 * 60 * 1000) // 365 days
                });
                
                // Close the tab
                chrome.tabs.remove(tabId);
                chrome.tabs.onUpdated.removeListener(listener);
                resolve();
              } else {
                reject(new Error('No session cookie found'));
              }
            });
          }
        }
      });
    });
  });
}

// Logout
async function handleLogout() {
  // Clear local storage
  await chrome.storage.local.remove(['session', 'sessionExpiry', 'user']);
  
  // Delete cookie
  await chrome.cookies.remove({
    url: CONFIG.appDomain,
    name: 'pp_app_session'
  });
  
  // Call logout endpoint
  await fetch(`${CONFIG.gatewayUrl}/v1/auth/logout`, {
    method: 'POST',
    credentials: 'include',
  });
}

// Get current user
async function getUser() {
  const { session, sessionExpiry } = await chrome.storage.local.get(['session', 'sessionExpiry']);
  
  if (!session || Date.now() > sessionExpiry) {
    throw new Error('Not authenticated');
  }
  
  // Check cache first
  const { user } = await chrome.storage.local.get('user');
  if (user) {
    return user;
  }
  
  // Fetch from API
  const response = await fetch(`${CONFIG.gatewayUrl}/v1/me`, {
    headers: {
      'Cookie': `pp_app_session=${session}`
    },
    credentials: 'include',
  });
  
  if (!response.ok) {
    throw new Error('Failed to fetch user');
  }
  
  const data = await response.json();
  
  // Cache user data
  await chrome.storage.local.set({ user: data.user });
  
  return data.user;
}

Create popup.html:

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    body {
      width: 300px;
      padding: 20px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }
    
    .login-view, .user-view {
      display: none;
    }
    
    .login-view.active, .user-view.active {
      display: block;
    }
    
    button {
      width: 100%;
      padding: 12px;
      margin: 8px 0;
      border: none;
      border-radius: 6px;
      font-size: 14px;
      font-weight: 500;
      cursor: pointer;
    }
    
    .btn-google {
      background: #4285f4;
      color: white;
    }
    
    .btn-github {
      background: #24292e;
      color: white;
    }
    
    .btn-logout {
      background: #ef4444;
      color: white;
    }
    
    .user-info {
      text-align: center;
      margin-bottom: 20px;
    }
    
    .avatar {
      width: 64px;
      height: 64px;
      border-radius: 50%;
      margin-bottom: 12px;
    }
    
    .loading {
      text-align: center;
      padding: 20px;
    }
  </style>
</head>
<body>
  <div id="loading" class="loading">
    <p>Loading...</p>
  </div>
  
  <div id="loginView" class="login-view">
    <h2>Login to Your Extension</h2>
    <button id="googleLogin" class="btn-google">Login with Google</button>
    <button id="githubLogin" class="btn-github">Login with GitHub</button>
  </div>
  
  <div id="userView" class="user-view">
    <div class="user-info">
      <img id="userAvatar" class="avatar" src="" alt="User avatar">
      <h3 id="userName"></h3>
      <p id="userEmail"></p>
    </div>
    <button id="logoutBtn" class="btn-logout">Logout</button>
  </div>
  
  <script src="popup.js"></script>
</body>
</html>

Create popup.js:

javascript
// popup.js

// Check authentication status on popup open
document.addEventListener('DOMContentLoaded', async () => {
  try {
    const response = await chrome.runtime.sendMessage({ action: 'getUser' });
    
    if (response.success && response.user) {
      showUserView(response.user);
    } else {
      showLoginView();
    }
  } catch (error) {
    console.error('Error checking auth:', error);
    showLoginView();
  }
});

// Login buttons
document.getElementById('googleLogin')?.addEventListener('click', async () => {
  await handleLogin('google');
});

document.getElementById('githubLogin')?.addEventListener('click', async () => {
  await handleLogin('github');
});

// Logout button
document.getElementById('logoutBtn')?.addEventListener('click', async () => {
  await handleLogout();
});

async function handleLogin(provider) {
  document.getElementById('loading').style.display = 'block';
  document.getElementById('loginView').classList.remove('active');
  
  try {
    const response = await chrome.runtime.sendMessage({
      action: 'login',
      provider,
    });
    
    if (response.success) {
      // Refresh to show user view
      const userResponse = await chrome.runtime.sendMessage({ action: 'getUser' });
      if (userResponse.success) {
        showUserView(userResponse.user);
      }
    } else {
      alert('Login failed: ' + response.error);
      showLoginView();
    }
  } catch (error) {
    console.error('Login error:', error);
    alert('Login failed');
    showLoginView();
  }
  
  document.getElementById('loading').style.display = 'none';
}

async function handleLogout() {
  try {
    await chrome.runtime.sendMessage({ action: 'logout' });
    showLoginView();
  } catch (error) {
    console.error('Logout error:', error);
    alert('Logout failed');
  }
}

function showLoginView() {
  document.getElementById('loading').style.display = 'none';
  document.getElementById('loginView').classList.add('active');
  document.getElementById('userView').classList.remove('active');
}

function showUserView(user) {
  document.getElementById('loading').style.display = 'none';
  document.getElementById('loginView').classList.remove('active');
  document.getElementById('userView').classList.add('active');
  
  document.getElementById('userAvatar').src = user.avatar_url || '/icons/icon48.png';
  document.getElementById('userName').textContent = user.name || 'Anonymous';
  document.getElementById('userEmail').textContent = user.email || '';
}

Content Script (Optional)

If you need to access auth in content scripts:

javascript
// content.js

// Get current user from background
async function getCurrentUser() {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ action: 'getUser' }, (response) => {
      if (response.success) {
        resolve(response.user);
      } else {
        reject(new Error(response.error));
      }
    });
  });
}

// Use in your content script
(async () => {
  try {
    const user = await getCurrentUser();
    console.log('Logged in as:', user.email);
    
    // Do something with user data
    injectUserInfo(user);
  } catch (error) {
    console.log('Not authenticated');
  }
})();

Add to manifest.json:

json
{
  "content_scripts": [
    {
      "matches": ["https://your-target-site.com/*"],
      "js": ["content.js"]
    }
  ]
}

Testing Your Extension

Security Best Practices

1. Never Hardcode Secrets

javascript
// ❌ DON'T: Hardcode app token
const appToken = 'nube_sk_live_abc123...';

// ✅ DO: Only use public App ID
const appId = 'APP0abc123...';

2. Validate Redirect URIs

Ensure callback URL matches exactly:

javascript
const VALID_CALLBACK_PATH = '/auth/callback';

if (url.pathname !== VALID_CALLBACK_PATH) {
  return; // Ignore non-callback URLs
}

3. Use Secure Storage

javascript
// Store sensitive data properly
await chrome.storage.local.set({
  session: sessionValue,
  sessionExpiry: Date.now() + ttl,
});

// Clear on logout
await chrome.storage.local.clear();

4. Handle Session Expiry

javascript
async function isSessionValid() {
  const { sessionExpiry } = await chrome.storage.local.get('sessionExpiry');
  return Date.now() < sessionExpiry;
}

Common Issues

Cookies Not Accessible

Problem: Cannot read pp_app_session cookie

Solution:

  1. Add domain to host_permissions in manifest
  2. Ensure HTTPS in production
  3. Check cookie SameSite policy

OAuth Redirect Fails

Problem: OAuth window doesn't close

Solution:

  1. Verify redirect URI in Admin Dashboard
  2. Check callback URL handling
  3. Ensure tab listener is set up correctly

Session Not Persisting

Problem: User logged out on browser restart

Solution:

  1. Check chrome.storage.local (not sessionStorage)
  2. Verify session expiry is set correctly
  3. Ensure session cookie has long TTL

Deployment

Chrome Web Store

  1. Create developer account
  2. Prepare store listing
  3. Upload extension ZIP
  4. Submit for review

Firefox Add-ons

  1. Create Mozilla account
  2. Package extension: web-ext build
  3. Upload to addons.mozilla.org
  4. Submit for review

Edge Add-ons

Chrome extensions work on Edge with no changes!

Next Steps

Support

Need help with your extension?

Released under the MIT License.