State not included in redirect token (error: "State in the token does not match the /continue state")

I am using Action flows and creating a redirect token, that is then validated upon a secondary call to /continue endpoint on my auth0 domain.

The initiating code is:

exports.onExecutePostLogin = async (event, api) => {
  // Run only for the ASPSP SCA SPA client
  if (event.client.client_id !== FRONTEND_CLIENT_ID) {
    return;
  }

  if (event.authorization) {
    // Strong customer authentication / SCA / MFA
    const scopes = extractScopesFromEvent(event); // defined on top and works well
    if (scopes.includes("sca:required")) {
      // Create token
      const session_token = api.redirect.encodeToken({
        secret: event.secrets.REDIRECT_SECRET,
        expiresInSeconds: 60, 
        payload: {
          sub: event.user.user_id,
          email: event.user.email,
          continue_uri: `https://${AUTH0_DOMAIN}/continue`,
          // state to be added here? <---------------------------- notice 
        },
      });

      // REDIRECT
      var redirectUri = `${event.request.hostname}/callback/confirm`
      if (event.transaction) {
        redirectUri = `${event.transaction.redirect_uri}`
      }
      api.redirect.sendUserTo(redirectUri, { 
        query: { 
          session_token: session_token, 
          state: `over-ride` 
        }
      });
    }
  }
  // Add the Authentication Method Reference to amr custom claim
  api.idToken.setCustomClaim(NAMESPACE + "amr", event.authentication?.methods);
  return;
};

Without including the scope, the flow is simple and returns a token with the custom claim “amr” value with a {name:"pwd", time:"time"}. When the scope sca:required is used, I want to create a session_token and redirect the user to a custom web page before adding additional claims and authorizing them. The page on the redirection will give the user extra info (this is not relevant for my issue) and then call the /continue endpoint.

The redirectUri parameter works well, and redirects me to http://localhost:3000/callback/confirm?session_token=<session_token>

The code on my custom application then sends this call via regular HTML :

consent: true
session_token: <session_token gotten from URI query param in the redirect>
state: <the state gotten from URI query param in the redirect>

The continue function is as follows:

exports.onContinuePostLogin = async (event, api) => {
  const payload = api.redirect.validateToken({
    secret: event.secrets.REDIRECT_SECRET,
    tokenParameterName: 'session_token',
  });
  api.multifactor.enable("any", { allowRememberBrowser: false });
  api.idToken.setCustomClaim(NAMESPACE + "amr", event.authentication?.methods);
  api.idToken.setCustomClaim(NAMESPACE + "consent", payload.consent ?? "");
  return;
};

The error I get is “The session token is invalid: State in the token does not match the /continue state”. I understand that I have not added a “state” in the payload of my session_token, but the documentation at Redirect with Actions states that " The code above will append a session_token query string parameter to URL used in the redirect (in addition to the state parameter that Auth0 will add automatically)". However it does not.

I have tried adding event.transaction?.state and also arbitrary values to the token, but the state parameter I get back in the redirect is automatically created (and can’t be overloaded, it seems), so what should I do here?

Thanks

This person got the same error:

Though they did not add what code was missing. I have tried adding a state to the session_token as such:

const session_token = api.redirect.encodeToken({
        secret: event.secrets.REDIRECT_SECRET,
        expiresInSeconds: 60, 
        payload: {
          sub: event.user.user_id,
          email: event.user.email,
          continue_uri: `https://${AUTH0_DOMAIN}/continue`,
          state: "abc123"
        },
      });

But obviously it is not a valid state, because it was not generated by Auth0 and does not match the /continue state wanted.

I have also tried to exchange the state from abc123 to state: event.transaction?.state but that is also not the correct state to add. The state from the URI is something long like this: hKFo2SBRNklVMFVPM2FkODh4ZUs4bGhwOWVyUEQtSW5nRTl2NKFuqHJlZGlyZWN0o3RpZNkgNnpzSXdMbzNkRGZluy0tdkowbm5nSG5BM2t5aTdSRkujY2lk2SBFNmdEV3ZmUVlIYmpPT3h3RDM5bk1pNlIzSklVcTQwMg whereas the state from the transaction is something short like this: S2NpVk1NU1JlbWFwMWxfY05vbnJIQmphMkNMVAZFcGt4UDNzMHJ5TUxibA==.

I get either the first error that it is a mismatch, or that the configuration is wrong.

This is the session_token I get back. There is no “state” in it, if I do not define it (but again, the doc says: “in addition to the state parameter that Auth0 will add automatically”)

I solved it.

SOLUTION

There are two states parameters at play here:

  1. The first one holds the state of the chain of redirects, and is part of the GET query, i.e. https://YOUR_DOMAIN?state=[STATE]: this needs to be added in your post URI back to the /continue endpoint in a query parameter called state
  2. The second one is a state for the token itself to check that it has not been tampered with (I guess?), which needs to be extracted from the token and included in the POST payload in a parameter called state.

tl;dr - All in all, you need to do:

POST https://YOUR_AUTH0_DOMAIN/continue?state=<state1>

{
    session_token: <sesston_token>,
    state: <state2 - parsed from your session_token JWT payload> 
}

So, two state parameters needs to be fetched.

GET parameter

The state parameter given in the redirect URL needs to be sent back in your POST action. So in your HTML form (or what it is you have): do something like this:

const uri_state = new URLSearchParams(window.location.search).get("state")
document.getElementById("form").action = `https://YOUR_AUTH0_DOMAIN/continue?state=${uri_state}`

| ! Notice
| It needs to be a POST

POST payload parameter

Then the session_token parameter needs to be in your POST body. It must include a state build from the transaction of the first call:

const session_token = api.redirect.encodeToken({
        secret: event.secrets.REDIRECT_SECRET,
        expiresInSeconds: 60, 
        payload: {
          sub: event.user.user_id,
          continue_uri: `https://${AUTH0_DOMAIN}/continue`,
          state: "abc123" // <--- NOTICE
        },
      });
api.redirect.sendUserTo("http://localhost/callback/confirm, {
        query: { 
          session_token: session_token, 
        }
      });

| This code is inside the onExecutePostLogin function

On the application end, the session_token parameter needs to be parsed from the URI, and then the state needs to be extracted like this:

const sessionToken = new URLSearchParams(window.location.search).get("session_token")
const sessionTokenState = jwt_decode(sessionToken)["state"]

| ! Notice
| jwt_decode which is a function from GitHub - auth0/jwt-decode: Decode JWT tokens; useful for browser applications.

Then post the form like this:

POST https://YOUR_AUTH0_DOMAIN/continue?state=${uri_state}

{
    session_token: ${sessionToken},
    state: ${sessionTokenState}
}

Then in your validation is this:

const payload = api.redirect.validateToken({
    secret: event.secrets.REDIRECT_SECRET,
    tokenParameterName: 'session_token',
  });

| This code is iside the onContinuePostLogin function
| ! Notice
| the “session_token” is the same as your parameter name in your POST payload

Though I found the solution, I urge that the documentation should be updated.

  1. The state is not automatically added as per " in addition to the state parameter that Auth0 will add automatically", on page: Redirect with Actions
  2. It should be stated that the URI state and the session_token state are two separate things that are both validated in the /continue; the URI state token itself to check flow, and the session_token state in the api.redirect.validateToken function call

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.