1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
require "test_helper"
class UserOtpTest < ActiveSupport::TestCase
fixtures :users
def setup
@user = users(:quentin)
end
test "begin_otp_enrollment! stores a pending secret and yields a provisioning URI" do
uri = @user.begin_otp_enrollment!
assert @user.otp_pending_secret.present?
assert_not @user.otp_enrolled?
assert_match %r{\Aotpauth://totp/}, uri
assert_includes uri, "issuer="
end
test "confirm_otp_enrollment! promotes the pending secret and witnesses it" do
@user.begin_otp_enrollment!
code = ROTP::TOTP.new(@user.otp_pending_secret).now
assert @user.confirm_otp_enrollment!(code)
assert @user.otp_enrolled?
assert_nil @user.otp_pending_secret
action = NodeAction.where(:action => "otp_enroll").last
assert_equal @user, action.user
assert_equal [["User", @user.id]],
action.action_participants.map { |p| [p.subject_type, p.subject_id] }
end
test "confirm_otp_enrollment! rejects a wrong code and stays unenrolled" do
@user.begin_otp_enrollment!
assert_not @user.confirm_otp_enrollment!("000000")
assert_not @user.otp_enrolled?
assert @user.otp_pending_secret.present?
end
test "verify_otp! accepts a current code exactly once" do
@user.update!(:otp_secret => ROTP::Base32.random)
code = ROTP::TOTP.new(@user.otp_secret).now
assert @user.verify_otp!(code)
assert_not @user.verify_otp!(code), "replayed code must be rejected"
end
test "the confirmation code cannot be replayed at login" do
@user.begin_otp_enrollment!
code = ROTP::TOTP.new(@user.otp_pending_secret).now
@user.confirm_otp_enrollment!(code)
assert_not @user.verify_otp!(code)
end
test "verify_otp! rejects wrong codes and unenrolled users" do
assert_not @user.verify_otp!("123456")
enroll!(@user)
assert_not @user.verify_otp!("000000")
end
test "disable_otp! by the user themselves is witnessed as otp_disable" do
enroll!(@user)
assert @user.disable_otp!(:actor => @user)
assert_not @user.otp_enrolled?
assert_equal "otp_disable", NodeAction.last.action
end
test "an admin clearing another user's factor is witnessed as otp_reset" do
enroll!(@user)
admin = users(:aaron)
assert @user.disable_otp!(:actor => admin)
action = NodeAction.where(:action => "otp_reset").last
assert_equal admin, action.user
assert_equal @user.login, action.metadata["target_login"]
end
test "an admin without a factor can start enrollment" do
admin = users(:aaron)
assert admin.is_admin?
assert_not admin.otp_enrolled?
uri = admin.begin_otp_enrollment!
assert admin.reload.otp_pending_secret.present?
assert uri.present?
end
test "an admin disabling their factor keeps the role" do
admin = users(:aaron)
admin.update_column(:otp_secret, ROTP::Base32.random)
admin.disable_otp!(:actor => admin)
assert_not admin.reload.otp_enrolled?
assert admin.is_admin?
end
test "a fellow admin can reset an admin's factor" do
admin = users(:aaron)
admin.update_column(:otp_secret, ROTP::Base32.random)
actor = users(:redella)
admin.disable_otp!(:actor => actor)
assert_not admin.reload.otp_enrolled?
assert_equal "otp_reset", NodeAction.last.action
end
private
def enroll!(user)
user.begin_otp_enrollment!
user.confirm_otp_enrollment!(ROTP::TOTP.new(user.otp_pending_secret).now)
end
end
|