Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
86ee69d
add copy for feature
jmdembe Dec 20, 2023
dfc39aa
modify list element
jmdembe Dec 21, 2023
73b4bdc
update strings and yml keys
jmdembe Dec 21, 2023
87c2808
Merge branch 'main' into jd-LG-11758-edit-auth-apps
jmdembe Dec 22, 2023
158b63e
create controller and form
jmdembe Dec 22, 2023
b41928a
create controller
jmdembe Dec 22, 2023
d27848c
add destroy for app auth deletion
jmdembe Dec 22, 2023
2a58a90
create page to edit auth app nickname
jmdembe Dec 22, 2023
338ae43
create routes
jmdembe Dec 22, 2023
2fcd33a
Merge branch 'main' into jd-LG-11758-edit-auth-apps
jmdembe Jan 2, 2024
93ac353
enhance edit functionality
jmdembe Jan 2, 2024
b2bc727
add tests for update nickname
jmdembe Jan 3, 2024
31ab114
create test for app auth update form
jmdembe Jan 3, 2024
19056aa
add test for app update form
jmdembe Jan 3, 2024
d564848
fix functionality for deleting auth app
jmdembe Jan 3, 2024
b6f8c55
remove binding.pry, add test for renaming auth app
jmdembe Jan 4, 2024
0e96216
remove binding.pry, update validate_unique_name validator
jmdembe Jan 4, 2024
dc3f78f
create test for app auth deletion
jmdembe Jan 4, 2024
34d3d17
create test for auth app controller api
jmdembe Jan 4, 2024
a6340f2
fix test for unique name error
jmdembe Jan 4, 2024
8d12282
lintfix
jmdembe Jan 4, 2024
f8818df
update deletion test
jmdembe Jan 4, 2024
374103c
fix test for update totp
jmdembe Jan 5, 2024
c12e6cf
Merge branch 'main' into jd-LG-11758-edit-auth-apps
jmdembe Jan 5, 2024
a5ed70b
removed unused keys
jmdembe Jan 5, 2024
775966a
add test for auth apps show
jmdembe Jan 5, 2024
187e910
add missing keys
jmdembe Jan 5, 2024
8cf5e54
Something is not right
jmdembe Jan 5, 2024
8ce9a81
Merge branch 'main' into jd-LG-11758-edit-auth-apps
jmdembe Jan 8, 2024
1b78914
update text for change nickname
jmdembe Jan 8, 2024
a51b436
fix lint
jmdembe Jan 8, 2024
bd946a0
WIP: delete spec from account shows
jmdembe Jan 8, 2024
4ffb39f
remove test since show/hide feature is being done elsewhere
jmdembe Jan 8, 2024
41fdd48
Merge branch 'main' into jd-LG-11758-edit-auth-apps
jmdembe Jan 9, 2024
5f8eb06
Merge branch 'main' into jd-LG-11758-edit-auth-apps
jmdembe Jan 9, 2024
f1684ac
Merge branch 'main' into jd-LG-11758-edit-auth-apps
jmdembe Jan 10, 2024
edd3a10
Merge branch 'main' into jd-LG-11758-edit-auth-apps
jmdembe Jan 16, 2024
094177d
Merge branch 'main' into jd-LG-11758-edit-auth-apps
jmdembe Jan 18, 2024
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,56 @@
module Api
module Internal
module TwoFactorAuthentication
class AuthAppController < 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::AuthAppUpdateForm.new(
user: current_user,
configuration_id: params[:id],
).submit(name: params[:name])

analytics.auth_app_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::AuthAppDeleteForm.new(
user: current_user,
configuration_id: params[:id],
).submit

analytics.auth_app_delete_submitted(**result.to_h)

if result.success?
create_user_event(:authenticator_disabled)
revoke_remember_device(current_user)
event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user)
PushNotification::HttpPush.deliver(event)
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
65 changes: 65 additions & 0 deletions app/controllers/users/auth_app_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module Users
class AuthAppController < 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.auth_app_update_name_submitted(**result.to_h)

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

def destroy
result = form.submit

analytics.auth_app_delete_submitted(**result.to_h)

if result.success?
flash[:success] = t('two_factor_authentication.auth_app.deleted')
create_user_event(:authenticator_disabled)
revoke_remember_device(current_user)
event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user)
PushNotification::HttpPush.deliver(event)
redirect_to account_path
else
flash[:error] = result.first_error_message
redirect_to edit_auth_app_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::AuthAppUpdateForm
when 'destroy'
TwoFactorAuthentication::AuthAppDeleteForm
end
end
Comment on lines +52 to +59
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this abstraction is more difficult to follow and it would be preferable to explicitly instantiate the form in each of the controllers. It would also allow dropping the set_form alias and before_action.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

