After a few days of intense work and a few roadblocks, I’m excited to share a solution that finally works. I wanted to document this process for anyone else who might encounter similar challenges.
Background
Our goal was to create a Chrome Extension that functions whether or not the user is logged in to our Bubble application. Initially, the flow seemed straightforward: we used a backend action to log in the user by passing their email and password, and Bubble returned a session token. Easy, right? Well, it got more complex from there.
Here’s what I’ll cover:
getAuthToken
vs launchWebAuthFlow
- Issues with Google Login
- Implementing 3rd Party OAuth / SAML Access
Authentication Approaches for Chrome Extensions
If you’re adding OAuth to a Chrome Extension, you’ll likely start with chrome.identity.getAuthToken
and quickly discover it only works in Chrome, not in other Chromium-based browsers like Brave. The alternative is chrome.identity.launchWebAuthFlow
, which requires managing the token exchange and refresh flow manually.
To set this up, create a page (we named it third-part-auth
) as the redirect URL, which allows you to obtain the code
. Here’s how to retrieve it:
- Obtain the code: Call the following API with the
client_id
and redirect_uri
parameters:
https://[Your-App-URL]/version-test/api/1.1/oauth/authorize
- Exchange code for an access token: The external service must make a POST request to:
https://[Your-App-URL]/version-test/api/1.1/oauth/access_token
After creating the new SAML Access, you’ll see a Client Secret and Client ID. Important: Do not expose these in frontend code, as this could expose sensitive data enabled by your Data API settings.
Instead, use a proxy server like Firebase or Cloudflare to make HTTPS requests with securely stored client secret.
function launchWebAuthFlow(url: string): Promise<string | undefined> {
return new Promise((resolve, reject) => {
chrome.identity.launchWebAuthFlow(
{
url,
interactive: true,
},
(redirectUrl) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(redirectUrl);
}
}
);
});
}
function extractCodeFromUrl(url: string): string | null {
const parsedUrl = new URL(url);
return parsedUrl.searchParams.get("code");
}
async function fetchAccessToken(code: string) {
const response = await fetch(httpGetToken, { //keep this server side boys
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
base_url: baseUrl,
code,
redirect_uri: redirectUri,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_description);
}
return response.json();
}
async function refreshToken(data: AppDataType) {
const response = await fetch(httpRefreshToken, { //this one also, since it's using client secret
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
base_url: baseUrl,
refresh_token: data.refreshToken,
grant_type: "refresh_token",
}),
});
if (response.ok) {
const result = await response.json();
if (result.access_token) {
return chrome.storage.local.set({
app_data: {
...data,
accessToken: result.access_token,
refreshToken: result.refresh_token,
expiresAt: new Date().valueOf() + result.expires_in * 1000,
},
});
}
}
await setAuthStatus(
"error",
"Your session has expired. Please sign in again."
);
}
- The flow is initiated with
chrome.identity.launchWebAuthFlow
and the url
is set to the authUrl
that we created. The interactive
is set to true
so that the user is prompted to sign in. The callback function will be called with the redirectUrl
which will contain the authorization code that we need to exchange for an access token and refresh token.
- The
redirect_uri
allows the OAuth flow to redirect back to the extension after the user signs in. There is a method, chrome.identity.getRedirectURL
, that you can use to get the redirect URL for the extension but this will include a trailing slash which will not be accepted as an authorized redirect URI in the Google Cloud Console. So, you’ll need to manually construct the redirect URL without the trailing slash or remove if you use the method.
- The
redirectUrl
is parsed using URLSearchParams
to get the authorization code which is then used to exchange for an access token and refresh token.
function extractCodeFromUrl(url: string): string | null {
const parsedUrl = new URL(url);
return parsedUrl.searchParams.get("code");
}
Setting the Redirect URL in Google Cloud Console
The authorized callback URL in the Google Cloud Console must match the redirect URL used in your extension, or the OAuth flow will fail. Here’s how to set it up:
- In Google Cloud Console, navigate to: Credentials > OAuth 2.0 Client IDs > Web client (auto created by Google Service) > Edit
- Add your redirect URL (e.g.,
https://${chrome.runtime.id}.chromiumapp.org
) under Authorized redirect URIs.
Updating manifest.json
The manifest.json
file will need to include the identity
permission to use chrome.identity
methods. If you’re using getAuthToken
, update the oauth2
section to include the scopes and client ID. Note that the oauth2
section is only used with getAuthToken
, not launchWebAuthFlow
.
"oauth2": {
"client_id": "<client-id>",
"scopes": [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile"
]
}
There are a few minor details I may have missed, but this should be a good starting point if you’re working on something similar. Hopefully, this helps unblock you if you’re building a comparable project.
Best,
Mladen
Hiveyard