Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/images/webauthn-mismatch/webauthn-checked.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/assets/images/webauthn-mismatch/webauthn-unchecked.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions app/controllers/concerns/mfa_deletion_concern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module MfaDeletionConcern
include RememberDeviceConcern

def handle_successful_mfa_deletion(event_type:)
create_user_event(event_type)
revoke_remember_device(current_user)
event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user)
PushNotification::HttpPush.deliver(event)
nil
end
end
82 changes: 82 additions & 0 deletions app/controllers/users/webauthn_setup_mismatch_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

module Users
class WebauthnSetupMismatchController < ApplicationController
include MfaSetupConcern
include MfaDeletionConcern
include SecureHeadersConcern
include ReauthenticationRequiredConcern

before_action :confirm_user_authenticated_for_2fa_setup
before_action :apply_secure_headers_override
before_action :confirm_recently_authenticated_2fa
before_action :validate_session_mismatch_id

def show
analytics.webauthn_setup_mismatch_visited(
configuration_id: configuration.id,
platform_authenticator: platform_authenticator?,
)

@presenter = WebauthnSetupMismatchPresenter.new(configuration:)
end

def update
analytics.webauthn_setup_mismatch_submitted(
configuration_id: configuration.id,
platform_authenticator: platform_authenticator?,
confirmed_mismatch: true,
)

redirect_to next_setup_path || after_mfa_setup_path
end

def destroy
result = ::TwoFactorAuthentication::WebauthnDeleteForm.new(
user: current_user,
configuration_id: webauthn_mismatch_id,
skip_multiple_mfa_validation: in_multi_mfa_selection_flow?,
).submit

analytics.webauthn_setup_mismatch_submitted(**result.to_h, confirmed_mismatch: false)

if result.success?
handle_successful_mfa_deletion(event_type: :webauthn_key_removed)
redirect_to retry_setup_url
else
flash.now[:error] = result.first_error_message
@presenter = WebauthnSetupMismatchPresenter.new(configuration:)
render :show
end
end

private

def retry_setup_url
# These are intentionally inverted: if the authenticator was set up as a platform
# authenticator but was flagged as a mismatch, it implies that the user had originally
# intended to add a security key.
if platform_authenticator?
webauthn_setup_url
else
webauthn_setup_url(platform: true)
end
end

def webauthn_mismatch_id
user_session[:webauthn_mismatch_id]
end

def configuration
return @configuration if defined?(@configuration)
@configuration = current_user.webauthn_configurations.find_by(id: webauthn_mismatch_id)
end

def validate_session_mismatch_id
return if configuration.present?
redirect_to next_setup_path || after_mfa_setup_path
end

delegate :platform_authenticator?, to: :configuration
end
end
12 changes: 10 additions & 2 deletions app/forms/two_factor_authentication/webauthn_delete_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ class WebauthnDeleteForm
validate :validate_configuration_exists
validate :validate_has_multiple_mfa

def initialize(user:, configuration_id:)
def initialize(user:, configuration_id:, skip_multiple_mfa_validation: false)
@user = user
@configuration_id = configuration_id
@skip_multiple_mfa_validation = skip_multiple_mfa_validation
end

def submit
Expand All @@ -34,6 +35,10 @@ def configuration

private

attr_reader :skip_multiple_mfa_validation

alias_method :skip_multiple_mfa_validation?, :skip_multiple_mfa_validation

