Auth0 in Chrome Extension Content/Background script MV3

Am developing a Chrome Extension (Manifest Version 3 - MV3) which needs authentication inside content script or background script, to make an authorized backend API call. However, I couldn’t get it working for either of the options.

I want my extension to be shown on any of the websites. However, for security reasons, I don’t want to enable all the websites as allowed callback URLs in Auth0. Hence, when trying to get a token with Auth0 inside content script, I get a Timeout error (probably since the content script is running in the context of the webpage, and the webpage url is not in the allowed redirect_uri list).

I avoid this, I tried running Auth0 in the background script. However, since there is no DOM/window defined in the background script, Auth0 fails.

Am stuck on this one, and looking for some guidance on how to use Auth0 in either content script/background script, for calling my backend API with an access token. Please help.

PS: In the earlier version of my extension, I was using a popup, instead of content script. For using Auth0 in the popup, I could get it working, with the help of this article: Ultimate Guide to Auth0 In a Chrome Extension Popup.

1 Like

Same problem here. I wrote that article but only could get it to work using Manifest V2. I was wondering if storing refresh and jwt tokens in local storage could do the trick? Or is there a way for us to manually access the token that Auth0 stores as a cookie? Some help from Auth0 team would be extremely helpful.

For browser extensions, I would recommend keeping everything auth related in the background script. This also refers to the storage of access and/or refresh tokens. Storing tokens, especially refresh tokens, using the Web Storage API is most likely not the “safest” option but the only one I’m aware of that allows users to be logged in for longer period of times (assuming that the access tokens expire fairly quickly). Luckily, Auth0 introduced rotating refresh tokens which should resolve some of the concerns in regards to refresh tokens and storage.

So, if the content script needs to get data from a protected ressource, i.e. your backend API, it has to send a message to the background script. The background script can then send the request to your API (using the stored access token which it needs to periodically update using the refresh token) and send the data back to the content script.

In your backend script, you can’t use the Auth0 SDK as it won’t work (as you’ve mentioned). Instead, you need to write the authorization code grant (PKCE, as you don’t want to expose the client secret) logic yourself. Luckily, browser extensions offer the launchWebAuthFlow method which simplify most of the complexity. You can also use the getRedirectURL method to automatically get the unique extension URL (which you also need to store in your Auth0 SPA settings) in your background script. To implement the PKCE challenge you can either write that logic yourself (the math is fairly simple) or use PKCE libraries on NPM.

1 Like

Hi I got it working with manifest v3 & pure js:

I have a loginpopuphtml that references a login.js script. Here is the content of the login.js script. Heavily influnced by: Robert Tolton | Implementing Auth0 Authentication into a Chrome… and Call Your API Using the Authorization Code Flow with PKCE.

var btnLogin = document.getElementById("btnLogin");
var btnLogout = document.getElementById("btnLogout");

const fetchAuthConfig = () => fetch("../../auth_config.json");

function getRandomBytes(){
  const rndArray = new Uint8Array(44);
  window.crypto.getRandomValues(rndArray);
  return rndArray;
}

function buf2Base64(buffer) {
  return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))
          .replace(/\+/g, '-')
          .replace(/\//g, '_')
          .replace(/=/g, '');
}

function getParameterByName(name, url = window.location.href) {
  name = name.replace(/[\[\]]/g, '\\$&');
  var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
      results = regex.exec(url);
  if (!results) return null;
  if (!results[2]) return '';
  return decodeURIComponent(results[2].replace(/\+/g, ' '));
}

async function getConfig(){
  const response = await fetchAuthConfig();
  const config = await response.json();
  return config
}
async function windowSha256(buffer) {
  let bytes = new TextEncoder().encode(buffer)
  return await window.crypto.subtle.digest('SHA-256', bytes);
}

async function callOurApi(accessToken){
  const config = await getConfig();
  const headers = await getAuthHeaders(accessToken);
  const res = await axios.get(`${config.audience}/YOUR_API_ENDPOINT_TO_TEST`, headers);
  console.log('callOurApi', res);
}

async function getAuthHeaders(accessToken) {
  return {
    headers: {
      Authorization: `Bearer ${accessToken}`
    }
  };
}

const login = async () => {
  // First lets get the redirectUrl and config. The redirect url must be added to your allowed callbacks urls and Allowed Origins (CORS) in Auth0 application
  // looks like: https://EXTENSION_ID.chromiumapp.org/
  const redirectUrl = chrome.identity.getRedirectURL()
  console.log('redirectUrl', redirectUrl)
  const config = await getConfig();

  // next we are going to generate a codeChallenge Sha256 code challenge and verifier
  // https://auth0.com/docs/get-started/authentication-and-authorization-flow/call-your-api-using-the-authorization-code-flow-with-pkce#create-code-verifier
  const inputBytes = getRandomBytes();
  const verifier = buf2Base64(inputBytes) 

  const shaHash = await windowSha256(verifier)
  const codeChallenge = buf2Base64(shaHash);
  console.log('codeChallenge', codeChallenge)

  // Now we make a request to authorise the user using chrome's identity framework. We get a code back
  let options = {
    client_id: config.clientId,
    redirect_uri: redirectUrl,
    response_type: 'code',
    audience: config.audience,
    scope: 'openid',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256'
  };

  let resultUrl = await new Promise((resolve, reject) => {
    let queryString = new URLSearchParams(options).toString();
    let url = `https://${config.domain}/authorize?${queryString}`;
    console.log(url);
    chrome.identity.launchWebAuthFlow({
      url,
      interactive: true
    }, callbackUrl => {
      console.log(callbackUrl);
      resolve(callbackUrl);
    });
  });

  // We are now going to use that code to generate a token
  if (resultUrl) {
    const code = getParameterByName('code', resultUrl);
    console.log('code', code);

    const body = JSON.stringify({
      redirect_uri: redirectUrl,
      grant_type: 'authorization_code',
      client_id: config.clientId,
      code_verifier: verifier,
      code: code
    })
  
    // I couldn't get this working with fetch, so Axios it is.
    const result = await axios.post(`https://${config.domain}/oauth/token`, body, {
        headers: { 'Content-Type': 'application/json' }
    });
  
    console.log(result);

    if (
        result &&
        result.data &&
        result.data.access_token &&
        result.data.expires_in
    ) {

        console.log(result.data.access_token)
        console.log(result.data.expires_in)

        // keep the access_token somewhere can access from anywhere in the app. and reuse this token until it expires.
        // Can now make a network request
        console.log("Calling our api (Demo)")
        callOurApi(result.data.access_token)

    } else {
      console.log('Auth0 Authentication Data was invalid')
    }
  } else {
    console.log('Auth0 Cancelled or error. resultUrl', resultUrl)
  }
};
btnLogin.addEventListener("click", async function() {
    login();
});

btnLogout.addEventListener("click", async function() {
  await chrome.identity.clearAllCachedAuthTokens();
  // wipe access token from wherever you've kept it
});

3 Likes

Thanks for sharing that with the rest of community!