Skip to content

Lg 6167 second mfa for sms#6278

Merged
SammySteiner merged 42 commits intomainfrom
LG-6167-second-mfa-for-sms
May 9, 2022
Merged

Lg 6167 second mfa for sms#6278
SammySteiner merged 42 commits intomainfrom
LG-6167-second-mfa-for-sms

Conversation

@SammySteiner
Copy link
Contributor

Why

Phones are considered a restricted mfa method and are not considered as secure as other mfa methods. Therefore, we want to require all new users to setup their accounts with an additional mfa method if they select phone as their only mfa method. Ultimately, we'd like to force all users to have an authentication method that is not phone.

How

Creating an mfa checkbox component with validation and js to remove error when they select an additional mfa method.

Issues

Should this component be brought into the design system?

I still need to create a test to confirm the error disappears when another option is selected or phone is unselected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom element isn't really designed to be used independent of the ViewComponent implementation. An effect of this is there's no guaranteed it'd actually be defined in the first place. It might be currently working incidentally based on how Webpack is splitting JavaScript bundles, but this could break in the future.

Based on how this is implemented, I wonder if we could take advantage of the fact that ValidatedField is basically a wrapper for a SimpleForm input field, and instead implement this as a custom input, or a wrapper configuration like we do with other checkboxes and radios.

