active_model_otp 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/active_model/one_time_password.rb +47 -27
- data/lib/active_model/otp/version.rb +1 -1
- data/test/models/user.rb +1 -0
- data/test/one_time_password_test.rb +49 -18
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bdc023d65f130fca41bde8f20a7094e587c7940240ae5b36778e54d2efcf76ac
|
4
|
+
data.tar.gz: 1dab4dd23328538a3a3c8d9a712b9a16d01673b3d997538169126966c653cdf6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f896afd30554da73cff730f4277aefe8805b4cbb3da0bf550039b14b8d4611374498b1cf4b0eae2fe80d7a35bdaafdc0ae1558ffbdea3f2f708d2334a81e1af2
|
7
|
+
data.tar.gz: 0d3b2a4fe8d0e26e9c06bd71e471bcabf4a048f1f1bd531691c0afbc5feda757147ca045ddedec34e587240b0b1337960a9248c0340e6f57ec5d7057cec776ea
|
@@ -58,37 +58,17 @@ module ActiveModel
|
|
58
58
|
return true if backup_codes_enabled? && authenticate_backup_code(code)
|
59
59
|
|
60
60
|
if otp_counter_based
|
61
|
-
|
62
|
-
result = hotp.verify(code, otp_counter)
|
63
|
-
if result && options[:auto_increment]
|
64
|
-
self.otp_counter += 1
|
65
|
-
save if respond_to?(:changed?) && !new_record?
|
66
|
-
end
|
67
|
-
result
|
61
|
+
otp_counter == authenticate_hotp(code, options)
|
68
62
|
else
|
69
|
-
|
70
|
-
if drift = options[:drift]
|
71
|
-
totp.verify(code, drift_behind: drift)
|
72
|
-
else
|
73
|
-
totp.verify(code)
|
74
|
-
end
|
63
|
+
authenticate_totp(code, options).present?
|
75
64
|
end
|
76
65
|
end
|
77
66
|
|
78
67
|
def otp_code(options = {})
|
79
68
|
if otp_counter_based
|
80
|
-
|
81
|
-
self.otp_counter += 1
|
82
|
-
save if respond_to?(:changed?) && !new_record?
|
83
|
-
end
|
84
|
-
ROTP::HOTP.new(otp_column, digits: otp_digits).at(self.otp_counter)
|
69
|
+
hotp_code(options)
|
85
70
|
else
|
86
|
-
|
87
|
-
time = options.fetch(:time, Time.now)
|
88
|
-
else
|
89
|
-
time = options
|
90
|
-
end
|
91
|
-
ROTP::TOTP.new(otp_column, digits: otp_digits).at(time)
|
71
|
+
totp_code(options)
|
92
72
|
end
|
93
73
|
end
|
94
74
|
|
@@ -97,9 +77,13 @@ module ActiveModel
|
|
97
77
|
account ||= ""
|
98
78
|
|
99
79
|
if otp_counter_based
|
100
|
-
ROTP::HOTP
|
80
|
+
ROTP::HOTP
|
81
|
+
.new(otp_column, options)
|
82
|
+
.provisioning_uri(account, self.otp_counter)
|
101
83
|
else
|
102
|
-
ROTP::TOTP
|
84
|
+
ROTP::TOTP
|
85
|
+
.new(otp_column, options)
|
86
|
+
.provisioning_uri(account)
|
103
87
|
end
|
104
88
|
end
|
105
89
|
|
@@ -149,10 +133,46 @@ module ActiveModel
|
|
149
133
|
|
150
134
|
private
|
151
135
|
|
136
|
+
def authenticate_hotp(code, options = {})
|
137
|
+
hotp = ROTP::HOTP.new(otp_column, digits: otp_digits)
|
138
|
+
result = hotp.verify(code, otp_counter)
|
139
|
+
if result && options[:auto_increment]
|
140
|
+
self.otp_counter += 1
|
141
|
+
save if respond_to?(:changed?) && !new_record?
|
142
|
+
end
|
143
|
+
result
|
144
|
+
end
|
145
|
+
|
146
|
+
def authenticate_totp(code, options = {})
|
147
|
+
totp = ROTP::TOTP.new(otp_column, digits: otp_digits)
|
148
|
+
if (drift = options[:drift])
|
149
|
+
totp.verify(code, drift_behind: drift)
|
150
|
+
else
|
151
|
+
totp.verify(code)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def hotp_code(options = {})
|
156
|
+
if options[:auto_increment]
|
157
|
+
self.otp_counter += 1
|
158
|
+
save if respond_to?(:changed?) && !new_record?
|
159
|
+
end
|
160
|
+
ROTP::HOTP.new(otp_column, digits: otp_digits).at(otp_counter)
|
161
|
+
end
|
162
|
+
|
163
|
+
def totp_code(options = {})
|
164
|
+
time = if options.is_a?(Hash)
|
165
|
+
options.fetch(:time, Time.now)
|
166
|
+
else
|
167
|
+
options
|
168
|
+
end
|
169
|
+
ROTP::TOTP.new(otp_column, digits: otp_digits).at(time)
|
170
|
+
end
|
171
|
+
|
152
172
|
def authenticate_backup_code(code)
|
153
173
|
backup_codes_column_name = self.class.otp_backup_codes_column_name
|
154
174
|
backup_codes = public_send(backup_codes_column_name)
|
155
|
-
return false unless backup_codes.include?(code)
|
175
|
+
return false unless backup_codes.present? && backup_codes.include?(code)
|
156
176
|
|
157
177
|
if self.class.otp_one_time_backup_codes
|
158
178
|
backup_codes.delete(code)
|
data/test/models/user.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
2
4
|
|
3
5
|
class OtpTest < MiniTest::Test
|
4
6
|
def setup
|
@@ -58,30 +60,30 @@ class OtpTest < MiniTest::Test
|
|
58
60
|
|
59
61
|
@opt_in.otp_regenerate_secret
|
60
62
|
code = @opt_in.otp_code
|
61
|
-
|
63
|
+
assert_equal true, @opt_in.authenticate_otp(code)
|
62
64
|
end
|
63
65
|
|
64
66
|
def test_authenticate_with_otp_when_drift_is_allowed
|
65
67
|
code = @user.otp_code(Time.now - 30)
|
66
|
-
|
68
|
+
assert_equal true, @user.authenticate_otp(code, drift: 60)
|
67
69
|
|
68
70
|
code = @visitor.otp_code(Time.now - 30)
|
69
|
-
|
71
|
+
assert_equal true, @visitor.authenticate_otp(code, drift: 60)
|
70
72
|
end
|
71
73
|
|
72
74
|
def test_authenticate_with_backup_code
|
73
75
|
backup_code = @user.public_send(@user.otp_backup_codes_column_name).first
|
74
|
-
|
76
|
+
assert_equal true, @user.authenticate_otp(backup_code)
|
75
77
|
|
76
78
|
backup_code = @user.public_send(@user.otp_backup_codes_column_name).last
|
77
79
|
@user.otp_regenerate_backup_codes
|
78
|
-
|
80
|
+
assert_equal true, !@user.authenticate_otp(backup_code)
|
79
81
|
end
|
80
82
|
|
81
83
|
def test_authenticate_with_one_time_backup_code
|
82
84
|
backup_code = @user.public_send(@user.otp_backup_codes_column_name).first
|
83
|
-
|
84
|
-
|
85
|
+
assert_equal true, @user.authenticate_otp(backup_code)
|
86
|
+
assert_equal true, !@user.authenticate_otp(backup_code)
|
85
87
|
end
|
86
88
|
|
87
89
|
def test_otp_code
|
@@ -100,22 +102,51 @@ class OtpTest < MiniTest::Test
|
|
100
102
|
end
|
101
103
|
|
102
104
|
def test_provisioning_uri_with_provided_account
|
103
|
-
|
104
|
-
|
105
|
-
|
105
|
+
totp = %r{^otpauth://totp/roberto\?secret=\w{32}$}
|
106
|
+
hotp = %r{^otpauth://hotp/roberto\?secret=\w{32}&counter=1$}
|
107
|
+
|
108
|
+
assert_match totp, @user.provisioning_uri('roberto')
|
109
|
+
assert_match totp, @visitor.provisioning_uri('roberto')
|
110
|
+
assert_match hotp, @member.provisioning_uri('roberto')
|
106
111
|
end
|
107
112
|
|
108
113
|
def test_provisioning_uri_with_email_field
|
109
|
-
|
110
|
-
|
111
|
-
|
114
|
+
totp = %r{^otpauth://totp/roberto%40heapsource\.com\?secret=\w{32}$}
|
115
|
+
hotp = %r{^otpauth://hotp/\?secret=\w{32}&counter=1$}
|
116
|
+
|
117
|
+
assert_match totp, @user.provisioning_uri
|
118
|
+
assert_match totp, @visitor.provisioning_uri
|
119
|
+
assert_match hotp, @member.provisioning_uri
|
112
120
|
end
|
113
121
|
|
114
122
|
def test_provisioning_uri_with_options
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
123
|
+
account = %r{
|
124
|
+
^otpauth://totp/Example\:roberto\?secret=\w{32}&issuer=Example$
|
125
|
+
}x
|
126
|
+
|
127
|
+
email = %r{
|
128
|
+
^otpauth://totp/Example\:roberto%40heapsource\.com\?secret=\w{32}
|
129
|
+
&issuer=Example$
|
130
|
+
}x
|
131
|
+
|
132
|
+
assert_match(
|
133
|
+
account, @user.provisioning_uri('roberto', issuer: 'Example')
|
134
|
+
)
|
135
|
+
|
136
|
+
assert_match(
|
137
|
+
account, @visitor.provisioning_uri('roberto', issuer: 'Example')
|
138
|
+
)
|
139
|
+
|
140
|
+
assert_match email, @user.provisioning_uri(nil, issuer: 'Example')
|
141
|
+
assert_match email, @visitor.provisioning_uri(nil, issuer: 'Example')
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_provisioning_uri_with_incremented_counter
|
145
|
+
2.times { @member.otp_code(auto_increment: true) }
|
146
|
+
|
147
|
+
hotp = %r{^otpauth://hotp/\?secret=\w{32}&counter=3$}
|
148
|
+
|
149
|
+
assert_match hotp, @member.provisioning_uri
|
119
150
|
end
|
120
151
|
|
121
152
|
def test_regenerate_otp
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_model_otp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillermo Iguaran
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2021-
|
13
|
+
date: 2021-05-30 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activemodel
|