active_model_otp 2.2.0 → 2.3.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/.github/workflows/active_model_otp.yml +43 -0
- data/README.md +22 -0
- data/lib/active_model/one_time_password.rb +37 -12
- data/lib/active_model/otp/version.rb +1 -1
- data/test/models/default_interval_user.rb +5 -0
- data/test/models/interval_user.rb +5 -0
- data/test/one_time_password_test.rb +20 -0
- data/test/schema.rb +16 -0
- metadata +7 -3
- data/.travis.yml +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a57a05fda7ae2023dc877a96228e00e5dbca948bc8d17c1058232a42086e285
|
4
|
+
data.tar.gz: 9d3a965117edbf33fb77585126be84e2fd44b613bbbc435c74627b198b87ee5f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 847accabdd4eff2942a917f497197b3be90bb5cf9e9caefb0cd753635994e3bb8b730aab0d9387aa13c4e929793816333d094b620037a8f9c002a7cc3f3ebdcb
|
7
|
+
data.tar.gz: 6a47a8a378bdb2d9155a165e503f672b5c57576c7f9d4fdef271767422a2d9804a141252707cb0181b02c36a91024d1867b9114cdbc95e475c2e4b292f750b65
|
@@ -0,0 +1,43 @@
|
|
1
|
+
name: Active Model OTP
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [main]
|
6
|
+
pull_request:
|
7
|
+
types: [opened, synchronize, reopened, edited]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
ci:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
|
13
|
+
strategy:
|
14
|
+
matrix:
|
15
|
+
gemfile: [rails_4.2, rails_5.0, rails_5.1, rails_5.2, rails_6.0, rails_6.1]
|
16
|
+
ruby-version: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0]
|
17
|
+
exclude:
|
18
|
+
- { gemfile: rails_6.0, ruby-version: 2.3 }
|
19
|
+
- { gemfile: rails_6.1, ruby-version: 2.3 }
|
20
|
+
- { gemfile: rails_6.0, ruby-version: 2.4 }
|
21
|
+
- { gemfile: rails_6.1, ruby-version: 2.4 }
|
22
|
+
- { gemfile: rails_4.2, ruby-version: 2.7 }
|
23
|
+
- { gemfile: rails_4.2, ruby-version: 3.0 }
|
24
|
+
- { gemfile: rails_5.0, ruby-version: 3.0 }
|
25
|
+
- { gemfile: rails_5.1, ruby-version: 3.0 }
|
26
|
+
- { gemfile: rails_5.2, ruby-version: 3.0 }
|
27
|
+
|
28
|
+
env:
|
29
|
+
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
|
30
|
+
|
31
|
+
steps:
|
32
|
+
- uses: actions/checkout@v2
|
33
|
+
|
34
|
+
- name: Install Ruby ${{ matrix.ruby-version }}
|
35
|
+
uses: ruby/setup-ruby@v1
|
36
|
+
with:
|
37
|
+
ruby-version: ${{ matrix.ruby-version }}
|
38
|
+
|
39
|
+
- name: Install dependencies
|
40
|
+
run: bundle install
|
41
|
+
|
42
|
+
- name: Run tests with Ruby ${{ matrix.ruby-version }} and Gemfile ${{ matrix.gemfile }}
|
43
|
+
run: bundle exec rake
|
data/README.md
CHANGED
@@ -213,6 +213,28 @@ user.provisioning_uri(nil, issuer: 'MYAPP') #=> 'otpauth://totp/hello@heapsource
|
|
213
213
|
|
214
214
|
This can then be rendered as a QR Code which can be scanned and added to the users list of OTP credentials.
|
215
215
|
|
216
|
+
### Setting up a customer interval
|
217
|
+
|
218
|
+
If you define a custom interval for TOTP codes, just as `has_one_time_password interval: 10` (for example), remember to include the interval also in `provisioning_uri` method. If not defined, the default value is 30 seconds (according to ROTP gem: https://github.com/mdp/rotp/blob/master/lib/rotp/totp.rb#L9)
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
class User < ApplicationRecord
|
222
|
+
has_one_time_password interval: 10 # the interval value is in seconds
|
223
|
+
end
|
224
|
+
|
225
|
+
user = User.new
|
226
|
+
user.provisioning_uri("hello", interval: 10) # => 'otpauth://totp/hello?secret=2z6hxkdwi3uvrnpn&period=10'
|
227
|
+
|
228
|
+
# This code snippet generates OTP codes that expires every 10 seconds.
|
229
|
+
```
|
230
|
+
|
231
|
+
**Note**: Only some authenticator apps are compatible with custom `period` of tokens, for more details check these links:
|
232
|
+
|
233
|
+
- https://labanskoller.se/blog/2019/07/11/many-common-mobile-authenticator-apps-accept-qr-codes-for-modes-they-dont-support
|
234
|
+
- https://www.ibm.com/docs/en/sva/9.0.7?topic=authentication-configuring-totp-one-time-password-mechanism
|
235
|
+
|
236
|
+
So, be careful and aware when using custom intervals/periods for your TOTP codes beyond the default 30 seconds :)
|
237
|
+
|
216
238
|
### Working example
|
217
239
|
|
218
240
|
Scan the following barcode with your phone, using Google Authenticator
|
@@ -2,25 +2,42 @@ module ActiveModel
|
|
2
2
|
module OneTimePassword
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
+
OTP_DEFAULT_COLUMN_NAME = 'otp_secret_key'.freeze
|
6
|
+
OTP_DEFAULT_COUNTER_COLUMN_NAME = 'otp_counter'.freeze
|
7
|
+
OTP_DEFAULT_BACKUP_CODES_COLUMN_NAME = 'otp_backup_codes'.freeze
|
8
|
+
OTP_DEFAULT_DIGITS = 6
|
9
|
+
OTP_DEFAULT_BACKUP_CODES_COUNT = 12
|
10
|
+
OTP_COUNTER_ENABLED_BY_DEFAULT = false
|
11
|
+
OTP_BACKUP_CODES_ENABLED_BY_DEFAULT = false
|
12
|
+
|
5
13
|
module ClassMethods
|
6
14
|
def has_one_time_password(options = {})
|
7
15
|
cattr_accessor :otp_column_name, :otp_counter_column_name,
|
8
16
|
:otp_backup_codes_column_name
|
9
17
|
class_attribute :otp_digits, :otp_counter_based,
|
10
|
-
:otp_backup_codes_count, :otp_one_time_backup_codes
|
11
|
-
|
12
|
-
self.otp_column_name = (options[:column_name] || "otp_secret_key").to_s
|
13
|
-
self.otp_digits = options[:length] || 6
|
14
|
-
|
15
|
-
self.otp_counter_based = (options[:counter_based] || false)
|
16
|
-
self.otp_counter_column_name = (options[:counter_column_name] || "otp_counter").to_s
|
18
|
+
:otp_backup_codes_count, :otp_one_time_backup_codes,
|
19
|
+
:otp_interval
|
17
20
|
|
21
|
+
self.otp_column_name = (
|
22
|
+
options[:column_name] || OTP_DEFAULT_COLUMN_NAME
|
23
|
+
).to_s
|
24
|
+
self.otp_digits = options[:length] || OTP_DEFAULT_DIGITS
|
25
|
+
self.otp_counter_based = (
|
26
|
+
options[:counter_based] || OTP_COUNTER_ENABLED_BY_DEFAULT
|
27
|
+
)
|
28
|
+
self.otp_counter_column_name = (
|
29
|
+
options[:counter_column_name] || OTP_DEFAULT_COUNTER_COLUMN_NAME
|
30
|
+
).to_s
|
31
|
+
self.otp_interval = options[:interval]
|
18
32
|
self.otp_backup_codes_column_name = (
|
19
|
-
options[:backup_codes_column_name] ||
|
33
|
+
options[:backup_codes_column_name] ||
|
34
|
+
OTP_DEFAULT_BACKUP_CODES_COLUMN_NAME
|
20
35
|
).to_s
|
21
|
-
self.otp_backup_codes_count =
|
36
|
+
self.otp_backup_codes_count = (
|
37
|
+
options[:backup_codes_count] || OTP_DEFAULT_BACKUP_CODES_COUNT
|
38
|
+
)
|
22
39
|
self.otp_one_time_backup_codes = (
|
23
|
-
options[:one_time_backup_codes] ||
|
40
|
+
options[:one_time_backup_codes] || OTP_BACKUP_CODES_ENABLED_BY_DEFAULT
|
24
41
|
)
|
25
42
|
|
26
43
|
include InstanceMethodsOnActivation
|
@@ -144,7 +161,11 @@ module ActiveModel
|
|
144
161
|
end
|
145
162
|
|
146
163
|
def authenticate_totp(code, options = {})
|
147
|
-
totp = ROTP::TOTP.new(
|
164
|
+
totp = ROTP::TOTP.new(
|
165
|
+
otp_column,
|
166
|
+
digits: otp_digits,
|
167
|
+
interval: otp_interval
|
168
|
+
)
|
148
169
|
if (drift = options[:drift])
|
149
170
|
totp.verify(code, drift_behind: drift)
|
150
171
|
else
|
@@ -166,7 +187,11 @@ module ActiveModel
|
|
166
187
|
else
|
167
188
|
options
|
168
189
|
end
|
169
|
-
ROTP::TOTP.new(
|
190
|
+
ROTP::TOTP.new(
|
191
|
+
otp_column,
|
192
|
+
digits: otp_digits,
|
193
|
+
interval: otp_interval
|
194
|
+
).at(time)
|
170
195
|
end
|
171
196
|
|
172
197
|
def authenticate_backup_code(code)
|
@@ -162,4 +162,24 @@ class OtpTest < MiniTest::Test
|
|
162
162
|
def test_otp_random_secret
|
163
163
|
assert_match(/^.{32}$/, @user.class.otp_random_secret)
|
164
164
|
end
|
165
|
+
|
166
|
+
def test_otp_interval
|
167
|
+
@interval_user = IntervalUser.new
|
168
|
+
@interval_user.email = 'roberto@heapsource.com'
|
169
|
+
@interval_user.run_callbacks :create
|
170
|
+
otp_code = @interval_user.otp_code
|
171
|
+
2.times { assert_match(otp_code, @interval_user.otp_code) }
|
172
|
+
sleep 5
|
173
|
+
refute_match(otp_code, @interval_user.otp_code)
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_otp_default_interval
|
177
|
+
@default_interval_user = DefaultIntervalUser.new
|
178
|
+
@default_interval_user.email = 'roberto@heapsource.com'
|
179
|
+
@default_interval_user.run_callbacks :create
|
180
|
+
otp_code = @default_interval_user.otp_code
|
181
|
+
2.times { assert_match(otp_code, @default_interval_user.otp_code) }
|
182
|
+
sleep 5
|
183
|
+
assert_match(otp_code, @default_interval_user.otp_code)
|
184
|
+
end
|
165
185
|
end
|
data/test/schema.rb
CHANGED
@@ -8,4 +8,20 @@ ActiveRecord::Schema.define do
|
|
8
8
|
t.string :otp_secret_key
|
9
9
|
t.timestamps
|
10
10
|
end
|
11
|
+
|
12
|
+
create_table :interval_users, force: true do |t|
|
13
|
+
t.string :key
|
14
|
+
t.string :email
|
15
|
+
t.integer :otp_counter
|
16
|
+
t.string :otp_secret_key
|
17
|
+
t.timestamps
|
18
|
+
end
|
19
|
+
|
20
|
+
create_table :default_interval_users, force: true do |t|
|
21
|
+
t.string :key
|
22
|
+
t.string :email
|
23
|
+
t.integer :otp_counter
|
24
|
+
t.string :otp_secret_key
|
25
|
+
t.timestamps
|
26
|
+
end
|
11
27
|
end
|
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.3.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-06-22 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activemodel
|
@@ -120,8 +120,8 @@ executables: []
|
|
120
120
|
extensions: []
|
121
121
|
extra_rdoc_files: []
|
122
122
|
files:
|
123
|
+
- ".github/workflows/active_model_otp.yml"
|
123
124
|
- ".gitignore"
|
124
|
-
- ".travis.yml"
|
125
125
|
- Appraisals
|
126
126
|
- CHANGELOG.md
|
127
127
|
- Gemfile
|
@@ -139,6 +139,8 @@ files:
|
|
139
139
|
- lib/active_model/otp/version.rb
|
140
140
|
- lib/active_model_otp.rb
|
141
141
|
- test/models/activerecord_user.rb
|
142
|
+
- test/models/default_interval_user.rb
|
143
|
+
- test/models/interval_user.rb
|
142
144
|
- test/models/member.rb
|
143
145
|
- test/models/opt_in_two_factor.rb
|
144
146
|
- test/models/user.rb
|
@@ -171,6 +173,8 @@ specification_version: 4
|
|
171
173
|
summary: Adds methods to set and authenticate against one time passwords.
|
172
174
|
test_files:
|
173
175
|
- test/models/activerecord_user.rb
|
176
|
+
- test/models/default_interval_user.rb
|
177
|
+
- test/models/interval_user.rb
|
174
178
|
- test/models/member.rb
|
175
179
|
- test/models/opt_in_two_factor.rb
|
176
180
|
- test/models/user.rb
|
data/.travis.yml
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
rvm:
|
2
|
-
- 2.3
|
3
|
-
- 2.4
|
4
|
-
- 2.5
|
5
|
-
- 2.6
|
6
|
-
- 2.7
|
7
|
-
- 3.0
|
8
|
-
- ruby-head
|
9
|
-
gemfile:
|
10
|
-
- gemfiles/rails_4.2.gemfile
|
11
|
-
- gemfiles/rails_5.0.gemfile
|
12
|
-
- gemfiles/rails_5.1.gemfile
|
13
|
-
- gemfiles/rails_5.2.gemfile
|
14
|
-
- gemfiles/rails_6.0.gemfile
|
15
|
-
- gemfiles/rails_6.1.gemfile
|
16
|
-
matrix:
|
17
|
-
exclude:
|
18
|
-
- rvm: 2.3
|
19
|
-
gemfile: gemfiles/rails_6.0.gemfile
|
20
|
-
- rvm: 2.3
|
21
|
-
gemfile: gemfiles/rails_6.1.gemfile
|
22
|
-
- rvm: 2.4
|
23
|
-
gemfile: gemfiles/rails_6.0.gemfile
|
24
|
-
- rvm: 2.4
|
25
|
-
gemfile: gemfiles/rails_6.1.gemfile
|
26
|
-
- rvm: 2.7
|
27
|
-
gemfile: gemfiles/rails_4.2.gemfile
|
28
|
-
- rvm: 3.0
|
29
|
-
gemfile: gemfiles/rails_4.2.gemfile
|
30
|
-
- rvm: 3.0
|
31
|
-
gemfile: gemfiles/rails_5.0.gemfile
|
32
|
-
- rvm: 3.0
|
33
|
-
gemfile: gemfiles/rails_5.1.gemfile
|
34
|
-
- rvm: 3.0
|
35
|
-
gemfile: gemfiles/rails_5.2.gemfile
|
36
|
-
fast_finish: true
|
37
|
-
allow_failures:
|
38
|
-
- rvm: ruby-head
|
39
|
-
# include:
|
40
|
-
# - rvm: jruby
|
41
|
-
# env: JRUBY_OPTS="--1.9 --server -Xcext.enabled=true"
|
42
|
-
notifications:
|
43
|
-
email: false
|