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.pngManifest 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;
}Popup UI
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:
- Add domain to
host_permissionsin manifest - Ensure HTTPS in production
- Check cookie SameSite policy
OAuth Redirect Fails
Problem: OAuth window doesn't close
Solution:
- Verify redirect URI in Admin Dashboard
- Check callback URL handling
- Ensure tab listener is set up correctly
Session Not Persisting
Problem: User logged out on browser restart
Solution:
- Check
chrome.storage.local(notsessionStorage) - Verify session expiry is set correctly
- Ensure session cookie has long TTL
Deployment
Chrome Web Store
- Create developer account
- Prepare store listing
- Upload extension ZIP
- Submit for review
Firefox Add-ons
- Create Mozilla account
- Package extension:
web-ext build - Upload to addons.mozilla.org
- Submit for review
Edge Add-ons
Chrome extensions work on Edge with no changes!
Next Steps
- Integration Quick Start - General integration guide
- Admin Dashboard - Configure OAuth providers
- API Reference - API documentation
Support
Need help with your extension?
- 📧 Email: support@nubeauth.com
- 💬 GitHub Issues: github.com/0xdps/nube-auth/issues
