active_model_otp 1.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
- SHA1:
3
- metadata.gz: a53fb4e15fa9408222f26928a84d9b1f67919b86
4
- data.tar.gz: f436275d42c9e8e78c074809be7cbc06da3e9c16
2
+ SHA256:
3
+ metadata.gz: 2a57a05fda7ae2023dc877a96228e00e5dbca948bc8d17c1058232a42086e285
4
+ data.tar.gz: 9d3a965117edbf33fb77585126be84e2fd44b613bbbc435c74627b198b87ee5f
5
5
  SHA512:
6
- metadata.gz: 4c409cf8191c2511f7f7c984d0b2623463f65a835e5b86d6cec3dd59b9f91836f8a5d5308b164d328c773dbebe76c51e0c9ae0503048bc81e8326280d57a2379
7
- data.tar.gz: 2b69f18d93545ea5e0e41bc3bc10930887f2f7f1b95dbee947b6e9eed256cc5b78f80014fa7e11e2f3d271f204ccf5dad9b412f9c1193404dcda64478e1d116a
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/.gitignore CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  .ruby-version
19
+ gemfiles/*.lock
data/Appraisals ADDED
@@ -0,0 +1,36 @@
1
+ appraise "rails-4.2" do
2
+ gem "activemodel", "~> 4.2"
3
+ gem "sqlite3", "~> 1.3.6"
4
+ end
5
+
6
+ appraise "rails-5.0" do
7
+ gem "activemodel", "~> 5.0"
8
+ gem "activemodel-serializers-xml"
9
+ gem "sqlite3", "~> 1.3.6"
10
+ end
11
+
12
+ appraise "rails-5.1" do
13
+ gem "activemodel", "~> 5.1"
14
+ gem "activemodel-serializers-xml"
15
+ gem "sqlite3", "~> 1.3.6"
16
+ end
17
+
18
+ appraise "rails-5.2" do
19
+ gem "activemodel", "~> 5.2"
20
+ gem "activemodel-serializers-xml"
21
+ gem "sqlite3", "~> 1.3.6"
22
+ end
23
+
24
+ appraise "rails-6.0" do
25
+ gem "activerecord", "~> 6.0"
26
+ gem "activemodel", "~> 6.0"
27
+ gem "activemodel-serializers-xml"
28
+ gem "sqlite3", "~> 1.4"
29
+ end
30
+
31
+ appraise "rails-6.1" do
32
+ gem "activerecord", "~> 6.1"
33
+ gem "activemodel", "~> 6.1"
34
+ gem "activemodel-serializers-xml"
35
+ gem "sqlite3", "~> 1.4"
36
+ end
data/CHANGELOG.md CHANGED
@@ -1,17 +1 @@
1
- #v1.2.0
2
- - Added Counter based OTP (HOTP) (@ResultsMayVary ) https://github.com/heapsource/active_model_otp/pull/19
3
- - Adding options to provisioning uri, so we can include issuer (@doon) https://github.com/heapsource/active_model_otp/pull/15
4
-
5
- #v1.1.0
6
- - Add function to re-geterante the OTP secret (@TikiTDO) https://github.com/heapsource/active_model_otp/pull/14
7
- - Added option to pass OTP length (@shivanibhanwal) https://github.com/heapsource/active_model_otp/pull/13
8
-
9
- #v1.0.0
10
- - Avoid overriding predefined otp_column value when initializing resource (Ilan Stern) https://github.com/heapsource/active_model_otp/pull/10
11
- - Pad OTP codes with less than 6 digits (Johan Brissmyr) https://github.com/heapsource/active_model_otp/pull/7
12
- - Get rid of deprecation warnings in Rails 4.1 (Nick DeMonner)
13
-
14
- #v0.1.0
15
- - OTP codes can be in 5 or 6 digits (André Luis Leal Cardoso Junior)
16
- - Require 'cgi', rotp needs it for encoding parameters (André Luis Leal Cardoso Junior)
17
- - Change column name for otp secret key (robertomiranda)
1
+ CHANGELOG it's been deprecated in favor of https://github.com/heapsource/active_model_otp/releases
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Guillermo Iguaran, Roberto Miranda, Firebase.co
1
+ Copyright (c) 2013-2019 Roberto Miranda, Guillermo Iguaran and contributors.
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,12 +1,16 @@
1
1
  [![Build Status](https://travis-ci.org/heapsource/active_model_otp.png)](https://travis-ci.org/heapsource/active_model_otp)
2
2
  [![Gem Version](https://badge.fury.io/rb/active_model_otp.svg)](http://badge.fury.io/rb/active_model_otp)
3
- [![Dependency Status](https://gemnasium.com/heapsource/active_model_otp.svg)](https://gemnasium.com/heapsource/active_model_otp)
4
- [![Code Climate](https://codeclimate.com/github/heapsource/active_model_otp/badges/gpa.svg)](https://codeclimate.com/github/heapsource/active_model_otp)
3
+ [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
5
4
 
6
5
 
7
6
  # ActiveModel::Otp
8
7
 
9
- **ActiveModel::Otp** makes adding **Two Factor Authentication** (TFA) to a model simple. Let's see what's required to get AMo::Otp working in our Application, using Rails 4.0 (AMo::Otp is also compatible with Rails 3.x versions). We're going to use a User model and some authentication to do it. Inspired by AM::SecurePassword
8
+ **ActiveModel::Otp** makes adding **Two Factor Authentication** (TFA) to a model simple. Let's see what's required to get AMo::Otp working in our Application, using Rails 5.0 (AMo::Otp is also compatible with Rails 4.x versions). We're going to use a User model and try to add options provided by **ActiveModel::Otp**. Inspired by AM::SecurePassword
9
+
10
+ ## Dependencies
11
+
12
+ * [ROTP](https://github.com/mdp/rotp) 6.2.0 or higher
13
+ * Ruby 2.3 or greater
10
14
 
11
15
  ## Installation
12
16
 
@@ -36,27 +40,27 @@ rails g migration AddOtpSecretKeyToUsers otp_secret_key:string
36
40
  We’ll then need to run rake db:migrate to update the users table in the database. The next step is to update the model code. We need to use has_one_time_password to make it use TFA.
37
41
 
38
42
  ```ruby
39
- class User < ActiveRecord::Base
43
+ class User < ApplicationRecord
40
44
  has_one_time_password
41
45
  end
42
46
  ```
43
47
 
44
48
  Note: If you're adding this to an existing user model you'll need to generate *otp_secret_key* with a migration like:
45
49
  ```ruby
46
- User.all.each { |user| user.update_attribute(:otp_secret_key, ROTP::Base32.random_base32) }
50
+ User.find_each { |user| user.update_attribute(:otp_secret_key, User.otp_random_secret) }
47
51
  ```
48
52
 
49
53
  To use a custom column to store the secret key field you can use the column_name option. It is also possible to generate codes with a specified length.
50
54
 
51
55
  ```ruby
52
- class User < ActiveRecord::Base
56
+ class User < ApplicationRecord
53
57
  has_one_time_password column_name: :my_otp_secret_column, length: 4
54
58
  end
55
59
  ```
56
60
 
57
61
  ## Usage
58
62
 
59
- The has_one_time_password statement provides to the model some useful methods in order to implement our TFA system. AMo:Otp generates one time passwords according to [TOTP RFC 6238](http://tools.ietf.org/html/rfc4226) and the [HOTP RFC 4226](http://tools.ietf.org/html/rfc4226). This is compatible with Google Authenticator apps available for Android and iPhone, and now in use on GMail.
63
+ The has_one_time_password statement provides to the model some useful methods in order to implement our TFA system. AMo:Otp generates one time passwords according to [TOTP RFC 6238](https://tools.ietf.org/html/rfc6238) and the [HOTP RFC 4226](https://www.ietf.org/rfc/rfc4226). This is compatible with Google Authenticator apps available for Android and iPhone, and now in use on GMail.
60
64
 
61
65
  The otp_secret_key is saved automatically when an object is created,
62
66
 
@@ -76,9 +80,6 @@ user.otp_code # => '850738'
76
80
 
77
81
  # Override current time
78
82
  user.otp_code(time: Time.now + 3600) # => '317438'
79
-
80
- # Don't zero-pad to six digits
81
- user.otp_code(padding: false) # => '438'
82
83
  ```
83
84
 
84
85
  ### Authenticating using a code
@@ -108,10 +109,15 @@ rails g migration AddCounterForOtpToUsers otp_counter:integer
108
109
  create db/migrate/20130707010931_add_counter_for_otp_to_users.rb
109
110
  ```
110
111
 
112
+ Set default value for otp_counter to 0.
113
+ ```ruby
114
+ change_column :users, :otp_counter, :integer, default: 0
115
+ ```
116
+
111
117
  In addition set the counter flag option to true
112
118
 
113
119
  ```ruby
114
- class User < ActiveRecord::Base
120
+ class User < ApplicationRecord
115
121
  has_one_time_password counter_based: true
116
122
  end
117
123
  ```
@@ -119,7 +125,7 @@ end
119
125
  And for a custom counter column
120
126
 
121
127
  ```ruby
122
- class User < ActiveRecord::Base
128
+ class User < ApplicationRecord
123
129
  has_one_time_password counter_based: true, counter_column_name: :my_otp_secret_counter_column
124
130
  end
125
131
  ```
@@ -144,6 +150,51 @@ user.otp_code(auto_increment: true) # => '002811'
144
150
  user.otp_code # => '002811'
145
151
  ```
146
152
 
153
+ ## Backup codes
154
+
155
+ We're going to add a field to our ``User`` Model, so each user can have an otp backup codes. The next step is to run the migration generator in order to add the backup codes field.
156
+
157
+ ```ruby
158
+ rails g migration AddOtpBackupCodesToUsers otp_backup_codes:text
159
+ =>
160
+ invoke active_record
161
+ create db/migrate/20210126030834_add_otp_backup_codes_to_users.rb
162
+ ```
163
+
164
+ You can change backup codes column name by option `backup_codes_column_name`:
165
+
166
+ ```ruby
167
+ class User < ApplicationRecord
168
+ has_one_time_password backup_codes_column_name: 'secret_codes'
169
+ end
170
+ ```
171
+
172
+ Then use array type in schema or serialize attribute in model as Array (depending on used db type). Or even consider to use some libs like (lockbox)[https://github.com/ankane/lockbox] with type array.
173
+
174
+ After that user can use one of automatically generated backup codes for authentication using same method `authenticate_otp`.
175
+
176
+ By default it generates 12 backup codes. You can change it by option `backup_codes_count`:
177
+
178
+ ```ruby
179
+ class User < ApplicationRecord
180
+ has_one_time_password backup_codes_count: 6
181
+ end
182
+ ```
183
+
184
+ By default each backup code can be reused an infinite number of times. You can
185
+ change it with option `one_time_backup_codes`:
186
+
187
+ ```ruby
188
+ class User < ApplicationRecord
189
+ has_one_time_password one_time_backup_codes: true
190
+ end
191
+ ```
192
+
193
+ ```ruby
194
+ user.authenticate_otp('186522') # => true
195
+ user.authenticate_otp('186522') # => false
196
+ ```
197
+
147
198
  ## Google Authenticator Compatible
148
199
 
149
200
  The library works with the Google Authenticator iPhone and Android app, and also includes the ability to generate provisioning URI's to use with the QR Code scanner built into the app.
@@ -154,10 +205,36 @@ user.provisioning_uri # => 'otpauth://totp/hello@heapsource.com?secret=2z6hxkdwi
154
205
 
155
206
  # Use a custom field to generate the provisioning_url
156
207
  user.provisioning_uri("hello") # => 'otpauth://totp/hello?secret=2z6hxkdwi3uvrnpn'
208
+
209
+ # You can customize the generated url, by passing a hash of Options
210
+ # `:issuer` lets you set the Issuer name in Google Authenticator, so it doesn't show as a blank entry.
211
+ user.provisioning_uri(nil, issuer: 'MYAPP') #=> 'otpauth://totp/hello@heapsource.com?secret=2z6hxkdwi3uvrnpn&issuer=MYAPP'
157
212
  ```
158
213
 
159
214
  This can then be rendered as a QR Code which can be scanned and added to the users list of OTP credentials.
160
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
+
161
238
  ### Working example
162
239
 
163
240
  Scan the following barcode with your phone, using Google Authenticator
@@ -168,6 +245,7 @@ Now run the following and compare the output
168
245
 
169
246
  ```ruby
170
247
  require "active_model_otp"
248
+
171
249
  class User
172
250
  extend ActiveModel::Callbacks
173
251
  include ActiveModel::Validations
@@ -178,6 +256,7 @@ class User
178
256
 
179
257
  has_one_time_password
180
258
  end
259
+
181
260
  user = User.new
182
261
  user.email = 'roberto@heapsource.com'
183
262
  user.otp_secret_key = "2z6hxkdwi3uvrnpn"
@@ -187,10 +266,10 @@ puts "Current code #{user.otp_code}"
187
266
  **Note:** otp_secret_key must be generated using RFC 3548 base32 key strings (for compatilibity with google authenticator)
188
267
 
189
268
  ### Useful Examples
190
-
269
+ - [Drifting Ruby Tutorial](https://www.driftingruby.com/episodes/two-factor-authentication)
191
270
  - [Generate QR code with rqrcode gem](https://github.com/heapsource/active_model_otp/wiki/Generate-QR-code-with-rqrcode-gem)
192
271
  - Generating QR Code with Google Charts API
193
- - [Sendind code via email with Twilio](https://github.com/heapsource/active_model_otp/wiki/Send-code-via-Twilio-SMS)
272
+ - [Sending code via SMS with Twilio](https://github.com/heapsource/active_model_otp/wiki/Send-code-via-Twilio-SMS)
194
273
  - [Using with Mongoid](https://github.com/heapsource/active_model_otp/wiki/Using-with-Mongoid)
195
274
 
196
275
  ## Contributing
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.version = ActiveModel::Otp::VERSION
9
9
  spec.authors = ["Guillermo Iguaran", "Roberto Miranda", "Heapsource"]
10
10
  spec.email = ["guilleiguaran@gmail.com", "rjmaltamar@gmail.com", "hello@firebase.co"]
11
- spec.description = %q{Adds methods to set and authenticate against one time passwords. Inspired in AM::SecurePassword"}
11
+ spec.description = %q{Adds methods to set and authenticate against one time passwords 2FA(Two factor Authentication). Inspired in AM::SecurePassword"}
12
12
  spec.summary = "Adds methods to set and authenticate against one time passwords."
13
13
  spec.homepage = ""
14
14
  spec.license = "MIT"
@@ -18,10 +18,19 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.required_ruby_version = ">= 2.3"
22
+
21
23
  spec.add_dependency "activemodel"
22
- spec.add_dependency "rotp"
24
+ spec.add_dependency "rotp", "~> 6.2.0"
23
25
 
24
- spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "activerecord"
25
27
  spec.add_development_dependency "rake"
26
28
  spec.add_development_dependency "minitest", "~> 5.4.2"
29
+ spec.add_development_dependency "appraisal"
30
+
31
+ if RUBY_PLATFORM == "java"
32
+ spec.add_development_dependency "activerecord-jdbcsqlite3-adapter"
33
+ else
34
+ spec.add_development_dependency "sqlite3"
35
+ end
27
36
  end
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", "~> 4.2"
6
+ gem "sqlite3", "~> 1.3.6"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", "~> 5.0"
6
+ gem "activemodel-serializers-xml"
7
+ gem "sqlite3", "~> 1.3.6"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", "~> 5.1"
6
+ gem "activemodel-serializers-xml"
7
+ gem "sqlite3", "~> 1.3.6"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", "~> 5.2"
6
+ gem "activemodel-serializers-xml"
7
+ gem "sqlite3", "~> 1.3.6"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.0"
6
+ gem "activemodel", "~> 6.0"
7
+ gem "activemodel-serializers-xml"
8
+ gem "sqlite3", "~> 1.4"
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.1"
6
+ gem "activemodel", "~> 6.1"
7
+ gem "activemodel-serializers-xml"
8
+ gem "sqlite3", "~> 1.4"
9
+
10
+ gemspec path: "../"
@@ -2,25 +2,50 @@ module ActiveModel
2
2
  module OneTimePassword
3
3
  extend ActiveSupport::Concern
4
4
 
5
- module ClassMethods
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
6
12
 
13
+ module ClassMethods
7
14
  def has_one_time_password(options = {})
8
- cattr_accessor :otp_column_name, :otp_counter_column_name
9
- class_attribute :otp_digits, :otp_counter_based
10
-
11
- self.otp_column_name = (options[:column_name] || "otp_secret_key").to_s
12
- self.otp_digits = options[:length] || 6
13
-
14
- self.otp_counter_based = (options[:counter_based] || false)
15
+ cattr_accessor :otp_column_name, :otp_counter_column_name,
16
+ :otp_backup_codes_column_name
17
+ class_attribute :otp_digits, :otp_counter_based,
18
+ :otp_backup_codes_count, :otp_one_time_backup_codes,
19
+ :otp_interval
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
+ )
15
28
  self.otp_counter_column_name = (
16
- options[:counter_column_name] || "otp_counter"
17
- ).to_s
29
+ options[:counter_column_name] || OTP_DEFAULT_COUNTER_COLUMN_NAME
30
+ ).to_s
31
+ self.otp_interval = options[:interval]
32
+ self.otp_backup_codes_column_name = (
33
+ options[:backup_codes_column_name] ||
34
+ OTP_DEFAULT_BACKUP_CODES_COLUMN_NAME
35
+ ).to_s
36
+ self.otp_backup_codes_count = (
37
+ options[:backup_codes_count] || OTP_DEFAULT_BACKUP_CODES_COUNT
38
+ )
39
+ self.otp_one_time_backup_codes = (
40
+ options[:one_time_backup_codes] || OTP_BACKUP_CODES_ENABLED_BY_DEFAULT
41
+ )
18
42
 
19
43
  include InstanceMethodsOnActivation
20
44
 
21
- before_create do
45
+ before_create(**options.slice(:if, :unless)) do
22
46
  self.otp_regenerate_secret if !otp_column
23
47
  self.otp_regenerate_counter if otp_counter_based && !otp_counter
48
+ otp_regenerate_backup_codes if backup_codes_enabled?
24
49
  end
25
50
 
26
51
  if respond_to?(:attributes_protected_by_default)
@@ -29,11 +54,17 @@ module ActiveModel
29
54
  end
30
55
  end
31
56
  end
57
+
58
+ # Defaults to 160 bit long secret
59
+ # (meaning a 32 character long base32 secret)
60
+ def otp_random_secret(length = 20)
61
+ ROTP::Base32.random(length)
62
+ end
32
63
  end
33
64
 
34
65
  module InstanceMethodsOnActivation
35
66
  def otp_regenerate_secret
36
- self.otp_column = ROTP::Base32.random_base32
67
+ self.otp_column = self.class.otp_random_secret
37
68
  end
38
69
 
39
70
  def otp_regenerate_counter
@@ -41,67 +72,140 @@ module ActiveModel
41
72
  end
42
73
 
43
74
  def authenticate_otp(code, options = {})
75
+ return true if backup_codes_enabled? && authenticate_backup_code(code)
76
+
44
77
  if otp_counter_based
45
- hotp = ROTP::HOTP.new(otp_column, digits: otp_digits)
46
- result = hotp.verify(code, otp_counter)
47
- if result && options[:auto_increment]
48
- self.otp_counter += 1
49
- save if !new_record?
50
- end
51
- result
78
+ otp_counter == authenticate_hotp(code, options)
52
79
  else
53
- totp = ROTP::TOTP.new(otp_column, digits: otp_digits)
54
- if drift = options[:drift]
55
- totp.verify_with_drift(code, drift)
56
- else
57
- totp.verify(code)
58
- end
80
+ authenticate_totp(code, options).present?
59
81
  end
60
82
  end
61
83
 
62
84
  def otp_code(options = {})
63
85
  if otp_counter_based
64
- if options[:auto_increment]
65
- self.otp_counter += 1
66
- save if !new_record?
67
- end
68
- ROTP::HOTP.new(otp_column, digits: otp_digits).at(self.otp_counter)
86
+ hotp_code(options)
69
87
  else
70
- if options.is_a? Hash
71
- time = options.fetch(:time, Time.now)
72
- padding = options.fetch(:padding, true)
73
- else
74
- time = options
75
- padding = true
76
- end
77
- ROTP::TOTP.new(otp_column, digits: otp_digits).at(time, padding)
88
+ totp_code(options)
78
89
  end
79
90
  end
80
91
 
81
92
  def provisioning_uri(account = nil, options = {})
82
93
  account ||= self.email if self.respond_to?(:email)
94
+ account ||= ""
83
95
 
84
96
  if otp_counter_based
85
- ROTP::HOTP.new(otp_column, options).provisioning_uri(account)
97
+ ROTP::HOTP
98
+ .new(otp_column, options)
99
+ .provisioning_uri(account, self.otp_counter)
86
100
  else
87
- ROTP::TOTP.new(otp_column, options).provisioning_uri(account)
101
+ ROTP::TOTP
102
+ .new(otp_column, options)
103
+ .provisioning_uri(account)
88
104
  end
89
105
  end
90
106
 
91
107
  def otp_column
92
- self.send(self.class.otp_column_name)
108
+ self.public_send(self.class.otp_column_name)
93
109
  end
94
110
 
95
111
  def otp_column=(attr)
96
- self.send("#{self.class.otp_column_name}=", attr)
112
+ self.public_send("#{self.class.otp_column_name}=", attr)
97
113
  end
98
114
 
99
115
  def otp_counter
100
- self.send(self.class.otp_counter_column_name)
116
+ if self.class.otp_counter_column_name != "otp_counter"
117
+ self.public_send(self.class.otp_counter_column_name)
118
+ else
119
+ super
120
+ end
101
121
  end
102
122
 
103
123
  def otp_counter=(attr)
104
- self.send("#{self.class.otp_counter_column_name}=", attr)
124
+ if self.class.otp_counter_column_name != "otp_counter"
125
+ self.public_send("#{self.class.otp_counter_column_name}=", attr)
126
+ else
127
+ super
128
+ end
129
+ end
130
+
131
+ def serializable_hash(options = nil)
132
+ options ||= {}
133
+ options[:except] = Array(options[:except])
134
+ options[:except] << self.class.otp_column_name
135
+ super(options)
136
+ end
137
+
138
+ def otp_regenerate_backup_codes
139
+ otp = ROTP::OTP.new(otp_column)
140
+ backup_codes = Array.new(self.class.otp_backup_codes_count) do
141
+ otp.generate_otp((SecureRandom.random_number(9e5) + 1e5).to_i)
142
+ end
143
+
144
+ public_send("#{self.class.otp_backup_codes_column_name}=", backup_codes)
145
+ end
146
+
147
+ def backup_codes_enabled?
148
+ self.class.attribute_method?(self.class.otp_backup_codes_column_name)
149
+ end
150
+
151
+ private
152
+
153
+ def authenticate_hotp(code, options = {})
154
+ hotp = ROTP::HOTP.new(otp_column, digits: otp_digits)
155
+ result = hotp.verify(code, otp_counter)
156
+ if result && options[:auto_increment]
157
+ self.otp_counter += 1
158
+ save if respond_to?(:changed?) && !new_record?
159
+ end
160
+ result
161
+ end
162
+
163
+ def authenticate_totp(code, options = {})
164
+ totp = ROTP::TOTP.new(
165
+ otp_column,
166
+ digits: otp_digits,
167
+ interval: otp_interval
168
+ )
169
+ if (drift = options[:drift])
170
+ totp.verify(code, drift_behind: drift)
171
+ else
172
+ totp.verify(code)
173
+ end
174
+ end
175
+
176
+ def hotp_code(options = {})
177
+ if options[:auto_increment]
178
+ self.otp_counter += 1
179
+ save if respond_to?(:changed?) && !new_record?
180
+ end
181
+ ROTP::HOTP.new(otp_column, digits: otp_digits).at(otp_counter)
182
+ end
183
+
184
+ def totp_code(options = {})
185
+ time = if options.is_a?(Hash)
186
+ options.fetch(:time, Time.now)
187
+ else
188
+ options
189
+ end
190
+ ROTP::TOTP.new(
191
+ otp_column,
192
+ digits: otp_digits,
193
+ interval: otp_interval
194
+ ).at(time)
195
+ end
196
+
197
+ def authenticate_backup_code(code)
198
+ backup_codes_column_name = self.class.otp_backup_codes_column_name
199
+ backup_codes = public_send(backup_codes_column_name)
200
+ return false unless backup_codes.present? && backup_codes.include?(code)
201
+
202
+ if self.class.otp_one_time_backup_codes
203
+ backup_codes.delete(code)
204
+ public_send("#{backup_codes_column_name}=", backup_codes)
205
+ save if respond_to?(:changed?) && !new_record?
206
+ end
207
+
208
+ true
105
209
  end
106
210
  end
107
211
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  module Otp
3
- VERSION = "1.2.0"
3
+ VERSION = '2.3.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,3 @@
1
+ class ActiverecordUser < ActiveRecord::Base
2
+ has_one_time_password counter_based: true
3
+ 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
@@ -0,0 +1,10 @@
1
+ class Member
2
+ extend ActiveModel::Callbacks
3
+ include ActiveModel::Validations
4
+ include ActiveModel::OneTimePassword
5
+
6
+ define_model_callbacks :create
7
+ attr_accessor :otp_secret_key, :otp_counter, :email
8
+
9
+ has_one_time_password counter_based: true
10
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OptInTwoFactor
4
+ extend ActiveModel::Callbacks
5
+ include ActiveModel::Validations
6
+ include ActiveModel::OneTimePassword
7
+
8
+ define_model_callbacks :create
9
+ attr_accessor :otp_secret_key, :email
10
+
11
+ has_one_time_password unless: :otp_opt_in?
12
+
13
+ def otp_opt_in?
14
+ true
15
+ end
16
+ end
data/test/models/user.rb CHANGED
@@ -1,10 +1,15 @@
1
1
  class User
2
2
  extend ActiveModel::Callbacks
3
+ include ActiveModel::Serializers::JSON
3
4
  include ActiveModel::Validations
4
5
  include ActiveModel::OneTimePassword
5
6
 
6
7
  define_model_callbacks :create
7
- attr_accessor :otp_secret_key, :email
8
+ attr_accessor :otp_secret_key, :otp_backup_codes, :email
8
9
 
9
- has_one_time_password
10
+ has_one_time_password one_time_backup_codes: true
11
+
12
+ def attributes
13
+ { "otp_secret_key" => otp_secret_key, "email" => email }
14
+ end
10
15
  end
@@ -1,6 +1,8 @@
1
- require "test_helper"
1
+ # frozen_string_literal: true
2
2
 
3
- class OtpTest < MiniTest::Unit::TestCase
3
+ require 'test_helper'
4
+
5
+ class OtpTest < MiniTest::Test
4
6
  def setup
5
7
  @user = User.new
6
8
  @user.email = 'roberto@heapsource.com'
@@ -9,23 +11,79 @@ class OtpTest < MiniTest::Unit::TestCase
9
11
  @visitor = Visitor.new
10
12
  @visitor.email = 'roberto@heapsource.com'
11
13
  @visitor.run_callbacks :create
14
+
15
+ @member = Member.new
16
+ @member.email = nil
17
+ @member.run_callbacks :create
18
+
19
+ @ar_user = ActiverecordUser.new
20
+ @ar_user.email = 'roberto@heapsource.com'
21
+ @ar_user.run_callbacks :create
22
+
23
+ @opt_in = OptInTwoFactor.new
24
+ @opt_in.email = 'roberto@heapsource.com'
25
+ @opt_in.run_callbacks :create
12
26
  end
13
27
 
14
28
  def test_authenticate_with_otp
15
29
  code = @user.otp_code
16
-
17
30
  assert @user.authenticate_otp(code)
18
31
 
19
32
  code = @visitor.otp_code
20
33
  assert @visitor.authenticate_otp(code)
21
34
  end
22
35
 
36
+ def test_counter_based_otp
37
+ code = @member.otp_code
38
+ assert @member.authenticate_otp(code)
39
+ assert @member.authenticate_otp(code, auto_increment: true)
40
+ assert !@member.authenticate_otp(code)
41
+ @member.otp_counter -= 1
42
+ assert @member.authenticate_otp(code)
43
+ assert code == @member.otp_code
44
+ assert code != @member.otp_code(auto_increment: true)
45
+ end
46
+
47
+ def test_counter_based_otp_active_record
48
+ code = @ar_user.otp_code
49
+ assert @ar_user.authenticate_otp(code)
50
+ assert @ar_user.authenticate_otp(code, auto_increment: true)
51
+ assert !@ar_user.authenticate_otp(code)
52
+ @ar_user.otp_counter -= 1
53
+ assert @ar_user.authenticate_otp(code)
54
+ assert code == @ar_user.otp_code
55
+ assert code != @ar_user.otp_code(auto_increment: true)
56
+ end
57
+
58
+ def test_opt_in_two_factor
59
+ assert @opt_in.otp_column.nil?
60
+
61
+ @opt_in.otp_regenerate_secret
62
+ code = @opt_in.otp_code
63
+ assert_equal true, @opt_in.authenticate_otp(code)
64
+ end
65
+
23
66
  def test_authenticate_with_otp_when_drift_is_allowed
24
67
  code = @user.otp_code(Time.now - 30)
25
- assert @user.authenticate_otp(code, drift: 60)
68
+ assert_equal true, @user.authenticate_otp(code, drift: 60)
26
69
 
27
70
  code = @visitor.otp_code(Time.now - 30)
28
- assert @visitor.authenticate_otp(code, drift: 60)
71
+ assert_equal true, @visitor.authenticate_otp(code, drift: 60)
72
+ end
73
+
74
+ def test_authenticate_with_backup_code
75
+ backup_code = @user.public_send(@user.otp_backup_codes_column_name).first
76
+ assert_equal true, @user.authenticate_otp(backup_code)
77
+
78
+ backup_code = @user.public_send(@user.otp_backup_codes_column_name).last
79
+ @user.otp_regenerate_backup_codes
80
+ assert_equal true, !@user.authenticate_otp(backup_code)
81
+ end
82
+
83
+ def test_authenticate_with_one_time_backup_code
84
+ backup_code = @user.public_send(@user.otp_backup_codes_column_name).first
85
+ assert_equal true, @user.authenticate_otp(backup_code)
86
+ assert_equal true, !@user.authenticate_otp(backup_code)
29
87
  end
30
88
 
31
89
  def test_otp_code
@@ -34,38 +92,61 @@ class OtpTest < MiniTest::Unit::TestCase
34
92
  end
35
93
 
36
94
  def test_otp_code_with_specific_length
37
- assert_match(/^\d{4}$/, @visitor.otp_code(time: 2160, padding: true).to_s)
38
- assert_operator(@visitor.otp_code(time: 2160, padding: false).to_s.length, :<= , 4)
95
+ assert_match(/^\d{4}$/, @visitor.otp_code(2160).to_s)
96
+ assert_operator(@visitor.otp_code(2160).to_s.length, :<=, 4)
39
97
  end
40
98
 
41
99
  def test_otp_code_without_specific_length
42
- assert_match(/^\d{6}$/, @user.otp_code(time: 2160, padding: true).to_s)
43
- assert_operator(@user.otp_code(time: 2160, padding: false).to_s.length, :<= , 6)
44
- end
45
-
46
- def test_otp_code_padding
47
- @user.otp_column = 'kw5jhligwqaiw7jc'
48
- assert_match(/^\d{6}$/, @user.otp_code(time: 2160, padding: true).to_s)
49
- # Modified this spec as it is not guranteed that without padding we will always
50
- # get a 3 digit number
51
- assert_operator(@user.otp_code(time: 2160, padding: false).to_s.length, :<= , 6)
100
+ assert_match(/^\d{6}$/, @user.otp_code(2160).to_s)
101
+ assert_operator(@user.otp_code(2160).to_s.length, :<=, 6)
52
102
  end
53
103
 
54
104
  def test_provisioning_uri_with_provided_account
55
- assert_match %r{otpauth://totp/roberto\?secret=\w{16}}, @user.provisioning_uri("roberto")
56
- assert_match %r{otpauth://totp/roberto\?secret=\w{16}}, @visitor.provisioning_uri("roberto")
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')
57
111
  end
58
112
 
59
113
  def test_provisioning_uri_with_email_field
60
- assert_match %r{otpauth://totp/roberto@heapsource\.com\?secret=\w{16}}, @user.provisioning_uri
61
- assert_match %r{otpauth://totp/roberto@heapsource\.com\?secret=\w{16}}, @visitor.provisioning_uri
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
62
120
  end
63
121
 
64
122
  def test_provisioning_uri_with_options
65
- assert_match %r{otpauth://totp/roberto@heapsource\.com\?issuer=Example&secret=\w{16}},@user.provisioning_uri(nil,issuer: "Example")
66
- assert_match %r{otpauth://totp/roberto@heapsource\.com\?issuer=Example&secret=\w{16}}, @visitor.provisioning_uri(nil,issuer: "Example")
67
- assert_match %r{otpauth://totp/roberto\?issuer=Example&secret=\w{16}}, @user.provisioning_uri("roberto", issuer: "Example")
68
- assert_match %r{otpauth://totp/roberto\?issuer=Example&secret=\w{16}}, @visitor.provisioning_uri("roberto", issuer: "Example")
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
69
150
  end
70
151
 
71
152
  def test_regenerate_otp
@@ -73,4 +154,32 @@ class OtpTest < MiniTest::Unit::TestCase
73
154
  @user.otp_regenerate_secret
74
155
  assert secret != @user.otp_column
75
156
  end
157
+
158
+ def test_hide_secret_key_in_serialize
159
+ refute_match(/otp_secret_key/, @user.to_json)
160
+ end
161
+
162
+ def test_otp_random_secret
163
+ assert_match(/^.{32}$/, @user.class.otp_random_secret)
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
76
185
  end
data/test/schema.rb ADDED
@@ -0,0 +1,27 @@
1
+ ActiveRecord::Schema.define do
2
+ self.verbose = false
3
+
4
+ create_table :activerecord_users, force: true do |t|
5
+ t.string :key
6
+ t.string :email
7
+ t.integer :otp_counter
8
+ t.string :otp_secret_key
9
+ t.timestamps
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
27
+ end
data/test/test_helper.rb CHANGED
@@ -8,5 +8,14 @@ require "rubygems"
8
8
  require "active_model_otp"
9
9
  require "minitest/autorun"
10
10
  require "minitest/unit"
11
+ require "active_record"
12
+
13
+ begin
14
+ require "activemodel-serializers-xml"
15
+ rescue LoadError
16
+ end
17
+
18
+ ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
19
+ load "#{ File.dirname(__FILE__) }/schema.rb"
11
20
 
12
21
  Dir["models/*.rb"].each {|file| require file }
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_model_otp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillermo Iguaran
8
8
  - Roberto Miranda
9
9
  - Heapsource
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2015-02-26 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
@@ -30,30 +30,30 @@ dependencies:
30
30
  name: rotp
31
31
  requirement: !ruby/object:Gem::Requirement
32
32
  requirements:
33
- - - ">="
33
+ - - "~>"
34
34
  - !ruby/object:Gem::Version
35
- version: '0'
35
+ version: 6.2.0
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
39
39
  requirements:
40
- - - ">="
40
+ - - "~>"
41
41
  - !ruby/object:Gem::Version
42
- version: '0'
42
+ version: 6.2.0
43
43
  - !ruby/object:Gem::Dependency
44
- name: bundler
44
+ name: activerecord
45
45
  requirement: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - "~>"
47
+ - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: '1.3'
49
+ version: '0'
50
50
  type: :development
51
51
  prerelease: false
52
52
  version_requirements: !ruby/object:Gem::Requirement
53
53
  requirements:
54
- - - "~>"
54
+ - - ">="
55
55
  - !ruby/object:Gem::Version
56
- version: '1.3'
56
+ version: '0'
57
57
  - !ruby/object:Gem::Dependency
58
58
  name: rake
59
59
  requirement: !ruby/object:Gem::Requirement
@@ -82,8 +82,36 @@ dependencies:
82
82
  - - "~>"
83
83
  - !ruby/object:Gem::Version
84
84
  version: 5.4.2
85
- description: Adds methods to set and authenticate against one time passwords. Inspired
86
- in AM::SecurePassword"
85
+ - !ruby/object:Gem::Dependency
86
+ name: appraisal
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ - !ruby/object:Gem::Dependency
100
+ name: sqlite3
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ description: Adds methods to set and authenticate against one time passwords 2FA(Two
114
+ factor Authentication). Inspired in AM::SecurePassword"
87
115
  email:
88
116
  - guilleiguaran@gmail.com
89
117
  - rjmaltamar@gmail.com
@@ -92,26 +120,39 @@ executables: []
92
120
  extensions: []
93
121
  extra_rdoc_files: []
94
122
  files:
123
+ - ".github/workflows/active_model_otp.yml"
95
124
  - ".gitignore"
96
- - ".travis.yml"
125
+ - Appraisals
97
126
  - CHANGELOG.md
98
127
  - Gemfile
99
128
  - LICENSE.txt
100
129
  - README.md
101
130
  - Rakefile
102
131
  - active_model_otp.gemspec
132
+ - gemfiles/rails_4.2.gemfile
133
+ - gemfiles/rails_5.0.gemfile
134
+ - gemfiles/rails_5.1.gemfile
135
+ - gemfiles/rails_5.2.gemfile
136
+ - gemfiles/rails_6.0.gemfile
137
+ - gemfiles/rails_6.1.gemfile
103
138
  - lib/active_model/one_time_password.rb
104
139
  - lib/active_model/otp/version.rb
105
140
  - lib/active_model_otp.rb
141
+ - test/models/activerecord_user.rb
142
+ - test/models/default_interval_user.rb
143
+ - test/models/interval_user.rb
144
+ - test/models/member.rb
145
+ - test/models/opt_in_two_factor.rb
106
146
  - test/models/user.rb
107
147
  - test/models/visitor.rb
108
148
  - test/one_time_password_test.rb
149
+ - test/schema.rb
109
150
  - test/test_helper.rb
110
151
  homepage: ''
111
152
  licenses:
112
153
  - MIT
113
154
  metadata: {}
114
- post_install_message:
155
+ post_install_message:
115
156
  rdoc_options: []
116
157
  require_paths:
117
158
  - lib
@@ -119,20 +160,25 @@ required_ruby_version: !ruby/object:Gem::Requirement
119
160
  requirements:
120
161
  - - ">="
121
162
  - !ruby/object:Gem::Version
122
- version: '0'
163
+ version: '2.3'
123
164
  required_rubygems_version: !ruby/object:Gem::Requirement
124
165
  requirements:
125
166
  - - ">="
126
167
  - !ruby/object:Gem::Version
127
168
  version: '0'
128
169
  requirements: []
129
- rubyforge_project:
130
- rubygems_version: 2.2.2
131
- signing_key:
170
+ rubygems_version: 3.0.3
171
+ signing_key:
132
172
  specification_version: 4
133
173
  summary: Adds methods to set and authenticate against one time passwords.
134
174
  test_files:
175
+ - test/models/activerecord_user.rb
176
+ - test/models/default_interval_user.rb
177
+ - test/models/interval_user.rb
178
+ - test/models/member.rb
179
+ - test/models/opt_in_two_factor.rb
135
180
  - test/models/user.rb
136
181
  - test/models/visitor.rb
137
182
  - test/one_time_password_test.rb
183
+ - test/schema.rb
138
184
  - test/test_helper.rb
data/.travis.yml DELETED
@@ -1,10 +0,0 @@
1
- rvm:
2
- - 1.9.3
3
- - 2.0.0
4
- - 2.1
5
- matrix:
6
- include:
7
- - rvm: jruby
8
- env: JRUBY_OPTS="--1.9 --server -Xcext.enabled=true"
9
- notifications:
10
- email: false