How to Decode Session Tokens in Auth0 Actions

In my auth0 app

I am sending a session token to a checkout page in a redirect action and i want to receive it back for validation when calling the continue route

but i get this error page that saying that this error is happening when the received session token is not matching what was sent to the continue route

i notice that the session token changes when sent b/c it is encoded
how do i decode the session token when receiving it and send it back to properly continue the login flow

auth0 action:

exports.onExecutePostLogin = async (event, api) => {
  try {
    const isPaid = event.user.app_metadata.isPaid;

    if (event.stats.logins_count !== 1 && isPaid) {
      return;
    } else {
      if (event.user.app_metadata.stripe_customer_id) {
          const sessionToken = api.redirect.encodeToken({
            secret: event.secrets.NEW_STATE,
            payload: {
              customerId: event.user.app_metadata.stripe_customer_id,
            },
          });
          console.log(sessionToken)

          // Redirect the user to the Stripe checkout page with session_token query parameter
          api.redirect.sendUserTo('https://www.************.com/checkout', { 
            query: 
            { 
              session_token: sessionToken, 
              redirect_uri: `https://******************.us.auth0.com/continue`,

            },

          });
      }
      
    }
  } catch (error) {
    console.log(error.message);

    api.access.deny(
      "We could not create your account, problem with stripe redirection.\n" +
        "Please contact support for assistance."
    );
  }
};

exports.onContinuePostLogin = async (event, api, ) => {
  try {
    let decodedToken;

    decodedToken = api.redirect.validateToken({
      secret: event.secrets.NEW_STATE,
      tokenParameterName: 'session_token',

    });


      // Now you can use the decoded token as needed
      console.log(decodedToken);

      // Set the app metadata if needed
      api.user.setAppMetadata('isPaid', true);

    
  } catch (error) {
    console.log('Error receiving and validating the token and with using the continue endpoint');
    return api.access.deny('Error occurred during redirect.');
  }
};

checkout.tsx (receives session token and sends it back)

import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import Stripe from 'stripe';
import queryString from 'query-string';
import jwt from 'jsonwebtoken'; // Import the JWT library

const stripe = new Stripe('*********************************', {
  apiVersion: '2022-11-15',
});

const Checkout = () => {
  const { user } = useAuth0();
  const priceId = '*************************';
  const successUrl = 'https://**********.us.auth0.com/continue';
  const cancelUrl = 'https://www.**************.com/about';

  async function createCheckoutSession() {
    // Parse the URL to get state, session_token, customer_id, and redirect_uri
    const parsedUrl = queryString.parse(window.location.search);
    const sessionToken = parsedUrl.session_token as string;
    //const customerId = parsedUrl.customer_id;
    const redirectUri = parsedUrl.redirect_uri;
    const state = parsedUrl.state;

    // Check if the session_token, customer_id, and redirectUri exist
    if (sessionToken && redirectUri) {
   //trying to decode token don't know how to do it
      const decodedToken = jwt.decode(sessionToken);
      if(!decodedToken){
        console.log('not decoding properly')
      }
      // Note: You might need to adjust the validation logic based on your token structure
        const newURI = `${successUrl}?session_token=${sessionToken}&${state}`;
        const session = await stripe.checkout.sessions.create({
          customer: user?.app_metadata?.stripe_customer_id,
          payment_method_types: ['card'],
          line_items: [{ price: priceId, quantity: 1 }],
          subscription_data: {
            trial_period_days: 5
          },
          mode: 'subscription',
          success_url: newURI,
          cancel_url: cancelUrl,
        });

        if (session.url) {
          window.location.href = session.url; // Redirect to Stripe checkout page
        }

    }
  }

  return (
    <div>
      <br></br><br></br><br></br>
      <button onClick={createCheckoutSession}>Click Here To Checkout For Our ECOmium Plan!</button>
    </div>
  );
};

export default Checkout;

What Auth0 validates on the return is the state parameter, rather than the session token. The problem could be with how you are building the /continue URL:

const newURI = `${successUrl}?session_token=${sessionToken}&${state}`;

The state should be sent in a parameter named state, so it should be corrected as follows:

const newURI = `${successUrl}?session_token=${sessionToken}&state=${state}`;
1 Like

