Redirect with Actions

Redirect with Actions

You can use post-login Actions to redirect users before an authentication transaction is complete. This lets you implement custom authentication flows that require additional user interaction beyond the standard login form.

Redirects are commonly used to do custom Multi-factor Authentication (MFA) in Auth0, but they can also be used to:

  • Allow for custom privacy policy acceptance, terms of service, and data disclosure forms.

  • Perform a one-time collection of additional required profile data securely.

  • Allow remote Active Directory users to change their password.

  • Require users to provide additional verification when logging in from unknown locations.

  • Gather more information about your users than they provided at initial signup.

Overview

At a high level, a Redirect Action works in the following manner:

  1. An Action issues a redirect to a URL.

  2. The Actions pipeline is suspended after that Action completes its execution.

  3. The user is redirect to the URL along with a state parameter.

  4. When the external flow has concluded, the external site redirects the user to a /continue endpoint along with the state parameter.

  5. The Actions pipeline is resumed on the same Action that invoked the redirect.

Start a redirect

Call the api.redirect.sendUserTo() function as follows:

/**
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  api.redirect.sendUserTo("https://my-app.exampleco.com");
};

Was this helpful?

/

Actions will finish the execution of this Action, and then suspend the actions pipeline to send the user to the https://my-app.exampleco.com. In other words, any Actions that are bound to the post-login triggers that run after the Action invoking the redirect will not execute until the authentication flow has been resumed. If you are familiar with Redirect Rules, then note that this is a key difference between Redirect Actions and Redirect Rules.

After the Action has finished executing, Auth0 redirects the user to the URL specified in the api.redirect.sendUserTo() function. Auth0 also passes a state parameter in that URL. For example: 

https://my-app.exampleco.com/?state=abc123

Your redirect URL will need to extract the state parameter and send it back to Auth0 to resume the authentication transaction. State is an opaque value, used to prevent Cross-Site Request Forgery (CSRF) attacks.

Resume the Authentication Flow

After the redirect, resume authentication by redirecting the user to the /continue endpoint and include the state parameter you received in the URL. If you do not send the original state back to the /continue endpoint, Auth0 will lose the context of the login transaction and the user will not be able to log in due to an invalid_request error.

For example:

https://{yourAuth0Domain}/continue?state=THE_ORIGINAL_STATE

In this example, THE_ORIGINAL_STATE is the value that Auth0 generated and sent to the redirect URL. For example, if your action redirects to https://my-app.exampleco.com/, Auth0 would use a redirect URL similar to https://my-app.exampleco.com/?state=abc123. Making the abc123 parameter the THE_ORIGINAL_STATE. To resume the authentication transaction, you would redirect to:

https://{yourAuth0Domain}/continue?state=abc123

When a user has been redirected to the /continue endpoint, the Actions pipeline will resume on the same Action that invoked the redirect by calling the onContinuePostLogin function. For redirects to work properly, you must have a function with the following signature in the same Action that invoked the redirect:

/**
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  api.redirect.sendUserTo("https://my-app.exampleco.com");
};

/**
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/

exports.onContinuePostLogin = async (event, api) => {
}

Was this helpful?

/

Pass data to the external site

To pass data to the external site, we recommend encoding that data in a signed JWT so that your application can be certain it was not tampered with during transit. With Actions, this can be done with the api.redirect.encodeToken and api.redirect.sendUserTo functions:

/**
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  const YOUR_AUTH0_DOMAIN = event.secrets.YOUR_AUTH0_DOMAIN || event.request.hostname

  // Craft a signed session token
  const token = api.redirect.encodeToken({
    secret: event.secrets.MY_REDIRECT_SECRET,
    expiresInSeconds: 60, 
    payload: {
      // Custom claims to be added to the token
      email: event.user.email,
      externalUserId: 1234,
      continue_uri: `https://${YOUR_AUTH0_DOMAIN}/continue`
    },
  });

  // Send the user to https://my-app.exampleco.com along
  // with a `session_token` query string param including
  // the email.
  api.redirect.sendUserTo("https://my-app.exampleco.com", {
    query: { session_token: token }
  });
}

Was this helpful?

/

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). This token will contain the following:

Token Element Description
sub Auth0 user_id of the user.
iss Hostname of your Auth0 tenant domain (e.g., example.auth0.com).
exp Expiration time (in seconds) specified with the expiresInSeconds parameter. Should be as short as possible to avoid re-use of the token. Defaults to 900 seconds (15 minutes).
ip IP Address of the originating authentication request.
email Custom claim with a value specified in the payload.email parameter.
externalUserId Custom claim with a value specified in the payload.externalUserId parameter.
signature Using the secret specified above, the token will be signed with HS256 algorithm.

Ensure the token has not been tampered with

The external system should verify that this token has not been tampered with during transit. To accomplish this, the remote system should ensure the token’s signature is valid and, if applicable, that the session in the external system belongs to the same Auth0 user provided in the sub claim of the token.

Pass data back to Auth0

After the user completes the custom flow in the external site, they should be redirected to the /continue endpoint. In some situations, you may want to pass data back to Auth0 to impact the authentication or authorization flow for that user (for example, if you are implementing CAPTCHA checks or custom MFA).

Use app metadata where possible

If possible, the remote system should use the Management API to store custom information as application metadata on the Auth0 user profile. When the Auth0 Action flow is resumed, this information will be available on the event.user.app_metadata object. This approach avoids passing sensitive information to Auth0 on the front channel.

Be selective when storing data on the Auth0 user profile

Beware of storing too much data in the Auth0 profile. This data is intended to be used for authentication and authorization purposes. The metadata and search capabilities of Auth0 are not designed for marketing research or anything else that requires heavy search or update frequency. Your system is likely to run into scalability and performance issues if you use Auth0 for this purpose. If your application requires this kind of information, the recommended approach is to store that data in an external system and store a pointer (the user ID) in Auth0 so that backend systems can fetch the data if needed.

Send data on the front channel

Passing information back and forth in the front channel opens up surface area for bad actors to attack. If information must be sent on the front channel, then consider the following guidance:

Pass information back to the Action

A signed session token should be used to send sensitive information back to Auth0. This token can be easily validated within an Action with the following code:

/**
 * @param {Event} event - Details about the user and the context in which they are logging in.
 * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
 */