With form_class, we use that method to define the form in each controller and the validate_configuration_exists before_action. Wouldn't dropping set_form break the validation before_action?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, I see what you mean. It would break that, but I think it may be worth splitting it up still and being explicit about that too since this makes it difficult to respond with something other than render_not_found. The existing controller redirects, and I think that might be preferable (perhaps with an error message?).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After some thought, I will stick to what I am proposing--we are using one view to rename and delete an app method and the form that we need in the view is toggled based on the desired action.


def validate_configuration_exists
render_not_found if form.configuration.blank?
end
end
end
57 changes: 57 additions & 0 deletions app/forms/two_factor_authentication/auth_app_delete_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module TwoFactorAuthentication
class AuthAppDeleteForm
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.auth_app_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/auth_app_update_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module TwoFactorAuthentication
class AuthAppUpdateForm
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.auth_app_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.auth_app_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
39 changes: 39 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,45 @@ def add_phone_setup_visit
)
end

# Tracks when a user deletes their auth app from account
# @param [Boolean] success
# @param [Hash] error_details
# @param [Integer] configuration_id
def auth_app_delete_submitted(
success:,
configuration_id:,
error_details: nil,
**extra
)
track_event(
:auth_app_delete_submitted,
success:,
error_details:,
configuration_id:,
**extra,
)
end

# When a user updates name for auth app
# @param [Boolean] success
# @param [Hash] error_details
# @param [Integer] configuration_id
# Tracks when user submits a name change for an Auth App configuration
def auth_app_update_name_submitted(
success:,
configuration_id:,
error_details: nil,
**extra
)
track_event(
:auth_app_update_name_submitted,
success:,
error_details:,
configuration_id:,
**extra,
)
end

# When a user views the "you are already signed in with the following email" screen
def authentication_confirmation
track_event('Authentication Confirmation')
Expand Down
30 changes: 16 additions & 14 deletions app/views/accounts/_auth_apps.html.erb
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
<h2 class="margin-top-0 margin-bottom-1">
<%= t('headings.account.authentication_apps') %>
</h2>
<div class="border-bottom border-primary-light">
<% MfaContext.new(current_user).auth_app_configurations.each do |auth_app_configuration| %>
<div class="padding-1 grid-row border-top border-left border-right border-primary-light">
<div class="grid-col-8">
<div class="grid-col-12 tablet:grid-col-6">
<%= auth_app_configuration.name %>
</div>
</div>
<% if MfaPolicy.new(current_user).multiple_factors_enabled? %>
<div class="grid-col-4 text-right">
<%= render 'accounts/actions/disable_totp', id: auth_app_configuration.id %>
</div>
<% end %>
</div>

<div role="list">
<% MfaContext.new(current_user).auth_app_configurations.each do |configuration| %>
<%= render ManageableAuthenticatorComponent.new(
configuration:,
user_session:,
manage_url: edit_auth_app_path(id: configuration.id),
manage_api_url: api_internal_two_factor_authentication_auth_app_path(id: configuration.id),
custom_strings: {
deleted: t('two_factor_authentication.auth_app.deleted'),
renamed: t('two_factor_authentication.auth_app.renamed'),
manage_accessible_label: t('two_factor_authentication.auth_app.manage_accessible_label'),
},
role: 'list-item',
) %>
<% end %>
</div>

<% if current_user.auth_app_configurations.count < IdentityConfig.store.max_auth_apps_per_account %>
<%= render ButtonComponent.new(
action: ->(**tag_options, &block) do
Expand Down
40 changes: 40 additions & 0 deletions app/views/users/auth_app/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<% self.title = t('two_factor_authentication.auth_app.edit_heading') %>

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

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

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

<%= render ButtonComponent.new(
action: ->(**tag_options, &block) do
button_to(
auth_app_path(id: @form.configuration.id),
form: { aria: { label: t('two_factor_authentication.auth_app.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.auth_app.delete')) %>

<%= render 'shared/cancel', link: account_path %>
8 changes: 8 additions & 0 deletions config/locales/two_factor_authentication/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ en:
attempt_remaining_warning_html:
one: You have <strong>%{count} attempt</strong> remaining.
other: You have <strong>%{count} attempts</strong> remaining.
auth_app:
change_nickname: Change nickname
delete: Delete this device
deleted: Successfully deleted an authentication app method
edit_heading: Manage your authentication app settings
manage_accessible_label: Manage authentication app
nickname: Nickname
renamed: Successfully renamed your authentication app method
backup_code_header_text: Enter your backup code
backup_code_prompt: You can use this backup code once. After you submit it,
you’ll need to use a new backup code next time.
Expand Down
Loading