Migrate from Rules to Actions

When converting existing Rules to Actions, you should associate the new Action with the Post-Login (post-login) Trigger of the Login Flow. If you follow the steps below and keep your Actions in the same order as your original Rules, the functionality should be identical.

Plan your migration

Post-Login Actions run after existing Rules, so you can either convert Rules one at a time in the Dashboard or all at once using the Management API.

You will need to convert code, and then activate the Action and deactivate the Rule. Activating the Action and deactivating the Rule can be done quickly in succession, but depending on the order, there might be a short period of time where either both or neither are running.

Because of this, we recommend migrating your pipeline step by step: convert pieces of your Rules code to Action code, test in a staging environment, then go live with one piece at a time. Because active Rules run before deployed Actions, if you start at the end of your Rules pipeline and work backwards, you can keep some logic in Rules as you build and test other logic in Actions.

Tips when planning your migration

  • Keep your Actions and Rules 1:1, so functionality can be turned off and on in blocks and tested.

  • Use flags in user metadata to avoid duplicating expensive or one-time operations.

  • Start at the end of your Rules pipeline and work backwards; because active Rules run before deployed Actions, you can keep some logic in Rules as you build and test other logic in Actions.

  • Make sure to run changes at a time when impact and traffic will be lowest.

  • Consider temporarily customizing your login page to halt logins if the cutover could cause invalid logins or gaps in protection.

  • Consider using the Auth0 Deploy CLI to script, test, and quickly implement the migration all at once or iteratively.

Understand limitations

While Actions can handle the vast majority of things that Rules can, you should be aware of a few limitations before you start your migration. (Remember: you can have both Rules and Actions running as you migrate.)

  • Rules can add properties to the user and context objects, which are accessible in subsequent Rules. An individual Action cannot share data directly with another Action.

  • Actions cannot persist data, like access tokens or API responses, across executions.

  • Actions are not provided with an access token for the Management API or access to the global auth0 object as in Rules. To learn how Management API calls can still be made, read the Convert Code section.

For the full list of limitations, see Actions Limitations.

Convert code

To convert a Rule to an Action, you must replace Rule-specific code with Actions code. This section covers the tasks you will need to perform to turn a functioning Rule into its equivalent Action.

Tips when converting code

  • In general, look for the read-only properties of Rules user and context objects on the Actions event object. Look for any side effects your Actions have on the system (like failing a login or updating user metadata) in the api object functions.

  • Use the Actions Code Editor in the Auth0 Dashboard to write your code; it will help by highlighting errors and supplying auto-complete suggestions.

  • Before you go live, thoroughly test your new Actions in a staging or test environment.

Copy Rule code to a new Action

  1. Log in to your production tenant, and copy the code from the Rule you want to convert.

  2. Switch to a non-production tenant, and navigate to Auth0 Dashboard > Actions > Library.

  3. Select Build Custom, then:

    • Enter a Name for your Action that matches the name of the Rule you're converting.

    • Locate Trigger, and select Login / Post Login.

    • Locate Runtime, and select Node 16.

    • Select Create.

  4. In the code block of the Actions Code Editor, paste the Rule code you want to convert below the exported onExecutePostLogin function.

  5. Make the changes detailed in the rest of this article as you move the code into the function.

Change the function declaration

Rules use a plain, declared function with user, context, and callback parameters, while Actions use a function exported to a specific name. Make the following change; for now, ignore any errors that appear.

Before

async function myRulesFunction(user, context, callback) {
    // ... additional code
}

Was this helpful?

/

After

exports.onExecutePostLogin = async (event, api) => {
	// ... additional code
};

Was this helpful?

/

Change how user data is accessed

In Rules, data about the user logging in is stored in the user object. In Actions, this data is found in the user property of the event object. The majority of existing properties are accessible in this new location.

Before

function myRulesFunction(user, context, callback) {
	const userEmail = user.email;
	const userId = user.user_id;

	// This property could be undefined in Rules.
	const userAppMetadata = user.app_metadata || {};

	// ... additional code
}

