4

We are using a custom policy with Azure B2C, and we are using email verification during sign-up to reduce spam accounts.

As many people know, B2C's default email verification process is a little awkward because there are multiple steps the user is expected to complete while on the same sign-up form:

  1. Enter email.
  2. Send verification code.
  3. Enter verification code into text box; re-send the code; or change the email address.
  4. Click button to verify code.
  5. Fill out the rest of the sign-up fields.
  6. Click button to complete sign-up

...all in ONE FORM. Basically, the cognitive load of the default form layout is too much for regular consumers.

To remedy this, we're using the split email verification and sign-up user journey provided by the CIAM sample journeys project. With that process, things are better, but still not perfect -- there's not enough guidance on each screen, user controls like buttons and fields disappear and re-appear during requests, and users find the "Continue" and "Cancel" buttons confusing. We still want to simplify this further and make the UI less "twitchy".

At a minimum, we need a way to apply different CSS styling depending upon which stage of the process the user is in. For example, we want to hide the "Continue" button if the user is on the steps where they're entering their email address or providing the verification code; and we want to override B2C's default behavior of hiding controls during AJAX/XHR requests. Unfortunately, it looks like B2C's JavaScript is a closed system that just adds and removes inline styling from all the UI controls on the page to accomplish what it wants. Making things worse, there's seemingly no CSS class or data attribute in the page to tell us what "step" of the process the widget is in.

Is there any way to hook into the B2C JavaScript so we can react to its logic? Ideally, we'd be able to set a "mode" data attribute on our page that we could then reference in stylesheets.

GuyPaddock
  • 2,233
  • 2
  • 23
  • 27
  • Why the downvote? I would appreciate it if those who are against the question and the approach would explain what the alternative is, as the B2C verification flow is self-contained within a single DisplayControl. Were it the case that each of the pieces of email verification were a distinct DisplayControl and separate claims, I'd be able to do this in a much more robust way. – GuyPaddock Jul 18 '21 at 15:10
  • 1
    You can call the technical profiles directly from the user journey, eg generate code + send code. Then display a selfAsserted page to collect a code (with message saying code was sent), and then when user enters and submits the code, call the verify otp technical profile from the user journey. Only downside of this approach is no way to resend a code. There's many ways to skin this cat. – Jas Suri - MSFT Jul 19 '21 at 09:22
  • 1
    If using the display Control, you can also hide a lot of the buttons using CSS, and have it all automated on one form. Eg user lands on page, code is auto sent by using JS click(), and similarly to automate verify and continue buttons with JS click on 6char detection, and object mutator listener to hit continue button for the user. Try wemena.com website as an example. – Jas Suri - MSFT Jul 19 '21 at 09:23
  • I'll update my answer if we end up going with the custom display control. The nice thing with our current approach is that we're not depending upon the presentation layer to tell us what state we're in (i.e. no object mutators to rely on) and we're able to smoothly crossfade/transition between steps, while allowing the user to still edit their email address to transition back to the very first step. We don't automate clicking "Continue" in case the user realizes too late that they verified the wrong email address (e.g. work vs. personal). – GuyPaddock Jul 20 '21 at 00:25

1 Answers1

5

To accomplish this, we ended up using jQuery to listen to the actual XHR requests that B2C sends out, and then binding to their responses to determine what mode we're in. There's a risk that Microsoft could change how the API works and completely break this implementation, but it's more reliable than anything else I could come up with.

Prior to that approach, we tried looking for CSS classes that B2C adds to fields that have invalid values, as well as for basing the mode on what UI controls are or are not visible, but those approaches were extremely fragile. Either our code was getting invoked before Azure's (so the classes in the page weren't yet up to date), or our code didn't handle all of the error cases we needed to respond to, for when the email address is invalid, the user has tried too many times, etc.

Our approach takes the form of JavaScript and CSS embedded in the custom HTML template for the flow. We've also made sure to enable JavaScript in the User Journey.

Here's the code we're using:

const STATUS_OK        = 0,
      STATUS_BAD_EMAIL = 2;

const REQUEST_TYPE_CODE_REQUEST    = 'VERIFICATION_REQUEST',
      REQUEST_TYPE_CODE_VALIDATION = 'VALIDATION_REQUEST';

let $body              = $('body'),
    $emailField        = $('#email');
    $changeEmailButton = $('#email_ver_but_edit');

/**
 * Switches the mode of the form into requesting a confirmation code.
 */
function switchToRequestConfirmationCodeMode() {
  $body.attr('data-mode', 'send-code');
}

/**
 * Switches the mode of the form into validating a confirmation code.
 */
function switchToVerifyCodeMode() {
  $body.attr('data-mode', 'verify-code');
}