hi @thameera
how do i use the state from the login flow before the action after validating the token

b/c now the states match on calling the continue endpoint

but it doesn’t match the original state used in the beginning of the universal login (i assume)

as i use a different new state for the login redirection but once it passes the continue route successfully, it gets redirected to ********.com/api/auth/callback… with an error in the URL for a redirection error and the state used in the redirection and not the normal state that was sent in the beginning of the regular login flow which is what could be causing the error - generic error page that just says

“This page is not working, if problem contact site owner”

made the change to the state you suggested and it worked better so ill just show the action code to ask whether there is anything to change there

thanks again!

AUTH0 ACTION

exports.onExecutePostLogin = async (event, api) => {
  try {
    const isPaid = event.user.app_metadata.isPaid;

    if (event.stats.logins_count !== 1 && isPaid) {
      return;
    } else {
      if (event.user.app_metadata.stripe_customer_id) {
          const sessionToken = api.redirect.encodeToken({
            secret: event.secrets.NEW_STATE,
            payload: {
              customerId: event.user.app_metadata.stripe_customer_id,
            },
          });
          console.log(sessionToken)

          // Redirect the user to the Stripe checkout page with session_token query parameter
          api.redirect.sendUserTo('https://www.***************.com/checkout', { 
            query: 
            { 
              session_token: sessionToken, 
              redirect_uri: `https://**********.us.auth0.com/continue`,

            },

          });
      }
      
    }
  } catch (error) {
    console.log(error.message);

    api.access.deny(
      "We could not create your account, problem with stripe redirection.\n" +
        "Please contact support for assistance."
    );
  }
};

exports.onContinuePostLogin = async (event, api, ) => {
  try {
    let decodedToken;

    decodedToken = api.redirect.validateToken({
      secret: event.secrets.NEW_STATE,
      tokenParameterName: 'state',

    });

    // Check if the algorithm is correct


      // Now you can use the decoded token as needed
      console.log(decodedToken);

      // Set the app metadata if needed
      api.user.setAppMetadata('isPaid', true);

    
  } catch (error) {
    console.log('Error receiving and validating the token and with using the continue endpoint');
    return api.access.deny('Error occurred during redirect.');
  }
};

getting an error in my auth0 logs blurring out all sensitive info

{
  "date": "2023-11-22T02:10:39.933Z",
  "type": "f",
  "description": "Error occurred during redirect.",
  "connection": "Username-Password-Authentication",
  "connection_id": "**************************",
  "client_id": "**************************",
  "client_name": "**************************",
  "ip": "**************************",
  "user_agent": "**************************",
  "details": {
    "body": {},
    "qs": {
      "state": "**************************"
    },
    "connection": "Username-Password-Authentication",
    "error": {
      "message": "Error occurred during redirect.",
      "oauthError": "Error occurred during redirect.",
      "type": "access_denied"
    },
    "session_id": "**************************",
    "actions": {
      "executions": [
        "**************************"
      ]
    },
    "stats": {
      "loginsCount": 47
    }
  },
  "hostname": "**************************.us.auth0.com",
  "user_id": "**************************",
  "user_name": "**************************",
  "strategy": "auth0",
  "strategy_type": "database",
  "audience": "https://**************************.us.auth0.com/**************************",
  "scope": [
    "openid",
    "profile",
    "email"
  ],
  "log_id": "**************************",
  "_id": "**************************",
  "isMobile": false,
  "id": "**************************"
}

One issue I can see is the tokenParameterName in api.redirect.validateToken() - it is set to state. However, this field is asking where you are sending the JWT payload, not the state. So it should be set to session_token based on the query parameter in your original post.

If it still doesn’t work, can you post the header and payload of the session_token you are sending back to Auth0?

1 Like

the problem is that when i use the session token it doesn’t validate
as it says it doesn’t match the previous session token - i don’t understand how to decode it and make it match

here is the tokens i am sending:

