IntermediateAuthentication

Change Password

JWT-authenticated password change with current password verification and strength enforcement.

Click to expand
Flow StartAPI POSTData MapperdataMapperData ValidatorinputValidatorTRUEFALSESimple OutputError 400Query Datajwt_secretCodejwt_checkConditionIFELSESimple OutputError 401Query Datauser_dataCodecp_resultConditionIFELSESimple OutputError 400Write DataUPDATE usersSimple OutputSuccess 200

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
email 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_secret exists 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:

  1. Flow Start - Update CORS origins to your production domain
  2. Vault - Ensure my_jwt_secret exists in Settings → Vault
  3. Query Data (Find User) - Select your users table, map the email column
  4. Write Data (Update Password) - Select your users table, map the password column, set the WHERE filter column to email

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