Auth0 on Chrome Extension - launchWebAuthFlow

I have successfully integrated auth0 into my Chrome extension with the help of this community.

Right now I am able to get the auth token of the user from a pop-up window via chrome.identity.launchWebAuthFlow. This login flow works for both email-password and Google login.

Everything is handled in the background script, and the token is saved to chrome local storage.

The current problem I am facing is that if the user enters the wrong email or password, the pop-up window closes abruptly without showing the user an error. When I check the logs in the console, I see a 400 error from auth0 right before the window closes.

Ideally, the pop-up window doesn’t close during an error and auth0 refreshes the page to show that there is an error in the entered user info.

The moment the pop-up window closes, I see the below log in my background service worker console:

chrome.runtime.lastError.message Authorization page could not be loaded.

I am sharing my code below.

let auth0Domain = "dev-<SOME_NUMBERS>.us.auth0.com";
let CLIENT_ID = "<SOME_NUMBERS>";
const AUTH0_AUDIENCE = "https://dev-<SOME_NUMBERS>.us.auth0.com/api/v2/";

const chromeRedirectUrl = chrome.identity.getRedirectURL("");
console.log("chromeRedirectUrl", chromeRedirectUrl);
let nonce = generateRandomString();
let state = generateRandomString();

let authUrl =
  `https://${auth0Domain}/authorize?` +
  "client_id=" +
  encodeURIComponent(CLIENT_ID) +
  "&response_type=id_token token" +
  "&prompt=login" +
  "&redirect_uri=" +
  encodeURIComponent(chromeRedirectUrl) +
  "&scope=" +
  encodeURIComponent("openid profile email") +
  "&audience=" +
  encodeURIComponent(AUTH0_AUDIENCE) +
  "&nonce=" +
  nonce +
  "&state=" +
  state;

function parseRedirectUrl(redirectUrl: string) {
  let hashFragment = redirectUrl.split("#")[1];
  let params = new URLSearchParams(hashFragment);
  let idToken = params.get("id_token");
  let accessToken = params.get("access_token");
  let expiresIn = params.get("expires_in");

  if (!idToken || !accessToken || !expiresIn) {
    throw new Error("Missing required parameters in redirect URL.");
  }

  return { idToken, accessToken, expiresIn };
}

function generateRandomString() {
  var array = new Uint32Array(28);
  crypto.getRandomValues(array);
  return Array.from(array, (dec) => ("0" + dec.toString(16)).substr(-2)).join(
    ""
  );
}

export const launchAuth0 = ({
  interactive = true,
}: {
  interactive?: boolean;
}): Promise<[string, string]> => {
  return new Promise<[string, string]>((resolve, reject) => {
    chrome.identity.launchWebAuthFlow(
      { url: authUrl, interactive: interactive },
      function (redirectUrl) {
        console.log("redirectUrl", redirectUrl);
        if (chrome.runtime.lastError) {
          console.log(
            "chrome.runtime.lastError.message",
            chrome.runtime.lastError.message
          );
          reject(chrome.runtime.lastError);
        }

        if (!redirectUrl) {
          console.log("Authorization failed");
          reject("Authorization failed");
          return;
        }

        const { accessToken, expiresIn, idToken } =
          parseRedirectUrl(redirectUrl);

        chrome.storage.local.set(
          {
            access_token: accessToken,
            id_token: idToken,
            expiration_time: new Date().getTime() / 1000 + parseInt(expiresIn),
          },
          function () {
            resolve([accessToken, idToken]); // Here the Promise resolves with a string token
          }
        );
      }
    );
  });
};

export const logout = (): Promise<void> => {
  console.log("logout is called");
  let logoutUrl = `https://${auth0Domain}/v2/logout?client_id=${encodeURIComponent(
    CLIENT_ID
  )}&returnTo=${encodeURIComponent(chromeRedirectUrl)}`;

  return new Promise((resolve, reject) => {
    chrome.identity.launchWebAuthFlow(
      { url: logoutUrl, interactive: false },
      function () {
        if (chrome.runtime.lastError) {
          console.error(
            "chrome.runtime.lastError.message",
            chrome.runtime.lastError.message
          );
          reject(chrome.runtime.lastError);
        } else {
          // Optionally clear the locally stored token
          chrome.storage.local.remove(
            ["access_token", "expiration_time", "id_token"],
            function () {
              console.log("User is logged out");
              resolve(); // Here the Promise resolves
            }
          );
        }
      }
    );
  });
};

