Unable to logout from Chrome Extension using Auth0 /v2/logout endpoint

Hey, I’m trying to integrate Auth0 in my chrome extension (manifest v3) and I’ve encountered some unexcpeted issues.

I’m using this boiler plate code as the base of my extension. My first approach was to try use auth0-react in the Popup component, and to be honest it was pretty straight forward:

Popup/index.jsx

<Auth0Provider
  domain={`${process.env.AUTH0_DOMAIN}`}
  clientId={`${process.env.AUTH0_CLIENTID}`}
  authorizationParams={{
    redirect_uri: chrome.runtime.getURL('popup.html'),
  }}
>
  <Popup />
</Auth0Provider>

Popup/Popup.jsx

const PopUp = () => {
  const { loginWithPopup } = useAuth0();

  return (
    <button onClick={() => loginWithPopup()}>Login</button>
  )
}

The problem with this approach is that it works perfectly only on windows, running this in Linux or Mac the behaviour is that you click on the login button and the extension’s popup closes and the login doesn’t finish. Fun fact: if you click on the login button while having the devtools opened it will work as expected on Linux and Mac.

So I tried moving the login trigger to the background.js file, and since there is no document or window objects I can’t use auth0-spa-js neither. So, searching for guides I found this comment from which I took everything that I got working now (I introduced some changes), this is how far I’ve got:

Popup/Popup.jsx

const PopUp = () => {
  const { loginWithPopup } = useAuth0();

  return (
    <>
      <button onClick={() => chrome.runtime.sendMessage('login')}>Login</button>
      <button onClick={() => chrome.runtime.sendMessage('logout')}>Logout</button>
    </>
  )
}

Background/index.js (did a few changes here, adding crypto instead of using window.crypto)

import crypto from "crypto-js";

function getRandomBytes() {
  const rndArray = crypto.lib.WordArray.random(44);
  const rndBytes = [];
  for (let i = 0; i < rndArray.sigBytes; i++) {
    rndBytes.push((rndArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
  }
  return new Uint8Array(rndBytes);
}

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() {
  return {
    clientId: process.env.AUTH0_CLIENTID,
    audience: process.env.AUTH0_AUDIENCE,
    domain: process.env.AUTH0_DOMAIN,
  };
}

async function windowSha256(buffer) {
  const message = new TextEncoder().encode(buffer);
  const hashBuffer = await crypto.SHA256(message);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

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();
  // const redirectUrl = chrome.runtime.getURL('');
  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}`;
    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);

    const body = JSON.stringify({
      redirect_uri: redirectUrl,
      grant_type: 'authorization_code',
      client_id: config.clientId,
      code_verifier: verifier,
      code: code
    })

    try {
      const result = await fetch(`https://${config.domain}/oauth/token`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: body
      });

      console.log(result);

      if (result.ok) {
        const data = await result.json();
        if (
          data &&
          data.access_token &&
          data.expires_in
        ) {

          // Temporarily save token an expirancy date in localStorage
          chrome.storage.local.set({
            session: {
              token: data.access_token,
              expires_in: data.expires_in,
            },
          }, () => {
            console.log('Session saved in storage');
          });
        } else {
          console.log('Auth0 Authentication Data was invalid')
        }
      }
    } catch (error) {
      console.error(error);
    }
  } else {
    console.log('Auth0 Cancelled or error. resultUrl', resultUrl)
  }
};

const logout = async () => {
  const config = await getConfig();
  chrome.storage.local.get(['session'], async (result) => {
    const { token, expires_in } = result.session;

    const queryParams = {
      returnTo: `${chrome.identity.getRedirectURL()}`,
      client_id: `${config.clientId}`,
    }

    try {
      const queryString = new URLSearchParams(queryParams).toString();
      const logoutUrl = `https://${config.domain}/v2/logout?${queryString}`;

      const response = await fetch(logoutUrl);

      if (response.ok) {
        console.log('User logged out successfully');
      } else {
        console.error('Logout request failed:', response.status, response.statusText);
      }
    } catch (error) {
      console.error('Logout request failed:', error);
    }
  });
};

chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
  if (message === 'login') {
    login();
  }

  if (message === 'logout') {
    logout();
  }
});

Ok, so, the login seems to be working fine because I see my universal login, which returns me a valid token that I can use to call my API. But I’m struggling to get the logout working because at the moment that I want to make the request to v2/logout it always fails because of CORS.

Error:

Access to fetch at 'https://{myDomain}/v2/logout?returnTo=https%3A%2F%2F{myExtensionId}.chromiumapp.org%2F&client_id={myClientId}' from origin 'chrome-extension://{myExtensionId}' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Yes, the first thing I thought was adding chrome-extension://{myExtensionId} to my Allowed Logout URLs and Allowed Origins (CORS) in any shape or form but it doesn’t really do anything.

Evil workaround, don’t hit the /logout endpoint with a request, but with a new tab:

chrome.tabs.create({ url: logoutUrl });

Since I don’t get why I’m getting CORS issues even though I have configured the URLs in my Auth0 application I started to think about other approachs:

  • As long as the login is working and I can get valid tokens, that’s fine I guess. I can encrypt and that information in chrome.store and with a intermediari manage expirancy times and all that stuff, and regarding “logout”, just delete the the token from chrome.store and don’t bother. One downside I see on this is that, when signing in I get to see my login page where I can choose which google account I want to use and all that, but if I click again on Login without having logged out I won’t see this login screen and I’m just going to get a new token.
  • Just as I was writing this post I thought having this code in the background.js, maybe try putting it back in the /Popup as a React hook and maybe making the request from there would make a difference? I’ll try to do this because of the downside of the first approach and update this post when I see what results I get.

Well, just wanted to share my experience trying to use Auth0 in a chrome extension so maybe someone finds this informatio useful in any way.

Any comments will be appreciated. Thanks.

Hey @konrad.sopala , sorry for tagging you but I wanted to know if at least you have encountered something like this before. At least the part where the loginWithPopup doesn’t work on chrome extensions running on Linux and Mac.

Have you tried using the federated logout endpoint?
https://{myDomain}/v2/Logout?federated

It’ll log the user out of every application that they signed in using your auth server so if you have multiple applications it’ll sign them out of those too.