Action Required: Configure Your Account Link Extension
To enhance security, we are requiring an update to the Auth0 Account Link extension Rule. This requires moving your clientSecret from a hard-coded value in your Rule script to a secure Rules Configuration variable.
This guide provides the methods to apply this security enhancement. Please read the next section carefully to determine the appropriate method for your implementation.
- Customers using the Auth0 Account Link extension that is implemented as a Rule.
Options
Please review the following options to select the appropriate method for your tenant. Detailed solutions are below.
Solution 1: Reinstall the Extension (Simple, Potentially Disruptive)
This is the simplest method, but it is potentially disruptive. Choose this solution ONLY if:
- The
auth0-account-linkapplication (client) created by the extension is not used by any other applications, custom scripts, APIs, or test suites. - You are comfortable with the existing
auth0-account-linkapplication being deleted and a new one being created in its place (with a newclientIDandclientSecret).
Solution 2: Automated Update via Node.js Script (Recommended)
This is the recommended method for all users as it is non-disruptive and fully automated. Choose this solution if you are comfortable running a simple, pre-built Node.js script. The script handles all the necessary API calls for you; you only need to provide a Management API token.
Solution 3: Manually Update the Rule via API (Advanced)
This is an advanced, non-disruptive method for users who cannot or prefer not to run the automated Node.js script. Choose this solution if:
- You use the
auth0-account-linkapplication elsewhere and cannot have it deleted. - You prefer a targeted, manual approach without reinstalling the extension.
- You are comfortable using the command line and the Auth0 Management API.
Solution 1: Reinstall the Extension (Simple Path)
This action will uninstall the existing extension, which deletes the "auth0-account-link" application and its associated Rule, and then reinstall a fresh, secure version.
⚠️ WARNING: Potential Side Effects
- Application Deletion: The existing auth0-account-link application will be permanently deleted.
- Broken References: If any other part of your system (e.g., test suites, custom Actions, API calls) references the clientID of the old auth0-account-link application, those integrations will break. You will need to manually update them with the clientID of the newly created application.
Steps:
- Navigate to the Auth0 Dashboard.
- Go to Extensions in the left-side menu.
- Go to the Installed Extensions tab.
- Click the three dots icon for the extension and select Uninstall.
- Once uninstalled, find the Auth0 Account Link extension again in the main Extensions list and reinstall it.
Solution 2: Automated Update via Node.js Script (Recommended Path)
This solution uses a Node.js script to perform a safe, non-disruptive update. It will automatically find the correct Rule and Application, update the Rule's code to use a configuration variable, and securely set that variable with the correct clientSecret.
Prerequisites
- You must have Node.js >=18 installed on your machine.
- Note: The generated recovery command requires the command-line tool jq to be installed on your system.
- Recommended: To mitigate misuse of a leaked secret, it’s recommended to rotate your
auth0-account-linkclient secret before continuing. This can be done by navigating to Applications > auth0-account-link > Settings in the Auth0 Dashboard.
Step 1: Get a Management API Token
Create a temporary Management API token with the following scopes: read:rules, update:rules, update:rules_configs, read:clients, read:client_keys.
- Follow this guide to get a token: Get Management API Access Tokens for Testing.
- You can set the token as an environment variable named
AUTH0_MGMT_API_TOKEN, or alternatively, paste it into the scripts directly when prompted.
Step 2: Back Up Your Rule (Recommended)
Before running the main update script, we strongly recommend backing up your existing Rule script. This provides a simple, one-command restore option in the unlikely event of an issue. The backup script will save the current code to a local file and generate the exact shell command needed to restore it.
Save the code below into a file named backup-rule.js.
#!/usr/bin/env node
/**
* @fileoverview Backs up the script content of a specific Auth0 Rule to a local file.
*
* @requires Node.js v18+
*/
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { Buffer } from "node:buffer";
import { writeFile } from "node:fs/promises";
// --- Configuration ---
const RULE_NAME = "auth0-account-link-extension";
const REQUIRED_SCOPES = ["read:rules"];
// --- Console Coloring ---
const colors = {
reset: "\x1b[0m",
green: "\x1b[32m",
yellow: "\x1b[33m",
red: "\x1b[31m",
cyan: "\x1b[36m",
};
const log = {
info: (msg) => console.log(`${colors.cyan}ℹ ${msg}${colors.reset}`),
success: (msg) => console.log(`${colors.green}✔ ${msg}${colors.reset}`),
warn: (msg) => console.warn(`${colors.yellow}⚠ ${msg}${colors.reset}`),
error: (msg) => console.error(`${colors.red}✖ ${msg}${colors.reset}`),
};
/**
* Main function to run the backup script.
*/
async function main() {
console.log("--- Auth0 Rule Backup Utility ---");
// 1. Get output file path from command-line arguments
const outputFile = process.argv[2];
if (!outputFile) {
log.error("Output file path is required.");
console.log("\nUsage: ./rule-backup.js <path-to-your-backup-file.js>\n");
process.exit(1);
}
// 2. Get Management API Token
const rl = createInterface({ input, output });
const tokenFromEnv = process.env.AUTH0_MGMT_API_TOKEN|| "";
const mgmtApiToken = tokenFromEnv.trim()
? tokenFromEnv
: await rl.question(
`\nEnter your Management API token (scope required: ${REQUIRED_SCOPES.join(", ")}):\n> `,
);
rl.close();
if (!mgmtApiToken) {
log.error("Management API token is required.");
process.exit(1);
}
// 3. Parse token to get domain and validate scopes
let domain;
try {
const payload = JSON.parse(
Buffer.from(mgmtApiToken.split(".")[1], "base64").toString(),
);
domain = new URL(payload.iss).hostname;
const tokenScopes = payload.scope?.split(" ") || [];
log.info(`Operating on tenant: ${domain}`);
const missingScopes = REQUIRED_SCOPES.filter(
(scope) => !tokenScopes.includes(scope),
);
if (missingScopes.length > 0) {
throw new Error(`Token is missing required scopes: ${missingScopes.join(", ")}`);
}
} catch (e) {
log.error(`Token validation failed: ${e.message}`);
process.exit(1);
}
/**
* Helper to make authenticated requests to the Auth0 Management API.
*/
const apiRequest = async (method, path) => {
const url = `https://${domain}${path}`;
const options = {
method,
headers: {
Authorization: `Bearer ${mgmtApiToken}`,
Accept: "application/json",
},
};
const response = await fetch(url, options);
if (!response.ok) {
const errorBody = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(`API Error: ${response.status}. ${errorBody.message || ""}`);
}
return response.json();
};
// 4. Fetch rule and write to file
try {
log.info(`🔍 Fetching rule named "${RULE_NAME}"...`);
const allRules = await apiRequest("GET", "/api/v2/rules");
const rule = allRules.find((r) => r.name === RULE_NAME);
if (!rule) {
throw new Error(`Could not find a rule named "${RULE_NAME}".`);
}
log.success("Rule found.");
const { id: ruleId, script: ruleScript } = rule;
log.info(`💾 Saving rule script to ${outputFile}...`);
await writeFile(outputFile, ruleScript, "utf8");
log.success(`✅ Successfully backed up rule to ${outputFile}`);
const restoreCommand = `
---------------------------------------------------------------------
RECOVERY COMMAND 📋: To restore from this backup, run the following:
---------------------------------------------------------------------
# 1. Ensure your API token with 'update:rules' scope is exported:
export AUTH0_MGMT_API_TOKEN="your_api_token_here"
# 2. Run the command:
jq -R -s '{script: .}' '${outputFile}' \\
| curl -X PATCH \\
"https://${domain}/api/v2/rules/${ruleId}" \\
-H "Authorization: Bearer $AUTH0_MGMT_API_TOKEN" \\
-H 'Content-Type: application/json' \\
--data @-
---------------------------------------------------------------------
`;
console.log(restoreCommand);
} catch (err) {
log.error(`An unexpected error occurred: ${err.message}`);
process.exit(1);
}
}
// Run the script
main();
Open your terminal, navigate to the directory where you saved backup-rule.js, and run the following command.
node backup-rule.js <output-file-name>
Upon success, the script will create the backup file and print a RECOVERY COMMAND to your terminal. Copy this entire command block and save it in a safe place. You can use it to restore your Rule to its original state if needed.
Step 3: Update Your Rule and Config
Save the code below into a file named update-rule.js.
#!/usr/bin/env node
/**
* @fileoverview Automates the process of moving the Auth0 Account Linking
* extension's client secret from a hard-coded value in a Rule to a secure
* Rules Configuration variable.
*
* @requires Node.js v18+
*/
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { Buffer } from "node:buffer";
// --- Configuration ---
const RULE_NAME = "auth0-account-link-extension";
const CLIENT_NAME = "auth0-account-link";
const CONFIG_KEY = "AUTH0_ACCOUNT_LINKING_EXTENSION_CLIENT_SECRET";
const REQUIRED_SCOPES = ["read:rules", "update:rules", "read:client_keys", "read:clients", "update:rules_configs"];
// --- Console Coloring ---
const colors = {
reset: "\x1b[0m",
green: "\x1b[32m",
yellow: "\x1b[33m",
red: "\x1b[31m",
cyan: "\x1b[36m",
};
const log = {
info: (msg) => console.log(`${colors.cyan}ℹ ${msg}${colors.reset}`),
success: (msg) => console.log(`${colors.green}✔ ${msg}${colors.reset}`),
warn: (msg) => console.warn(`${colors.yellow}⚠ ${msg}${colors.reset}`),
error: (msg) => console.error(`${colors.red}✖ ${msg}${colors.reset}`),
};
/**
* Main function to run the remediation script.
*/
async function main() {
console.log("--- Auth0 Account Linking Extension Secret Remediation ---");
log.info(
"This script will securely move your client secret from the Rule to Rules Configuration.",
);
const rl = createInterface({ input, output });
const tokenFromEnv = process.env.AUTH0_MGMT_API_TOKEN || "";
const mgmtApiToken = tokenFromEnv.trim()
? tokenFromEnv
: await rl.question(
`\nEnter your Management API token (with scopes: ${REQUIRED_SCOPES.join(", ")}):\n> `,
);
rl.close();
if (!mgmtApiToken) {
log.error("Management API token is required.");
process.exit(1);
}
let domain;
let tokenScopes = [];
try {
const payload = JSON.parse(
Buffer.from(mgmtApiToken.split(".")[1], "base64").toString(),
);
domain = new URL(payload.iss).hostname;
tokenScopes = payload.scope?.split(" ") || [];
log.info(`Operating on tenant: ${domain}`);
} catch (e) {
log.error(
'The provided token is invalid. Could not parse domain from the "iss" claim.',
);
process.exit(1);
}
const missingScopes = REQUIRED_SCOPES.filter(
(scope) => !tokenScopes.includes(scope),
);
if (missingScopes.length > 0) {
log.error(
`The provided token is missing required scopes: ${missingScopes.join(", ")}`,
);
process.exit(1);
}
/**
* A helper to make authenticated requests to the Auth0 Management API.
* @param {string} method - The HTTP method (e.g., 'GET', 'PATCH').
* @param {string} path - The API endpoint path (e.g., '/api/v2/rules').
* @param {object} [body] - The JSON body for the request.
* @returns {Promise<any>} The JSON response from the API.
*/
const apiRequest = async (method, path, body) => {
const url = `https://${domain}${path}`;
const options = {
method,
headers: {
Authorization: `Bearer ${mgmtApiToken}`,
"Content-Type": "application/json",
Accept: "application/json",
},
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
const errorBody = await response
.json()
.catch(() => ({ message: response.statusText }));
throw new Error(
`API Error on ${method} ${path}: ${response.status} ${response.statusText}. ${errorBody.message || ""}`,
);
}
// DELETE returns 204 No Content
return response.status === 204 ? null : response.json();
};
try {
// 1. Fetch required resources
log.info("🔍 Fetching Account Linking Rule and Client details...");
const [allRules, allClients] = await Promise.all([
apiRequest("GET", "/api/v2/rules"),
apiRequest(
"GET",
`/api/v2/clients?fields=name,client_id,client_secret&include_fields=true`,
),
]);
const rule = allRules.find((r) => r.name === RULE_NAME);
if (!rule) {
throw new Error(`Could not find the Rule named "${RULE_NAME}".`);
}
const client = allClients.find((c) => c.name === CLIENT_NAME);
if (!client) {
throw new Error(
`Could not find the Application (Client) named "${CLIENT_NAME}".`,
);
}
const { id: ruleId, script: originalScript } = rule;
const { client_secret: clientSecret } = client;
if (!clientSecret) {
throw new Error(
`The client secret for "${CLIENT_NAME}" could not be retrieved. Ensure your token has sufficient permissions.`,
);
}
log.success("Found required Rule and Client.");
// 2. Idempotency Check: See if the work is already done.
if (originalScript.includes(`configuration.${CONFIG_KEY}`)) {
log.success("🎉 Rule script is already updated. No action needed.");
process.exit(0);
}
// 3. Prepare the new script content in memory
log.info("🔧 Modifying Rule script to use a configuration variable...");
const secretRegex = /(clientSecret\s*:\s*)(['"`])(.*?)\2/;
// (clientSecret\s*:\s*) - Group 1
// (['"`]) - Group 2
// (.*?) - Group 3, captures the secret string.
// . -> matches any character
// *? -> matches the previous token zero or more times, non-greedy
//
// \2 - Backreference, ensures the closing delimiter is the exact same character that was captured in Group 2.
if (!secretRegex.test(originalScript)) {
throw new Error(
'Could not find the "clientSecret" property in the Rule script. The script may have been manually altered. Please review and update it manually.',
);
}
const modifiedScript = originalScript.replace(
secretRegex,
`$1configuration.${CONFIG_KEY}`,
);
log.success("Script modification is ready.");
// 4. Execute the transactional update
await runUpdateTransaction(
apiRequest,
ruleId,
modifiedScript,
clientSecret,
);
} catch (err) {
log.error(`An unexpected error occurred during setup: ${err.message}`);
process.exit(1);
}
}
/**
* Executes the PUT and PATCH operations as a transaction.
* If the PATCH fails, it rolls back the PUT.
*/
async function runUpdateTransaction(
apiRequest,
ruleId,
modifiedScript,
clientSecret,
) {
let configWasSet = false;
try {
console.log("\n--- Starting Update Process ---");
// Step 1: Set the secret in Rules Configuration
log.info(`[1/2] Setting secret value in Rules Configuration...`);
await apiRequest("PUT", `/api/v2/rules-configs/${CONFIG_KEY}`, {
value: clientSecret,
});
configWasSet = true;
log.success("Secret stored securely as a configuration variable.");
// Step 2: Update the Rule to use the new configuration
log.info(`[2/2] Updating Rule script to reference the new variable...`);
await apiRequest("PATCH", `/api/v2/rules/${ruleId}`, {
script: modifiedScript,
});
log.success("Rule script updated successfully.");
log.success(
"\n🚀 Remediation complete! Please test your account linking flow to verify the changes.",
);
} catch (error) {
log.error(
`\nAn error occurred during the update process: ${error.message}`,
);
// If the first step succeeded but the second one failed, roll back.
if (configWasSet) {
log.warn(
"\n🔄 Attempting to roll back changes by deleting the Rules Configuration variable...",
);
try {
await apiRequest("DELETE", `/api/v2/rules-configs/${CONFIG_KEY}`);
log.success(
"✅ Rollback successful. Your tenant is in its original state.",
);
} catch (rollbackError) {
log.error(`🚨 CRITICAL: ROLLBACK FAILED. ${rollbackError.message}`);
log.error(
`The configuration variable '${CONFIG_KEY}' was created but the script update failed.`,
);
log.error(
"Please manually delete this key from your Auth0 Dashboard (Auth Pipeline -> Rules -> Settings).",
);
}
}
process.exit(1);
}
}
// Run the script
main();
Open your terminal, navigate to the directory where you saved update-rule.js, and run the following command.
node update-rule.js
The script will prompt you for your Management API token and will print its progress.
Step 4: Verify
After the script completes, thoroughly test your account link flow to ensure it works as expected.
Solution 3: Manually Update the Rule (Advanced Path)
This solution is for advanced users who cannot run the recommended Node.js script. It involves manually performing a series of API calls to update your Rule script and securely set the configuration variable. Since Rules are a deprecated feature, these updates must be performed using the Auth0 Management API.
Prerequisites
You must have the following command-line tools installed:
- curl: A standard tool for making HTTP requests, available on most operating systems.
- jq: A command-line JSON processor. If you don't have it, you can install it from the official jq website.
- Recommended: to mitigate misuse of a leaked secret, it’s recommended to rotate your auth0-account-link client secret before continuing. This can be done by navigating to Applications -> auth0-account-link -> Settings in the Auth0 Dashboard.
Step 1: Set Up Environment
-
Set the following environment variables. You will need them for the commands below.
# Set Your Auth0 Domain
# Create a token with scopes read:rules, update:rules, read:client_keys, read:clients, update:rules_configs.
# Follow this guide: https://auth0.com/docs/secure/tokens/access-tokens/management-api-access-tokens/get-management-api-access-tokens-for-testing.
export AUTH0_MGMT_API_TOKEN=ey...
# Set your Management API Token
# Found in your Auth0 Dashboard under Applications -> <any application> -> Settings.
export AUTH0_DOMAIN=your-tenant.locality.auth0.com
# Set your Client Secret
# Found in your Auth0 Dashboard under Applications -> auth0-account-link -> Settings.
export ACCOUNT_LINK_CLIENT_SECRET="xxxxxxxxxxxxxxxxxxxx"
-
Run the command below to list all your Rules. Find the rule related to Account Link (named auth0-account-link-extension) and copy its id.
# Fetch all rules, showing only the ID and name for clarity
curl "https://$AUTH0_DOMAIN/api/v2/rules" \
-H "Authorization: Bearer $AUTH0_MGMT_API_TOKEN" \
-H 'Accept: application/json' \
| jq -r '.[] | {id, name}'
# jq: processes the JSON response to show only the id and name of each rule.
-
Set the account-link rule as an environment variable
export YOUR_RULE_ID=rul_...
Step 2: Back Up Your Current Rule Script
Retrieve your existing Rule script and save it to a file named rule-backup.js.
# Fetch a specific rule by its ID and save its script to a file
curl -sS "https://$AUTH0_DOMAIN/api/v2/rules/$YOUR_RULE_ID" \
-H "Authorization: Bearer $AUTH0_MGMT_API_TOKEN" \
-H 'Accept: application/json' \
| jq -r '.script' \
> rule-backup.js
# jq: extracts the value of the 'script' property from the JSON response
# and saves the output to the file 'rule-backup.js'
Step 3: Modify the Rule Script
- Make a copy of your backup file: cp rule-backup.js rule-modified.js
- Open rule-modified.js in a text editor.
- Locate the var config = { ... }; block.
- Find the clientSecret line and replace its value with the literal string configuration.AUTH0_ACCOUNT_LINKING_EXTENSION_CLIENT_SECRET.
- ❗️ IMPORTANT: Do not replace this with your actual secret. You are adding the literal text below, which acts as a variable placeholder that Auth0 will securely populate at runtime.
-
Before:
var config = {
// ...
clientSecret: 'YOUR_HARDCODED_CLIENT_SECRET',
// ...
};
-
After:
var config = {
// ...
clientSecret: configuration.AUTH0_ACCOUNT_LINKING_EXTENSION_CLIENT_SECRET,
// ...
};
-
Save the rule-modified.js file.
Step 4: Set the Client Secret in Rules Configuration
Set the secure value for the configuration variable. Use the client secret you gathered in Step 0.
Note: The key name “AUTH0_ACCOUNT_LINKING_EXTENSION_CLIENT_SECRET” is a literal string, not a variable. It must be exactly as written.
# Set the value for the Rules Config variable
curl --request PUT \
--url https://$AUTH0_DOMAIN/api/v2/rules-configs/AUTH0_ACCOUNT_LINKING_EXTENSION_CLIENT_SECRET \
--header "authorization: Bearer $AUTH0_MGMT_API_TOKEN" \
--header 'content-type: application/json' \
--data '{
"value": "'"$ACCOUNT_LINK_CLIENT_SECRET"'"
}'
A successful request will return a 200 OK status and a JSON payload showing your secret's key and value.
Step 5: Update the Rule via API
Upload the modified script back to Auth0. This command reads your modified rule-modified.js file and sends it in a PATCH request.
# Prepare the rule.js content as a JSON payload and send it to update the rule
jq -R -s '{script: .}' rule-modified.js \
| curl -X PATCH \
"https://$AUTH0_DOMAIN/api/v2/rules/$YOUR_RULE_ID" \
-H "Authorization: Bearer $AUTH0_MGMT_API_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
--data @-
# jq: wraps the entire content of rule.js into a JSON object like {"script": "..."}.
# curl --data @-: sends the JSON received from jq as the request body.
You should receive a 200 OK response confirming the update was successful.
Step 6: Verify
After completing all the steps, thoroughly test your account linking flow to ensure it works as expected. If you encounter any issues, you can restore your Rule using your rule-backup.js file or reach out to Auth0 Support for assistance.