Implementing User Authorization in Browser Extension
Authorization in an extension is technically more complex than in a regular web app: no built-in sessions, cannot use httpOnly cookies, tokens must be stored in chrome.storage, and OAuth2 flow requires special handling.
OAuth2 via chrome.identity API
// manifest.json
{
"permissions": ["identity", "storage"],
"oauth2": {
"client_id": "YOUR_GOOGLE_CLIENT_ID",
"scopes": ["openid", "email", "profile"]
}
}
// Authorize via Google OAuth2
async function authenticateWithGoogle() {
return new Promise((resolve, reject) => {
chrome.identity.getAuthToken({ interactive: true }, async (token) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
// Exchange Google token for our app token
const resp = await fetch('https://api.example.com/v1/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ google_token: token }),
});
const { access_token, refresh_token } = await resp.json();
await chrome.storage.local.set({
access_token,
refresh_token,
token_expiry: Date.now() + 3600 * 1000,
});
resolve({ access_token });
});
});
}
Authorization via Own Service (email/password)
// Open authorization page in new tab
async function loginWithCredentials() {
const loginUrl = `https://app.example.com/extension-login?` +
`redirect_uri=${encodeURIComponent('https://app.example.com/extension-callback')}`;
// Open login page
chrome.tabs.create({ url: loginUrl });
// Listen for message from page after authorization
return new Promise((resolve) => {
const listener = (message) => {
if (message.type === 'AUTH_SUCCESS') {
chrome.runtime.onMessage.removeListener(listener);
storeTokens(message.tokens);
resolve(message.tokens);
}
};
chrome.runtime.onMessage.addListener(listener);
});
}
// On /extension-callback page after authorization:
// window.postMessage('extension-auth', ...) or chrome.runtime.sendMessage
Token Storage and Refresh
class TokenManager {
async getValidToken() {
const stored = await chrome.storage.local.get(['access_token', 'refresh_token', 'token_expiry']);
if (stored.access_token && stored.token_expiry > Date.now() + 60000) {
return stored.access_token;
}
// Need to refresh token
if (stored.refresh_token) {
return this.refreshToken(stored.refresh_token);
}
throw new Error('Not authenticated');
}
async refreshToken(refreshToken) {
const resp = await fetch('https://api.example.com/v1/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refresh_token: refreshToken }),
});
const tokens = await resp.json();
await chrome.storage.local.set({
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
token_expiry: Date.now() + tokens.expires_in * 1000,
});
return tokens.access_token;
}
async logout() {
await chrome.storage.local.remove(['access_token', 'refresh_token', 'token_expiry']);
chrome.identity.clearAllCachedAuthTokens(() => {});
}
}
Popup UI for Authorization
// popup.tsx
import { useState, useEffect } from 'react';
export function Popup() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
chrome.storage.local.get(['access_token'], async ({ access_token }) => {
if (access_token) {
const profile = await fetchUserProfile(access_token);
setUser(profile);
}
});
}, []);
if (!user) {
return (
<div className="p-4 w-72">
<h1 className="text-lg font-bold mb-4">Sign In</h1>
<button
onClick={() => chrome.runtime.sendMessage({ action: 'login' })}
className="btn-primary w-full"
>
Sign in with Google
</button>
</div>
);
}
return (
<div className="p-4 w-72">
<div className="flex items-center gap-3">
<img src={user.avatar} className="w-8 h-8 rounded-full" />
<span>{user.name}</span>
</div>
</div>
);
}
Timeline
Authorization in extension with OAuth2 and refresh tokens: 3–5 working days.







