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
andcontext
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
andcontext
objects on the Actionsevent
object. Look for any side effects your Actions have on the system (like failing a login or updating user metadata) in theapi
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
Log in to your production tenant, and copy the code from the Rule you want to convert.
Switch to a non-production tenant, and navigate to Auth0 Dashboard > Actions > Library.
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.
In the code block of the Actions Code Editor, paste the Rule code you want to convert below the exported
onExecutePostLogin
function.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.
Search for
require
statements inside your Rule code.Remove version numbers, but make a note of them.
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).
Move the found
require
statements outside of thefunction
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:
Save the values needed for the specific Action you are working on.
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.
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:
Register a Machine-to-Machine application and authorize it for the Management API.
Save the Client ID and Client Secret in the Action.
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.