mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-12-26 09:42:46 +08:00
Add 2fa to user settings page
This commit is contained in:
parent
d88f789e44
commit
4b9881e31e
10 changed files with 102 additions and 47 deletions
1
Gemfile
1
Gemfile
|
@ -76,6 +76,7 @@ gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
|
|||
gem 'rgl' # Graph framework for project diagram calculations
|
||||
gem 'roo', '~> 2.8.2' # Spreadsheet parser
|
||||
gem 'rotp'
|
||||
gem 'rqrcode' # QR code generator
|
||||
gem 'rubyzip'
|
||||
gem 'scenic', '~> 1.4'
|
||||
gem 'sdoc', '~> 1.0', group: :doc
|
||||
|
|
|
@ -186,6 +186,7 @@ GEM
|
|||
activesupport
|
||||
childprocess (1.0.1)
|
||||
rake (< 13.0)
|
||||
chunky_png (1.3.11)
|
||||
coderay (1.1.2)
|
||||
coffee-rails (5.0.0)
|
||||
coffee-script (>= 2.2.0)
|
||||
|
@ -471,6 +472,10 @@ GEM
|
|||
rubyzip (>= 1.2.1, < 2.0.0)
|
||||
rotp (6.0.0)
|
||||
addressable (~> 2.7)
|
||||
rqrcode (1.1.2)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 0.1)
|
||||
rqrcode_core (0.1.2)
|
||||
rspec-core (3.8.2)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-expectations (3.8.4)
|
||||
|
@ -671,6 +676,7 @@ DEPENDENCIES
|
|||
rgl
|
||||
roo (~> 2.8.2)
|
||||
rotp
|
||||
rqrcode
|
||||
rspec-rails (>= 4.0.0.beta2)
|
||||
rubocop (>= 0.75.0)
|
||||
rubocop-performance
|
||||
|
|
|
@ -80,7 +80,22 @@
|
|||
$fileInput[0].value = '';
|
||||
});
|
||||
|
||||
$('#twoFactorAuthentication').click(function() {
|
||||
$('#twoFactorAuthenticationDisable').click(function() {
|
||||
$('#twoFactorAuthenticationModal').modal('show');
|
||||
});
|
||||
|
||||
$('#twoFactorAuthenticationEnable').click(function() {
|
||||
$.get(this.dataset.qrCodeUrl, function(result) {
|
||||
$('#twoFactorAuthenticationModal .qr-code').html(result.qr_code);
|
||||
$('#twoFactorAuthenticationModal').modal('show');
|
||||
});
|
||||
});
|
||||
|
||||
$('#twoFactorAuthenticationModal .2fa-enable-form').on('ajax:error', function(e, data) {
|
||||
$(this).find('.submit-code-field').addClass('error').attr('data-error-text', data.responseJSON.error);
|
||||
});
|
||||
|
||||
$('#twoFactorAuthenticationModal .2fa-disable-form').on('ajax:error', function(e, data) {
|
||||
$(this).find('.password-field').addClass('error').attr('data-error-text', data.responseJSON.error);
|
||||
});
|
||||
}());
|
||||
|
|
|
@ -182,6 +182,32 @@ class Users::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
end
|
||||
|
||||
def two_factor_enable
|
||||
totp = ROTP::TOTP.new(current_user.otp_secret, issuer: 'SciNote')
|
||||
if totp.verify(params[:submit_code], drift_behind: 10)
|
||||
current_user.update!(two_factor_auth_enabled: true)
|
||||
redirect_to edit_user_registration_path
|
||||
else
|
||||
render json: { error: t('users.registrations.edit.2fa_errors.wrong_submit_code') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def two_factor_disable
|
||||
if current_user.valid_password?(params[:password])
|
||||
current_user.update!(two_factor_auth_enabled: false, otp_secret: nil)
|
||||
redirect_to edit_user_registration_path
|
||||
else
|
||||
render json: { error: t('users.registrations.edit.2fa_errors.wrong_password') }, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
def two_factor_qr_code
|
||||
current_user.ensure_2fa_token
|
||||
qr_code_url = ROTP::TOTP.new(current_user.otp_secret, issuer: 'SciNote').provisioning_uri(current_user.email)
|
||||
qr_code = RQRCode::QRCode.new(qr_code_url)
|
||||
render json: { qr_code: qr_code.as_svg }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Called upon creating User (before .save). Permits parameters and extracts
|
||||
|
|
|
@ -286,8 +286,6 @@ class User < ApplicationRecord
|
|||
foreign_key: :resource_owner_id,
|
||||
dependent: :delete_all
|
||||
|
||||
before_save :ensure_2fa_token, if: ->(user) { user.changed.include?('two_factor_auth_enabled') }
|
||||
before_create :assign_2fa_token
|
||||
before_destroy :destroy_notifications
|
||||
|
||||
def name
|
||||
|
@ -630,6 +628,13 @@ class User < ApplicationRecord
|
|||
totp.verify(otp, drift_behind: 10)
|
||||
end
|
||||
|
||||
def ensure_2fa_token
|
||||
return if otp_secret
|
||||
|
||||
assign_2fa_token
|
||||
save!
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def confirmation_required?
|
||||
|
@ -672,7 +677,4 @@ class User < ApplicationRecord
|
|||
self.otp_secret = ROTP::Base32.random
|
||||
end
|
||||
|
||||
def ensure_2fa_token
|
||||
assign_2fa_token unless otp_secret
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,9 +3,35 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
|
||||
<h2 class="modal-title"></h2>
|
||||
<h2 class="modal-title"><%= t("users.registrations.edit.2fa_modal.title") %></h2>
|
||||
</div>
|
||||
<% if current_user.two_factor_auth_enabled? %>
|
||||
<%= form_with(url: users_2fa_disable_path, method: "post", class: "2fa-disable-form") do %>
|
||||
<div class="modal-body">
|
||||
<div class="sci-input-container password-field">
|
||||
<%= label_tag :password, t("users.registrations.edit.2fa_modal.password_label") %>
|
||||
<%= password_field_tag :password, '', class: "sci-input-field" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<%= button_tag t("users.registrations.edit.2fa_modal.submit_button"), type: 'submit', class: "btn btn-danger" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= form_with(url: users_2fa_enable_path, method: "post", class: "2fa-enable-form") do %>
|
||||
<div class="modal-body">
|
||||
<div class="qr-code"></div>
|
||||
<div class="sci-input-container submit-code-field">
|
||||
<%= label_tag :submit_code, t("users.registrations.edit.2fa_modal.submit_code_label") %>
|
||||
<%= text_field_tag :submit_code, '', class: "sci-input-field" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<%= button_tag t("users.registrations.edit.2fa_modal.submit_button"), type: 'submit', class: "btn btn-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -142,7 +142,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<button class="btn btn-primary" id="twoFactorAuthentication"><%= t("users.registrations.edit.2fa_button") %></button>
|
||||
<% if current_user.two_factor_auth_enabled? %>
|
||||
<button class="btn btn-danger" id="twoFactorAuthenticationDisable"><%= t("users.registrations.edit.2fa_button_disable") %></button>
|
||||
<% else %>
|
||||
<button class="btn btn-primary" id="twoFactorAuthenticationEnable" data-qr-code-url="<%= users_2fa_qr_code_path %>"><%= t("users.registrations.edit.2fa_button") %></button>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="col-md-7 col-md-offset-1">
|
||||
<div class="row user-statistics">
|
||||
|
|
|
@ -1600,6 +1600,15 @@ en:
|
|||
new_password_label: "New password"
|
||||
new_password_2_label: "New password confirmation"
|
||||
2fa_button: "Enable two-factor authentication"
|
||||
2fa_button_disable: "Disable two-factor authentication"
|
||||
2fa_modal:
|
||||
title: 'Two-factor authentication'
|
||||
password_label: 'Password'
|
||||
submit_code_label: 'Submit code'
|
||||
submit_button: 'Submit'
|
||||
2fa_errors:
|
||||
wrong_submit_code: 'Not correct code'
|
||||
wrong_password: 'Not correct password'
|
||||
new:
|
||||
head_title: "Sign up"
|
||||
team_name_label: "Team name"
|
||||
|
|
|
@ -697,6 +697,10 @@ Rails.application.routes.draw do
|
|||
get 'users/sign_up_provider' => 'users/registrations#new_with_provider'
|
||||
post 'users/authenticate_with_two_factor' => 'users/sessions#authenticate_with_two_factor'
|
||||
post 'users/complete_sign_up_provider' => 'users/registrations#create_with_provider'
|
||||
|
||||
post 'users/2fa_enable' => 'users/registrations#two_factor_enable'
|
||||
post 'users/2fa_disable' => 'users/registrations#two_factor_disable'
|
||||
get 'users/2fa_qr_code' => 'users/registrations#two_factor_qr_code'
|
||||
end
|
||||
|
||||
namespace :api, defaults: { format: 'json' } do
|
||||
|
|
|
@ -332,44 +332,6 @@ describe User, type: :model do
|
|||
it { is_expected.to have_many(:system_notifications) }
|
||||
end
|
||||
|
||||
describe 'Callbacks' do
|
||||
describe 'after_create' do
|
||||
it 'sets token' do
|
||||
user = create :user
|
||||
|
||||
expect(user.otp_secret).to be_kind_of String
|
||||
end
|
||||
end
|
||||
|
||||
describe 'before_save' do
|
||||
let(:user) { create :user }
|
||||
|
||||
context 'when changing twofa_enabled' do
|
||||
context 'when user does not have otp_secret' do
|
||||
it 'sets token before save' do
|
||||
user.update_column(:otp_secret, nil)
|
||||
|
||||
expect { user.update(two_factor_auth_enabled: true) }.to(change { user.otp_secret })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does have otp_secret' do
|
||||
it 'does not set new token before save' do
|
||||
expect { user.update(two_factor_auth_enabled: true) }.not_to(change { user.otp_secret })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when changing not twofa_enabled and user does have otp_secret' do
|
||||
it 'does not set token before save' do
|
||||
user.update_column(:otp_secret, nil)
|
||||
|
||||
expect { user.update(name: 'SomeNewName') }.not_to(change { user.otp_secret })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'valid_otp?' do
|
||||
let(:user) { create :user }
|
||||
before do
|
||||
|
|
Loading…
Reference in a new issue