\n\n\n```\n\n*This HTML structure provides two view states: login prompt for unauthenticated users and welcome message for authenticated users, with simple show/hide toggling.*\n\n### **Popup JavaScript**\n\n```javascript\n// popup.js\nimport { authManager } from './auth-manager.js';\n\ndocument.addEventListener('DOMContentLoaded', async () => {\n const loginBtn = document.getElementById('loginBtn');\n const logoutBtn = document.getElementById('logoutBtn');\n const loginView = document.getElementById('login-view');\n const authenticatedView = document.getElementById('authenticated-view');\n const userName = document.getElementById('userName');\n\n // Check initial auth state\n const authState = await authManager.getState();\n updateUI(authState.authenticated, authState.profile);\n\n loginBtn.addEventListener('click', async () => {\n const result = await authManager.login({ force: false });\n if (result.success) {\n const newState = await authManager.getState();\n updateUI(newState.authenticated, newState.profile);\n }\n });\n\n logoutBtn.addEventListener('click', async () => {\n await authManager.logout();\n updateUI(false, null);\n });\n\n function updateUI(authenticated, profile) {\n if (authenticated) {\n loginView.style.display = 'none';\n authenticatedView.style.display = 'block';\n userName.textContent = profile?.name || profile?.email || 'User';\n } else {\n loginView.style.display = 'block';\n authenticatedView.style.display = 'none';\n }\n }\n});\n```\n\n*The popup script handles UI interactions, checks authentication state on load, and updates the interface dynamically based on user authentication status.*\n\n## **Key Security Considerations**\n\n1. **PKCE Flow**: Essential for public clients like Chrome Extensions to prevent authorization code interception attacks\n\n2. **Token Storage**: Use `chrome.storage.local` instead of localStorage for better security\n\n3. **Session Management**: Implement absolute session timeout (1 hour) regardless of token refresh\n\n4. **Secure Logout**: Clear all local storage and invoke Auth0's logout endpoint\n\n5. **HTTPS Only**: Ensure all API calls use HTTPS and validate Auth0 domains\n\n\n## **Testing and Debugging**\n\n1. **Load Extension**: Go to `chrome://extensions/`, enable Developer Mode, and Load Unpacked\n\n2. **Check Console**: Use Chrome DevTools for debugging\n\n3. **Network Inspection**: Monitor Auth0 API calls in Network tab\n\n4. **Storage Inspection**: Verify token storage in Application tab\n\n\n## **Conclusion**\n\nThis implementation provides a production-ready authentication system for Chrome Extensions with:\n\n* ✅ **Secure Auth0 Integration** with PKCE flow\n\n* ✅ **Multi-environment support** for dev/qa/prod\n\n* ✅ **Automatic token refresh** and session management\n\n* ✅ **Proper error handling** and user feedback\n\n* ✅ **Clean architecture** with separation of concerns\n\n\nThe solution handles the complexities of OAuth 2.0 in a Chrome Extension context while maintaining security best practices. You can extend this foundation with additional features like role-based access control, API integration, or enhanced UI components.\n\n* * *\n\n**Happy Coding!** 🚀\n\n## **Additional Resources**\n\n* [Auth0 Documentation](https://auth0.com/docs/)\n\n* [Chrome Extension Identity API](https://developer.chrome.com/docs/extensions/reference/identity/)\n\n* [OAuth 2.0 PKCE Specification](https://tools.ietf.org/html/rfc7636)","keywords":"Auth0, Security, chrome-extension","articleSection":"security","url":"https://deeptechhub.dev/blog/chrome-ext-auth"}
security

Securing Chrome Extension using Auth0 Authentication

Step by step guide for implementing Authentication using Auth0 in chrome extension

Reading Time: 10 min readAuthor: DeepTechHub
#Auth0#Security#chrome-extension
Securing Chrome Extension using Auth0 Authentication

Audio controls for Securing Chrome Extension using Auth0 Authentication

Listen to this article

Chrome Extensions are powerful tools that enhance our browsing experience. But when your extension needs to interact with user-specific data or private APIs, you need a robust and secure authentication system. Rolling your own auth can be risky and time-consuming.

That's where Auth0 comes in. It handles the complexities of security for you, providing features like social logins, multi-factor authentication, and secure token management.

In this article, we'll implement a complete authentication layer in a Chrome Extension using Auth0 with PKCE(Public Key for Code Exchange) flow and proper token management.

What We'll Build

