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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module Api
module Internal
module TwoFactorAuthentication
class WebauthnController < ApplicationController
include CsrfTokenConcern
include ReauthenticationRequiredConcern

before_action :render_unauthorized, unless: :recently_authenticated_2fa?

after_action :add_csrf_token_header_to_response

respond_to :json

def update
result = ::TwoFactorAuthentication::WebauthnUpdateForm.new(
user: current_user,
configuration_id: params[:id],
).submit(name: params[:name])

analytics.webauthn_update_name_submitted(**result.to_h)

if result.success?
render json: { success: true }
else
render json: { success: false, error: result.first_error_message }, status: :bad_request
end
end

def destroy
result = ::TwoFactorAuthentication::WebauthnDeleteForm.new(
user: current_user,
configuration_id: params[:id],
).submit

analytics.webauthn_delete_submitted(**result.to_h)

if result.success?
render json: { success: true }
else
render json: { success: false, error: result.first_error_message }, status: :bad_request
end
end

private

def render_unauthorized
render json: { error: 'Unauthorized' }, status: :unauthorized
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module ReauthenticationRequiredConcern
include TwoFactorAuthenticatableMethods

def confirm_recently_authenticated_2fa
return if !user_fully_authenticated? || auth_methods_session.recently_authenticated_2fa?
return if !user_fully_authenticated? || recently_authenticated_2fa?

