Skip to content
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

Add "Reset" button, and repurpose/hide "Disable" button for Mandatory OTP #83

Merged
merged 5 commits into from
Jun 9, 2024
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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

## UNRELEASED

Summary: Add reset token action, and hide/repurpose disable token action

Details:
- Add reset token action to disable OTP, reset token secret, and redirect to otp_tokens#edit to re-enable with new token secret;
- Update disable action to preserve the existing token secret (since the reset action now accomplishes this functionality);
- Hide disable button when mandatory OTP;
- Move disable button to bottom of page;

Breaking Changes (config/locales/en.yml):
- Add:
- reset\_link
- successfully\_reset\_otp
- Move/Update
- disable\_explain > reset\_explain
- disable\_explain\_warn > reset\_explain\_warn

## UNRELEASED

Fix regression due to warden session scope usage

Details:
Expand Down
10 changes: 9 additions & 1 deletion app/controllers/devise_otp/devise/otp_tokens_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def update
#
def destroy
if resource.disable_otp!
resource.clear_otp_fields!
otp_set_flash_message :success, :successfully_disabled_otp
end

Expand Down Expand Up @@ -95,6 +94,15 @@ def recovery
end
end

def reset
if resource.disable_otp!
resource.clear_otp_fields!
otp_set_flash_message :success, :successfully_reset_otp
end

redirect_to action: :edit
end

private

def ensure_credentials_refresh
Expand Down
6 changes: 3 additions & 3 deletions app/views/devise/otp_tokens/_token_secret.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
<code><%= resource.otp_auth_secret %></code>
</p>

<p><%= button_to I18n.t('disable_link', :scope => 'devise.otp.otp_tokens'), @resource, :method => :delete, :data => { "turbo-method": "DELETE" } %></p>
<p><%= button_to I18n.t('reset_link', :scope => 'devise.otp.otp_tokens'), reset_otp_token_path_for(resource), :method => :post , :data => { "turbo-method": "POST" } %></p>

<p>
<%= I18n.t('disable_explain', :scope => 'devise.otp.otp_tokens') %>
<strong><%= I18n.t('disable_explain_warn', :scope => 'devise.otp.otp_tokens') %></strong>
<%= I18n.t('reset_explain', :scope => 'devise.otp.otp_tokens') %>
<strong><%= I18n.t('reset_explain_warn', :scope => 'devise.otp.otp_tokens') %></strong>
</p>

<%- if recovery_enabled? %>
Expand Down
4 changes: 4 additions & 0 deletions app/views/devise/otp_tokens/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
<%- if resource.otp_enabled? %>
<%= render :partial => 'token_secret' if resource.otp_enabled? %>
<%= render :partial => 'trusted_devices' if trusted_devices_enabled? %>

<% unless otp_mandatory_on?(resource) %>
<%= button_to I18n.t('disable_link', :scope => 'devise.otp.otp_tokens'), @resource, :method => :delete, :data => { "turbo-method": "DELETE" } %>
<% end %>
<% else %>
<%= link_to I18n.t('enable_link', :scope => 'devise.otp.otp_tokens'), edit_otp_token_path_for(resource) %>
<% end %>
6 changes: 4 additions & 2 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ en:
title: 'Two-factors Authentication:'
enable_link: 'Enable Two-Factor Authentication'
disable_link: 'Disable Two-Factor Authentication'
disable_explain: 'This will disable Two-Factor authentication and clear the OTP secret.'
disable_explain_warn: 'To re-enable Two-Factor authentication, you will need to enroll your mobile device again.'
reset_link: 'Reset Token Secret'
reset_explain: 'Resetting your token secret will temporarilly disable Two-Factor authentication.'
reset_explain_warn: 'To re-enable Two-Factor authentication, you will need to re-enroll your mobile device with the new token secret.'
successfully_updated: 'Your two-factors authentication settings have been updated.'
could_not_confirm: 'The Confirmation Code you entered did not match the QR code shown below.'
successfully_disabled_otp: 'Two-Factor authentication has been disabled.'
successfully_reset_otp: 'Your token secret has been reset. Please confirm your new token secret below.'
successfully_set_persistence: 'Your device is now trusted.'
successfully_cleared_persistence: 'Your device has been removed from the list of trusted devices.'
successfully_reset_persistence: 'Your list of trusted devices has been cleared.'
Expand Down
9 changes: 6 additions & 3 deletions lib/devise_otp_authenticatable/controllers/public_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def self.define_helpers(mapping) #:nodoc:
def ensure_mandatory_#{mapping}_otp!
resource = current_#{mapping}
if !devise_controller?
if otp_mandatory_on?(resource)
if mandatory_otp_missing_on?(resource)
redirect_to edit_#{mapping}_otp_token_path
end
end
Expand All @@ -25,10 +25,13 @@ def ensure_mandatory_#{mapping}_otp!
end

