Chrome Extension OAuth & Identity — Developer Guide

17 min read

Identity & OAuth Guide

Overview

Google OAuth with getAuthToken

Chrome Identity & OAuth Guide

This guide covers authentication for Chrome Extensions using the chrome.identity API, which provides Google OAuth and custom OAuth flows.

Overview

The chrome.identity API provides methods for obtaining OAuth2 access tokens:

Method Purpose
getAuthToken() Get OAuth2 tokens for Google APIs
launchWebAuthFlow() Launch custom OAuth flows for third-party providers
getProfileUserInfo() Get user’s email and ID
removeCachedAuthToken() Clear cached tokens
onSignInChanged Listen for sign-in state changes

Add "identity" permission to manifest:

{ "permissions": ["identity"] }

Setting Up OAuth in manifest.json

Configure the oauth2 section for Google OAuth:

{
  "oauth2": {
    "client_id": "123456789-abc.apps.googleusercontent.com",
    "scopes": [
      "https://www.googleapis.com/auth/userinfo.email",
      "https://www.googleapis.com/auth/drive.readonly",
      "https://www.googleapis.com/auth/drive.file"
    ]
  }
}

Request only the minimum scopes needed.

Google API Console Configuration

  1. Create a project at Google Cloud Console
  2. Enable APIs in “APIs & Services” > “Library” (e.g., Google Drive API)
  3. Configure OAuth consent screen (“External”, add app name and email)
  4. Create credentials: “OAuth client ID” > “Chrome App”
  5. Enter your extension ID (from chrome://extensions)

Client ID format: 123456789-abc.apps.googleusercontent.com

chrome.identity.getAuthToken

Gets an OAuth2 access token for the signed-in user:

// Interactive: Shows sign-in prompt if needed
chrome.identity.getAuthToken({ interactive: true }, (token) => {
  if (chrome.runtime.lastError) {
    console.error('Error:', chrome.runtime.lastError.message);
    return;
  }
  console.log('Token:', token);
});

// Silent: Fails if user not already signed in
chrome.identity.getAuthToken({ interactive: false }, (token) => {
  console.log('User signed in:', !!token);
});

Token Management

chrome.identity.getProfileUserInfo

Gets email and ID without full OAuth scopes:

chrome.identity.getProfileUserInfo({ accountStatus: 'ANY' }, (userInfo) => {
  console.log('Email:', userInfo.email);
  console.log('ID:', userInfo.id);
});

Third-Party OAuth with launchWebAuthFlow

chrome.identity.removeCachedAuthToken

Removes a token from Chrome’s cache:

// Remove cached token
chrome.identity.removeCachedAuthToken({ token: currentToken }, () => {
  console.log('Token removed');
});

// Revoke token completely
function revokeToken(token) {
  chrome.identity.removeCachedAuthToken({ token }, () => {
    fetch(`https://accounts.google.com/o/oauth2/revoke?token=${token}`);
  });
}

chrome.identity.onSignInChanged

Listen for sign-in state changes:

chrome.identity.onSignInChanged.addListener((account, signedIn) => {
  console.log(`Account ${account.id}: ${signedIn ? 'signed in' : 'signed out'}`);
  if (!signedIn) clearStoredTokens();
});

chrome.identity.launchWebAuthFlow

Use for non-Google OAuth providers:

function startAuthFlow() {
  const redirectUrl = chrome.identity.getRedirectURL('github');
  
  const authUrl = new URL('https://github.com/login/oauth/authorize');
  authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
  authUrl.searchParams.set('redirect_uri', redirectUrl);
  authUrl.searchParams.set('scope', 'repo user:email');
  authUrl.searchParams.set('state', crypto.randomUUID());
  
  chrome.identity.launchWebAuthFlow(
    { url: authUrl.toString(), interactive: true },
    (responseUrl) => {
      if (chrome.runtime.lastError) return;
      const code = new URL(responseUrl).searchParams.get('code');
      exchangeCodeForToken(code);
    }
  );
}

Microsoft OAuth

function authenticateWithMicrosoft() {
  const authUrl = new URL('https://login.microsoftonline.com/common/oauth2/v2.0/authorize');
  authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
  authUrl.searchParams.set('redirect_uri', chrome.identity.getRedirectURL('microsoft'));
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('scope', 'openid profile email User.Read');
  authUrl.searchParams.set('state', crypto.randomUUID());
  
  chrome.identity.launchWebAuthFlow(
    { url: authUrl.toString(), interactive: true },
    async (responseUrl) => {
      const code = new URL(responseUrl).searchParams.get('code');
      await exchangeMicrosoftToken(code);
    }
  );
}

Storing Auth State

import { createStorage, defineSchema } from '@theluckystrike/webext-storage';
## Using Tokens with Google APIs

### Fetch User Profile

async function saveAuth(token, refresh, expiry, email) {
  await storage.setMany({
    authToken: token,
    refreshToken: refresh,
    tokenExpiry: expiry,
    userEmail: email
  });
}

async function getAuth() {
  return await storage.getMany(['authToken', 'refreshToken', 'tokenExpiry', 'userEmail']);
}

async function clearAuth() {
  await storage.removeMany(['authToken', 'refreshToken', 'tokenExpiry', 'userEmail']);
}

Token Refresh Pattern

function getUserProfile(token) {
  fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
    headers: { 'Authorization': `Bearer ${token}` }
  })
  .then(res => res.json())
  .then(user => console.log('Name:', user.name, 'Email:', user.email));
}

Common Mistakes


Turn Your Extension Into a Business

Ready to monetize? The Extension Monetization Playbook covers freemium models, Stripe integration, subscription architecture, and growth strategies for Chrome extension developers.


Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.

List Google Drive Files