the encoded token received by the checkout.tsx:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA2ODQ5MTcsImlzcyI6InNlYXJjaGVjb21tLnVzLmF1dGgwLmNvbSIsInN1YiI6ImF1dGgwfDY1NTU5NzNlMjFkODcyYzMwYTdhOTM2ZiIsImV4cCI6MTcwMDY4NTgxNywiaXAiOiIyNjAwOjg4MDI6MTkwMTo1YjAwOjZkMTE6MTQ3YjpmYTYwOjU2N2EiLCJjdXN0b21lcklkIjoiY3VzX1AweXdpMGtaNzFSRUVQIn0.K1V3GeyxCvq4KrPC5MGeA0HurG32HchwoD49NnbPWKQ

sent back token to /continue

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA2ODQ5MTcsImlzcyI6InNlYXJjaGVjb21tLnVzLmF1dGgwLmNvbSIsInN1YiI6ImF1dGgwfDY1NTU5NzNlMjFkODcyYzMwYTdhOTM2ZiIsImV4cCI6MTcwMDY4NTgxNywiaXAiOiIyNjAwOjg4MDI6MTkwMTo1YjAwOjZkMTE6MTQ3YjpmYTYwOjU2N2EiLCJjdXN0b21lcklkIjoiY3VzX1AweXdpMGtaNzFSRUVQIn0.K1V3GeyxCvq4KrPC5MGeA0HurG32HchwoD49NnbPWKQ

both jwt token are matching so when the same encoded token is sent back to the auth0 action and validated now with this logic

    let decodedToken;

    decodedToken = api.redirect.validateToken({
      secret: event.secrets.NEW_STATE,

      tokenParameterName: 'session_token',

    });

and sent to the user like this

          const sessionToken = api.redirect.encodeToken({
            secret: event.secrets.NEW_STATE,
            payload: {
              customerId: event.user.app_metadata.stripe_customer_id,
            },
            
          });
....

api.redirect.sendUserTo('https://www.**************.com/checkout', { 
            query: 
            { 
              session_token: sessionToken, 
              redirect_uri: `https://***********.us.auth0.com/continue`,
              customer_id: event.user.app_metadata.stripe_customer_id

            },

          });

is there any reason why the code isn’t able to validate the token?

thanks again @thameera

the problem is that when i use the session token it doesn’t validate

Do you mean that the validation fails in your app’s code (during the redirect), or in the onContinuePostLogin section of the Action? This can generally happen when the secrets don’t match, but it’s difficult to say without more details.

If nothing works out, can you try this minimal redirect Action along with the example express server? https://gist.github.com/thameera/2dfb3dff6ed2ec461aef7a7a2e3d3250
If you can get that working, we can check how your setup differs from that.

1 Like

im sure its with the /continue action as that’s what the error indicates

but what I’ve been asking is how to do this in my nextjs app - send back the token in the right way validating it with jwt (if that is necessary) b/c that is the only difference with my redirect, other than the fact that i send a customer_id also in the final redirect

const validateIncomingToken = (req) => {
  if (!req.query || !req.query.session_token) {
    throw 'No session_token found'
  }

  try {
    const decoded = jwt.verify(req.query.session_token, SECRET)
    return decoded
  } catch (e) {
    throw 'Invalid session_token'
  }
}

/*
 * Generate new session token to be sent to Auth0
 */
const generateNewToken = (data, state) => {
  const payload = {
    sub: data.sub, // Mandatory, must match incoming token's sub
    iss: 'my-redirect-app', // Optional, not validated
    state, // Mandatory, validated by Auth0
    color: 'blue', // Optional custom parameters to be used in Actions
  }
  // Even though iat and exp are not added above, they are implicitly added by the jwt library

  const token = jwt.sign(payload, SECRET, { expiresIn: '60s' })
  return token
}

app.get('/redirect', (req, res) => {
  try {
    const incomingData = validateIncomingToken(req)

    const newToken = generateNewToken(incomingData, req.query.state)

    const url = `https://${TENANT_DOMAIN}/continue?state=${req.query.state}&my_token=${newToken}`

    res.redirect(url)
  } catch (e) {
    res.send(e)
  }
})

does this make a difference and if so how could i implement that into my checkou.tsx page:

import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import Stripe from 'stripe';
import queryString from 'query-string';
import jwt from 'jsonwebtoken'; // Import the JWT library

const stripe = new Stripe('******************************', {
  apiVersion: '2022-11-15',
});

