active_model_otp 2.1.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|