def validate_configuration_exists
return if configuration.present?
errors.add(
Expand All @@ -44,7 +49,10 @@ def validate_configuration_exists
end

def validate_has_multiple_mfa
return if !configuration || MfaPolicy.new(user).multiple_factors_enabled?
return if skip_multiple_mfa_validation? ||
!configuration ||
MfaPolicy.new(user).multiple_factors_enabled?

errors.add(
:configuration_id,
:only_method,
Expand Down
47 changes: 47 additions & 0 deletions app/presenters/webauthn_setup_mismatch_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

class WebauthnSetupMismatchPresenter
include ActionView::Helpers::TranslationHelper

attr_reader :configuration

def initialize(configuration:)
@configuration = configuration
end

def heading
if platform_authenticator?
t('webauthn_setup_mismatch.heading.webauthn_platform')
else
t('webauthn_setup_mismatch.heading.webauthn')
end
end

def description
if platform_authenticator?
t('webauthn_setup_mismatch.description.webauthn_platform')
else
t('webauthn_setup_mismatch.description.webauthn')
end
end

def correct_image_path
if platform_authenticator?
'webauthn-mismatch/webauthn-platform-checked.svg'
else
'webauthn-mismatch/webauthn-checked.svg'
end
end

def incorrect_image_path
if platform_authenticator?
'webauthn-mismatch/webauthn-unchecked.svg'
else
'webauthn-mismatch/webauthn-platform-unchecked.svg'
end
end

private

delegate :platform_authenticator?, to: :configuration
end
49 changes: 48 additions & 1 deletion app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7706,7 +7706,54 @@ def webauthn_platform_recommended_visited
track_event(:webauthn_platform_recommended_visited)
end

# @param [Hash] platform_authenticator
# @param [Boolean] platform_authenticator Whether authentication method was registered as platform
# authenticator
# @param [Number] configuration_id Database ID of WebAuthn configuration
# @param [Boolean] confirmed_mismatch Whether user chose to confirm and continue with interpreted
# platform attachment
# @param [Boolean] success Whether the deletion was successful, if user chose to undo interpreted
# platform attachment
# @param [Hash] error_details Details for errors that occurred in unsuccessful deletion
# User submitted confirmation screen after setting up WebAuthn with transports mismatched with the
# expected platform attachment
def webauthn_setup_mismatch_submitted(
configuration_id:,
platform_authenticator:,
confirmed_mismatch:,
success: nil,
error_details: nil,
**extra
)
track_event(
:webauthn_setup_mismatch_submitted,
configuration_id:,
platform_authenticator:,
confirmed_mismatch:,
success:,
error_details:,
**extra,
)
end

# @param [Boolean] platform_authenticator Whether authentication method was registered as platform
# authenticator
# @param [Number] configuration_id Database ID of WebAuthn configuration
# User visited confirmation screen after setting up WebAuthn with transports mismatched with the
# expected platform attachment
def webauthn_setup_mismatch_visited(
configuration_id:,
platform_authenticator:,
**extra
)
track_event(
:webauthn_setup_mismatch_visited,
configuration_id:,
platform_authenticator:,
**extra,
)
end

# @param [Boolean] platform_authenticator
# @param [Boolean] success
# @param [Hash, nil] errors
# @param [Boolean] in_account_creation_flow Whether user is going through account creation flow
Expand Down
42 changes: 42 additions & 0 deletions app/views/users/webauthn_setup_mismatch/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<% self.title = @presenter.heading %>

<div class="display-flex flex-align-center margin-bottom-4">
<%= image_tag(
@presenter.correct_image_path,
width: 104,
height: 116,
alt: '',
aria: { hidden: true },
) %>
<%= image_tag(
@presenter.incorrect_image_path,
width: 64,
height: 71,
class: 'margin-left-2',
alt: '',
aria: { hidden: true },
) %>
</div>

<%= render PageHeadingComponent.new.with_content(@presenter.heading) %>

<p><%= @presenter.description %></p>

<p><%= t('webauthn_setup_mismatch.description_undo') %>

<div class="margin-top-5 margin-bottom-2">
<%= render ButtonComponent.new(
url: webauthn_setup_mismatch_url,
method: :patch,
big: true,
wide: true,
).with_content(t('forms.buttons.continue')) %>
</div>

<%= render ButtonComponent.new(
url: webauthn_setup_mismatch_url,
method: :delete,
big: true,
wide: true,
outline: true,
).with_content(t('webauthn_setup_mismatch.undo')) %>
Loading