Skip to content

Commit 40dac57

Browse files
authored
Merge pull request #1164 from code0-tech/improve-totp-query
Improve usability of totp generate secret mutation
2 parents 57e9dd4 + b0a5e6c commit 40dac57

11 files changed

Lines changed: 87 additions & 15 deletions

File tree

app/graphql/mutations/users/mfa/totp/generate_secret.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@ module Totp
77
class GenerateSecret < BaseMutation
88
description 'Generates an encrypted totp secret'
99

10-
field :secret, String, null: true, description: 'The created and signed secret'
10+
field :secret, String, null: true, description: 'The created secret'
11+
field :signed_secret, String, null: true, description: 'The created and signed secret'
1112

1213
def resolve
13-
::Users::Mfa::Totp::GenerateSecretService.new(current_authentication).execute
14-
.to_mutation_response(success_key: :secret)
14+
response = ::Users::Mfa::Totp::GenerateSecretService.new(current_authentication).execute
15+
16+
return response.to_mutation_response unless response.success?
17+
18+
{
19+
secret: response.payload[:secret],
20+
signed_secret: response.payload[:signed_secret],
21+
errors: [],
22+
}
1523
end
1624
end
1725
end

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: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ def execute
2222
totp_secret = ROTP::Base32.random
2323

2424
ServiceResponse.success(message: 'TOTP secret generated',
25-
payload: Rails.application.message_verifier(:totp_secret)
26-
.generate(totp_secret))
25+
payload: {
26+
signed_secret: Rails.application
27+
.message_verifier(:totp_secret)
28+
.generate(totp_secret, expires_in: 30.minutes),
29+
secret: totp_secret,
30+
})
2731
end
2832
end
2933
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

docs/graphql/mutation/usersmfatotpgeneratesecret.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ Generates an encrypted totp secret
1616
|------|------|-------------|
1717
| `clientMutationId` | [`String`](../scalar/string.md) | A unique identifier for the client performing the mutation. |
1818
| `errors` | [`[Error!]!`](../object/error.md) | Errors encountered during execution of the mutation. |
19-
| `secret` | [`String`](../scalar/string.md) | The created and signed secret |
19+
| `secret` | [`String`](../scalar/string.md) | The created secret |
20+
| `signedSecret` | [`String`](../scalar/string.md) | The created and signed secret |

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
usersMfaTotpGenerateSecret(input: {}) {
1414
#{error_query}
1515
secret
16+
signedSecret
1617
}
1718
}
1819
QUERY
@@ -25,7 +26,13 @@
2526

2627
it 'generates secret' do
2728
mutate!
28-
expect(graphql_data_at(:users_mfa_totp_generate_secret, :secret)).to be_present
29+
secret = graphql_data_at(:users_mfa_totp_generate_secret, :secret)
30+
signed_secret = graphql_data_at(:users_mfa_totp_generate_secret, :signed_secret)
31+
32+
expect(secret).to be_present
33+
34+
signed_totp = Rails.application.message_verifier(:totp_secret).verified(signed_secret)
35+
expect(signed_totp).to eq(secret)
2936
end
3037
end
3138
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: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +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.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
34+
end
35+
36+
it 'returns a matching secret' do
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
3149
end
3250
end
3351
end

0 commit comments

Comments
 (0)