def otp_mandatory_on?(resource)
return true if resource.class.otp_mandatory && !resource.otp_enabled
return false unless resource.respond_to?(:otp_mandatory)

resource.otp_mandatory && !resource.otp_enabled
resource.class.otp_mandatory or resource.otp_mandatory
end

def mandatory_otp_missing_on?(resource)
otp_mandatory_on?(resource) && !resource.otp_enabled
end

end
Expand Down
5 changes: 5 additions & 0 deletions lib/devise_otp_authenticatable/controllers/url_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ def edit_otp_token_path_for(resource_or_scope, opts = {})
send("edit_#{scope}_otp_token_path", opts)
end

def reset_otp_token_path_for(resource_or_scope, opts = {})
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
send("reset_#{scope}_otp_token_path", opts)
end

def otp_credential_path_for(resource_or_scope, opts = {})
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
send("#{scope}_otp_credential_path", opts)
Expand Down
2 changes: 2 additions & 0 deletions lib/devise_otp_authenticatable/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ class Engine < ::Rails::Engine
ActiveSupport.on_load(:devise_controller) do
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
include DeviseOtpAuthenticatable::Controllers::Helpers
include DeviseOtpAuthenticatable::Controllers::PublicHelpers
end

ActiveSupport.on_load(:action_view) do
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
include DeviseOtpAuthenticatable::Controllers::Helpers
include DeviseOtpAuthenticatable::Controllers::PublicHelpers
end

# See: https://guides.rubyonrails.org/engines.html#separate-assets-and-precompiling
Expand Down
1 change: 1 addition & 0 deletions lib/devise_otp_authenticatable/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def devise_otp(mapping, controllers)
end

get :recovery
post :reset
end

resource :credential, only: [:show, :update],
Expand Down
53 changes: 53 additions & 0 deletions test/integration/disable_token_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require "test_helper"
require "integration_tests_helper"

class DisableTokenTest < ActionDispatch::IntegrationTest

def setup
# log in 1fa
@user = enable_otp_and_sign_in
assert_equal user_otp_credential_path, current_path

# otp 2fa
fill_in "user_token", with: ROTP::TOTP.new(@user.otp_auth_secret).at(Time.now)
click_button "Submit Token"
assert_equal root_path, current_path
end

def teardown
Capybara.reset_sessions!
end

test "disabling OTP after successfully enabling" do
# disable OTP
disable_otp

assert page.has_content? "Disabled"

# logout
sign_out

# log back in 1fa
sign_user_in(@user)

assert_equal root_path, current_path
end

test "disabling OTP does not reset token secrets" do
# get otp secrets
@user.reload
auth_secret = @user.otp_auth_secret
recovery_secret = @user.otp_recovery_secret

# disable OTP
disable_otp

# compare otp secrets
assert_not_nil @user.otp_auth_secret
assert_equal @user.otp_auth_secret, auth_secret

assert_not_nil @user.otp_recovery_secret
assert_equal @user.otp_recovery_secret, recovery_secret
end

end
45 changes: 45 additions & 0 deletions test/integration/reset_token_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require "test_helper"
require "integration_tests_helper"

class ResetTokenTest < ActionDispatch::IntegrationTest

def setup
# log in 1fa
@user = enable_otp_and_sign_in
assert_equal user_otp_credential_path, current_path

# otp 2fa
fill_in "user_token", with: ROTP::TOTP.new(@user.otp_auth_secret).at(Time.now)
click_button "Submit Token"
assert_equal root_path, current_path
end


def teardown
Capybara.reset_sessions!
end

test "redirects to otp_tokens#edit page" do
reset_otp

assert_equal "/users/otp/token/edit", current_path
end

test "generates new token secrets" do
# get auth secrets
auth_secret = @user.otp_auth_secret
recovery_secret = @user.otp_recovery_secret

# reset otp
reset_otp

# compare auth secrets
@user.reload
assert_not_nil @user.otp_auth_secret
assert_not_equal @user.otp_auth_secret, auth_secret

assert_not_nil @user.otp_recovery_secret
assert_not_equal @user.otp_recovery_secret, recovery_secret
end

end
32 changes: 0 additions & 32 deletions test/integration/token_test.rb

This file was deleted.

5 changes: 5 additions & 0 deletions test/integration_tests_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ def disable_otp
click_button "Disable Two-Factor Authentication"
end

def reset_otp
visit user_otp_token_path
click_button "Reset Token Secret"
end

def sign_out
logout :user
end
Expand Down
Loading