exports.onContinuePostLogin = async (event, api) => {
  const payload = api.redirect.validateToken({
    secret: event.secrets.PRECONFIGURED_SECRET,
    tokenParameterName: 'my_token',
  });

  // use the data encoded in the token, such as: 
  api.idToken.setCustomClaim('color', payload.favorite_color);
}

Was this helpful?

/

The token will be validated to ensure that:

  • The signature is valid

  • The token is not expired

  • The state claim within the token matches the state parameter used as part of the redirect

Token Element Description
sub Auth0 user_id of the user.
iss Application that is targeted for the redirect.
exp Should be as short as possible to avoid reuse of the token.
state state parameter sent to the remote site as part of the redirect. This must be included in the token to avoid replay attacks.
other Any other custom claims will be exposed as the payload in the code above.
signature Token should be signed with the HS256 algorithm.

To avoid replay attacks, the token should be sent back to Auth0 by making a POST request to the /continue endpoint. The tokenParameterName option in the code allows you to specify the name of the field that contains your token.

Custom authentication methods

After a successful redirect in the login pipeline, Actions can record custom authentication method events in the user's session. The event.authentication.methods array will contain an entry for the custom method for the duration of the user's browser session. Each entry in this array has a timestamp indicating when the authentication method was recorded.

A custom action can trigger a redirect if the required custom method is not in the event.authentication.methods array or if the entry is too old.

You can use api.redirect.sendUserTo() to send the user to a page that implements a custom authentication method. You can use the api.authentication.recordMethod() in the exports.onContinuePostLogin handler to store a record of the completed method in the user's session.

The record stored in the event.authentication.methods array will have a name property matching the URL chosen in api.authentication.recordMethod(). The URL captured here allows you to search through the current transaction's completed authentication methods to determine if your custom method already completed.

Your workflow may require the custom method to be re-performed periodically during the life of a user's session. For example, custom MFA scenarios may require user re-verification after a specified timeframe.

The example below compares the timestamp of an existing record to determine when to rerun the custom method:

const CUSTOM_METHOD_URL = "https://path.to.prompt";
const PROMPT_TTL = 1000 * 60 * 60 * 24; // 24h