Was this helpful?

/

After

exports.onExecutePostLogin = async (event, api) => {
	const userEmail = event.user.email;
	const userId = event.user.user_id;

	// This property will never be undefined in Actions.
	const userAppMetadata = event.user.app_metadata;

	// ... additional code
};

Was this helpful?

/

Change how context data is accessed

In Rules, data about the current login session is stored in the context object. For Actions, this data has been reshaped and moved to the event object. Many of the properties moved over as-is, but some have been combined to increase clarity.

Before

function myRulesFunction(user, context, callback) {
	const clientId = context.clientID;
	const clientMetadata = context.clientMetadata || {};

	const connectionId = context.connectionID;
	const connectionMetadata = context.connectionMetadata || {};

	const protocol = context.protocol;

	const tenant = context.tenant;

	// ... additional code
}

Was this helpful?

/

After

exports.onExecutePostLogin = async (event, api) => {
	const clientId = event.client.client_id;
	const clientMetadata = event.client.metadata;

	const connectionId = event.connection.id;
	const connectionMetadata = event.connection.metadata;

	const protocol = event.transaction.protocol;

	const tenant = event.tenant.id;

	// ... additional code
};

Was this helpful?

/

Convert dependencies

Rules include dependencies in a way that requires including the version number in a require statement. Actions use a more standard CommonJS syntax and require that the versions be indicated outside of the code editor.

In Rules, only specific versions of specific packages are allowed, and adding new packages and versions requires a request to Auth0. In Actions, you can require any package that is available in the npm Registry.

  1. Search for require statements inside your Rule code.

  2. Remove version numbers, but make a note of them.

  3. Add the dependency by following the steps in the "Add a Dependency" section of Write Your First Action (if the dependency is not a core NodeJS module; if the dependency is a core NodeJS module, you do not need to include it).

  4. Move the found require statements outside of the function declaration:

Before

function myRulesFunction(user, context, callback) {
	const dependency = require("dependency@1.2.3");

	// ... additional code
}

Was this helpful?

/

After

const dependency = require("dependency"); // v1.2.3
exports.onExecutePostLogin = async (event, api) => {
	// ... additional code
};

Was this helpful?

/

Convert callbacks

When a Rule is finished processing, it must call the callback() function and pass in an error if the login fails. Conversely, Actions can return on success, or call an api method with a message if the login fails. All instances of callback() in a Rule should be removed or replaced with api.access.deny() for failure. In both Rules and Actions, if processing needs to stop for a specific condition, use a return statement.

Before

function myRulesFunction(user, context, callback) {
	const userAppMetadata = user.app_metadata || {};
	if (userAppMetadata.condition === "success") {
		// This Rule succeeded, proceed with next Rule.
		return callback(null, user, context);
	}

	if (userAppMetadata.condition === "failure") {
		// This Rule failed, stop the login with an error response.
		return callback(new Error("Failure message"));
	}

	// ... additional code
}

Was this helpful?

/

After

exports.onExecutePostLogin = async (event, api) => {
	if (event.user.app_metadata.condition === "success") {
		// This Action succeeded, proceed with next Action.
		return;
	}

	if (event.user.app_metadata.condition === "failure") {
		// This Action failed, stop the login with an error response.
		return api.access.deny("Failure message");
	}

	// ... additional code
};

Was this helpful?

/

Change handling of secrets

In Rules, you set configuration values globally, which means that all Rules can access all secret values. (To learn more, read Store Rule Configurations.) In Actions, you set configuration values for each individual Action. You can't access an Action's secret value from outside the context of the Action.

To convert secrets from Rules to Actions:

  1. Save the values needed for the specific Action you are working on.

  2. Add a Secret for each value you need to access from inside the Action. To learn how, read the Add a Secret section in Write Your First Action.

  3. Convert your code:

Before

