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.
- Chrome generates this; check
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.localsurvives extension reload but not uninstall. - Multiple profiles, each Chrome profile is separate storage.
- MV3 service worker, your background script may be unloaded; persist state.