function listDriveFiles(token, folderId = 'root') {
  const query = `'${folderId}' in parents and trashed = false`;
  const fields = 'files(id, name, mimeType, webViewLink)';
  
  fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=${encodeURIComponent(fields)}`, {
    headers: { 'Authorization': `Bearer ${token}` }
  })
  .then(res => res.json())
  .then(data => data.files.forEach(f => console.log(f.name)));
}

Token Expiry and Refresh

Google tokens expire after 1 hour:

class TokenManager {
  constructor() { this.tokenCache = null; this.expiryTime = null; }
  
  async getValidToken() {
    if (this.tokenCache && this.expiryTime > Date.now()) return this.tokenCache;
    
    return new Promise((resolve, reject) => {
      chrome.identity.getAuthToken({ interactive: false }, (token) => {
        if (token) { this.cacheToken(token); resolve(token); }
        else { this.getInteractiveToken().then(resolve).catch(reject); }
      });
    });
  }
  
  async getInteractiveToken() {
    return new Promise((resolve, reject) => {
      chrome.identity.getAuthToken({ interactive: true }, (token) => {
        if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
        this.cacheToken(token); resolve(token);
      });
    });
  }
  
  cacheToken(token) { this.tokenCache = token; this.expiryTime = Date.now() + (55 * 60 * 1000); }
  
  async refreshToken() { await this.removeCachedToken(); return this.getValidToken(); }
  
  removeCachedToken() {
    return new Promise((resolve) => {
      if (this.tokenCache) {
        chrome.identity.removeCachedAuthToken({ token: this.tokenCache }, () => {
          this.tokenCache = null; this.expiryTime = null; resolve();
        });
      } else resolve();
    });
  }
}

Handle 401 errors:

async function safeApiCall(url, options = {}) {
  const token = await tokenManager.getValidToken();
  let response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } });
  
  if (response.status === 401) {
    await tokenManager.refreshToken();
    const newToken = await tokenManager.getValidToken();
    response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${newToken}` } });
  }
  return response;
}

Non-Google OAuth: GitHub with PKCE

Always use PKCE for public clients:

class GitHubAuth {
  constructor(clientId) { this.clientId = clientId; this.redirectUrl = chrome.identity.getRedirectURL('github'); }
  
  async authorize() {
    const state = crypto.randomUUID();
    const verifier = this.generateCodeVerifier();
    const challenge = await this.generateCodeChallenge(verifier);
    
    await chrome.storage.session.set({ oauth_state: state, code_verifier: verifier });
    
    const authUrl = new URL('https://github.com/login/oauth/authorize');
    authUrl.searchParams.set('client_id', this.clientId);
    authUrl.searchParams.set('redirect_uri', this.redirectUrl);
    authUrl.searchParams.set('scope', 'repo user:email');
    authUrl.searchParams.set('state', state);
    authUrl.searchParams.set('code_challenge', challenge);
    authUrl.searchParams.set('code_challenge_method', 'S256');
    
    return new Promise((resolve, reject) => {
      chrome.identity.launchWebAuthFlow({ url: authUrl.toString(), interactive: true },
        async (responseUrl) => {
          if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
          const params = new URL(responseUrl).searchParams;
          const returnedState = params.get('state');
          if (returnedState !== state) { reject(new Error('State mismatch')); return; }
          const code = params.get('code');
          const { code_verifier } = await chrome.storage.session.get('code_verifier');
          resolve(await this.exchangeCodeForToken(code, code_verifier));
        });
    });
  }
  
  generateCodeVerifier() {
    const array = new Uint8Array(32); crypto.getRandomValues(array);
    return btoa(String.fromCharCode(...array)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  }
  
  async generateCodeChallenge(verifier) {
    const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
    return btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  }
  
  async exchangeCodeForToken(code, verifier) {
    const res = await fetch('https://github.com/login/oauth/access_token', {
      method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
      body: JSON.stringify({ client_id: this.clientId, code, redirect_uri: this.redirectUrl, code_verifier: verifier })
    });
    return res.json();
  }
}

Building a Google Drive File Picker

class DriveFilePicker {
  constructor(tokenManager) { this.tokenManager = tokenManager; }
  
  async openFilePicker() {
    const token = await this.tokenManager.getValidToken();
    const files = await this.listFiles(token, 'root');
    return this.renderFileList(files);
  }
  
  async listFiles(token, folderId) {
    const query = `'${folderId}' in parents and trashed = false`;
    const fields = 'files(id, name, mimeType, webViewLink)';
    const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=${encodeURIComponent(fields)}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    return (await res.json()).files || [];
  }
  
  async getFileContent(token, fileId) {
    const meta = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?fields=mimeType`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    const { mimeType } = await meta.json();
    
    const exports = {
      'application/vnd.google-apps.document': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      'application/vnd.google-apps.spreadsheet': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    };
    
    if (exports[mimeType]) {
      return (await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${exports[mimeType]}`, {
        headers: { 'Authorization': `Bearer ${token}` }
      })).blob();
    }
    return (await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, {
      headers: { 'Authorization': `Bearer ${token}` }
    })).blob();
  }
}

Security Best Practices

1. Store Tokens Securely

// DO: Use chrome.storage.local
chrome.storage.local.set({ authToken: token });

// DON'T: localStorage (never)
localStorage.setItem('token', token);

2. Always Use PKCE

Generate code verifier/challenge before auth, include in URL, exchange with verifier.

3. Use Minimal Scopes

// Bad
"scopes": ["https://www.googleapis.com/auth/drive"]

// Good
"scopes": ["https://www.googleapis.com/auth/drive.file"]

4. Clear Tokens on Uninstall

chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'uninstall') {
    chrome.storage.local.clear();
    chrome.runtime.setUninstallURL('https://yoursite.com/uninstall');
  }
});

Reference