/**
 * Handler that will be called during the execution of a PostLogin flow.
 *
 * @param {Event} event - Details about the user and the context in which
 * they are logging in.
 * @param {PostLoginAPI} api - Interface whose methods can be used to
 * change the behavior of the login.
 */
exports.onExecutePostLogin = async (event, api) => {
  // Search authentication method records for an entry representing our
  // custom method.
  const methodRecord = event.authentication?.methods.find((record) =>
    validateCustomRecord(record, CUSTOM_METHOD_URL, PROMPT_TTL)
  );

  if (!methodRecord) {
    const sessionToken = api.redirect.encodeToken({
      payload: {
        user_id: event.user.user_id,
      },
      secret: event.secrets.SESSION_TOKEN_SECRET,
    });

    // We didn't find a valid record, so we send the user to the
    // URL that implements the custom method with the signed
    // data we encoded in `sessionToken`.
    api.redirect.sendUserTo(CUSTOM_METHOD_URL, {
      query: { session_token: sessionToken },
    });
  }
};

/**
 * Handler that will be invoked when this action is resuming after an
 * external redirect. If your onExecutePostLogin function does not perform
 * a redirect, this function can be safely ignored.
 *
 * @param {Event} event - Details about the user and the context in which
 * they are logging in.
 * @param {PostLoginAPI} api - Interface whose methods can be used to
 * change the behavior of the login.
 */
exports.onContinuePostLogin = async (event, api) => {
  const payload = api.redirect.validateToken({
    secret: event.secrets.SESSION_TOKEN_SECRET,
    tokenParameterName: "session_token",
  });

  if (!validateSessionToken(payload)) {
    return api.access.deny("Unauthorized");
  }

  // Record the completion of our custom authentication method.
  // THIS NEW API IS ONLY AVAILABLE IN `onContinuePostLogin`.
  api.authentication.recordMethod(CUSTOM_METHOD_URL);
};

function validateCustomRecord(record, url, ttl) {
  if (!record) {
    // No record means it isn't valid.
    return false;
  }

  if (record.url !== url) {
    // This isn't a record of our custom method.
    return false;
  }

  // Timestamps are rendered as ISO8601 strings.
  const timestamp = new Date(record.timestamp);

  // The record is valid if it was recorded recently enough.
  return timestamp.valueOf() >= Date.now() - ttl;
}

function validateSessionToken(payload) {
  // Custom validation logic for the data returned by the
  // custom method goes here.
  return true;
}

Was this helpful?

/

The api.authentication.recordMethod() API is only available in the exports.onContinuePostLogin handler. This avoids potential login exploits by recording the custom method after completing the redirect.

Restrictions and limitations

Redirect Actions won't work with:

Resource Owner endpoint

It is impossible to use redirect Actions in the context where you are calling /oauth/token directly for the Resource Owner Password Grant. Since the user is not in a redirect flow to begin with, you can not redirect the user in an Action.

Flows where prompt=none

Since the goal of prompt=none is to avoid any scenario where the user will be required to enter input, any redirection will result in an error=interaction_required.

Since Actions run after an authentication session is created, you cannot use prompt=none if you have a redirect rule that is attempting to block access to tokens under certain conditions (for example, custom MFA, CAPTCHA with login, and so on).

You cannot create a redirect flow that blocks token access and bypasses the redirect Action if prompt=none because after a failed attempt, a user can simply call again with prompt=none and get tokens because their authentication session has been created even though Actions failed the first time.

Refresh tokens

Due to the fact that using a refresh token requires a backchannel call to /oauth/token, this will also fail if attempting to redirect.

It is difficult to securely verify that any restrictions on login were carried out. There is not a consistent session ID in the context that could be used to collect information associated with the session such as this user passed MFA challenges. Therefore, you cannot use prompt=none at all.

Any time api.redirect.sendUserTo() is called in an Action, if prompt=none was passed, then the authorization fails with error=interaction_required, but since the user's session is created even if Actions fail, we can't trust that a user passed redirect challenges and therefore can't use prompt=none as a way to get tokens.

In this specific case, we recommend that you use refresh tokens exclusively, because you can ensure that a user passed challenges if those challenges are required to generate a refresh token.

Learn more