summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-24 13:11:51 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-24 13:11:51 +0200
commit02e1aefa24cdd0339995d14431713822f4bf4718 (patch)
treea622727150214f8e9dab165ae48803180c38403d /test
parent38920de3910705dac42af2370da7b3ba504577a9 (diff)
Add TOTP enrollment and verification to User, witnessed in the action log
Diffstat (limited to 'test')
-rw-r--r--test/models/user_otp_test.rb83
1 files changed, 83 insertions, 0 deletions
diff --git a/test/models/user_otp_test.rb b/test/models/user_otp_test.rb
new file mode 100644
index 00000000..81f25575
--- /dev/null
+++ b/test/models/user_otp_test.rb
@@ -0,0 +1,83 @@
1require "test_helper"
2
3class UserOtpTest < ActiveSupport::TestCase
4 fixtures :users
5
6 def setup
7 @user = users(:quentin)
8 end
9
10 test "begin_otp_enrollment! stores a pending secret and yields a provisioning URI" do
11 uri = @user.begin_otp_enrollment!
12 assert @user.otp_pending_secret.present?
13 assert_not @user.otp_enrolled?
14 assert_match %r{\Aotpauth://totp/}, uri
15 assert_includes uri, "issuer="
16 end
17
18 test "confirm_otp_enrollment! promotes the pending secret and witnesses it" do
19 @user.begin_otp_enrollment!
20 code = ROTP::TOTP.new(@user.otp_pending_secret).now
21
22 assert @user.confirm_otp_enrollment!(code)
23 assert @user.otp_enrolled?
24 assert_nil @user.otp_pending_secret
25
26 action = NodeAction.where(:action => "otp_enroll").last
27 assert_equal @user, action.user
28 assert_equal [["User", @user.id]],
29 action.action_participants.map { |p| [p.subject_type, p.subject_id] }
30 end
31
32 test "confirm_otp_enrollment! rejects a wrong code and stays unenrolled" do
33 @user.begin_otp_enrollment!
34 assert_not @user.confirm_otp_enrollment!("000000")
35 assert_not @user.otp_enrolled?
36 assert @user.otp_pending_secret.present?
37 end
38
39 test "verify_otp! accepts a current code exactly once" do
40 @user.update!(:otp_secret => ROTP::Base32.random)
41 code = ROTP::TOTP.new(@user.otp_secret).now
42
43 assert @user.verify_otp!(code)
44 assert_not @user.verify_otp!(code), "replayed code must be rejected"
45 end
46
47 test "the confirmation code cannot be replayed at login" do
48 @user.begin_otp_enrollment!
49 code = ROTP::TOTP.new(@user.otp_pending_secret).now
50 @user.confirm_otp_enrollment!(code)
51 assert_not @user.verify_otp!(code)
52 end
53
54 test "verify_otp! rejects wrong codes and unenrolled users" do
55 assert_not @user.verify_otp!("123456")
56 enroll!(@user)
57 assert_not @user.verify_otp!("000000")
58 end
59
60 test "disable_otp! by the user themselves is witnessed as otp_disable" do
61 enroll!(@user)
62 assert @user.disable_otp!(:actor => @user)
63 assert_not @user.otp_enrolled?
64 assert_equal "otp_disable", NodeAction.last.action
65 end
66
67 test "an admin clearing another user's factor is witnessed as otp_reset" do
68 enroll!(@user)
69 admin = users(:aaron)
70 assert @user.disable_otp!(:actor => admin)
71
72 action = NodeAction.where(:action => "otp_reset").last
73 assert_equal admin, action.user
74 assert_equal @user.login, action.metadata["target_login"]
75 end
76
77 private
78
79 def enroll!(user)
80 user.begin_otp_enrollment!
81 user.confirm_otp_enrollment!(ROTP::TOTP.new(user.otp_pending_secret).now)
82 end
83end