function myRulesFunction (user, context, callback) {
  const { CLIENT_ID, CLIENT_SECRET } = configuration;

  // ... additional code
}

Was this helpful?

/

After

exports.onExecutePostLogin = async (event, api) => {
  const { CLIENT_ID, CLIENT_SECRET } = event.secrets;

  // ... additional code
}

Was this helpful?

/

As with Rules, Auth0 encrypts all secret values at rest.

Convert custom claims in tokens

Rules and Actions can both add custom claims to ID and access tokens. In Rules, this is a property of the context object, while Actions uses a method on the api object.

Before

function myRulesFunction(user, context, callback) {
	const userAppMetadata = user.app_metadata || {};
	const namespace = "https://namespace/";

	context.idToken[`${namespace}/emp_id`] = userAppMetadata.emp_id;
	context.accessToken[`${namespace}/emp_id`] = userAppMetadata.emp_id;

	// ... additional code
}

Was this helpful?

/

After

exports.onExecutePostLogin = async (event, api) => {
	const namespace = "https://namespace/";

	api.idToken.setCustomClaim(
		`${namespace}/emp_id`, 
		event.user.app_metadata.emp_id
	); 		   

	api.accessToken.setCustomClaim(
		`${namespace}/emp_id`, 
		event.user.app_metadata.emp_id
	);

	// ... additional code
};

Was this helpful?

/

Convert multi-factor triggering

In Rules, multi-factor authentication can be triggered by modifying the multifactor property of the context object. In Actions, this is done with a method on the api object.

Before

function myRulesFunction(user, context, callback) {
	if (user.app_metadata.needs_mfa === true) {
		context.multifactor = { 
			provider: "any", 
			allowRememberBrowser: false,
		};
	}

	// ... additional code
}

Was this helpful?

/

After

exports.onExecutePostLogin = async (event, api) => {
	if (event.user.app_metadata.needs_mfa === true) {
		api.multifactor.enable("any", { allowRememberBrowser: false });
	}

	// ... additional code
};

Was this helpful?

/

Convert user metadata updates

Updating the user_metadata and app_metadata properties in Rules requires a call to the Management API, which can lead to rate limit errors. Actions, however, provides a way to indicate multiple user metadata changes but only call the Management API once.

Before

function myRulesFunction(user, context, callback) {
	user.app_metadata = user.app_metadata || {}; 
	user.app_metadata.roles = user.app_metadata.roles || [];
	user.app_metadata.roles.push("administrator"); 

	auth0.users
		.updateAppMetadata(user.user_id, user.app_metadata) 
		.then(() => callback(null, user, context))
		.catch((err) => callback(err));

	// ... additional code
}

Was this helpful?

/

If subsequent Rules need to update the user metadata, then they would have to call the Management API separately, making it more likely that you would hit the rate limit.

After

exports.onExecutePostLogin = async (event, api) => {
	const userRolesUpdated = event.user.app_metadata.roles || [];
	userRolesUpdated.push("administrator"); 

	// Note the two different methods here. 
	api.user.setAppMetadata("roles", userRolesUpdated);
	api.user.setUserMetadata("hasRoles", true);

	// ... additional code
};

Was this helpful?

/

If subsequent Actions needed to update the user metadata, then they would need to call api.user.setUserMetadata or api.user.setAppMetadata. In Actions, multiple calls to these functions across one or more Actions will result in a single Management API call once the flow is complete.

Convert other Management API calls

In general, we do not recommend calling the Management API from a high-traffic, critical path like Rules or Actions. Requests to all Auth0 APIs are rate limited, including calls from extensibility points, and calling an API for all logins could easily result in failed logins at high-traffic times.

However, If the calls are necessary and are configured to avoid rate limits, it's possible to call the Management API from within Actions. As mentioned in the "Understand limitations" section earlier in this article, Actions are not provided with an access token for the Management API, so you will need to get an access token before activating your Action:

  1. Register a Machine-to-Machine application and authorize it for the Management API.

  2. Save the Client ID and Client Secret in the Action.

  3. Get an access token for the Management API.

  4. Call the Management API:

