Choosing Between getAuthToken and launchWebAuthFlow
chrome.identity.getAuthToken is shorter and Chrome-only; launchWebAuthFlow is portable and more work. Compare the two on scope, caching, sign-out and store review.
Table of Contents
chrome.identity.getAuthToken is four lines and returns a working Google access token. chrome.identity.launchWebAuthFlow is about eighty lines and works everywhere. Picking the short one because it is short is how extensions end up rewriting their entire authentication layer six months later when the Firefox port starts. This page is part of Identity & OAuth Authentication and lays out what each method actually gives you so the decision is made once, deliberately.
Root cause: they solve different problems that look the same
getAuthToken is not a general OAuth client. It is a bridge to the Google account already signed into the Chrome profile: the browser owns the credential, the browser caches the token, and the browser decides when to show a consent screen. launchWebAuthFlow is a generic redirect catcher — it opens a URL and tells you where it ended up. Everything else, including the token, is yours to manage.
Step 1. Answer the portability question first
If the extension will ever ship on Firefox or Safari, the decision is already made — getAuthToken does not exist there, and there is no polyfill, because there is no equivalent browser-managed account to bridge to. Everything after this is only relevant to Chrome-and-Edge-only products.
1// capability probe — run it once and log the result during evaluation
2const hasNativeGoogleAuth = typeof chrome !== "undefined"
3 && typeof chrome.identity?.getAuthToken === "function";
4
5console.log("getAuthToken available:", hasNativeGoogleAuth);
Execution context: Service worker or any extension page. In Chrome and Edge this logs true; in Firefox and Safari chrome.identity exists — mapped from browser.identity — but getAuthToken is absent, so the probe reads false without throwing. Feature-probing the method rather than the browser is the pattern described in feature detection instead of browser sniffing, and it is what lets a single build fall back cleanly.
Step 2. Compare the code you actually maintain
The Chrome path relies on the oauth2 manifest key, which the browser reads to decide which client and scopes to request.
1{
2 "permissions": ["identity"],
3 "oauth2": {
4 "client_id": "1234567890-abcdef.apps.googleusercontent.com",
5 "scopes": ["https://www.googleapis.com/auth/drive.file"]
6 },
7 "key": "MIIBIjANBgkq…" // pins the extension ID so the client_id keeps matching
8}
Execution context: Manifest, read at install time. The oauth2 key is ignored entirely by Firefox and Safari — it is not an error there, it simply does nothing, which is why a broken Chrome-only auth path can survive a cross-browser build unnoticed. The key field pins the extension ID during development so the OAuth client registration keeps matching before the extension is published.
1// sw.js — the entire Chrome-only sign-in
2async function getGoogleToken({ interactive = false } = {}) {
3 const { token } = await chrome.identity.getAuthToken({ interactive });
4 return token;
5}
6
7async function invalidate(token) {
8 await chrome.identity.removeCachedAuthToken({ token }); // next call fetches a fresh one
9}
Execution context: Service worker. There is no code exchange, no PKCE, no expiry tracking and no refresh token — the browser holds all of it. The catch is removeCachedAuthToken: the browser’s cache does not notice a token the resource server has rejected, so a 401 from a Google API must be followed by an explicit invalidation and one retry, or the same dead token is handed back on every call.
Step 3. Weigh the four differences that bite later
Account choice is the one that generates support tickets. getAuthToken uses the account signed into the Chrome profile; a user with a personal profile and a work Google account cannot choose the other one from inside your extension. launchWebAuthFlow sends them to Google’s own account chooser, where they can.
Sign-out is the second. removeCachedAuthToken clears the browser’s copy and nothing else — the grant still exists on the user’s Google account, so the next call signs them straight back in with no prompt. To genuinely sign out you must call the revocation endpoint yourself, at which point you are writing part of the flow you chose getAuthToken to avoid.
The third is scope escalation. With getAuthToken the scopes are baked into the manifest, so adding one means shipping a new version and waiting for every user to receive it before the feature can work — and the consent screen appears at a moment the user did not choose, usually the first time the extension happens to wake up after the update. launchWebAuthFlow builds its scope list at call time, so an incremental grant can be requested exactly when the user turns the feature on, with your own explanation on screen next to it. For an extension that starts with read access and later offers write, that difference is the whole onboarding story.
The fourth is testability. The native path cannot be exercised without a real signed-in Chrome profile, which makes it awkward in continuous integration: the browser owns the account, so there is nothing to stub at the API boundary. The portable path is an ordinary fetch against a token endpoint, which a local fixture server can stand in for, so the whole flow runs headless. If your test suite is expected to cover sign-in rather than skip it, that is worth weighing before choosing.
Step 4. Ship both when it is worth it
For a Chrome-first product with a real cross-browser roadmap, the pragmatic answer is one interface with two implementations selected by capability. Keep the seam narrow: everything above it asks for a token and knows nothing else.
1// auth/index.js — the only module that knows which path is in use
2import * as native from "./native-google.js";
3import * as web from "./web-auth-flow.js";
4
5const impl = typeof chrome.identity?.getAuthToken === "function" ? native : web;
6
7export const getAccessToken = impl.getAccessToken;
8export const signIn = impl.signIn;
9export const signOut = impl.signOut;
Execution context: Service worker module, resolved once at script evaluation. Both modules must present the same three functions and the same rejection shapes, or the calling code grows engine-specific branches and you have lost the benefit. The dual path costs roughly the eighty lines of the portable implementation plus twenty for the native one — worth it when Chrome is the overwhelming majority of installs and the Google account integration is a headline feature, and not worth it otherwise.
Cross-browser variation
- Chrome/Edge: both methods available.
getAuthTokenrequires theoauth2manifest key and works only with Google accounts; Edge implements it against Microsoft’s Chromium base and it behaves the same for Google clients. - Firefox:
getAuthTokenandremoveCachedAuthTokenare absent.launchWebAuthFlowandgetRedirectURLare present with native Promises. - Safari: same as Firefox — no
getAuthToken.launchWebAuthFlowis supported from Safari 16.4 with an app-derived redirect base. - All engines: the
oauth2manifest key is silently ignored where it is not implemented, so a missing token on a non-Chrome build looks like a runtime bug rather than a configuration gap.
Verification
- Load the extension in Chrome and confirm the capability probe logs
true, then in Firefox and confirm it logsfalsewithout throwing. - With the native path active, call a Google API with a manually corrupted token. The first call should 401,
removeCachedAuthTokenshould run, and the retry should succeed. - Sign out through your UI, then call
getAuthToken({ interactive: false }). If it still returns a token, the grant was never revoked — only the cache was cleared. - Switch the Chrome profile to a second Google account and reopen the extension. With
getAuthTokenthe identity follows the profile; with the portable path the account chooser appears.
FAQ
Can I use getAuthToken for a non-Google provider?
No. It is wired to the browser’s Google account integration and has no provider parameter.
Does getAuthToken avoid the install-time permission warning?
No. Both paths need the identity permission and produce the same warning. The oauth2 scopes are additionally shown by Google’s own consent screen.
Is launchWebAuthFlow slower for the user?
Marginally, on the first sign-in, because the provider renders a page. On subsequent silent refreshes there is no UI at all in either path.
Which one do reviewers prefer?
Neither is favoured. What matters is that the scopes you request match what the extension does — the argument you make in writing a permission justification that passes.
Related
- Implementing OAuth 2.0 with launchWebAuthFlow — the portable path in full.
- Refreshing and storing access tokens securely — the work
getAuthTokendoes for you. - Fixing OAuth redirect URI mismatches — the cost of the portable path, and how to pay it once.
- Identity & OAuth Authentication — the guide this page belongs to.