SiteGenesis Passwords

This topic describes how to create secure passwords for SiteGenesis Pipelines (SGPP) and SiteGenesis JavaScript Controllers (SGJC)

Salesforce B2C Commerce has changed the platform requirements for password account creation as part of an ongoing effort to help our customers improve site security.

As of 17.4, if you create a new customer list, by default, the platform requires a password that has:
  • at least eight characters
  • at least one uppercase character
  • at least one lowercase character
  • at least one number
  • no spaces
  • no special characters

You can now set password requirements in Business Manager. This means that you don't have to create a regex in your code to change your password requirements for each password form. Instead, you can set them in Business Manager for each customer list and reference them in your SiteGenesis code.

Migrating to More Secure Passwords

If your site is built on SGPP or SGJC, B2C Commerce recommends that you upgrade the security of your storefront passwords. You can do this by making sure your SiteGenesis password validation code has requirements at least as strong as those set for your customer list or by changing your code to reference the customer list requirements. The recommended password requirements are best practice to protect your customers.

Configure Customer Security Settings

Configure the customer security settings on the Customer List page:
  1. Password minimum length: 8
  2. Enforce letters in passwords (including case sensitivity): true
  3. Enforce numbers in passwords: true
  4. Enforce special characters in passwords: true
  5. Maximum age of passwords: (any value).
These settings are also modifiable via customer list import. Audit and security log entries are written when any security setting changes. If you have existing customer lists, the settings on those customer lists are not changed by this new feature. However, any new customer lists are affected by this change.
Note: Customer lists created by the platform after a db-init use the new requirements by default. If you have not migrated to more secure password validation, you must import the desired settings.
Important:

SiteGenesis does not include the default password requirements in the code for SGPP or SGJC, so you must add it if your storefront site is based on either of these versions of SiteGensis.

Salesforce recommends that you use the configurable, server-side validation available through the B2C Commerce Script API and inform the user about the configured settings. If you do not, your account creation form might error out and not let new accounts be created.

Update Storefront Forms for Pipelines or Controllers

In the /Templates/Default/Account/User/Registration.isml File

Find the passwordconfirm field:
<isinputfield formfield="${pdict.CurrentForms.profile.login.password}" type="password" dynamicname="true" attributes="${autocomplete_attributes}"/>

Add the following code for the field, so that the customer gets feedback on how they need to change their password so that it can pass the validation:

<div class="form-row label-inline form-indent">
  <isinclude template="account/passwordhint"/>
</div>
<isif condition="${!(customer.authenticated && customer.registered)}">
  <isinputfield formfield="${pdict.CurrentForms.profile.login.passwordconfirm}" type="password" dynamicname="true" attributes="${autocomplete_attributes}"/>
    <isif condition="${pdict.passwordnomatch && pdict.passwordnomatch == true}">
      <div class="error">
        <div class="form-caption error-message">${Resource.msg('profile.passwordnomatch','forms',null)}</div>
      </div>
</isif>
Find the field for the profile login password and add validation.
<isinputfield formfield="${pdict.CurrentForms.profile.login.currentpassword}" type="password" dynamicname="true" attributes="${autocomplete_attributes}"/>
  <isif condition="${pdict.passwordisbad && pdict.passwordisbad == 'true'}">
    <div class="error">
      <div class="form-caption error-message">${Resource.msg('profile.currentpasswordnomatch','forms',null)}</div>
    </div>
  </isif>
<isinputfield formfield="${pdict.CurrentForms.profile.login.newpassword}" type="password" dynamicname="true" attributes="${autocomplete_attributes}"/>
<div class="form-row label-inline form-indent">
	<isinclude template="account/passwordhint"/>
</div>
<isinputfield formfield="${pdict.CurrentForms.profile.login.newpasswordconfirm}" type="password" dynamicname="true" attributes="${autocomplete_attributes}"/>
<isif condition="${pdict.passwordnomatch && pdict.passwordnomatch == 'true'}">
  <div class="error">
    <div class="form-caption error-message">${Resource.msg('profile.passwordnomatch','forms',null)}</div>
  </div>
</isif>
<iselse/>

Add a new script to validate the password form field.

The example below is named CustomerPasswordValidator.ds.
/**
 * CustomerPasswordValidator.ds
 *
 * This script gets a submitted password
 *
 * @input customterPasswordFormField : dw.web.FormField the password field being tested
 */
importPackage( dw.web );

exports.validateCustomerPassword = function( customterPasswordFormField : FormField )
{
  var customerMgr = require('dw/customer/CustomerMgr');
  var isAcceptable = customerMgr.isAcceptablePassword(customterPasswordFormField.value);
  return isAcceptable;
}

Updating Pipelines

To update your forms to accommodate the new password requirements, you must edit the following SiteGenesis files in the app_storefront_core cartridge, or the equivalent functionality in your custom code:

If you are using pipelines, in the /pipelines/Account.xml file:

  1. Find the Assign node that is directly before the decision node for logging in a new customer.
  2. Add a passwordisbad property and assign a default value of false to the property.

  3. Find the transition out of the InvalidateFormElement and add an Assign element that includes the passwordisbad property and assigns true as the value.

Updating Controllers

If you are using controllers, in the controllers/Account.js file:

Edit the EditProfile Function