Before

function myRulesFunction(user, context, callback) {
	const ManagementClient = require("auth0@2.9.1").ManagementClient; 
	const managementClientInstance = new ManagementClient({
		// These come from built-in Rules globals
		token: auth0.accessToken, 
		domain: auth0.domain,
	}); 

	managementClientInstance.users.assignRoles(
		{ id: user.user_id }, 
		{ roles: ["ROLE_ID_TO_ADD"] }, 
		(error, user) => {
			if (error) {
				return callback(error);
			}

			// ... additional code
		}
	);
}

Was this helpful?

/

After

const auth0Sdk = require("auth0");
exports.onExecutePostLogin = async (event, api) => {
	const ManagementClient = auth0Sdk.ManagementClient;

	// This will make an Authentication API call
	const managementClientInstance = new ManagementClient({
		// These come from a machine-to-machine application
		domain: event.secrets.M2M_DOMAIN,
		clientId: event.secrets.M2M_CLIENT_ID,
		clientSecret: event.secrets.M2M_CLIENT_SECRET,
		scope: "update:users"
	});

	managementClientInstance.users.assignRoles(
		{ id: event.user.user_id }, 
		{ roles: ["ROLE_ID_TO_ADD"]}, 
		(error, user) => {
			if (error) {
				return api.access.deny(error.message);
			}

			// ... additional code
		}
	);
};

Was this helpful?

/

Convert redirects

Rules can redirect a user who is logging in to an external page, then wait for a response. In this case, all Rules before the redirection will run twice--once before the redirect and once on the response. The logic for the redirect and the response are typically contained in the same Rule.

In Actions, the Action pipeline is paused when the redirect happens and picks up once the user returns. Also, the exported redirect triggering function is separate from the redirect callback.

Before

function myRulesFunction(user, context, callback) {
    if (context.protocol === "redirect-callback") {
        // User was redirected to the /continue endpoint
        user.app_metadata.wasRedirected = true;
        return callback(null, user, context);
    } else if (
        context.protocol === "oauth2-password" ||
        context.protocol === "oauth2-refresh-token" ||
        context.protocol === "oauth2-resource-owner"
    ) {
        // User cannot be redirected
        return callback(null, user, context);
    }
    // User is logging in directly
    if (!user.app_metadata.wasRedirected) {
        context.redirect = {
            url: "https://example.com",
        };
        callback(null, user, context);
    }
}

Was this helpful?

/

After

exports.onExecutePostLogin = async (event, api) => {
    if (!event.user.app_metadata.wasRedirected && api.redirect.canRedirect()) {
        api.redirect.sendUserTo("https://example.com");
    }
};

exports.onContinuePostLogin = async (event, api) => {
    api.user.setAppMetadata("wasRedirected", true);
};

Was this helpful?

/

Convert current SSO clients references

The Rules context.sso object provides details about the current session and clients using it. For more information, see the context.sso entry in Context Object Properties in Rules. Similar information is available in the Actions event.session object.

Before

function (user, context, callback) {

  const clients = context.sso?.current_clients ?? []; 

  if (clients.length > 0) { 
	context.idToken.clients = clients.join(" "); 
  }

  return callback(null, user, context);
}

Was this helpful?

/

After

exports.onExecutePostLogin = async (event, api) => {
  const clients = event?.session?.clients ?? []; 

  if (clients.length > 0) { 
    api.idToken.setCustomClaim('clients', clients.map(c=> c?.client_id).join(" ")); 
  }
};

Was this helpful?

/

Complete the migration

Once your new Actions code has been written and tested, you must activate the Action and deactivate the Rule. These two tasks can be done quickly in succession, but depending on the order, there might be a short period of time where either both or neither are running. Because active Rules run before deployed Actions, if you start at the end of your Rules pipeline and work backwards, you can keep some logic in Rules as you build and test other logic in Actions.