This would also help avoid reimplementing error alert messages, which ValidatedField is already meant to be handling to create accessible linking between the field and the error message (not present here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback, I think I understood your comment, but let me know if I'm missing something.

I switched it to a div with a class for the scss to hit it.

I initially tried to use ValidatedField for this but ran into a couple issues.

  1. The phone input is only invalid if no other input is selected, I wasn't able to figure out how to have validated field check other inputs to determine validation.
  2. ValidatedField requires a :name to wire everything up properly. That name must be an attribute on the model, in this case the two_factor_options_form. However the form has a selection attribute that is an array, and name is only compatible with strings. I spent about a day trying to figure out how to convert from two_factor_options_form[:selection][] (or two_factor_options_form[:selection][:phone], etc.) to a usable :name.
  3. Refactoring two_factor_options_form to have the appropriate attributes so that I can use validated field adds a bunch of extra work for a requirement with a lot of urgency behind it and doesn't clear up the first issue for me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty specific to the one form, and not really a reusable component. Also, ValidatedField is already implementing quite a bit around field validation that seems to be repeated here.

Could we target a class or element name instead? Also, if we apply validity to the input using input.setCustomValidity, the default behavior of ValidatedField might be able to automatically handle parts of showing / hiding error messages (see PhoneInputComponent for example), as well as do some client-side validation, so we don't have to do a full server form submission to validate the input.

const checkboxes = document.querySelectorAll('.some-checkbox-class');
const phoneCheckbox = checkboxes.find((checkbox) => checkbox.name === 'phone');
checkboxes.forEach((checkbox) => {
  checkbox.addEventListener('input', () => {
    const checked = checkboxes
      .filter((checkbox) => checkbox.checked)
      .map((checkbox) => checkbox.name);
    const hasPhone = checked.includes('phone');
    const hasOther = hasPhone && checked.length > 1;
    phoneCheckbox.setCustomValidity(hasPhone && !hasOther ? 'some error' : '');
  });
});

Copy link
Contributor Author

@SammySteiner SammySteiner May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't reuse validated field for the reasons I mentioned in my other comment above.

I'm targeting a pretty specific label name to get a list of the labels I want to target already. Not sure what using the querySelectorAll would add.

One note is that the intended behavior is for the error to clear when the user selects another input (or unselects phone input), but not come back if they unselect the other input (or reselect just the phone input). It would only show again if they tried to resubmit with just phone selected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the key thing for me here is that a ViewComponent implies some amount of reusability, or at least leverages the separation of the class from the view for managing a complex view model. I don't really see either of those here, so I'd almost wonder if a partial makes more sense, located somewhere alongside other MFA form views.

Calling document.getElementsByName('two_factor_options_form[selection][]') will only work for this one specific form, so the idea with querySelectorAll was to at least make it generic enough if that form name were to change, or if the component/partial were to be used anywhere else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I turned it into a partial since I think this component and validation requirement is super specific to this form and I move the js into a javascript/pack.

@SammySteiner SammySteiner force-pushed the LG-6167-second-mfa-for-sms branch from 42ff7ff to e22e96a Compare May 2, 2022 19:22
Comment on lines +19 to +21
document
.getElementsByClassName('usa-checkbox__label-description')[4]
.appendChild(document.createTextNode('<YOUR_CONTENT>'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this temporary debugging code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I was having trouble getting the js to run in the feature test and I was confirming that was the issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been removed.

SammySteiner and others added 7 commits May 3, 2022 15:50
Co-authored-by: Zach Margolis <zachmargolis@users.noreply.github.com>
Co-authored-by: Zach Margolis <zachmargolis@users.noreply.github.com>
@@ -0,0 +1,35 @@
require 'rails_helper'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component doesn't exist anymore, should be adapted to a partial spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for catching that.

<%= render 'shared/cancel', link: destroy_user_session_path %>

<%= javascript_packs_tag_once('webauthn-unhide') %>
<%= javascript_packs_tag_once('mfa_selection_component') %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the script applies to partial content, probably makes sense for the javascript_packs_tag_once call to occur inside the partial.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I tried to put it there initially, but it wasn't working. Turns out I had to restart the application for that to work.

end

def phone_selected?
return false if !IdentityConfig.store.select_multiple_mfa_options
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name and way this method is called wouldn't imply it depends on a feature flag. I suppose if it's temporary, it's not a big deal, but alternative we could name the method accordingly, or update the if: condition to apply multiple conditions.

def enforce_multiple_mfa_with_phone?
validates :selection,
          length: { minimum: 2, message: 'phone' },
          if: [:multiple_mfa_options_enabled?, :phone_selected?]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, that's cleaner.


def phone_selected?
return false if !IdentityConfig.store.select_multiple_mfa_options
selection.include?('phone') || selection.include?('voice') || selection.include?('sms')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorta confused by what selections this class is expecting to receive. In practice, we only offer the user a single "phone" option when configuring MFA, so when would it be "voice" or "sms"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's for editing the users's selection later. After they change their preference, we still need to enforce the requirement for alternatives to phone, sms, and voice.

</div>
<% end %>
<% if option.type === "phone" && flash[:phone_error] %>
<%= render AlertComponent.new(type: :error, id: 'phone_error', class: 'margin-bottom-1 margin-top-neg-1 checkbox__alert', message: flash[:phone_error]) %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can tell, how we're using this alert is as an error message, so seems we can just use the default styling, rather than creating new variant of alert.

https://design.login.gov/components/form-fields/#form-validation-errors

Suggested change
<%= render AlertComponent.new(type: :error, id: 'phone_error', class: 'margin-bottom-1 margin-top-neg-1 checkbox__alert', message: flash[:phone_error]) %>
<div id="phone_error" class="usa-error-message margin-bottom-1 margin-top-neg-1" role="alert">
<%= flash[:phone_error] %>
</div>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. I removed the unnecessary sass as well

Copy link
Contributor

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing tag is main thing to be fixed, but otherwise LGTM 👍

@@ -0,0 +1,17 @@
function clearPhoneSelectionError() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're TypeScript-first now, can this be a .ts file? Should hopefully just be a matter of renaming.

SammySteiner and others added 4 commits May 5, 2022 13:42
…_component.html.erb

Co-authored-by: Andrew Duthie <andrew.duthie@gsa.gov>
…n_component.html.erb_spec.rb

Co-authored-by: Andrew Duthie <andrew.duthie@gsa.gov>
Comment on lines -136 to +137
phone_info: Receive a secure code by (SMS) text or phone call to your device.
phone_info_html: Receive a secure code by (SMS) text or phone call. <strong>You
need to select another method in addition to this one.</strong>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is feature-flagged, do we need to make sure we only include the bolded message if the feature flag is enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, that is a really good point. Checking with content.

…_component.html.erb

Co-authored-by: Andrew Duthie <andrew.duthie@gsa.gov>
@SammySteiner SammySteiner merged commit 0f0f117 into main May 9, 2022
@SammySteiner SammySteiner deleted the LG-6167-second-mfa-for-sms branch May 9, 2022 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants