active_model_otp 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdc023d65f130fca41bde8f20a7094e587c7940240ae5b36778e54d2efcf76ac
4
- data.tar.gz: 1dab4dd23328538a3a3c8d9a712b9a16d01673b3d997538169126966c653cdf6
3
+ metadata.gz: 2a57a05fda7ae2023dc877a96228e00e5dbca948bc8d17c1058232a42086e285
4
+ data.tar.gz: 9d3a965117edbf33fb77585126be84e2fd44b613bbbc435c74627b198b87ee5f
5
5
  SHA512:
6
- metadata.gz: f896afd30554da73cff730f4277aefe8805b4cbb3da0bf550039b14b8d4611374498b1cf4b0eae2fe80d7a35bdaafdc0ae1558ffbdea3f2f708d2334a81e1af2
7
- data.tar.gz: 0d3b2a4fe8d0e26e9c06bd71e471bcabf4a048f1f1bd531691c0afbc5feda757147ca045ddedec34e587240b0b1337960a9248c0340e6f57ec5d7057cec776ea
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] || 'otp_backup_codes'
33
+ options[:backup_codes_column_name] ||
34
+ OTP_DEFAULT_BACKUP_CODES_COLUMN_NAME
20
35
  ).to_s
21
- self.otp_backup_codes_count = options[:backup_codes_count] || 12
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] || false
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(otp_column, digits: otp_digits)
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(otp_column, digits: otp_digits).at(time)
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)
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  module Otp
3
- VERSION = "2.2.0".freeze
3
+ VERSION = '2.3.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DefaultIntervalUser < ActiveRecord::Base
4
+ has_one_time_password interval: 500
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class IntervalUser < ActiveRecord::Base
4
+ has_one_time_password interval: 2
5
+ end
@@ -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.2.0
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-05-30 00:00:00.000000000 Z
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