Let AI write emails and messages for you 🔥

Authenticate user in Chrome extension and web app with single auth flow using Supabase

Gourav Goyal

Gourav Goyal

Mar 9, 2023

There are multiple approaches to implement Supabase auth but this one allows users to log in/signup into the web app and chrome extension with the single auth flow given that supabase is used in the web app as well. This provides a better UX as the user need not repeat the auth flow for the web app. However, if the user first signs in using the web app he/she still needs to go through the auth flow for the chrome extension.

Prefer Firebase Auth instead? checkout my other guide: Authenticate user in Chrome extension and web app with single auth flow using Firebase (gourav.io)

User flow

  1. User invokes the extension popup
  2. User clicks on the login/signup button
  3. User is redirected to the supabase auth page in a new tab
  4. Once the user auth flow is completed, the user gets logged in to the chrome extension and web app.

Implementation

Add tabs permission to manifest in order to query auth tab and parse access and refresh token:

"permissions": [
      "tabs"
    ]

Redirect user to auth page on signup/login action from popup / content script:

import { createClient, User } from "@supabase/supabase-js";
export const supabase = createClient(
  SUPABASE_URL,
  SUPABASE_KEY
);

// get auth url
export async function signInWithGoogle() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "google"
  });

// tell background service worker to create a new tab with that url
  await chrome.runtime.sendMessage({
    action: "signInWithGoogle",
    payload: { url: data.url } // url is something like: https://[project_id].supabase.co/auth/v1/authorize?provider=google
  });
}

in the background page, trigger auth flow and save tokens from url query string params that are necessary to set the user session in the Chrome extension later.

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  switch (request.action) {
    case "signInWithGoogle": {

			// remove any old listener if exists
      chrome.tabs.onUpdated.removeListener(setTokens)
      const url = request.payload.url;

      // create new tab with that url
      chrome.tabs.create({ url: url, active: true }, (tab) => {
		 // add listener to that url and watch for access_token and refresh_token query string params
      chrome.tabs.onUpdated.addListener(setTokens)
      sendResponse(request.action + " executed")
      })
     
      break
    }

    default:
      break
  }

  return true
})


const chromeStorageKeys = {
  gauthAccessToken: "gauthAccessToken",
  gauthRefreshToken: "gauthRefreshToken"
}


const setTokens = async (
  tabId: number,
  changeInfo: chrome.tabs.TabChangeInfo,
  tab: chrome.tabs.Tab
) => {

  // once the tab is loaded
  if (tab.status === "complete") {
    if (!tab.url) return
    const url = new URL(tab.url)

    // at this point user is logged-in to the web app
    // url should look like this: https://my.webapp.com/#access_token=zI1NiIsInR5c&expires_in=3600&provider_token=ya29.a0AVelGEwL6L&refresh_token=GEBzW2vz0q0s2pww&token_type=bearer
    // parse access_token and refresh_token from query string params
    if (url.origin === "https://my.webapp.com") {
      const params = new URL(url.href).searchParams;
      const accessToken = params.get("accessToken");
			const refreshToken = params.get("refreshToken");

      if (accessToken && refreshToken) {
        if (!tab.id) return

        // we can close that tab now
        await chrome.tabs.remove(tab.id)


        // store access_token and refresh_token in storage as these will be used to authenticate user in chrome extension
        await chrome.storage.sync.set({
          [chromeStorageKeys.gauthAccessToken]: accessToken
        })
        await chrome.storage.sync.set({
          [chromeStorageKeys.gauthRefreshToken]: refreshToken
        })

        // remove tab listener as tokens are set
        chrome.tabs.onUpdated.removeListener(setTokens)
      }
    }
  }
}

Set supabase user session in popup / content script page:

// get logged-in user info when invoking popup or initialising content script on the page 
    getCurrentUser().then((resp) => {
      if (resp) {
        console.log("user id:", resp.user.id);
      } else {
        console.log("user is not found");
      }
    });
  }

export async function getCurrentUser(): Promise<null | {
  user: User;
  accessToken: string;
}> {
  const gauthAccessToken = (
    await chrome.storage.sync.get(chromeStorageKeys.gauthAccessToken)
  )[chromeStorageKeys.gauthAccessToken];
  const gauthRefreshToken = (
    await chrome.storage.sync.get(chromeStorageKeys.gauthRefreshToken)
  )[chromeStorageKeys.gauthRefreshToken];

  if (gauthAccessToken && gauthRefreshToken) {
    try {
      // set user session from access_token and refresh_token
      const resp = await supabase.auth.setSession({
        access_token: gauthAccessToken,
        refresh_token: gauthRefreshToken,
      });

      const user = resp.data?.user;
      const supabaseAccessToken = resp.data.session?.access_token;

      if (user && supabaseAccessToken) {
        return { user, accessToken: supabaseAccessToken };
      }
    } catch (e: any) {
      console.error("Error: ", e);
    }
  }

  return null;
}

Note that I skipped the part to implement Supabase auth in the web app as there are multiple guides already available on the internet for that.

That's all, folks!

Gourav Goyal

Gourav Goyal