In the editProfile() function, add passwordnomatch and passwordisbad keys with values from the HTTPParameterMap to the data model.
app.getView({
  passwordnomatch: request.httpParameterMap.passwordnomatch,
  passwordisbad: request.httpParameterMap.passwordisbad,
  bctext2: Resource.msg('account.user.registration.editaccount', 'account', null),
  Action: 'edit',
  ContinueURL: URLUtils.https('Account-EditForm')
}).render('account/user/registration');
}

Edit the EditForm Changepassword Action

In the editForm() function, for the handleForm function changepassword action, add a currentPasswordIsValid variable and a newPasswordsMatch variable with a default value of true.

changepassword: function () {
    var isProfileUpdateValid = true;
    var hasEditSucceeded = false;
    var currentPasswordIsValid = true;
    var newPasswordsMatch = true;
    var Customer = app.getModel('Customer');
Set the currentPasswordIsValid variable or newPasswordsMatch variable to false if the customer profile is invalid and true if it's valid and the profile edit succeeds:
if (!Customer.checkUserName()) {
    app.getForm('profile.customer.email').invalidate();
    isProfileUpdateValid = false;
    currentPasswordIsValid = false;
}
if (app.getForm('profile.login.newpassword').value() !== app.getForm('profile.login.newpasswordconfirm').value()) {
    isProfileUpdateValid = false;
    newPasswordsMatch = false;
}

if (isProfileUpdateValid) {
    hasEditSucceeded = Customer.editAccount(app.getForm('profile.customer.email').value(), app.getForm('profile.login.newpassword').value(), app.getForm('profile.login.currentpassword').value(), app.getForm('profile'));
    if (!hasEditSucceeded) {
        currentPasswordIsValid = false;
    }
}
if (isProfileUpdateValid && hasEditSucceeded) {
    response.redirect(URLUtils.https('Account-Show'));
} else {
    if (!currentPasswordIsValid) {
        response.redirect(URLUtils.https('Account-EditProfile', 'invalid', 'true', 'passwordisbad', 'true'));                    
    } else if (!newPasswordsMatch) {
        response.redirect(URLUtils.https('Account-EditProfile', 'invalid', 'true', 'passwordnomatch', 'true'));
    }
}

Edit the EditForm Error Action

In the editForm() function, for the handleForm function error action, add additional error handling if the password doesn't meet the requirements.

error: function () {
    // first, make sure current password is good
    if (app.getForm('profile.login.currentpassword').value()) {
      var Customer = app.getModel('Customer');
      var validPassword = Customer.checkPassword(app.getForm('profile.login.currentpassword').value());
      if (!validPassword) {
        response.redirect(URLUtils.https('Account-EditProfile', 'invalid', 'true', 'passwordisbad', 'true'));
      }
    }
    
    var complianceErrors = false;
  if (app.getForm('profile.login.password').value() && !app.getForm('profile.login.password').object.valid) {
        app.getForm('profile.login.password').invalidate();
        complianceErrors = true;
    }
  if (app.getForm('profile.login.newpassword').value() && !app.getForm('profile.login.newpassword').object.valid) {
        app.getForm('profile.login.newpassword').invalidate();
        complianceErrors = true;
    }
    if (app.getForm('profile.login.newpasswordconfirm').value() && !app.getForm('profile.login.newpasswordconfirm').object.valid) {
        app.getForm('profile.login.newpasswordconfirm').invalidate();
        complianceErrors = true;
    }
    
    if (complianceErrors) {
      response.redirect(URLUtils.https('Account-EditProfile', 'invalid', 'true'));
    }
    
}

Edit the SetNewPasswordForm Error Action

In the setNewPasswordForm() function, for the handleForm function error action, render the setnewpassword page for the form.

error: function () {
    app.getView({
        ContinueURL: URLUtils.https('Account-SetNewPasswordForm')
    }).render('account/password/setnewpassword');
},
In the startRegister() function, add passwordnomatch with a value from the HTTPParameterMap to the data model.
app.getView({
    passwordnomatch: request.httpParameterMap.passwordnomatch,
    ContinueURL: URLUtils.https('Account-RegistrationForm')
}).render('account/user/registration');

Edit the RegistrationForm Error Action

In the registrationForm() function, for the handleForm function error action, validate the profile login password and render the setnewpassword page for the form.

app.getForm('profile').handleAction({
    error: function () {
        // Profile Password field validation
        if (app.getForm('profile.login.password').value() && !app.getForm('profile.login.password').object.valid) {
            app.getForm('profile.login.password').invalidate();
        }
        if (app.getForm('profile.login.passwordconfirm').value() && !app.getForm('profile.login.passwordconfirm').object.valid) {
            app.getForm('profile.login.passwordconfirm').invalidate();
        }
        
        app.getView({
            ContinueURL: URLUtils.https('Account-RegistrationForm')
        }).render('account/user/registration');
    },

Edit the RegistrationForm Confirm Action

In the registrationForm() function, for the handleForm function confirm action, if the profile login isn't valid, set passwordsmatch to false, set passwordnomatch to true, and render the registration page.

confirm: function () {
    var email, emailConfirmation, orderNo, profileValidation, password, passwordConfirmation, existingCustomer, Customer, target;
    var passwordsmatch = true;
...
    if (password !== passwordConfirmation) {
        app.getForm('profile.login.passwordconfirm').invalidate();
        profileValidation = false;
        passwordsmatch = false;
    }
...
    if (!profileValidation) {
        if (!passwordsmatch) {
            app.getView({    
                passwordnomatch: true,
                ContinueURL: URLUtils.https('Account-RegistrationForm')
            }).render('account/user/registration');