-
Notifications
You must be signed in to change notification settings - Fork 166
Handle Twilio errors more gracefully #2308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ class TwoFactorAuthenticationController < ApplicationController | |
|
|
||
| before_action :check_remember_device_preference | ||
|
|
||
| # rubocop:disable Metrics/MethodLength | ||
| def show | ||
| if current_user.piv_cac_enabled? | ||
| redirect_to login_two_factor_piv_cac_url | ||
|
|
@@ -14,19 +15,23 @@ def show | |
| else | ||
| redirect_to two_factor_options_url | ||
| end | ||
| rescue Twilio::REST::RestError, PhoneVerification::VerifyError => exception | ||
| invalid_phone_number(exception, action: 'show') | ||
| end | ||
| # rubocop:enable Metrics/MethodLength | ||
|
|
||
| def send_code | ||
| result = otp_delivery_selection_form.submit(delivery_params) | ||
| analytics.track_event(Analytics::OTP_DELIVERY_SELECTION, result.to_h) | ||
|
|
||
| if result.success? | ||
| handle_valid_otp_delivery_preference(user_selected_otp_delivery_preference) | ||
| update_otp_delivery_preference_if_needed | ||
| else | ||
| handle_invalid_otp_delivery_preference(result) | ||
| end | ||
| rescue Twilio::REST::RestError, PhoneVerification::VerifyError => exception | ||
| invalid_phone_number(exception) | ||
| invalid_phone_number(exception, action: 'send_code') | ||
| end | ||
|
|
||
| private | ||
|
|
@@ -44,19 +49,56 @@ def validate_otp_delivery_preference_and_send_code | |
| end | ||
| end | ||
|
|
||
| def update_otp_delivery_preference_if_needed | ||
| OtpDeliveryPreferenceUpdater.new( | ||
| user: current_user, | ||
| preference: delivery_params[:otp_delivery_preference], | ||
| context: otp_delivery_selection_form.context | ||
| ).call | ||
| end | ||
|
|
||
| def handle_invalid_otp_delivery_preference(result) | ||
| flash[:error] = result.errors[:phone].first | ||
| preference = current_user.otp_delivery_preference | ||
| redirect_to login_two_factor_url(otp_delivery_preference: preference) | ||
| end | ||
|
|
||
| def invalid_phone_number(exception) | ||
| code = exception.code | ||
| analytics.track_event( | ||
| Analytics::TWILIO_PHONE_VALIDATION_FAILED, error: exception.message, code: code | ||
| def invalid_phone_number(exception, action:) | ||
| capture_analytics_for_exception(exception) | ||
|
|
||
| if action == 'show' | ||
| redirect_to_otp_verification_with_error | ||
| else | ||
| flash[:error] = error_message(exception.code) | ||
| redirect_back(fallback_location: account_url) | ||
| end | ||
| end | ||
|
|
||
| def redirect_to_otp_verification_with_error | ||
| flash[:error] = t('errors.messages.phone_unsupported') | ||
| redirect_to login_two_factor_url( | ||
| otp_delivery_preference: current_user.otp_delivery_preference, reauthn: reauthn? | ||
| ) | ||
| flash[:error] = error_message(code) | ||
| redirect_back(fallback_location: account_url) | ||
| end | ||
|
|
||
| # rubocop:disable Metrics/MethodLength | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is a bit long. What would you think about moving the top part into a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I can do that. Ideally, we'd get rid of the background jobs for voice/sms and use a Form Object instead, and we can move all this logic inside there, and avoid using exceptions for control flow. |
||
| def capture_analytics_for_exception(exception) | ||
| attributes = { | ||
| error: exception.message, | ||
| code: exception.code, | ||
| context: context, | ||
| country: parsed_phone.country, | ||
| } | ||
| if exception.is_a?(PhoneVerification::VerifyError) | ||
| attributes[:status] = exception.status | ||
| attributes[:response] = exception.response | ||
| end | ||
| analytics.track_event(Analytics::TWILIO_PHONE_VALIDATION_FAILED, attributes) | ||
| end | ||
| # rubocop:enable Metrics/MethodLength | ||
|
|
||
| def parsed_phone | ||
| @parsed_phone ||= Phonelib.parse(phone_to_deliver_to) | ||
| end | ||
|
|
||
| def error_message(code) | ||
|
|
@@ -68,7 +110,9 @@ def twilio_errors | |
| end | ||
|
|
||
| def otp_delivery_selection_form | ||
| OtpDeliverySelectionForm.new(current_user, phone_to_deliver_to, context) | ||
| @otp_delivery_selection_form ||= OtpDeliverySelectionForm.new( | ||
| current_user, phone_to_deliver_to, context | ||
| ) | ||
| end | ||
|
|
||
| def reauthn_param | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,4 +6,8 @@ def uuid | |
| def second_factor_locked_at | ||
| nil | ||
| end | ||
|
|
||
| def phone | ||
| nil | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| class OtpDeliveryPreferenceUpdater | ||
| def initialize(user:, preference:, context:) | ||
| @user = user | ||
| @preference = preference | ||
| @context = context | ||
| end | ||
|
|
||
| def call | ||
| user_attributes = { otp_delivery_preference: preference } | ||
| UpdateUser.new(user: user, attributes: user_attributes).call if should_update_user? | ||
| end | ||
|
|
||
| private | ||
|
|
||
| attr_reader :user, :preference, :context | ||
|
|
||
| def should_update_user? | ||
| return false unless user | ||
| otp_delivery_preference_changed? && !idv_context? | ||
| end | ||
|
|
||
| def otp_delivery_preference_changed? | ||
| preference != user.otp_delivery_preference | ||
| end | ||
|
|
||
| def idv_context? | ||
| context == 'idv' | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,8 @@ Referer: <%= @request.referer %> | |
| <% session['user_return_to'] = session['user_return_to']&.split('?')&.first %> | ||
| Session: <%= session %> | ||
|
|
||
| User UUID: <%= @kontroller.analytics_user.uuid %> | ||
| <% user = @kontroller.analytics_user %> | ||
| User UUID: <%= user.uuid %> | ||
| User's Country (based on phone): <%= Phonelib.parse(user.phone).country %> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kontroller.analytics_user may be nil, so in some circumstances the exception notifier itself is exploding here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is a controller, analytics_user cannot be
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There won't be a controller if rails dies before serving a request. Encountered this in my environment.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually that message pretty strongly points to analytics_user being nil when the controller is present.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case triggered by a redis connection failure.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see it now in the logs. Looks like it's happening before it gets to the controller. I will fix.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Thanks, no rush |
||
|
|
||
| Visitor ID: <%= @request.cookies['ahoy_visitor'] %> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes a lot more sense here