We'll create a Chrome Extension with a popup that has two states:

  1. A "Log In" button for unauthenticated users

  2. A welcome message and "Log Out" button for authenticated users

  3. Advanced features: token refresh, session management, and environment configuration

Prerequisites

Step 1: Getting a Static Extension ID

Chrome generates random Extension IDs for unpacked extensions, which causes problems for Auth0 whitelisting. Here's how to get a consistent ID:

Method 1: Chrome Developer Dashboard (Recommended)

  1. Package your extension directory into a .zip file

  2. Upload to Chrome Developer Dashboard

  3. Go to the "Package" tab and click "View public key"

  4. Copy the public key (remove newlines to create a single string)

  5. Add it to your manifest.json:

{
  "manifest_version": 3,
  "name": "Your Extension",
  "version": "1.0.0",
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAYourPublicKeyStringHere..."
}

This ensures Chrome uses the same extension ID every time you load the unpacked extension by providing a fixed public key.

Step 2: Auth0 Application Configuration

  1. Create Application: In Auth0 Dashboard, go to Applications > Applications > Create Application

  2. Choose Type: Select "Native" (Chrome Extensions are public clients)

  3. Configure Settings:

Application Type: Native
Allowed Callback URLs:
  https://<EXTENSION_ID>.chromiumapp.org/auth0,
  https://<EXTENSION_ID>.chromiumapp.org/
 
Allowed Logout URLs:
  https://<EXTENSION_ID>.chromiumapp.org/,
  chrome-extension://<EXTENSION_ID>/post-logout
 
Allowed Web Origins:
  chrome-extension://<EXTENSION_ID>,
  https://<EXTENSION_ID>.chromiumapp.org

Note: Replace <EXTENSION_ID> with your actual extension ID.

These URLs tell Auth0 which origins are allowed to use the authentication service, preventing unauthorized applications from using your Auth0 setup.

Step 3: Project Structure and Configuration

Environment Configuration

Create environment.js for multi-environment support:

// environment.js
export const ENVIRONMENTS = {
  dev: {
    key: 'dev',
    label: 'Development',
    apiBaseUrl: 'https://api.dev.example.com',
    auth0: {
      domain: 'dev-tenant.auth0.com',
      clientId: 'DEV_CLIENT_ID',
      audience: 'https://api.dev.example.com',
      scopes: 'openid profile email offline_access'
    },
    applicationId: '24c15a98-d4ff-3127-81ct-e0592e913f8d'
  },
  qa: {
    key: 'qa',
    label: 'QA',
    apiBaseUrl: 'https://api.qa.example.com',
    auth0: {
      domain: 'qa-tenant.auth0.com',
      clientId: 'QA_CLIENT_ID',
      audience: 'https://api.qa.example.com',
      scopes: 'openid profile email offline_access'
    },
    applicationId: '34c15a98-d4ff-3127-81ct-e0592e913f8d'
  },
  prod: {
    key: 'prod',
    label: 'Production',
    apiBaseUrl: 'https://api.prod.example.com',
    auth0: {
      domain: 'prod-tenant.auth0.com',
      clientId: 'PROD_CLIENT_ID',
      audience: 'https://api.prod.example.com',
      scopes: 'openid profile email offline_access'
    },
    applicationId: '44c15a98-d4ff-3127-81ct-e0592e913f8d'
  }
};
 
export const DEFAULT_ENV = 'dev';
export function getEnvironmentConfig(envKey) {
  return ENVIRONMENTS[envKey] || ENVIRONMENTS[DEFAULT_ENV];
}

This configuration object allows you to easily switch between different environments (dev/qa/prod) with their specific Auth0 and API settings.

Updated Manifest.json

{
  "manifest_version": 3,
  "name": "Auth0 Protected Extension",
  "description": "Chrome Extension with Auth0 Authentication",
  "version": "1.0",
  "key": "YourPublicKeyHere",
  "action": {
    "default_popup": "popup.html",
    "default_title": "Auth0 Example"
  },
  "permissions": [
    "scripting",
    "storage",
    "identity",
    "activeTab"
  ],
  "host_permissions": [
    "https://*.auth0.com/*",
    "https://api.dev.example.com/*",
    "https://api.qa.example.com/*",
    "https://api.prod.example.com/*"
  ],
  "background": {
    "service_worker": "background.js"
  }
}

The manifest declares necessary permissions: 'identity' for OAuth flows, 'storage' for token persistence, and host permissions for Auth0 and your APIs.

Constants Definition