const Checkout = () => {
  const { user } = useAuth0();
  const priceId = '*******************';
  const successUrl = 'https://***********.us.auth0.com/continue';
  const cancelUrl = 'https://www.searchecomm.com/about';

  async function createCheckoutSession() {
    // Parse the URL to get state, session_token, customer_id, and redirect_uri
    const parsedUrl = queryString.parse(window.location.search);
    const sessionToken = parsedUrl.session_token as string;
    //const customerId = parsedUrl.customer_id;
    const redirectUri = parsedUrl.redirect_uri;
    const state = parsedUrl.state;
    const customerId = parsedUrl.customer_id as string;


    // Check if the session_token, customer_id, and redirectUri exist
    if (sessionToken && redirectUri && customerId) {
      const decodedToken = jwt.decode(sessionToken);
      if(!decodedToken){
        console.log('not decoding properly')
      }
      // Validate the decoded token (check expiration, etc.)
      // Note: You might need to adjust the validation logic based on your token structure
      //if (decodedToken) {
        const newURI = `${successUrl}?session_token=${sessionToken}`;      
          const session = await stripe.checkout.sessions.create({
          customer: customerId,
          payment_method_types: ['card'],
          line_items: [{ price: priceId, quantity: 1 }],
          subscription_data: {
            trial_period_days: 5
          },
          mode: 'subscription',
          success_url: newURI,
          cancel_url: cancelUrl,
        });

        if (session.url) {
          window.location.href = session.url; // Redirect to Stripe checkout page
        }
      //} else {
      //  console.error('Invalid or expired session token');
        // Handle the case where the session token is invalid or expired
      //}
    }
  }

  return (
    <div>
      <br></br><br></br><br></br>
      <button onClick={createCheckoutSession}>Click Here To Checkout For Our ********* Plan!</button>
    </div>
  );
};

I just noticed that the session token does not contain a state parameter, since you are sending the same session token back. Auth0 expects a state claim in the JWT’s body as mentioned here: https://auth0.com/docs/customize/actions/flows-and-triggers/login-flow/redirect-with-actions#pass-information-back-to-the-action

Its value should be that of the state passed in the URL.

This means, sending the same token back will not work. Can you try generating a new JWT like in the example I provided? You can add any additional claims like customer_id, but it should also contain the other required claims.

1 Like

i changed my code to include all of that and i send the state and now it won’t decode the session_token properly

i have this code here - it is able to decode it in const decodedPayload = jwt.decode(sessionToken);, meaning the token is good and present, but is unable to verify it in the validateIncomingToken function:

CHECKOUT.TSX

import React from 'react';
import Stripe from 'stripe';
import queryString from 'query-string';
import jwt from 'jsonwebtoken'; // Import the JWT library

const MY_SECRET = '*****************'

const stripe = new Stripe('*****************************', {
  apiVersion: '2022-11-15',
});

const Checkout = () => {
  const priceId = '*******************';
  const successUrl = 'https://**********************.us.auth0.com/continue';
  const cancelUrl = 'https://www.********************.com/about';

  async function createCheckoutSession() {
    // Parse the URL to get state, session_token, customer_id, and redirect_uri
    const parsedUrl = queryString.parse(window.location.search);
    const sessionToken = parsedUrl.session_token as string;
    const redirectUri = parsedUrl.redirect_uri;
    const recievedState = parsedUrl.state;
    const customerId = parsedUrl.customer_id as string;

    const decodedPayload = jwt.decode(sessionToken);
    console.log(decodedPayload);

    const validateIncomingToken = () => {
      if (!sessionToken) {
        console.log('No session_token found');
      }else{
        console.log(sessionToken)
      }
    
      try {
        const decoded = jwt.verify(sessionToken, MY_SECRET);
        return decoded;
      } catch (e:any) {
        console.error('Error verifying session token:', e.message);  // Log the error message for debugging
        throw 'Invalid session_token';
      }
    };
    const decodedData = validateIncomingToken()
    const generateNewToken = (data: any, state: any) => {
      const payload = {
        sub: data.sub, // Mandatory, must match incoming token's sub
        state,// Mandatory, validated by Auth0
      }
      // Even though iat and exp are not added above, they are implicitly added by the jwt library
    
      const token = jwt.sign(payload, MY_SECRET, { expiresIn: '60s' })
      return token
    }

    const newToken = generateNewToken(decodedData, recievedState)
    // Check if the session_token, customer_id, and redirectUri exist
    if (sessionToken && redirectUri && customerId) {
        const newURI = `${successUrl}?state=${recievedState}&session_token=${newToken}`;      
          const session = await stripe.checkout.sessions.create({
          customer: customerId,
          payment_method_types: ['card'],
          line_items: [{ price: priceId, quantity: 1 }],
          subscription_data: {
            trial_period_days: 5
          },
          mode: 'subscription',
          success_url: newURI,
          cancel_url: cancelUrl,
        });

        if (session.url) {
          window.location.href = session.url; // Redirect to Stripe checkout page
        }
    }
  }

  return (
    <div>
      <br></br><br></br><br></br>
      <button onClick={createCheckoutSession}>Click Here To Checkout For Our *********** Plan!</button>
    </div>
  );
};