/**
 * Switches the mode of the form into completing sign-up.
 */
function switchToEmailVerifiedMode() {
  $body.attr('data-mode', 'email-verified');
}

/**
 * Callback invoked when a verification code is being requested via XHR.
 *
 * @param {jqXHR} jqXhr
 *   The XHR being sent out to get a verification code sent out.
 */
function handleSendVerificationCodeRequest(jqXhr) {
  let controlsToDisableDuringRequest = [
    '#email_ver_but_send',
  ];

  disableControlsDuringRequest(controlsToDisableDuringRequest, jqXhr);

  jqXhr.done((data) => {
    if ((data.status === "200") && (data.result === STATUS_OK)) {
      // Code sent successfully.
      switchToVerifyCodeMode();
    }
  });
}

/**
 * Callback invoked when a verification code is being validated via XHR.
 *
 * @param {jqXHR} jqXhr
 *   The XHR being sent out to validate a verification code entered by the
 *   user.
 */
function handleValidateCodeRequest(jqXhr) {
  let controlsToDisableDuringRequest = [
    '#email_ver_input',
    '#email_ver_but_verify',
    '#email_ver_but_resend',
  ];

  disableControlsDuringRequest(controlsToDisableDuringRequest, jqXhr);

  jqXhr.done((data) => {
    if (data.status === "200") {
      if (data.result === STATUS_OK) {
        // Code was accepted.
        switchToEmailVerifiedMode();
      } else if (data.result === STATUS_BAD_EMAIL) {
        // Too many attempts; switch back to requesting a new code.
        switchToRequestConfirmationCodeMode();
      }
    }
  });
}

/**
 * Disables the given controls during the provided XHR.
 *
 * @param {string[]} controls
 *   A list of jQuery selectors for the controls to disable during the
 *   XHR.
 * @param {jqXHR} jqXhr
 *   The XHR during which controls should be disabled.
 */
function disableControlsDuringRequest(controls, jqXhr) {
  let $controls = $(controls.join(','));

  // Disable controls during the XHR.
  $controls.prop("disabled", true);

  // Release the controls after the XHR, even on failure.
  jqXhr.always(() => {
    $controls.prop("disabled", false);
  })
}

// At present, there doesn't seem to be a way to be notified about/detect
// whether the user should be prompted for a verification code. But, for
// our styling needs, we need a way to determine what "mode" the form is
// in.
//
// To accomplish this, we listen for when B2C is sending out a
// verification code, and then toggle the "mode" of the form only if the
// request is successful. That way, we do not toggle the form if there are
// client-side or server-side validation errors with the email address
// that the user provided.
$(document).ajaxSend(function (e, jqXhr, settings) {
  if (settings.contentType.startsWith('application/x-www-form-urlencoded')) {
    let parsedData  = new URLSearchParams(settings.data),
        requestType = parsedData.get('request_type')

    if (requestType === REQUEST_TYPE_CODE_REQUEST) {
      handleSendVerificationCodeRequest(jqXhr);
    }
    else if (requestType === REQUEST_TYPE_CODE_VALIDATION) {
      handleValidateCodeRequest(jqXhr);
    }
  }
});

// Reset the mode of the form if the user changes the email address entry.
$emailField.keydown(() => switchToRequestConfirmationCodeMode());
$emailField.change(() => switchToRequestConfirmationCodeMode());

// Reset the mode of the form if the user decides to change emails after
// verification.
$changeEmailButton.click(() => switchToRequestConfirmationCodeMode());

switchToRequestConfirmationCodeMode();

Then our CSS looks like this:

/* Simplify UX by hiding buttons that aren't relevant. */
body[data-mode="send-code"] #continue,
body[data-mode="verify-code"] #continue {
  display: none;
}

/*
  Override B2C's inline style that sets "display: inline" on instructions
  and field labels.
 */
body[data-mode="send-code"] #email_intro,
body[data-mode="verify-code"] #email_info,
body[data-mode="verify-code"] #attributeList label,
body[data-mode="email-verified"] #email_success {
  display: block !important;
}

/*
  Override B2C's logic that hides buttons and controls during requests.
 */
body[data-mode="send-code"] #email_ver_but_send,
body[data-mode="verify-code"] #email_ver_input,
body[data-mode="verify-code"] #email_ver_but_verify,
body[data-mode="verify-code"] #email_ver_but_resend {
  display: block !important;
}

/*
  BUGBUG: This button serves no function but can randomly appear on the
  page if the user reloads it during sign-up.
 */
#email_ver_but_default {
  display: none !important;
}
GuyPaddock
  • 2,233
  • 2
  • 23
  • 27