// constants.js
export const STORAGE_KEYS = {
  AUTH_TOKEN: 'auth_token',
  REFRESH_TOKEN: 'refresh_token',
  USER_PROFILE: 'user_profile',
  ENVIRONMENT: 'environment',
  AUTH_TIME: 'auth_time'
};
 
export const EVENTS = {
  AUTH_CHANGED: 'auth.changed',
  AUTH_TOKEN_EXPIRING: 'auth.token_expiring',
  AUTH_TOKEN_REFRESH: 'auth.token_refresh',
  AUTH_LOGIN_REQUIRED: 'auth.login_required'
};

Centralized constants prevent typos and make the code more maintainable by providing single source of truth for storage keys and event names.

Step 4: Core Authentication Service

Auth Service Implementation

// auth-service.js
import { getEnvironmentConfig } from './environment.js';
 
export class AuthService {
  constructor() {
    this.envConfig = getEnvironmentConfig('dev');
    this._loginInProgress = false;
  }
 
  async login({ force = false } = {}) {
    if (this._loginInProgress) {
      throw new Error('login_in_progress');
    }
 
    this._loginInProgress = true;
 
    try {
      const { domain, clientId, audience, scopes } = this.envConfig.auth0;
      const redirectUri = chrome.identity.getRedirectURL();
 
      // PKCE Code Verifier and Challenge
      const codeVerifier = this.generateCodeVerifier();
      const codeChallenge = await this.generateCodeChallenge(codeVerifier);
 
      const authUrl = `https://${domain}/authorize?` + new URLSearchParams({
        response_type: 'code',
        code_challenge_method: 'S256',
        code_challenge: codeChallenge,
        client_id: clientId,
        redirect_uri: redirectUri,
        scope: scopes,
        audience: audience,
        ...(force && { prompt: 'login' })
      });
 
      // Launch Auth0 Universal Login
      const callbackUrl = await new Promise((resolve, reject) => {
        chrome.identity.launchWebAuthFlow(
          { url: authUrl, interactive: true },
          (url) => chrome.runtime.lastError ?
            reject(chrome.runtime.lastError) : resolve(url)
        );
      });
 
      const code = this.extractCodeFromCallback(callbackUrl);
 
      // Exchange code for tokens
      const tokens = await this.exchangeCodeForTokens(code, codeVerifier, redirectUri);
 
      await this.storeTokens(tokens);
      await this.fetchAndStoreUserProfile(tokens.access_token);
 
      return true;
    } finally {
      this._loginInProgress = false;
    }
  }
 
  async exchangeCodeForTokens(code, codeVerifier, redirectUri) {
    const { domain, clientId } = this.envConfig.auth0;
 
    const response = await fetch(`https://${domain}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        client_id: clientId,
        code_verifier: codeVerifier,
        code: code,
        redirect_uri: redirectUri,
      }),
    });
 
    if (!response.ok) throw new Error('Token exchange failed');
    return await response.json();
  }
 
  generateCodeVerifier() {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return btoa(String.fromCharCode(...array))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');
  }
 
  async generateCodeChallenge(verifier) {
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const digest = await crypto.subtle.digest('SHA-256', data);
    return btoa(String.fromCharCode(...new Uint8Array(digest)))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');
  }
 
  extractCodeFromCallback(callbackUrl) {
    const url = new URL(callbackUrl);
    return url.searchParams.get('code') ||
           new URLSearchParams(url.hash.substring(1)).get('code');
  }
 
  async storeTokens(tokens) {
    await chrome.storage.local.set({
      auth_token: tokens.access_token,
      refresh_token: tokens.refresh_token,
      auth_time: Date.now()
    });
  }
}

This service implements the PKCE OAuth flow: generates secure code verifier/challenge, launches Auth0 login, exchanges authorization code for tokens, and securely stores them.

Step 5: Authentication Manager

// auth-manager.js
import { AuthService } from './auth-service.js';
import { EVENTS, STORAGE_KEYS } from './constants.js';
 
class AuthManager {
  constructor() {
    this.authService = new AuthService();
    this._refreshTimer = null;
    this._SESSION_MAX_AGE_MS = 3600 * 1000; // 1 hour
  }
 
  async login({ force = false } = {}) {
    try {
      const success = await this.authService.login({ force });
      if (success) {
        await this.scheduleTokenRefresh();
        await this.scheduleSessionExpiry();
      }
      return { success };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
 
  async scheduleTokenRefresh() {
    const token = await chrome.storage.local.get(STORAGE_KEYS.AUTH_TOKEN);
    if (!token) return;
 
    const payload = JSON.parse(atob(token.split('.')[1]));
    const expiresAt = payload.exp * 1000;
    const refreshTime = expiresAt - Date.now() - (5 * 60 * 1000); // Refresh 5 min before expiry
 
    this._refreshTimer = setTimeout(async () => {
      await this.refreshTokens();
    }, Math.max(refreshTime, 0));
  }
 
  async refreshTokens() {
    const { refresh_token } = await chrome.storage.local.get(STORAGE_KEYS.REFRESH_TOKEN);
    if (!refresh_token) return null;
 
    const { domain, clientId } = this.envConfig.auth0;
 
    const response = await fetch(`https://${domain}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        client_id: clientId,
        refresh_token: refresh_token
      }),
    });
 
    if (response.ok) {
      const tokens = await response.json();
      await this.storeTokens(tokens);
      await this.scheduleTokenRefresh();
      return tokens.access_token;
    }
    return null;
  }
}

