Skip to content

LG-528 Use IdV specific phone otp confirmation#2430

Merged
jmhooper merged 26 commits intomasterfrom
jmhooper-idv-specific-otp-verification
Sep 5, 2018
Merged

LG-528 Use IdV specific phone otp confirmation#2430
jmhooper merged 26 commits intomasterfrom
jmhooper-idv-specific-otp-verification

Conversation

@jmhooper
Copy link
Contributor

@jmhooper jmhooper commented Aug 14, 2018

Why: The otp verification controller serves multiple puprose, phone
confirmation during sign up, sign in, edit phone, and idv. As a result
the logic is very complex and error prone. This commit creates a
separate controller specific for the IdV phone OTP confirmation since it
differs a good bit from the other use cases for phone confirmation.

This commit will need to be followed up by another to remove the old
code. I didn't do that here because the diff is already pretty
substantial without it.

Hi! Before submitting your PR for review, and/or before merging it, please
go through the checklists below. These represent the more critical elements
of our code quality guidelines. The rest of the list can be found in
CONTRIBUTING.md

Controllers

  • When adding a new controller that requires the user to be fully
    authenticated, make sure to add before_action :confirm_two_factor_authenticated
    as the first callback.

Database

  • Unsafe migrations are implemented over several PRs and over several
    deploys to avoid production errors. The strong_migrations gem
    will warn you about unsafe migrations and has great step-by-step instructions
    for various scenarios.

  • Indexes were added if necessary. This article provides a good overview
    of indexes in Rails.

  • Verified that the changes don't affect other apps (such as the dashboard)

  • When relevant, a rake task is created to populate the necessary DB columns
    in the various environments right before deploying, taking into account the users
    who might not have interacted with this column yet (such as users who have not
    set a password yet)

  • Migrations against existing tables have been tested against a copy of the
    production database. See LG-228 Make migrations safer and more resilient #2127 for an example when a migration caused deployment
    issues. In that case, all the migration did was add a new column and an index to
    the Users table, which might seem innocuous.

Encryption

  • The changes are compatible with data that was encrypted with the old code.

Routes

  • GET requests are not vulnerable to CSRF attacks (i.e. they don't change
    state or result in destructive behavior).

Session

  • When adding user data to the session, use the user_session helper
    instead of the session helper so the data does not persist beyond the user's
    session.

Testing

  • Tests added for this feature/bug
  • Prefer feature/integration specs over controller specs
  • When adding code that reads data, write tests for nil values, empty strings,
    and invalid inputs.

**Why**: The otp verification controller serves multiple puprose, phone
confirmation during sign up, sign in, edit phone, and idv. As a result
the logic is very complex and error prone. This commit creates a
separate controller specific for the IdV phone OTP confirmation since it
differs a good bit from the other use cases for phone confirmation.

This commit will need to be followed up by another to remove the old
code. I didn't do that here because the diff is already pretty
substantial without it.
jmhooper added a commit that referenced this pull request Aug 15, 2018
**Why**: We started using separate otp verification controllers for MFA
and IdV in #2430. This commit is a follow-on to that to remove the code
that supported IdV in the MFA controller.
module Idv
class OtpVerificationController < ApplicationController
include IdvSession
include TwoFactorAuthenticatable
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 only need a few things from TwoFactorAuthenticatable, what do you think about duplicating them here for the moment, and then once we see where else they are used, we can extract them into separate concerns or service objects?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, the thing I'm using them for displaying the error screens, which is very tricky w/o the concern. That goes pretty deep and I didn't wanna be ambitious. We can see how it looks though?

@@ -13,11 +13,8 @@ def new; end
def create
result = @otp_delivery_selection_form.submit(otp_delivery_selection_params)
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for starting this separation. One thing I noticed that I hadn't before is that we probably shouldn't be using the OtpDeliverySelectionForm here. Most of the logic only applies to 2FA in the authentication context. For example, we don't want to risk changing the user's OTP delivery preference, and the check for unsupported_phone? is redundant because the phone was already validated in the previous step in IdV::PhoneForm.

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 can build out a IdV specific form here, though I think we may want to open another PR for that to keep this one small.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

before_action :set_code, only: %i[show update]
before_action :set_otp_verification_presenter, only: %i[show update]

def new
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about creating a separate controller for sending the OTP and using create as the method? Seeing new for a non-user facing page keeps tripping me up. Ideally, sending the OTP would be a POST request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It gets a touch tricky since you can't redirect via a POST request. That said, we can change it up so the delivery method selection controller sends the initial OTP and this controller is used for resending. Didn't do that here since a GET request is the status quo and I didn't wanna take on too much at once, but I can if we think it is important.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moving the code to send the OTP to a service object will also make this a bit easier

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, that's what I was thinking. I'll leave it up to you. Eventually, my hope is that we replace the "get another code" link with a link to go back to the delivery method page and choose another way to send the OTP (or try the same method again).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'm thinking a service object to send the code that we use in the otp delivery preference controller and then in a new OtpResendController will come out a bit neater. Then the OtpVerification controller can focus on verifying OTPs.

@@ -0,0 +1,106 @@
module Idv
# :reek:InstanceVariableAssumption
class SendPhoneConfirmationOtpForm
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is not a user-facing form in the traditional sense, what do you think about making this a service object and dropping the Form from the name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, agree. This started it's life as something that made more sense as a form (it took the delivery method as a param like the form on the send_otp endpoint does) but is pretty far removed from that now.


attr_accessor :user, :idv_session, :locale

validates :otp_delivery_preference, inclusion: { in: %i[sms voice] }
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 we can remove this since the preference was already validated in the previous step.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, there's some slippery spots in the code that result from trying to validate this. I'll pull them out. I'll probs raise for an invalid delivery preference instead.

