How to login to Bubble using Google OAuth Extension

Hey everyone, I’ve been working on a Bubble app that allows users to log in via Google OAuth or the traditional username/password method. I’ve also created some browser extensions that integrate with this login system. For username/password login through the extensions, I use Bubble’s “Log the user in” action, obtain a session token, and everything works fine without needing to check the “Ignore privacy rules when running this workflow” option.

However, I’m running into issues with the Google login through the extension. Here’s the process I’m following for OAuth:

To initiate the OAuth flow and obtain the access/refresh token, I’m using:
chrome.identity.launchWebAuthFlow

Once I have the access token, from Bubble I call Google’s User Info API to retrieve the user’s email, check if it exists in my Bubble database, and return that info to the extension.

The problem is, unlike traditional username/password logins, I don’t get a Bubble session token for the Google login process. So, Bubble doesn’t recognize that the user is logged in and I can’t log him in because there’s no password, right?

My questions are:

  • Should I be handling the Google OAuth consent flow directly within Bubble instead, like a User-Agent flow?
  • How can I get Bubble to recognize this Google user and issue a session token after completing the OAuth flow?

Thanks in advance for your help!

1 Like

Hey, bubble veterans @keith @lottemint.md I saw two of your posts where you were included in the OAuth topics. I went through this topic too but that didn’t give me an idea of how I could achieve what I asked in the original post.

Any chance to give me some wind in the back?
Thanks in advance and apologies for ping once again!

Login/signup with a social network is the action you need :slight_smile:

Mm, we might not have understood each other. I need this action to be called from the backend workflows, which allow me to call some other APIs in the bubble and interact with the database.

Login/signup with a social network will only initiate OAuth from the Bubble.
This:

Ah, I see. Sorry, I haven’t done this particular integration before so can’t offer any (useful) contributions.

1 Like

Haven’t implemented OAuth via extension. However, I would recommend handling the Google OAuth consent flow directly within Bubble because Bubble’s authentication is designed to recognize the user sessions when handled within the app.

1 Like

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:

  1. 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
  2. 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."
  );
}
  1. 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.
  2. 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.
  3. 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:

  1. In Google Cloud Console, navigate to: Credentials > OAuth 2.0 Client IDs > Web client (auto created by Google Service) > Edit
  2. 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. :coffee::tea:

Best,
Mladen
Hiveyard