Chrome Extension Authentication Patterns — Best Practices
6 min readAuthentication Patterns for Chrome Extensions
This guide covers authentication patterns for Chrome extensions connecting to external services, including OAuth 2.0, token management, and security best practices.
Prerequisites
Declare the required permissions in your manifest:
{
"manifest_version": 3,
"permissions": ["identity", "storage"],
"host_permissions": ["https://*.example.com/*"]
}
OAuth 2.0 with chrome.identity.launchWebAuthFlow()
PKCE Flow Implementation
The PKCE (Proof Key for Code Exchange) flow is the recommended OAuth pattern for extensions:
// utils/oauth.ts
async function startOAuthFlow(): Promise<OAuthToken> {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', chrome.identity.getRedirectURL());
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'read write');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
const responseUrl = await chrome.identity.launchWebAuthFlow({
url: authUrl.toString(),
interactive: true
});
const code = new URL(responseUrl).searchParams.get('code');
return exchangeCodeForTokens(code, codeVerifier);
}
function generateCodeVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
Token Storage and Refresh
Never store tokens in localStorage or plain chrome.storage.local. Use encrypted storage:
// utils/tokenManager.ts
class TokenManager {
private static STORAGE_KEY = 'secure_tokens';
async storeTokens(tokens: { access: string; refresh: string; expires: number }): Promise<void> {
const encrypted = await this.encrypt(JSON.stringify(tokens));
await chrome.storage.session.set({ [TokenManager.STORAGE_KEY]: encrypted });
}
async getValidToken(): Promise<string | null> {
const stored = await chrome.storage.session.get(TokenManager.STORAGE_KEY);
const tokens = JSON.parse(await this.decrypt(stored[TokenManager.STORAGE_KEY]));
if (Date.now() >= tokens.expires - 60000) {
return this.refreshAccessToken(tokens.refresh);
}
return tokens.access;
}
async revokeTokens(): Promise<void> {
await chrome.storage.session.remove(TokenManager.STORAGE_KEY);
}
}
Google-Specific: chrome.identity.getAuthToken()
For Google APIs, use the built-in token management:
async function getGoogleAuthToken(): Promise<string> {
const result = await chrome.identity.getAuthToken({
interactive: true,
scopes: ['https://www.googleapis.com/auth/drive']
});
return result.token; // MV3 returns { token: string, grantedScopes: string[] }
}
async function removeGoogleToken(): Promise<void> {
const result = await chrome.identity.getAuthToken({ interactive: false });
await chrome.identity.removeCachedAuthToken({ token: result.token });
}
Login State UI
Dynamically show different popup content based on authentication state:
// popup/main.ts
document.addEventListener('DOMContentLoaded', async () => {
const tokens = await chrome.storage.session.get('secure_tokens');
const container = document.getElementById('app');
if (tokens['secure_tokens']) {
container.innerHTML = `
<div class="logged-in">
<img src="${user.avatar}" alt="Profile" />
<span>${user.name}</span>
<button id="logout">Sign Out</button>
</div>
`;
document.getElementById('logout').addEventListener('click', handleLogout);
} else {
container.innerHTML = `
<button id="login">Sign In with Google</button>
`;
document.getElementById('login').addEventListener('click', startOAuthFlow);
}
});
Multi-Account Support
Store multiple account tokens with account identifiers:
async function switchAccount(accountId: string): Promise<void> {
const accounts = await chrome.storage.session.get('oauth_accounts');
const tokens = JSON.parse(accounts['oauth_accounts'])[accountId];
await chrome.storage.session.set({ current_account: accountId });
await tokenManager.storeTokens(tokens);
}
Security Best Practices
- Never hardcode API keys — Use chrome.storage.local or an options page
- Never store passwords — Use token-based authentication only
- Validate tokens server-side — Never trust client-side token expiration alone
- Use secure storage — Prefer chrome.storage.session over chrome.storage.local
- Handle token revocation — Listen for forced logout events from your API
CORS and Credentials
When making authenticated API calls, configure credentials properly:
async function authenticatedFetch(url: string): Promise<Response> {
const token = await tokenManager.getValidToken();
return fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
credentials: 'omit' // Tokens are in headers, not cookies
});
}
Cross-References
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.