The AuthManager orchestrates the authentication flow, handles token refresh scheduling, and manages session lifecycle including automatic token renewal before expiry.

Step 6: UI Integration

Popup HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div id="app">
    <div class="auth-panel">
      <div class="brand-logo">
        <img src="icons/icon-48.png" alt="App Logo" />
      </div>
      <h2>My Secure App</h2>
 
      <div id="login-view">
        <p>Please sign in to continue</p>
        <button id="loginBtn" class="primary">Sign In</button>
      </div>
 
      <div id="authenticated-view" style="display: none;">
        <p>Welcome, <span id="userName"></span>!</p>
        <button id="logoutBtn" class="secondary">Sign Out</button>
      </div>
    </div>
  </div>
 
  <script type="module" src="popup.js"></script>
</body>
</html>

This HTML structure provides two view states: login prompt for unauthenticated users and welcome message for authenticated users, with simple show/hide toggling.

Popup JavaScript

// popup.js
import { authManager } from './auth-manager.js';
 
document.addEventListener('DOMContentLoaded', async () => {
  const loginBtn = document.getElementById('loginBtn');
  const logoutBtn = document.getElementById('logoutBtn');
  const loginView = document.getElementById('login-view');
  const authenticatedView = document.getElementById('authenticated-view');
  const userName = document.getElementById('userName');
 
  // Check initial auth state
  const authState = await authManager.getState();
  updateUI(authState.authenticated, authState.profile);
 
  loginBtn.addEventListener('click', async () => {
    const result = await authManager.login({ force: false });
    if (result.success) {
      const newState = await authManager.getState();
      updateUI(newState.authenticated, newState.profile);
    }
  });
 
  logoutBtn.addEventListener('click', async () => {
    await authManager.logout();
    updateUI(false, null);
  });
 
  function updateUI(authenticated, profile) {
    if (authenticated) {
      loginView.style.display = 'none';
      authenticatedView.style.display = 'block';
      userName.textContent = profile?.name || profile?.email || 'User';
    } else {
      loginView.style.display = 'block';
      authenticatedView.style.display = 'none';
    }
  }
});

The popup script handles UI interactions, checks authentication state on load, and updates the interface dynamically based on user authentication status.

Key Security Considerations

  1. PKCE Flow: Essential for public clients like Chrome Extensions to prevent authorization code interception attacks

  2. Token Storage: Use chrome.storage.local instead of localStorage for better security

  3. Session Management: Implement absolute session timeout (1 hour) regardless of token refresh

  4. Secure Logout: Clear all local storage and invoke Auth0's logout endpoint

  5. HTTPS Only: Ensure all API calls use HTTPS and validate Auth0 domains

Testing and Debugging

  1. Load Extension: Go to chrome://extensions/, enable Developer Mode, and Load Unpacked

  2. Check Console: Use Chrome DevTools for debugging

  3. Network Inspection: Monitor Auth0 API calls in Network tab

  4. Storage Inspection: Verify token storage in Application tab

Conclusion

This implementation provides a production-ready authentication system for Chrome Extensions with:

  • Secure Auth0 Integration with PKCE flow

  • Multi-environment support for dev/qa/prod

  • Automatic token refresh and session management

  • Proper error handling and user feedback

  • Clean architecture with separation of concerns

The solution handles the complexities of OAuth 2.0 in a Chrome Extension context while maintaining security best practices. You can extend this foundation with additional features like role-based access control, API integration, or enhanced UI components.


Happy Coding! 🚀

Additional Resources

Did you find this article useful?