analytics.user_2fa_reauthentication_required(
auth_method: auth_methods_session.last_auth_event&.[](:auth_method),
Expand All @@ -13,6 +13,10 @@ def confirm_recently_authenticated_2fa
prompt_for_second_factor
end

def recently_authenticated_2fa?
user_fully_authenticated? && auth_methods_session.recently_authenticated_2fa?
end

private

def prompt_for_second_factor
Expand Down
61 changes: 61 additions & 0 deletions app/controllers/users/webauthn_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module Users
class WebauthnController < ApplicationController
include ReauthenticationRequiredConcern

before_action :confirm_two_factor_authenticated
before_action :confirm_recently_authenticated_2fa
before_action :set_form
before_action :validate_configuration_exists

def edit; end

def update
result = form.submit(name: params.dig(:form, :name))

analytics.webauthn_update_name_submitted(**result.to_h)

if result.success?
flash[:success] = t('two_factor_authentication.webauthn_platform.renamed')
redirect_to account_path
else
flash.now[:error] = result.first_error_message
render :edit
end
end

def destroy
result = form.submit

analytics.webauthn_delete_submitted(**result.to_h)

if result.success?
flash[:success] = t('two_factor_authentication.webauthn_platform.deleted')
redirect_to account_path
else
flash[:error] = result.first_error_message
redirect_to edit_webauthn_path(id: params[:id])
end
end

private

def form
@form ||= form_class.new(user: current_user, configuration_id: params[:id])
end

alias_method :set_form, :form

def form_class
case action_name
when 'edit', 'update'
TwoFactorAuthentication::WebauthnUpdateForm
when 'destroy'
TwoFactorAuthentication::WebauthnDeleteForm
end
end

def validate_configuration_exists
render_not_found if form.configuration.blank?
end
end
end
7 changes: 1 addition & 6 deletions app/controllers/users/webauthn_setup_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,7 @@ def handle_failed_delete
end

def track_delete(success)
counts_hash = MfaContext.new(current_user.reload).enabled_two_factor_configuration_counts_hash
analytics.webauthn_deleted(
success: success,
mfa_method_counts: counts_hash,
pii_like_keypaths: [[:mfa_method_counts, :phone]],
)
analytics.webauthn_delete_submitted(success:, configuration_id: delete_params[:id])
end

def save_challenge_in_session
Expand Down
57 changes: 57 additions & 0 deletions app/forms/two_factor_authentication/webauthn_delete_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module TwoFactorAuthentication
class WebauthnDeleteForm
include ActiveModel::Model
include ActionView::Helpers::TranslationHelper

attr_reader :user, :configuration_id

validate :validate_configuration_exists
validate :validate_has_multiple_mfa

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

def submit
success = valid?

configuration.destroy if success

FormResponse.new(
success:,
errors:,
extra: extra_analytics_attributes,
serialize_error_details_only: true,
)
end

def configuration
@configuration ||= user.webauthn_configurations.find_by(id: configuration_id)
end

private

def validate_configuration_exists
return if configuration.present?
errors.add(
:configuration_id,
:configuration_not_found,
message: t('errors.manage_authenticator.internal_error'),
)
end

def validate_has_multiple_mfa
return if !configuration || MfaPolicy.new(user).multiple_factors_enabled?
errors.add(
:configuration_id,
:only_method,
message: t('errors.manage_authenticator.remove_only_method_error'),
)
end

def extra_analytics_attributes
{ configuration_id: }
end
end
end
68 changes: 68 additions & 0 deletions app/forms/two_factor_authentication/webauthn_update_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module TwoFactorAuthentication
class WebauthnUpdateForm
include ActiveModel::Model
include ActionView::Helpers::TranslationHelper

attr_reader :user, :configuration_id

validate :validate_configuration_exists
validate :validate_unique_name

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

def submit(name:)
@name = name

success = valid?
if valid?
configuration.name = name
success = configuration.valid?
errors.merge!(configuration.errors)
configuration.save if success
end

FormResponse.new(
success:,
errors:,
extra: extra_analytics_attributes,
serialize_error_details_only: true,
)
end

def name
return @name if defined?(@name)
@name = configuration&.name
end

def configuration
@configuration ||= user.webauthn_configurations.find_by(id: configuration_id)
end

private

def validate_configuration_exists
return if configuration.present?
errors.add(
:configuration_id,
:configuration_not_found,
message: t('errors.manage_authenticator.internal_error'),
)
end

def validate_unique_name
return unless user.webauthn_configurations.where.not(id: configuration_id).find_by(name:)
errors.add(
:name,
:duplicate,
message: t('errors.manage_authenticator.unique_name_error'),
)
end

def extra_analytics_attributes
{ configuration_id: }
end
end
end
40 changes: 33 additions & 7 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4718,14 +4718,21 @@ def vendor_outage(
end

# @param [Boolean] success
# @param [Integer] mfa_method_counts
# Tracks when WebAuthn is deleted
def webauthn_deleted(success:, mfa_method_counts:, pii_like_keypaths:, **extra)
# @param [Hash] error_details
# @param [Integer] configuration_id
# Tracks when user attempts to delete a WebAuthn configuration
Comment thread
aduth marked this conversation as resolved.
# @identity.idp.previous_event_name WebAuthn Deleted
def webauthn_delete_submitted(
success:,
configuration_id:,
error_details: nil,
**extra
)
track_event(
'WebAuthn Deleted',
success: success,
mfa_method_counts: mfa_method_counts,
pii_like_keypaths: pii_like_keypaths,
:webauthn_delete_submitted,
success:,
error_details:,
configuration_id:,
**extra,
)
end
Expand Down Expand Up @@ -4755,4 +4762,23 @@ def webauthn_setup_visit(platform_authenticator:, enabled_mfa_methods_count:, **
**extra,
)
end

# @param [Boolean] success
# @param [Hash] error_details
# @param [Integer] configuration_id
# Tracks when user submits a name change for a WebAuthn configuration
def webauthn_update_name_submitted(
success:,
configuration_id:,
error_details: nil,
**extra
)
track_event(
:webauthn_update_name_submitted,
success:,
error_details:,
configuration_id:,
**extra,
)
end
end
40 changes: 40 additions & 0 deletions app/views/users/webauthn/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<% self.title = t('two_factor_authentication.webauthn_platform.edit_heading') %>

<%= render PageHeadingComponent.new.with_content(t('two_factor_authentication.webauthn_platform.edit_heading')) %>

<%= simple_form_for(
@form,
as: :form,
method: :put,
html: { autocomplete: 'off' },
url: webauthn_path(id: @form.configuration.id),
) do |f| %>
<%= render ValidatedFieldComponent.new(
form: f,
name: :name,
label: t('two_factor_authentication.webauthn_platform.nickname'),
) %>

<%= f.submit(
t('two_factor_authentication.webauthn_platform.change_nickname'),
class: 'display-block margin-top-5',
) %>
<% end %>

<%= render ButtonComponent.new(
action: ->(**tag_options, &block) do
button_to(
webauthn_path(id: @form.configuration.id),
form: { aria: { label: t('two_factor_authentication.webauthn_platform.delete') } },
**tag_options,
&block
)
end,
method: :delete,
big: true,
wide: true,
danger: true,
class: 'display-block margin-top-2',
).with_content(t('two_factor_authentication.webauthn_platform.delete')) %>

<%= render 'shared/cancel', link: account_path %>
4 changes: 4 additions & 0 deletions config/locales/errors/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ en:
<strong>Try again in %{timeout}.</strong>
general: Oops, something went wrong. Please try again.
invalid_totp: Invalid code. Please try again.
manage_authenticator:
internal_error: There was an internal error processing your request. Please try again.
remove_only_method_error: You cannot remove your only authentication method.
unique_name_error: Name already in use. Please use a different name.
max_password_attempts_reached: You’ve entered too many incorrect passwords. You
can reset your password using the “Forgot your password?” link.
messages:
Expand Down
5 changes: 5 additions & 0 deletions config/locales/errors/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ es:
veces. <strong>Inténtelo de nuevo en %{timeout}.</strong>
general: '¡Oops! Algo salió mal. Inténtelo de nuevo.'
invalid_totp: El código es inválido. Vuelva a intentarlo.
manage_authenticator:
internal_error: Se produjo un error interno al procesar tu solicitud. Por favor,
inténtalo de nuevo.
remove_only_method_error: No puede eliminar su único método de autenticación.
unique_name_error: Nombre ya en uso. Utilice un nombre diferente.
max_password_attempts_reached: Ha ingresado demasiadas contraseñas incorrectas.
Puede restablecer su contraseña usando el enlace “¿Olvidó su contraseña?”.
messages:
Expand Down
Loading