LG-528 Use IdV specific phone otp confirmation#2430
Conversation
**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.
**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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
| before_action :set_code, only: %i[show update] | ||
| before_action :set_otp_verification_presenter, only: %i[show update] | ||
|
|
||
| def new |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Moving the code to send the OTP to a service object will also make this a bit easier
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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] } |
There was a problem hiding this comment.
I think we can remove this since the preference was already validated in the previous step.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
What do you think about calling this method call for consistency with other service objects, and for less redundancy with the class name?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
app/services/analytics.rb
Outdated
| 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 |
| end | ||
|
|
||
| def handle_too_many_otp_attempts | ||
| analytics.track_event(Analytics::IDV_PHONE_CONFIRMATION_OTP_RATE_LIMIT_ATTEMTS) |
app/services/analytics.rb
Outdated
| 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 |
app/services/analytics.rb
Outdated
| 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 |
| module Idv | ||
| # Ignore instance variable assumption on @user_locked_out :reek:InstanceVariableAssumption | ||
| class SendPhoneConfirmationOtp | ||
| attr_accessor :user, :idv_session, :locale |
There was a problem hiding this comment.
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?
|
@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 |
There was a problem hiding this comment.
It looks like this has changed the UI a bit compared to what's on master. Here is what's on master:

And here is what's on this PR:

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?
| @otp_delivery_selection_form ||= Idv::OtpDeliveryMethodForm.new | ||
| end | ||
|
|
||
| def invalid_phone_number(exception) |
There was a problem hiding this comment.
Thanks! I think we also want to copy over the capture_analytics_for_exception(exception) method to track these errors.
monfresh
left a comment
There was a problem hiding this comment.
LGTM % twilio error analytics
|
if it matters, this is an issue: https://cm-jira.usa.gov/browse/LG-618
i'd love to remove that *Get another code* button and make it
hierarchically equal to *Use a different number*
plus, it doesnt look like a game of tetris. 🎉
…On Fri, Aug 31, 2018 at 8:25 PM, Moncef Belyamani ***@***.***> wrote:
***@***.**** approved this pull request.
LGTM % twilio error analytics
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#2430 (review)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AL-58a_sO00EzIwTB_hHKWw8hGp5UISSks5uWdQVgaJpZM4V8-va>
.
|
|
@monfresh: I just updated against master and added the twilio error analytics events. Mind taking another look? |
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.
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.

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
authenticated, make sure to add
before_action :confirm_two_factor_authenticatedas 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
Routes
state or result in destructive behavior).
Session
user_sessionhelperinstead of the
sessionhelper so the data does not persist beyond the user'ssession.
Testing
and invalid inputs.