Auth0 Home Blog Docs

Invalid_token; `state` does not match

Recently, my web app started throwing the following:

{error: "invalid_token", errorDescription: "`state` does not match."}

This error only occurs on the production site, wherein, a login form on https://codus.io has a callback to https://app.codus.io (in development, the origins are 0.0.0.0:5000 and 0.0.0.0:5001).

I’m a little confused about the “state does not match” error, because according to Chrome’s developer tools, the request to /authenticate made by auth0.js a request is made with state=wf3x7LxpvZhvP~u8Gk1scsYhp24wEva6, and in the callback, the hash has the EXACT same state:

state=wf3x7LxpvZhvP~u8Gk1scsYhp24wEva6

I repeated this experiment a second time, and once again, the states were identical.

Following the advice of other similar topics, I checked that my parseHash function is only running once; I added a console.log before the only parseHash in the entire codebase, and it only logs once. Relevant code from my site:

if (window.location.hash) {
  await new Promise((resolve) => {
    console.log('Calling parseHash');
    webAuth.parseHash({ hash: window.location.hash }, (err, res) => {
      console.log('parseHash completed with err:', err, 'res:', res);
      resolve();
    });
  });
}

“calling parseHash” only appears once in the console, followed by

parseHash completed with err: {error: "invalid_token", errorDescription: "`state` does not match."} res: undefined

What could be causing this issue?

Also worth noting that the login shows as Success in Auth0 logs, and that if I manually feed my app the access_token and id_token that appear in the URL hash, they work perfectly well. The only issue is that parseHash chokes on a hash, saying that the state doesn’t match even though it appears to me that the state does, in fact, match the value sent to the /authorize endpoint. What should my next steps be in debugging?

Upon having been provided or automatically generating a state value for the use in an authentication request the Auth0.js library will store that value somewhere so that when receiving the callback it can compare the received state value to the one it originally sent.

At this time, the library makes use of cookie storage to store that value so one explanation for the error in question is that at the time the callback is received the cookie is unavailable so the validation fails.

You mention this fails for an authentication request started at https://codus.io which then is processed at https://app.codus.io. These are different domains so it’s likely that the cookie is being set in a way that is not shareable across sub-domains; in addition older versions of Auth0.js used web storage and web storage is isolated per domain so the outcome would be the same.

Is there a particular reason for starting the authentication at a different domain than the one that will process the authentication response? If not, then using the same domain would likely resolve the issue. If yes, you may want to consider managing the state yourself and providing a custom state both to start the request and also when parsing the response as in that way you fully control how that state is managed.

Thanks for the reply! How would I manage state myself if I wanted to pursue that approach ?

IIRC, both the call to authorize and parseHash will allow you to provide a state option so you can provide your own value both to initiate the authentication request and then also for request validation. If you’re using the implicit grant you may also need to do the same for the nonce parameter.

Have in mind that in order to leverage the same security benefits of using the states/nonces generated by default you need to ensure that the values you create are unique per authentication transaction and generated in an adequate way (https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator).

1 Like

Thanks! I’m using webAuth.login for authentication on the landing site. Is there a way I can get the state value that auth0.js generates before webAuth.login redirects the user’s browser? If I could retrieve the state value and process it, I could likely set up a system by which I mutate localStorage on the app.codus.io domain from the codus.io domain, and then I could retrieve the stored value on app.codus.io and pass it to parseHash.

Reading the source, it looks like state is just a randomly-generated string. If I create my own state as a random string of the same length, is that sufficiently secure?

The solution I went with, for anyone interested, was to:

  1. Generate nonce and state on my own. I used a randomString method adapted from auth0-js’s “random” helper:
function randomString(length = 32) {
  const bytes = new Uint8Array(length);
  const result = [];
  const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._~';

  const crypto = window.crypto || window.msCrypto;
  const random = crypto.getRandomValues(bytes);
  for (let i = 0; i < random.length; i += 1) result.push(charset[random[i] % charset.length]);

  return result.join('');
}

Then, I changed my login code to use that state and nonce:

webAuth.login({ email, password, state, nonce });
  1. Send nonce and state to localStorage on the callback domain using postMessage on an iframe. I host the following static file on the callback domain:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <script>
    // Recieve arbitrary localStorage entries from the landing page via postMessage
    // Used to transmit `state` and `nonce` keys before login, in order to check against to prevent
    // CSRF attacks
    window.addEventListener('message', (e) => {
      if (e.origin !== 'https://codus.io') return;
      const { data } = e;
      // Set localStorage items
      Object.entries(data || {}).forEach(([key, value]) => localStorage.setItem(key, value));
    });
  </script>
</body>
</html>

This script stores received localStorage values on the client domain. On the front-end, I open this page in an iframe, and I use postMessage to send the relevant localStorage items:

const frame = document.createElement('iframe');
frame.src = `${CODUS_APP_URL}/localstorage-iframe.html`;
frame.id = 'localstorage';
frame.style.display = 'none';
frame.onload = () => frame.setAttribute('loaded', '');
document.body.appendChild(frame);
const frame = document.querySelector('iframe#localstorage');
const payload = { state, nonce };
if (frame.hasAttribute('loaded')) frame.contentWindow.postMessage(payload, 'https://app.codus.io');
  1. In the app, read the state and nonce values out of localStorage:
webAuth.parseHash({
  hash: window.location.hash,
  state: localStorage.state, // localstorage-iframe receives and stores these values
  nonce: localStorage.nonce,
}, cb);

This system seems to work well, and, to my knowledge, maintains the security benefits of using state and nonce

1 Like