Configure Pre-Update Password Action with AWS Lambda
5 mins
Using AWS Lambda¶
This section describes how to implement the Pre-Update Password Action scenario using AWS Lambda with Node.js. In this approach, the validation logic is implemented in a Lambda function that checks whether a user’s password is compromised using the Have I Been Pwned (HIBP) API. If the password has been compromised, the user is disallowed from setting it.
The Lambda function queries the HIBP API to verify if the provided password appears in the database of known compromised passwords.
Set Up Your Node.js Project¶
Create a folder to hold your Lambda source code and its dependencies, so it can be packaged as a ZIP file to be uploaded as a Lambda function.
mkdir password-update-validator
cd password-update-validator
Run the following command to generate a package.json
file which helps manage your project dependencies:
npm init -y
This creates a basic package.json
with default values. The -y
flag automatically accepts all default settings, so
you don't have to manually answer prompts.
Install required dependencies for the use case. The Lambda function requires the following packages:
- axios – Enables the function to make HTTP requests to external services like the Have I Been Pwned (HIBP) API.
npm install axios
Create the Lambda Source Files for Deployment¶
Create a new file named index.js
, which will contain the implementation of the Lambda function.
touch index.js
Define the initial structure in the index.js
file as shown below; this will lay the groundwork for building the
password update validation logic.
const crypto = require("crypto");
const axios = require("axios");
Implement the Lambda function that listens for user password update requests from WSO2 Identity Server.
exports.handler = async (event) => {
try {
const method = event.requestContext?.http?.method;
const path = event.rawPath;
const headers = {"Content-Type": "application/json"};
if (method === "GET" && path === "/") {
return {
statusCode: 200,
headers,
body: JSON.stringify({
message: "Pre-password update service up and running!",
status: "OK",
}),
};
}
if (method === "POST" && path === "/passwordcheck") {
let body;
try {
body = JSON.parse(event.body);
} catch {
return {
statusCode: 400,
headers,
body: JSON.stringify({
actionStatus: "ERROR",
error: "invalid_request",
errorDescription: "Invalid JSON payload.",
}),
};
}
const cred = body?.event?.user?.updatingCredential;
if (!cred || cred.type !== "PASSWORD") {
return {
statusCode: 400,
headers,
body: JSON.stringify({
actionStatus: "ERROR",
error: "invalid_credential",
errorDescription: "No password credential found.",
}),
};
}
let plain = cred.value;
if (cred.format === "HASH") {
try {
plain = Buffer.from(cred.value, "base64").toString("utf8");
} catch {
return {
statusCode: 400,
headers,
body: JSON.stringify({
actionStatus: "ERROR",
error: "invalid_credential",
errorDescription: "Expects the encrypted credential.",
}),
};
}
}
const sha1 = crypto.createHash("sha1").update(plain).digest("hex").toUpperCase();
const prefix = sha1.slice(0, 5);
const suffix = sha1.slice(5);
const hibpResp = await axios.get(`https://api.pwnedpasswords.com/range/${prefix}`, {
headers: {
"Add-Padding": "true",
"User-Agent": "hibp-demo",
},
});
const hitLine = hibpResp.data
.split("\n")
.find((line) => line.startsWith(suffix));
const count = hitLine ? parseInt(hitLine.split(":")[1], 10) : 0;
if (count > 0) {
return {
statusCode: 200,
headers,
body: JSON.stringify({
actionStatus: "FAILED",
failureReason: "password_compromised",
failureDescription: "The provided password is compromised.",
}),
};
}
return {
statusCode: 200,
headers,
body: JSON.stringify({
actionStatus: "SUCCESS",
message: "Password is not compromised.",
}),
};
}
return {
statusCode: 404,
headers,
body: JSON.stringify({
error: "Not Found",
message: "Invalid route or method.",
}),
};
} catch (err) {
console.error("🔥", err);
const status = err.response?.status || 500;
const msg =
status === 429
? "External HIBP rate limit hit—try again in a few seconds."
: err.message || "Unexpected server error";
return {
statusCode: status,
headers: {"Content-Type": "application/json"},
body: JSON.stringify({error: msg}),
};
}
};
The above source code performs the following key tasks to help fulfill the use case defined earlier in this document:
- It exposes a simple health check endpoint (
GET /
) that returns a200 OK
response, confirming the service is running. - It defines a
POST /passwordcheck
endpoint to process password credential validation requests. - It validates that the request payload is a valid JSON structure, returning an error if not.
- It extracts the credential object from the event payload, and checks whether a password credential is present; returns
an error response if the credential is missing or not of type
PASSWORD
. - It handles both plain text and encrypted (Base64-encoded SHA-1 hash) passwords, decoding the credential value if it's in hashed format.
- It calculates the SHA-1 hash of the password and uses the Have I Been Pwned (HIBP) API’s k-anonymity model to check whether the password has been exposed in known data breaches.
- It sends only the first 5 characters of the SHA-1 hash to the HIBP API and then searches the returned suffixes for a match with the rest of the hash.
- If the password has been compromised, it returns an actionStatus:
FAILED
response with an appropriate reason and description. - If the password is not found in any known breaches, it returns an actionStatus:
SUCCESS
response. - It handles unexpected errors (e.g., HTTP errors from HIBP) and returns a relevant message, including handling HIBP
rate limits (
429 Too Many Requests
).
The final source code should look similar to the following.
const crypto = require("crypto");
const axios = require("axios");
exports.handler = async (event) => {
try {
const method = event.requestContext?.http?.method;
const path = event.rawPath;
const headers = { "Content-Type": "application/json" };
if (method === "GET" && path === "/") {
return {
statusCode: 200,
headers,
body: JSON.stringify({
message: "Pre-password update service up and running!",
status: "OK",
}),
};
}
if (method === "POST" && path === "/passwordcheck") {
let body;
try {
body = JSON.parse(event.body);
} catch {
return {
statusCode: 400,
headers,
body: JSON.stringify({
actionStatus: "ERROR",
error: "invalid_request",
errorDescription: "Invalid JSON payload.",
}),
};
}
const cred = body?.event?.user?.updatingCredential;
if (!cred || cred.type !== "PASSWORD") {
return {
statusCode: 400,
headers,
body: JSON.stringify({
actionStatus: "ERROR",
error: "invalid_credential",
errorDescription: "No password credential found.",
}),
};
}
let plain = cred.value;
if (cred.format === "HASH") {
try {
plain = Buffer.from(cred.value, "base64").toString("utf8");
} catch {
return {
statusCode: 400,
headers,
body: JSON.stringify({
actionStatus: "ERROR",
error: "invalid_credential",
errorDescription: "Expects the encrypted credential.",
}),
};
}
}
const sha1 = crypto.createHash("sha1").update(plain).digest("hex").toUpperCase();
const prefix = sha1.slice(0, 5);
const suffix = sha1.slice(5);
const hibpResp = await axios.get(`https://api.pwnedpasswords.com/range/${prefix}`, {
headers: {
"Add-Padding": "true",
"User-Agent": "hibp-demo",
},
});
const hitLine = hibpResp.data
.split("\n")
.find((line) => line.startsWith(suffix));
const count = hitLine ? parseInt(hitLine.split(":")[1], 10) : 0;
if (count > 0) {
return {
statusCode: 200,
headers,
body: JSON.stringify({
actionStatus: "FAILED",
failureReason: "password_compromised",
failureDescription: "The provided password is compromised.",
}),
};
}
return {
statusCode: 200,
headers,
body: JSON.stringify({
actionStatus: "SUCCESS",
message: "Password is not compromised.",
}),
};
}
return {
statusCode: 404,
headers,
body: JSON.stringify({
error: "Not Found",
message: "Invalid route or method.",
}),
};
} catch (err) {
console.error("🔥", err);
const status = err.response?.status || 500;
const msg =
status === 429
? "External HIBP rate limit hit—try again in a few seconds."
: err.message || "Unexpected server error";
return {
statusCode: status,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ error: msg }),
};
}
};
Create the Deployment Package¶
Since this project includes external libraries, the Lambda function needs to be packaged as a ZIP archive before uploading:
zip -r validate-user-password-update.zip .
This command includes all necessary files (index.js
, node_modules
) required by AWS Lambda.
Deploy the Function on AWS Lambda¶
Log in to the AWS Dashboard and navigate to the AWS Lambda Console. Once there, click Create function and choose Author from scratch.
Then, fill in the following details and create the function:
- Function name: validate-user-password-update
- Runtime: Node.js 22.x
- Architecture: x86
- Permissions: Choose an existing role or create a new one with basic Lambda permissions.
Once the function is created, go to the Code tab, upload the ZIP file (validate-user-password-update.zip) that was created earlier, and click Save to upload the source code.
Next, configure the Function URL:
- Navigate to the Configuration tab, then to the Function URL section.
- Click Create function URL and set the Auth type to None.
The generated function URL will be displayed in the Function overview section of the dashboard. Make sure to note this URL, as it will be used to expose the function to external services.
Test Deployed Service¶
To test the deployed service, you will need the function URL. A sample request for a successful scenario is shown below.
curl --location '<function_url>/passwordcheck' \
--header 'Content-Type: application/json' \
--data '{
"actionType": "PRE_UPDATE_PASSWORD",
"event": {
"tenant": {
"id": "2210",
"name": "testwso2"
},
"user": {
"id": "18b6b431-16e9-4107-a828-33778824c8af",
"updatingCredential": {
"type": "PASSWORD",
"format": "HASH",
"value": "ec4Zktg/dqruY3ZHVjwTCZ9422Bu0Xi3F56ZcFxkcjU=",
"additionalData": {
"algorithm": "SHA256"
}
}
},
"userStore": {
"id": "REVGQVVMVA==",
"name": "DEFAULT"
},
"initiatorType": "ADMIN",
"action": "UPDATE"
}
}'
Configure WSO2 Identity Server for Pre-Update Password Action Workflow¶
First, sign in to your WSO2 Identity Server account using your admin credentials, click on "Actions" and then select the action type Pre Update Password.
Add an action name, the endpoint extracted from the deployment, and the appropriate authentication mechanism. For AWS Lambda, use the generated function URL appended with the endpoint name, and set the authentication mechanism to None, as no authentication is required. For the password sharing mechanism, you can use either SHA-256 hashed or plain text, as the implementation supports both formats.
Once the action is configured, make sure it is marked as active to ensure it is triggered during relevant operations. Then, log in to the Console application using the administrator account, navigate to the User Management > Users section, and add a new user with a predefined password. This user will be used to test the configured pre-password update action.
Validate Pre-Update Password Action Workflow¶
To test this scenario, attempt to update a user's password using both compromised and uncompromised passwords, verified through Pwned Passwords. The update can be performed via either the Console ( administrator action) or the My Account application (user self-service) to ensure that only uncompromised passwords are accepted.
Console (administrator update)
- Log in to the WSO2 Identity Server Console application using an administrator account.
- Navigate to User Management > Users.
- Select a user and reset their password.
My Account (self-update)
- Log in to the WSO2 Identity Server My Account as a user.
- Go to Security > Change Password.
- Attempt to update the password.