And the background script calls launchAuth0 as below:

import { launchAuth0, logout } from "./auth0";
import { decodeToken } from "react-jwt";

console.log("background script is running");

function processToken(token: string | null, user?: any) {
  const message = {
    type: "LOGIN_STATUS_CHANGED",
    token,
    user,
  };
  chrome.runtime.sendMessage(message, () => {
    if (chrome.runtime.lastError) {
      console.warn(chrome.runtime.lastError.message);
    }
  });
  // also send this message to all tabs
  chrome.tabs.query({}, function (tabs) {
    for (var i = 0; i < tabs.length; ++i) {
      chrome.tabs.sendMessage(tabs[i].id!, message);
    }
  });
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  new Promise((resolve, reject) => {
    if (request.type === "LOGIN") {
      console.log("Background script - LOGIN");
      chrome.storage.local.get(
        ["access_token", "expiration_time", "id_token"],
        function (result: {
          access_token?: string;
          expiration_time?: number;
          id_token?: string;
        }) {
          if (result.access_token && result.expiration_time) {
            let current_time = new Date().getTime() / 1000;
            if (current_time > result.expiration_time) {
              launchAuth0({ interactive: false })
                .then(([accessToken, idToken]) => {
                  const user = decodeToken(idToken || "");
                  processToken(accessToken, user);
                  resolve({ token: result?.access_token, user: user });
                })
                .catch((error) => reject(error));
            } else {
              const token = result.access_token;
              processToken(token);
              const user = decodeToken(result?.id_token || "");
              resolve({ token: result?.access_token, user: user });
            }
          } else {
            console.log("Background script - LOGIN - no token found");
            launchAuth0({ interactive: true })
              .then(([accessToken, idToken]) => {
                const user = decodeToken(idToken || "");
                processToken(accessToken, user);
                resolve({ token: result?.access_token, user: user });
              })
              .catch((error) => {
                console.log("Background script - LOGIN - error: ", error);
                reject(error);
              });
          }
        }
      );
    } else if (request.type === "GET_LOGIN_STATUS") {
      console.log("Background script - GET_LOGIN_STATUS");
      chrome.storage.local.get(
        ["access_token", "expiration_time", "id_token"],
        function (result: {
          access_token?: string;
          expiration_time?: number;
          id_token?: string;
        }) {
          const user = decodeToken(result?.id_token || "");
          resolve({ token: result?.access_token, user: user });
        }
      );
    } else if (request.type === "LOGOUT") {
      console.log("Background -- LOGOUT");
      logout()
        .then(() => {
          console.log(
            "logout success -- sending message => LOGIN_STATUS_CHANGED"
          );
          const message = {
            type: "LOGIN_STATUS_CHANGED",
            token: null,
            user: null,
          };
          chrome.runtime.sendMessage(message, () => {
            if (chrome.runtime.lastError) {
              console.warn(chrome.runtime.lastError.message);
            }
          });
          processToken(null, null);
          resolve({ token: null, user: null });
        })
        .catch((error) => {
          console.log("logout error: ", error);
          reject(error);
        });
    } else {
      reject(new Error("Unknown request type: " + request.type));
    }
  })
    .then((result) => {
      console.log(
        "Request type:",
        request.type,
        " -- [OK] in background script",
        result
      );
      sendResponse(result);
    })
    .catch((error) => {
      console.log(
        "Request type:",
        request.type,
        " -- [ERROR] in background script",
        error
      );
      sendResponse({ error });
    });
  return true;
});

HI @Ali.B what is your login and callback URL for Oauth0. I also want to integrate Oauth0 in my extention but am unable how to do that