Add tests for recovery codes

This commit is contained in:
Dominik Kwiatek 2020-11-15 10:21:06 +01:00
parent d7cac67c22
commit aeb1396422
4 changed files with 50 additions and 14 deletions

View File

@ -175,10 +175,9 @@ class UserController < ApplicationController
def edit_security def edit_security
if current_user.otp_module_disabled? if current_user.otp_module_disabled?
current_user.otp_secret_key = User.otp_random_secret(26) current_user.otp_secret_key = User.otp_random_secret(25)
current_user.save current_user.save
@provisioning_uri = current_user.provisioning_uri(nil, issuer: APP_CONFIG[:hostname])
qr_code = RQRCode::QRCode.new(current_user.provisioning_uri("Retrospring:#{current_user.screen_name}", issuer: "Retrospring")) qr_code = RQRCode::QRCode.new(current_user.provisioning_uri("Retrospring:#{current_user.screen_name}", issuer: "Retrospring"))
@qr_svg = qr_code.as_svg({offset: 4, module_size: 4, color: '000;fill:var(--primary)'}).html_safe @qr_svg = qr_code.as_svg({offset: 4, module_size: 4, color: '000;fill:var(--primary)'}).html_safe
@ -192,7 +191,7 @@ class UserController < ApplicationController
current_user.otp_module = :enabled current_user.otp_module = :enabled
if current_user.authenticate_otp(req_params[:otp_validation], drift: APP_CONFIG.fetch(:otp_drift_period, 30).to_i) if current_user.authenticate_otp(req_params[:otp_validation], drift: APP_CONFIG.fetch(:otp_drift_period, 30).to_i)
@recovery_keys = TotpRecoveryCode.create!(Array.new(10) { {user: current_user, code: SecureRandom.base58(8).downcase} }) @recovery_keys = TotpRecoveryCode.generate_for(current_user)
current_user.save! current_user.save!
render 'settings/security/recovery_keys' render 'settings/security/recovery_keys'
@ -205,14 +204,14 @@ class UserController < ApplicationController
def destroy_2fa def destroy_2fa
current_user.otp_module = :disabled current_user.otp_module = :disabled
current_user.save! current_user.save!
TotpRecoveryCode.where(user_id: current_user.id).delete_all TotpRecoveryCode.remove_all_for(current_user)
flash[:success] = 'Two factor authentication has been disabled for your account.' flash[:success] = 'Two factor authentication has been disabled for your account.'
redirect_to edit_user_security_path redirect_to edit_user_security_path
end end
def reset_user_recovery_codes def reset_user_recovery_codes
TotpRecoveryCode.where(user_id: current_user.id).delete_all TotpRecoveryCode.remove_all_for(current_user)
@recovery_keys = TotpRecoveryCode.create!(Array.new(10) { {user: current_user, code: SecureRandom.base58(8).downcase} }) @recovery_keys = TotpRecoveryCode.generate_for(current_user)
render 'settings/security/recovery_keys' render 'settings/security/recovery_keys'
end end
end end

View File

@ -1,3 +1,17 @@
class TotpRecoveryCode < ApplicationRecord class TotpRecoveryCode < ApplicationRecord
NUMBER_OF_CODES_TO_GENERATE = 16
belongs_to :user belongs_to :user
# @param user [User]
# @return [Array<TotpRecoveryCode>]
def self.generate_for(user)
TotpRecoveryCode.create!(Array.new(16) { {user: user, code: SecureRandom.base58(8).downcase} })
end
# @param user [User]
# @return [Integer]
def self.remove_all_for(user)
TotpRecoveryCode.where(user: user).delete_all
end
end end

View File

@ -43,6 +43,7 @@ class User < ApplicationRecord
has_many :list_memberships, class_name: "ListMember", foreign_key: 'user_id', dependent: :destroy has_many :list_memberships, class_name: "ListMember", foreign_key: 'user_id', dependent: :destroy
has_many :subscriptions, dependent: :destroy has_many :subscriptions, dependent: :destroy
has_many :totp_recovery_codes, dependent: :destroy
has_one :theme, dependent: :destroy has_one :theme, dependent: :destroy

View File

@ -3,7 +3,9 @@
require "rails_helper" require "rails_helper"
describe UserController, type: :controller do describe UserController, type: :controller do
let(:user) { FactoryBot.create :user, otp_module: :disabled } let(:user) { FactoryBot.create :user,
otp_module: :disabled,
otp_secret_key: 'EJFNIJPYXXTCQSRTQY6AG7XQLAT2IDG5H7NGLJE3'}
describe "#edit" do describe "#edit" do
subject { get :edit } subject { get :edit }
@ -97,8 +99,7 @@ describe UserController, type: :controller do
context "user enters the incorrect code" do context "user enters the incorrect code" do
let(:update_params) do let(:update_params) do
{ {
user: { otp_secret_key: 'EJFNIJPYXXTCQSRTQY6AG7XQLAT2IDG5H7NGLJE3', user: { otp_validation: 123456 }
otp_validation: 123456 }
} }
end end
@ -106,6 +107,7 @@ describe UserController, type: :controller do
Timecop.freeze(Time.at(1603290888)) do Timecop.freeze(Time.at(1603290888)) do
subject subject
expect(response).to redirect_to :edit_user_security expect(response).to redirect_to :edit_user_security
expect(flash[:error]).to eq('The code you entered was invalid.')
end end
end end
end end
@ -113,21 +115,23 @@ describe UserController, type: :controller do
context "user enters the correct code" do context "user enters the correct code" do
let(:update_params) do let(:update_params) do
{ {
user: { otp_secret_key: 'EJFNIJPYXXTCQSRTQY6AG7XQLAT2IDG5H7NGLJE3', user: { otp_validation: 187894 }
otp_validation: 187894 }
} }
end end
it "enables 2FA for the logged in user" do it "enables 2FA for the logged in user and generates recovery keys" do
Timecop.freeze(Time.at(1603290888)) do Timecop.freeze(Time.at(1603290888)) do
subject subject
expect(response).to redirect_to :edit_user_security expect(response).to have_rendered(:recovery_keys)
expect(user.totp_recovery_codes.count).to be(TotpRecoveryCode::NUMBER_OF_CODES_TO_GENERATE)
end end
end end
it "shows an error if the user attempts to use the code once it has expired" do it "shows an error if the user attempts to use the code once it has expired" do
Timecop.freeze(Time.at(1603290910)) do Timecop.freeze(Time.at(1603290910)) do
subject subject
expect(response).to redirect_to :edit_user_security
expect(flash[:error]).to eq('The code you entered was invalid.') expect(flash[:error]).to eq('The code you entered was invalid.')
end end
end end
@ -142,13 +146,31 @@ describe UserController, type: :controller do
before(:each) do before(:each) do
user.otp_module = :enabled user.otp_module = :enabled
user.save user.save
sign_in user sign_in(user)
end end
it "disables 2FA for the logged in user" do it "disables 2FA for the logged in user" do
subject subject
user.reload user.reload
expect(user.otp_module_enabled?).to be_falsey expect(user.otp_module_enabled?).to be_falsey
expect(user.totp_recovery_codes.count).to be(0)
end
end
end
describe "#reset_user_recovery_codes" do
subject { delete :reset_user_recovery_codes }
context "user signed in" do
before(:each) do
sign_in(user)
end
it "regenerates codes on request" do
old_codes = user.totp_recovery_codes.pluck(:code)
subject
new_codes = user.totp_recovery_codes.pluck(:code)
expect(new_codes).not_to match_array(old_codes)
end end
end end
end end