the console decodes the full token, showing the
sub, exp, iat, etc.

and also logs the session_token

but throws these errors:

Error verifying session token: Right-hand side of 'instanceof' is not an object
(anonymous)
Uncaught (in promise) Invalid session_token

why is this and is there anything else i am missing
thanks again @thameera

btw the action code:

exports.onExecutePostLogin = async (event, api) => {
  try {
    const isPaid = event.user.app_metadata.isPaid;

    if (event.stats.logins_count !== 1 && isPaid) {
      return;
    } else {
      if (event.user.app_metadata.stripe_customer_id) {
          const sessionToken = api.redirect.encodeToken({
            secret: '***********************'
            //expiresInSeconds: 60, 

            payload: {
              email: event.user.email,
              externalUserId: 1234,

            },
            
          });
          console.log(sessionToken)

          // Redirect the user to the Stripe checkout page with session_token query parameter
          api.redirect.sendUserTo('https://www.*********************.com/checkout', { 
            query: 
            { 
              session_token: sessionToken, 
              customer_id: event.user.app_metadata.stripe_customer_id

            },

          });
      }
      
    }
  } catch (error) {
    console.log(error.message);

    api.access.deny(
      "We could not create your account, problem with stripe redirection.\n" +
        "Please contact support for assistance."
    );
  }
};

exports.onContinuePostLogin = async (event, api, ) => {
  try {
    let payload;

    payload = api.redirect.validateToken({
      secret: '********************',
      tokenParameterName: 'session_token',

    });

    // Check if the algorithm is correct
    if (payload){
      console.log('Token signature is valid');

      // Now you can use the decoded token as needed
      console.log(payload);

      // Set the app metadata if needed
      api.user.setAppMetadata('isPaid', true);
    } else {
      console.log('Invalid token signature');
    }
  } catch (error) {
    console.log('Error receiving and validating the token and with using the continue endpoint');
    return api.access.deny('Error occurred during redirect.');
  }
};

(secrets are the same)

did i need to verify and sign the jwt token is that why it wasn’t working before and if so how do i get it to work and if the jwt token isn’t a problem, then what it is?

made the redirect work

i just removed the sessiontoken. i thought you needed the session token but all i did was send over a cusid to the redirect, take the state from the url, and send it back

but im still wondering whether i could need the session token in the future
how would i get that to work

1 Like

Glad to hear you got it working. The session token is required only if you want to pass some information back to the Action. If not, you can simply omit it.

I don’t see any problem with your onContinuePostLogin Action, assuming the secret and the and the tokenParameterName are correct. If you can capture a HAR file of the flow and DM (after removing sensitive info like passwords), I’ll be able to check, but you may need to do some debugging in the code as well if the Github gist I provided works.

1 Like

Hey there!

As this topic is related to Actions and Rules & Hooks are being deprecated soon in favor of Actions, I’m excited to let you know about our next Ask me Anything session in the Forum on Thursday, January 18 with the Rules, Hooks and Actions team on Rules & Hooks and why Actions matter! Submit your questions in the thread above and our esteemed product experts will provide written answers on January 18. Find out more about Rules & Hooks and why Actions matter! Can’t wait to see you there!

Learn more here!

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