Skip to content

Commit b0a5e6c

Browse files
committed
Encrypt totp secret and add expiry to signed secret
1 parent 5739bda commit b0a5e6c

9 files changed

Lines changed: 59 additions & 12 deletions

File tree

app/models/user.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class User < ApplicationRecord
2424

2525
validates :firstname, length: { maximum: 50 }
2626
validates :lastname, length: { maximum: 50 }
27+
28+
encrypts :totp_secret
29+
2730
validates :totp_secret, length: { maximum: 32 }
2831

2932
has_many :backup_codes, inverse_of: :user

app/services/users/mfa/totp/generate_secret_service.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def execute
2525
payload: {
2626
signed_secret: Rails.application
2727
.message_verifier(:totp_secret)
28-
.generate(totp_secret),
28+
.generate(totp_secret, expires_in: 30.minutes),
2929
secret: totp_secret,
3030
})
3131
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
class EncryptTotpSecretOnUsers < Code0::ZeroTrack::Database::Migration[1.0]
4+
def up
5+
remove_index :users, :totp_secret, name: 'index_users_on_totp_secret'
6+
end
7+
8+
def down
9+
add_index :users, :totp_secret, unique: true, where: 'totp_secret IS NOT NULL',
10+
name: 'index_users_on_totp_secret'
11+
end
12+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3152386744f06e994087067a6e0367481df2ba73f4cff9a67401074b691f3833

db/structure.sql

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,8 +1703,6 @@ CREATE UNIQUE INDEX "index_users_on_LOWER_email" ON users USING btree (lower(ema
17031703

17041704
CREATE UNIQUE INDEX "index_users_on_LOWER_username" ON users USING btree (lower(username));
17051705

1706-
CREATE UNIQUE INDEX index_users_on_totp_secret ON users USING btree (totp_secret) WHERE (totp_secret IS NOT NULL);
1707-
17081706
ALTER TABLE ONLY node_parameters
17091707
ADD CONSTRAINT fk_rails_0d79310cfa FOREIGN KEY (node_function_id) REFERENCES node_functions(id) ON DELETE CASCADE;
17101708

spec/requests/graphql/mutation/users/mfa/totp/generate_secret_mutation_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
expect(secret).to be_present
3333

34-
signed_totp = Rails.application.message_verifier(:totp_secret).verify(signed_secret)
34+
signed_totp = Rails.application.message_verifier(:totp_secret).verified(signed_secret)
3535
expect(signed_totp).to eq(secret)
3636
end
3737
end

spec/requests/graphql/mutation/users/mfa/totp/validate_secret_mutation_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
context 'when user is valid' do
3434
let(:current_user) { create(:user) }
3535
let(:secret) { ROTP::Base32.random }
36-
let(:signed_secret) { Rails.application.message_verifier(:totp_secret).generate(secret) }
36+
let(:signed_secret) { Rails.application.message_verifier(:totp_secret).generate(secret, expires_in: 30.minutes) }
3737
let(:current_totp) { ROTP::TOTP.new(secret).now }
3838

3939
it 'validates secret and enables totp' do

spec/services/users/mfa/totp/generate_secret_service_spec.rb

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,31 @@
2121
end
2222

2323
context 'when totp secret is not set' do
24+
include ActiveSupport::Testing::TimeHelpers
25+
2426
let(:current_user) { create(:user) }
2527

2628
it { is_expected.to be_success }
2729

28-
it 'is valid totp secret' do
29-
totp = ROTP::TOTP.new(service_response.payload[:signed_secret].split('--').first)
30-
expect(totp.secret.length).to eq(48)
30+
it 'returns a valid totp secret' do
31+
secret = service_response.payload[:secret]
32+
totp = ROTP::TOTP.new(secret)
33+
expect(totp.now).to be_present
3134
end
3235

3336
it 'returns a matching secret' do
34-
signed_totp = Rails.application.message_verifier(:totp_secret).verify(service_response.payload[:signed_secret])
35-
expect(signed_totp).to eq(service_response.payload[:secret])
37+
signed_secret = Rails.application.message_verifier(:totp_secret).verified(
38+
service_response.payload[:signed_secret]
39+
)
40+
expect(signed_secret).to eq(service_response.payload[:secret])
41+
end
42+
43+
it 'returns a signed secret that expires' do
44+
signed_secret = service_response.payload[:signed_secret]
45+
46+
travel_to 31.minutes.from_now do
47+
expect(Rails.application.message_verifier(:totp_secret).verified(signed_secret)).to be_nil
48+
end
3649
end
3750
end
3851
end

spec/services/users/mfa/totp/validate_secret_service_spec.rb

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,37 @@
2828
context 'when user and secret is valid but totp is not' do
2929
let(:current_user) { create(:user) }
3030
let(:secret) { ROTP::Base32.random }
31-
let(:signed_secret) { Rails.application.message_verifier(:totp_secret).generate(secret) }
31+
let(:signed_secret) { Rails.application.message_verifier(:totp_secret).generate(secret, expires_in: 30.minutes) }
3232
let(:current_totp) { '00000' }
3333

3434
it { is_expected.not_to be_success }
3535
it { is_expected.not_to create_audit_event }
3636
end
3737

38+
context 'when signed secret has expired' do
39+
include ActiveSupport::Testing::TimeHelpers
40+
41+
# rubocop:disable RSpec/LetSetup -- referenced in the subject
42+
let!(:current_user) { create(:user) }
43+
let!(:secret) { ROTP::Base32.random }
44+
let!(:signed_secret) do
45+
Rails.application.message_verifier(:totp_secret).generate(secret, expires_in: 30.minutes)
46+
end
47+
let!(:current_totp) { ROTP::TOTP.new(secret).now }
48+
# rubocop:enable RSpec/LetSetup
49+
50+
it 'returns invalid_totp_secret error' do
51+
travel_to 31.minutes.from_now do
52+
expect(service_response).not_to be_success
53+
expect(service_response.payload[:error_code]).to eq(:invalid_totp_secret)
54+
end
55+
end
56+
end
57+
3858
context 'when user is valid and secret is valid and totp is valid' do
3959
let(:current_user) { create(:user) }
4060
let(:secret) { ROTP::Base32.random }
41-
let(:signed_secret) { Rails.application.message_verifier(:totp_secret).generate(secret) }
61+
let(:signed_secret) { Rails.application.message_verifier(:totp_secret).generate(secret, expires_in: 30.minutes) }
4262
let(:current_totp) { ROTP::TOTP.new(secret).now }
4363

4464
it { is_expected.to be_success }

0 commit comments

Comments
 (0)