Olympus Docs
CookbookIntegrations & billing

Browser extension auth

Auth a Chrome/Firefox extension against Olympus

Browser extensions are weird. They run inside the user's browser but as a separate origin (extension URL). OAuth2 flow needs special handling.

Approach: PKCE with launchWebAuthFlow

Chrome's chrome.identity.launchWebAuthFlow (and Firefox's equivalent) handle the OAuth2 redirect flow inside the browser.

manifest.json

{
  "manifest_version": 3,
  "permissions": ["identity", "storage"],
  "oauth2": {
    "client_id": "<your-olympus-client-id>",
    "scopes": ["openid", "profile", "email"]
  }
}

Login flow

// background.ts
chrome.action.onClicked.addListener(async () => {
  const codeVerifier = generateVerifier();
  const codeChallenge = await sha256(codeVerifier);
  const state = generateState();

  const url = new URL(`https://ciam.your-domain/oauth2/auth`);
  url.search = new URLSearchParams({
    client_id: chrome.runtime.getManifest().oauth2!.client_id,
    response_type: "code",
    scope: "openid profile email",
    redirect_uri: chrome.identity.getRedirectURL(),  // e.g. https://abc.chromiumapp.org/
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
    state,
  }).toString();

  chrome.identity.launchWebAuthFlow(
    { url: url.toString(), interactive: true },
    async (responseUrl) => {
      const code = new URL(responseUrl!).searchParams.get("code");
      // Exchange code for tokens
      const tokens = await fetch(`https://ciam.your-domain/oauth2/token`, {
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: "authorization_code",
          client_id: chrome.runtime.getManifest().oauth2!.client_id,
          code: code!,
          redirect_uri: chrome.identity.getRedirectURL(),
          code_verifier: codeVerifier,
        }),
      }).then((r) => r.json());

      await chrome.storage.local.set({ tokens });
    }
  );
});

Olympus OAuth2 client config

In Athena → OAuth2 Clients → new client:

  • Type: Public (no secret in the extension).
  • Grant: authorization_code, refresh_token.
  • Redirect URI: https://<extension-id>.chromiumapp.org/
    • Chrome generates this; check chrome.identity.getRedirectURL().
    • Firefox generates a similar URL.

Storing tokens

Use chrome.storage.local for refresh tokens. Don't use localStorage, Chrome storage is content-script-readable; extension-local is safer.

For paranoia: encrypt tokens with the extension's own key derived from user input.

Refresh

async function getValidAccessToken() {
  const { tokens } = await chrome.storage.local.get("tokens");
  if (tokens.expires_at < Date.now() + 60_000) {
    // Refresh
    const newTokens = await fetch(`https://ciam.your-domain/oauth2/token`, {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "refresh_token",
        client_id: chrome.runtime.getManifest().oauth2!.client_id,
        refresh_token: tokens.refresh_token,
      }),
    }).then((r) => r.json());
    await chrome.storage.local.set({ tokens: { ...newTokens, expires_at: Date.now() + newTokens.expires_in * 1000 } });
    return newTokens.access_token;
  }
  return tokens.access_token;
}

Logout

async function logout() {
  await chrome.storage.local.clear();
  // Trigger RP-initiated logout via launchWebAuthFlow
  await chrome.identity.launchWebAuthFlow({
    url: `https://ciam.your-domain/oauth2/sessions/logout?id_token_hint=${idToken}`,
    interactive: false,
  });
}

Edge cases

  • Extension reload, chrome.storage.local survives extension reload but not uninstall.
  • Multiple profiles, each Chrome profile is separate storage.
  • MV3 service worker, your background script may be unloaded; persist state.

On this page