else
redirect_to idv_otp_delivery_method_url
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

We will also need to rescue any Twilio errors, like in the main 2FA controller in case the user enters a phone Twilio doesn't like, and so we can display an appropriate message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, interesting. In the main 2FA controllers, the user is shown the errors on the phone entry screen. Here is would appear on the OTP delivery method selection screen. I wonder what the best way to deal with that is...

@@ -0,0 +1,8 @@
module Idv
class PhoneConfirmationOtpGenerator
def self.generate_otp
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about calling this method call for consistency with other service objects, and for less redundancy with the class name?

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 don't mind

Copy link
Contributor

Choose a reason for hiding this comment

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

Or maybe use a verb for the class name: GeneratePhoneConfirmationOtp?

parsed_phone = Phonelib.parse(phone)
{
otp_delivery_preference: otp_delivery_preference,
country_code: parsed_phone.country_code,
Copy link
Contributor

Choose a reason for hiding this comment

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

These should all be US numbers, but just in case, can we use country instead of country_code? The former will return the 2-letter abbreviation of the country, whereas the latter will be the country dialing code, and since non-US countries also use 1, it won't be easy to tell if it was a US number or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, good call

IDV_PHONE_CONFIRMATION_VENDOR = 'IdV: phone confirmation vendor'.freeze
IDV_PHONE_CONFIRMATION_OTP_SENT = 'IdV: phone confirmation otp sent'.freeze
IDV_PHONE_CONFIRMATION_OTP_SUBMITTED = 'IdV: phone confirmation otp submitted'.freeze
IDV_PHONE_CONFIRMATION_OTP_VISIT = 'IdV: phone confirmation otp visitted'.freeze
Copy link
Contributor

Choose a reason for hiding this comment

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

s/visitted/visited

end

def handle_too_many_otp_attempts
analytics.track_event(Analytics::IDV_PHONE_CONFIRMATION_OTP_RATE_LIMIT_ATTEMTS)
Copy link
Contributor

Choose a reason for hiding this comment

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

s/ATTEMTS/ATTEMPTS

IDV_JURISDICTION_FORM = 'IdV: jurisdiction form submitted'.freeze
IDV_PHONE_CONFIRMATION_FORM = 'IdV: phone confirmation form'.freeze
IDV_PHONE_CONFIRMATION_VENDOR = 'IdV: phone confirmation vendor'.freeze
IDV_PHONE_CONFIRMATION_OTP_RATE_LIMIT_ATTEMPTS = 'Idv: Phone OTP attempts rate limitted'.freeze
Copy link
Contributor

Choose a reason for hiding this comment

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

s/limitted/limited

IDV_PHONE_CONFIRMATION_VENDOR = 'IdV: phone confirmation vendor'.freeze
IDV_PHONE_CONFIRMATION_OTP_RATE_LIMIT_ATTEMPTS = 'Idv: Phone OTP attempts rate limitted'.freeze
IDV_PHONE_CONFIRMATION_OTP_RATE_LIMIT_LOCKED_OUT = 'Idv: Phone OTP rate limited user'.freeze
IDV_PHONE_CONFIRMATION_OTP_RATE_LIMIT_SENDS = 'Idv: Phone OTP sends rate limitted'.freeze
Copy link
Contributor

Choose a reason for hiding this comment

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

s/limitted/limited

module Idv
# Ignore instance variable assumption on @user_locked_out :reek:InstanceVariableAssumption
class SendPhoneConfirmationOtp
attr_accessor :user, :idv_session, :locale
Copy link
Contributor

Choose a reason for hiding this comment

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

Since these methods are not meant to be writable outside of the class, what do you think about replacing this with a private attr_reader, and using @user = user etc. in the initialize method?

@jmhooper
Copy link
Contributor Author

@monfresh: I just pushed some commits that I think address everything you've mentioned


.mt3.border-top
.mt1
= link_to t('links.cancel'), idv_cancel_path
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this has changed the UI a bit compared to what's on master. Here is what's on master:
screen shot 2018-08-31 at 2 50 36 pm

And here is what's on this PR:
screen shot 2018-08-31 at 2 33 14 pm

Perhaps we should ask @donjo and @rtwell for design feedback?

Should we keep the "entered the wrong number" text and link and add the new "choose another option" under that?

@jmhooper
Copy link
Contributor Author

I just pushed a commit to put the view back the way it was

image

@otp_delivery_selection_form ||= Idv::OtpDeliveryMethodForm.new
end

def invalid_phone_number(exception)
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks! I think we also want to copy over the capture_analytics_for_exception(exception) method to track these errors.

monfresh
monfresh previously approved these changes Sep 1, 2018
Copy link
Contributor

@monfresh monfresh left a comment

Choose a reason for hiding this comment

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

LGTM % twilio error analytics

@rtwell
Copy link

rtwell commented Sep 4, 2018 via email

@jmhooper
Copy link
Contributor Author

jmhooper commented Sep 4, 2018

@monfresh: I just updated against master and added the twilio error analytics events. Mind taking another look?

Copy link
Contributor

@monfresh monfresh left a comment

Choose a reason for hiding this comment

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

LGTM

@jmhooper jmhooper merged commit 635df15 into master Sep 5, 2018
jmhooper added a commit that referenced this pull request Sep 5, 2018
Why: We started using separate otp verification controllers for MFA
and IdV in #2430. This commit is a follow-on to that to remove the code
that supported IdV in the MFA controller.
jmhooper added a commit that referenced this pull request Sep 5, 2018
Why: We started using separate otp verification controllers for MFA
and IdV in #2430. This commit is a follow-on to that to remove the code
that supported IdV in the MFA controller.
@jmhooper jmhooper deleted the jmhooper-idv-specific-otp-verification branch February 15, 2019 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants