Change Password
Allow authenticated users to change their password by verifying their current password and enforcing strength rules on the new one.
What You'll Build
A secured API endpoint that:
- Requires a valid JWT token in the request body (proves the user is logged in)
- Validates the request schema using Data Mapper + Data Validator
- Verifies the current password with bcrypt
- Enforces password strength on the new password (same rules as registration)
- Hashes the new password with bcrypt before storing
- Returns generic error messages
Endpoint: POST /api/v1/YOUR_ID/change-password
Request body:
{
"jwt": "eyJhbGciOiJIUzI1NiIs...",
"currentPassword": "MyOldP@ss!",
"newPassword": "MyN3wP@ss!"
}
Responses:
| Scenario | Status | Body |
|---|---|---|
| Success | 200 | { "success": true, "message": "Password changed successfully." } |
| Invalid schema | 400 | { "valid": false, "errors": [...] } |
| Invalid JWT | 401 | { "success": false, "error": "Invalid or expired session." } |
| Wrong current password | 400 | { "success": false, "error": "Current password is incorrect." } |
| Weak new password | 400 | { "success": false, "error": "..." } |
| Same as current | 400 | { "success": false, "error": "New password must be different from current password." } |
| User not found | 400 | { "success": false, "error": "Invalid or expired session." } |
Prerequisites
Table: users
Same table used by registration and login. Required columns:
| Column | Type | Description |
|---|---|---|
| text | User's email (unique) | |
| password | text | Bcrypt hash of the current password |
Vault secret
Same my_jwt_secret stored in the Vault, used by the login workflow. In node fields, reference it using {{secrets.my_jwt_secret}}. In Code Execution nodes, access it via variables.secrets.my_jwt_secret.
Security Design
| Area | Implementation |
|---|---|
| HTTP method | POST only |
| Authentication | JWT verification (proves user identity) |
| Schema validation | Data Validator (jwt, currentPassword, newPassword required) |
| Current password | Verified with bcrypt before allowing change |
| Same password check | Prevents setting new password identical to current |
| Password strength | 8+ chars, uppercase, lowercase, number, special char |
| New password hashing | Bcrypt |
| Rate limit | 10/min |
| Timeout | 15s |
| Error messages | Generic where needed (no user enumeration) |
Workflow Flow
Flow Start (API POST /change-password)
↓
Data Mapper → dataMapper
↓
Data Validator → inputValidator
TRUE ↓ FALSE → Simple Output (400)
Query Data (jwt_secret) → jwt_secret
↓
Query Data (users) → user_data
↓
Code Node → cp_result
↓
Condition (valid?)
IF → Write Data (UPDATE users) → Simple Output (200)
ELSE → Simple Output (error)
Step 1 - Flow Start
| Setting | Value |
|---|---|
| Trigger Type | API |
| Method | POST only |
| Custom Path | change-password |
| Rate Limit | 10 requests/minute |
| Timeout | 15 seconds |
| CORS | Your production domain |
Step 2 - Data Mapper
Map the incoming request body fields.
Output variable: dataMapper
Template:
{
"jwt": "{{jwt}}",
"currentPassword": "{{currentPassword}}",
"newPassword": "{{newPassword}}"
}
Step 3 - Data Validator
Validate that all three fields are present.
Output variable: inputValidator
Input data: {{dataMapper}}
| Field | Type | Required | Validation |
|---|---|---|---|
jwt |
string | yes | minLength: 10 |
currentPassword |
string | yes | minLength: 1 |
newPassword |
string | yes | minLength: 8 |
| Port | Fires when | Next node |
|---|---|---|
| TRUE | All fields present and valid | → Step 4 (Query jwt_secret) |
| FALSE | Missing or invalid | → Simple Output (400 error) |
Step 3F - Simple Output (Validation Error)
| Setting | Value |
|---|---|
| Status | 400 |
| Type | JSON |
| Output | {{inputValidator}} |
Step 4 - Query Data (JWT Secret)
The JWT signing secret is stored in the Vault — no Query Data node is needed for this anymore. In Code Execution nodes, access it directly via variables.secrets.my_jwt_secret.
Make sure the secret
my_jwt_secretexists in your Vault (Settings → Vault).
Step 5 - Code Node (Verify JWT & Extract Email)
Verify the JWT and extract the user's email. This is a separate Code node so we can use the email to query the user.
Output variable: jwt_check
(function() {
var token = variables.dataMapper.jwt || '';
var secret = variables.secrets.my_jwt_secret || null;
if (!secret) {
return { valid: false, error: 'Server configuration error' };
}
try {
var decoded = jwtVerify(token, secret);
return { valid: true, email: decoded.sub || decoded.email || '' };
} catch(e) {
return { valid: false, error: 'Invalid or expired session.' };
}
})();
Step 6 - Condition (JWT Valid?)
| Branch | Action |
|---|---|
| IF | {{jwt_check.valid}} equals true → Continue to Step 7 |
| ELSE | → Simple Output (401 error) |
ELSE branch - Simple Output (Invalid JWT)
| Setting | Value |
|---|---|
| Status | 401 |
| Type | JSON |
| Output | { "success": false, "error": "{{jwt_check.error}}" } |
Step 7 - Query Data (Find User)
Look up the user by the email extracted from the JWT.
Output variable: user_data
| Setting | Value |
|---|---|
| Source | users (Structured) |
| Filter | email equals {{jwt_check.email}} |
| Limit | 1 |
Step 8 - Code Node (Validate & Hash)
Verify the current password, enforce strength rules on the new password, check they're different, and hash the new password.
Output variable: cp_result
(function() {
var currentPassword = variables.dataMapper.currentPassword || '';
var newPassword = variables.dataMapper.newPassword || '';
var userQuery = [{{user_data}}][0] || {};
var userRows = userQuery.rows || [];
var user = userRows.length > 0 ? userRows[0] : null;
if (!user) {
return { valid: false, error: 'Invalid or expired session.', statusCode: 400 };
}
// Verify current password
if (!bcryptVerify(currentPassword, user.password)) {
return { valid: false, error: 'Current password is incorrect.', statusCode: 400 };
}
// Check new password is different
if (bcryptVerify(newPassword, user.password)) {
return { valid: false, error: 'New password must be different from current password.', statusCode: 400 };
}
// Password strength checks
if (newPassword.length < 8) {
return { valid: false, error: 'Password must be at least 8 characters', statusCode: 400 };
}
if (!/[A-Z]/.test(newPassword)) {
return { valid: false, error: 'Password must contain at least one uppercase letter', statusCode: 400 };
}
if (!/[a-z]/.test(newPassword)) {
return { valid: false, error: 'Password must contain at least one lowercase letter', statusCode: 400 };
}
if (!/[0-9]/.test(newPassword)) {
return { valid: false, error: 'Password must contain at least one number', statusCode: 400 };
}
if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(newPassword)) {
return { valid: false, error: 'Password must contain at least one special character', statusCode: 400 };
}
var hashedPassword = bcryptHash(newPassword);
return {
valid: true,
email: user.email,
hashedPassword: hashedPassword,
statusCode: 200
};
})();
Why verify current password?
Even though the user has a valid JWT, requiring the current password prevents unauthorized changes if someone gains temporary access to the session (e.g., an unlocked device). This is standard practice for password changes.
Why check if new password equals current?
bcryptVerify(newPassword, user.password) returns true if the new password matches the stored hash. This prevents users from "changing" to the same password, which would be a no-op that gives a false sense of security.
Step 9 - Condition (Valid?)
| Branch | Action |
|---|---|
| IF | {{cp_result.valid}} equals true → Write Data |
| ELSE | → Simple Output (error) |
ELSE branch - Simple Output (Error)
| Setting | Value |
|---|---|
| Status | 400 |
| Type | JSON |
| Output | { "success": false, "error": "{{cp_result.error}}" } |
Step 10 - Write Data (Update Password)
| Setting | Value |
|---|---|
| Source | users (Update) |
| Filter | email equals {{cp_result.email}} |
| Column | Value |
|---|---|
| password | {{cp_result.hashedPassword}} |
Step 11 - Simple Output (Success)
| Setting | Value |
|---|---|
| Status | 200 |
| Type | JSON |
| Output | { "success": true, "message": "Password changed successfully." } |
Post-Import Setup
After importing this workflow, configure:
- Flow Start - Update CORS origins to your production domain
- Vault - Ensure
my_jwt_secretexists in Settings → Vault - Query Data (Find User) - Select your
userstable, map theemailcolumn - Write Data (Update Password) - Select your
userstable, map thepasswordcolumn, set the WHERE filter column toemail
Frontend Integration
async changePassword(jwt: string, currentPassword: string, newPassword: string) {
const response = await fetch('https://your-domain.com/api/v1/YOUR_ID/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jwt, currentPassword, newPassword })
});
return response.json();
}
The JWT should come from the token stored after login. Most frontends keep it in localStorage or a cookie.
Testing
Test 1 - Successful password change:
curl -X POST https://your-domain.com/api/v1/YOUR_ID/change-password \
-H "Content-Type: application/json" \
-d '{"jwt": "YOUR_VALID_JWT", "currentPassword": "MyOldP@ss!", "newPassword": "MyN3wP@ss!"}'
Expected: { "success": true, "message": "Password changed successfully." }
Test 2 - Wrong current password:
curl -X POST https://your-domain.com/api/v1/YOUR_ID/change-password \
-H "Content-Type: application/json" \
-d '{"jwt": "YOUR_VALID_JWT", "currentPassword": "wrongpassword", "newPassword": "MyN3wP@ss!"}'
Expected: { "success": false, "error": "Current password is incorrect." }
Test 3 - Same password:
curl -X POST https://your-domain.com/api/v1/YOUR_ID/change-password \
-H "Content-Type: application/json" \
-d '{"jwt": "YOUR_VALID_JWT", "currentPassword": "MyOldP@ss!", "newPassword": "MyOldP@ss!"}'
Expected: { "success": false, "error": "New password must be different from current password." }
Test 4 - Weak new password:
curl -X POST https://your-domain.com/api/v1/YOUR_ID/change-password \
-H "Content-Type: application/json" \
-d '{"jwt": "YOUR_VALID_JWT", "currentPassword": "MyOldP@ss!", "newPassword": "weak"}'
Expected: { "success": false, "error": "Password must be at least 8 characters" }
Test 5 - Invalid JWT:
curl -X POST https://your-domain.com/api/v1/YOUR_ID/change-password \
-H "Content-Type: application/json" \
-d '{"jwt": "invalid.jwt.token", "currentPassword": "MyOldP@ss!", "newPassword": "MyN3wP@ss!"}'
Expected: { "success": false, "error": "Invalid or expired session." }
Test 6 - Missing fields:
curl -X POST https://your-domain.com/api/v1/YOUR_ID/change-password \
-H "Content-Type: application/json" \
-d '{"jwt": "YOUR_VALID_JWT"}'
Expected: 400 with Data Validator error details
What to verify in the database
After a successful password change:
| Column | Expected |
|---|---|
| password | A new bcrypt hash (different from before) |
Security Checklist
| Control | Status |
|---|---|
| POST only (no GET) | ✅ |
| JWT authentication required | ✅ |
| Schema validation (Data Validator) | ✅ |
| Current password verification (bcrypt) | ✅ |
| Same password prevention | ✅ |
| Password strength enforcement | ✅ |
| New password hashed with bcrypt | ✅ |
| Generic error messages (no user enumeration) | ✅ |
| Rate limiting (10/min) | ✅ |
| Reduced timeout (15s) | ✅ |
| No real secrets in tutorial JSON | ✅ |