diff --git a/Gemfile b/Gemfile index 7cf883a0b..cacc6aa73 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.lock b/Gemfile.lock index 0f2a73a0b..599242fc6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/assets/javascripts/users/registrations/edit.js b/app/assets/javascripts/users/registrations/edit.js index 7d02da59b..388db117b 100644 --- a/app/assets/javascripts/users/registrations/edit.js +++ b/app/assets/javascripts/users/registrations/edit.js @@ -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); + }); }()); diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index ea2fac89e..67801ccaf 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 5b94c7b7f..eed3623c0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/users/registrations/_2fa_modal.html.erb b/app/views/users/registrations/_2fa_modal.html.erb index fea5db397..9b07d15bc 100644 --- a/app/views/users/registrations/_2fa_modal.html.erb +++ b/app/views/users/registrations/_2fa_modal.html.erb @@ -3,9 +3,35 @@
diff --git a/app/views/users/registrations/edit.html.erb b/app/views/users/registrations/edit.html.erb index aca9cef15..2fe957e58 100644 --- a/app/views/users/registrations/edit.html.erb +++ b/app/views/users/registrations/edit.html.erb @@ -142,7 +142,11 @@ <% end %> - + <% if current_user.two_factor_auth_enabled? %> + + <% else %> + + <% end %>