active_model_otp 2.1.0 → 2.3.1
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/Appraisals +11 -0
 - data/README.md +69 -2
 - data/active_model_otp.gemspec +3 -3
 - data/gemfiles/rails_4.2.gemfile +1 -0
 - data/gemfiles/rails_5.0.gemfile +1 -0
 - data/gemfiles/rails_5.1.gemfile +1 -0
 - data/gemfiles/rails_5.2.gemfile +1 -0
 - data/gemfiles/rails_6.1.gemfile +10 -0
 - data/lib/active_model/one_time_password.rb +123 -35
 - 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/models/user.rb +3 -3
 - data/test/one_time_password_test.rb +99 -17
 - data/test/schema.rb +16 -0
 - metadata +14 -9
 - data/.travis.yml +0 -26
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 906ff23803a070afb3df376eb45d4314640c4f4012028d7b7bfc16d9263def54
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: efde68de226fb2d7a5b2230fc19958502e678776781419ff89532d19d5e9682e
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 2805bf0a8dc09e6699b9617b60f662b4ff50c82c4f952d9e0fdc2a9c2c6c5a5829ea42ceb9bb2167d64d83c050a79272b3224cc0e21ecb93069fde5cc826f9ba
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: c893d4e40737f2a724e912dd84edfc1cbd3886515757596582436d2694d2ab4836ae7251a522e66c3ac95f31fef4c52f1d1f47836d09d39cb7d259c6c335c77d
         
     | 
| 
         @@ -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/Appraisals
    CHANGED
    
    | 
         @@ -1,20 +1,24 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            appraise "rails-4.2" do
         
     | 
| 
       2 
2 
     | 
    
         
             
              gem "activemodel", "~> 4.2"
         
     | 
| 
      
 3 
     | 
    
         
            +
              gem "sqlite3", "~> 1.3.6"
         
     | 
| 
       3 
4 
     | 
    
         
             
            end
         
     | 
| 
       4 
5 
     | 
    
         | 
| 
       5 
6 
     | 
    
         
             
            appraise "rails-5.0" do
         
     | 
| 
       6 
7 
     | 
    
         
             
              gem "activemodel", "~> 5.0"
         
     | 
| 
       7 
8 
     | 
    
         
             
              gem "activemodel-serializers-xml"
         
     | 
| 
      
 9 
     | 
    
         
            +
              gem "sqlite3", "~> 1.3.6"
         
     | 
| 
       8 
10 
     | 
    
         
             
            end
         
     | 
| 
       9 
11 
     | 
    
         | 
| 
       10 
12 
     | 
    
         
             
            appraise "rails-5.1" do
         
     | 
| 
       11 
13 
     | 
    
         
             
              gem "activemodel", "~> 5.1"
         
     | 
| 
       12 
14 
     | 
    
         
             
              gem "activemodel-serializers-xml"
         
     | 
| 
      
 15 
     | 
    
         
            +
              gem "sqlite3", "~> 1.3.6"
         
     | 
| 
       13 
16 
     | 
    
         
             
            end
         
     | 
| 
       14 
17 
     | 
    
         | 
| 
       15 
18 
     | 
    
         
             
            appraise "rails-5.2" do
         
     | 
| 
       16 
19 
     | 
    
         
             
              gem "activemodel", "~> 5.2"
         
     | 
| 
       17 
20 
     | 
    
         
             
              gem "activemodel-serializers-xml"
         
     | 
| 
      
 21 
     | 
    
         
            +
              gem "sqlite3", "~> 1.3.6"
         
     | 
| 
       18 
22 
     | 
    
         
             
            end
         
     | 
| 
       19 
23 
     | 
    
         | 
| 
       20 
24 
     | 
    
         
             
            appraise "rails-6.0" do
         
     | 
| 
         @@ -23,3 +27,10 @@ appraise "rails-6.0" do 
     | 
|
| 
       23 
27 
     | 
    
         
             
              gem "activemodel-serializers-xml"
         
     | 
| 
       24 
28 
     | 
    
         
             
              gem "sqlite3", "~> 1.4"
         
     | 
| 
       25 
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/README.md
    CHANGED
    
    | 
         @@ -1,4 +1,4 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            [](https://github.com/heapsource/active_model_otp/actions/workflows/active_model_otp.yml)
         
     | 
| 
       2 
2 
     | 
    
         
             
            [](http://badge.fury.io/rb/active_model_otp)
         
     | 
| 
       3 
3 
     | 
    
         
             
            [](https://houndci.com)
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
         @@ -9,7 +9,7 @@ 
     | 
|
| 
       9 
9 
     | 
    
         | 
| 
       10 
10 
     | 
    
         
             
            ## Dependencies
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
            * [ROTP](https://github.com/mdp/rotp)  
     | 
| 
      
 12 
     | 
    
         
            +
            * [ROTP](https://github.com/mdp/rotp) 6.2.0 or higher
         
     | 
| 
       13 
13 
     | 
    
         
             
            * Ruby 2.3 or greater
         
     | 
| 
       14 
14 
     | 
    
         | 
| 
       15 
15 
     | 
    
         
             
            ## Installation
         
     | 
| 
         @@ -150,6 +150,51 @@ user.otp_code(auto_increment: true) # => '002811' 
     | 
|
| 
       150 
150 
     | 
    
         
             
            user.otp_code # => '002811'
         
     | 
| 
       151 
151 
     | 
    
         
             
            ```
         
     | 
| 
       152 
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 
     | 
    
         
            +
             
     | 
| 
       153 
198 
     | 
    
         
             
            ## Google Authenticator Compatible
         
     | 
| 
       154 
199 
     | 
    
         | 
| 
       155 
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.
         
     | 
| 
         @@ -168,6 +213,28 @@ user.provisioning_uri(nil, issuer: 'MYAPP') #=> 'otpauth://totp/hello@heapsource 
     | 
|
| 
       168 
213 
     | 
    
         | 
| 
       169 
214 
     | 
    
         
             
            This can then be rendered as a QR Code which can be scanned and added to the users list of OTP credentials.
         
     | 
| 
       170 
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 
     | 
    
         
            +
             
     | 
| 
       171 
238 
     | 
    
         
             
            ### Working example
         
     | 
| 
       172 
239 
     | 
    
         | 
| 
       173 
240 
     | 
    
         
             
            Scan the following barcode with your phone, using Google Authenticator
         
     | 
    
        data/active_model_otp.gemspec
    CHANGED
    
    | 
         @@ -17,11 +17,11 @@ Gem::Specification.new do |spec| 
     | 
|
| 
       17 
17 
     | 
    
         
             
              spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
         
     | 
| 
       18 
18 
     | 
    
         
             
              spec.test_files    = spec.files.grep(%r{^(test|spec|features)/})
         
     | 
| 
       19 
19 
     | 
    
         
             
              spec.require_paths = ["lib"]
         
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
       21 
21 
     | 
    
         
             
              spec.required_ruby_version = ">= 2.3"
         
     | 
| 
       22 
22 
     | 
    
         | 
| 
       23 
23 
     | 
    
         
             
              spec.add_dependency "activemodel"
         
     | 
| 
       24 
     | 
    
         
            -
              spec.add_dependency "rotp", "~>  
     | 
| 
      
 24 
     | 
    
         
            +
              spec.add_dependency "rotp", "~> 6.2.0"
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
              spec.add_development_dependency "activerecord"
         
     | 
| 
       27 
27 
     | 
    
         
             
              spec.add_development_dependency "rake"
         
     | 
| 
         @@ -31,6 +31,6 @@ Gem::Specification.new do |spec| 
     | 
|
| 
       31 
31 
     | 
    
         
             
              if RUBY_PLATFORM == "java"
         
     | 
| 
       32 
32 
     | 
    
         
             
                spec.add_development_dependency "activerecord-jdbcsqlite3-adapter"
         
     | 
| 
       33 
33 
     | 
    
         
             
              else
         
     | 
| 
       34 
     | 
    
         
            -
                spec.add_development_dependency "sqlite3" 
     | 
| 
      
 34 
     | 
    
         
            +
                spec.add_development_dependency "sqlite3"
         
     | 
| 
       35 
35 
     | 
    
         
             
              end
         
     | 
| 
       36 
36 
     | 
    
         
             
            end
         
     | 
    
        data/gemfiles/rails_4.2.gemfile
    CHANGED
    
    
    
        data/gemfiles/rails_5.0.gemfile
    CHANGED
    
    
    
        data/gemfiles/rails_5.1.gemfile
    CHANGED
    
    
    
        data/gemfiles/rails_5.2.gemfile
    CHANGED
    
    
| 
         @@ -2,22 +2,50 @@ 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 
     | 
    
         
            -
                    cattr_accessor :otp_column_name, :otp_counter_column_name
         
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
                    self. 
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            +
                    )
         
     | 
| 
      
 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]
         
     | 
| 
      
 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 
     | 
    
         
            +
                    )
         
     | 
| 
       15 
42 
     | 
    
         | 
| 
       16 
43 
     | 
    
         
             
                    include InstanceMethodsOnActivation
         
     | 
| 
       17 
44 
     | 
    
         | 
| 
       18 
     | 
    
         
            -
                    before_create(options.slice(:if, :unless)) do
         
     | 
| 
      
 45 
     | 
    
         
            +
                    before_create(**options.slice(:if, :unless)) do
         
     | 
| 
       19 
46 
     | 
    
         
             
                      self.otp_regenerate_secret if !otp_column
         
     | 
| 
       20 
47 
     | 
    
         
             
                      self.otp_regenerate_counter if otp_counter_based && !otp_counter
         
     | 
| 
      
 48 
     | 
    
         
            +
                      otp_regenerate_backup_codes if backup_codes_enabled?
         
     | 
| 
       21 
49 
     | 
    
         
             
                    end
         
     | 
| 
       22 
50 
     | 
    
         | 
| 
       23 
51 
     | 
    
         
             
                    if respond_to?(:attributes_protected_by_default)
         
     | 
| 
         @@ -44,38 +72,21 @@ module ActiveModel 
     | 
|
| 
       44 
72 
     | 
    
         
             
                  end
         
     | 
| 
       45 
73 
     | 
    
         | 
| 
       46 
74 
     | 
    
         
             
                  def authenticate_otp(code, options = {})
         
     | 
| 
      
 75 
     | 
    
         
            +
                    return false if code.nil? || code.empty?
         
     | 
| 
      
 76 
     | 
    
         
            +
                    return true if backup_codes_enabled? && authenticate_backup_code(code)
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
       47 
78 
     | 
    
         
             
                    if otp_counter_based
         
     | 
| 
       48 
     | 
    
         
            -
                       
     | 
| 
       49 
     | 
    
         
            -
                      result = hotp.verify(code, otp_counter)
         
     | 
| 
       50 
     | 
    
         
            -
                      if result && options[:auto_increment]
         
     | 
| 
       51 
     | 
    
         
            -
                        self.otp_counter += 1
         
     | 
| 
       52 
     | 
    
         
            -
                        save if respond_to?(:changed?) && !new_record?
         
     | 
| 
       53 
     | 
    
         
            -
                      end
         
     | 
| 
       54 
     | 
    
         
            -
                      result
         
     | 
| 
      
 79 
     | 
    
         
            +
                      otp_counter == authenticate_hotp(code, options)
         
     | 
| 
       55 
80 
     | 
    
         
             
                    else
         
     | 
| 
       56 
     | 
    
         
            -
                       
     | 
| 
       57 
     | 
    
         
            -
                      if drift = options[:drift]
         
     | 
| 
       58 
     | 
    
         
            -
                        totp.verify(code, drift_behind: drift)
         
     | 
| 
       59 
     | 
    
         
            -
                      else
         
     | 
| 
       60 
     | 
    
         
            -
                        totp.verify(code)
         
     | 
| 
       61 
     | 
    
         
            -
                      end
         
     | 
| 
      
 81 
     | 
    
         
            +
                      authenticate_totp(code, options).present?
         
     | 
| 
       62 
82 
     | 
    
         
             
                    end
         
     | 
| 
       63 
83 
     | 
    
         
             
                  end
         
     | 
| 
       64 
84 
     | 
    
         | 
| 
       65 
85 
     | 
    
         
             
                  def otp_code(options = {})
         
     | 
| 
       66 
86 
     | 
    
         
             
                    if otp_counter_based
         
     | 
| 
       67 
     | 
    
         
            -
                       
     | 
| 
       68 
     | 
    
         
            -
                        self.otp_counter += 1
         
     | 
| 
       69 
     | 
    
         
            -
                        save if respond_to?(:changed?) && !new_record?
         
     | 
| 
       70 
     | 
    
         
            -
                      end
         
     | 
| 
       71 
     | 
    
         
            -
                      ROTP::HOTP.new(otp_column, digits: otp_digits).at(self.otp_counter)
         
     | 
| 
      
 87 
     | 
    
         
            +
                      hotp_code(options)
         
     | 
| 
       72 
88 
     | 
    
         
             
                    else
         
     | 
| 
       73 
     | 
    
         
            -
                       
     | 
| 
       74 
     | 
    
         
            -
                        time = options.fetch(:time, Time.now)
         
     | 
| 
       75 
     | 
    
         
            -
                      else
         
     | 
| 
       76 
     | 
    
         
            -
                        time = options
         
     | 
| 
       77 
     | 
    
         
            -
                      end
         
     | 
| 
       78 
     | 
    
         
            -
                      ROTP::TOTP.new(otp_column, digits: otp_digits).at(time)
         
     | 
| 
      
 89 
     | 
    
         
            +
                      totp_code(options)
         
     | 
| 
       79 
90 
     | 
    
         
             
                    end
         
     | 
| 
       80 
91 
     | 
    
         
             
                  end
         
     | 
| 
       81 
92 
     | 
    
         | 
| 
         @@ -84,9 +95,13 @@ module ActiveModel 
     | 
|
| 
       84 
95 
     | 
    
         
             
                    account ||= ""
         
     | 
| 
       85 
96 
     | 
    
         | 
| 
       86 
97 
     | 
    
         
             
                    if otp_counter_based
         
     | 
| 
       87 
     | 
    
         
            -
                      ROTP::HOTP 
     | 
| 
      
 98 
     | 
    
         
            +
                      ROTP::HOTP
         
     | 
| 
      
 99 
     | 
    
         
            +
                        .new(otp_column, options)
         
     | 
| 
      
 100 
     | 
    
         
            +
                        .provisioning_uri(account, self.otp_counter)
         
     | 
| 
       88 
101 
     | 
    
         
             
                    else
         
     | 
| 
       89 
     | 
    
         
            -
                      ROTP::TOTP 
     | 
| 
      
 102 
     | 
    
         
            +
                      ROTP::TOTP
         
     | 
| 
      
 103 
     | 
    
         
            +
                        .new(otp_column, options)
         
     | 
| 
      
 104 
     | 
    
         
            +
                        .provisioning_uri(account)
         
     | 
| 
       90 
105 
     | 
    
         
             
                    end
         
     | 
| 
       91 
106 
     | 
    
         
             
                  end
         
     | 
| 
       92 
107 
     | 
    
         | 
| 
         @@ -120,6 +135,79 @@ module ActiveModel 
     | 
|
| 
       120 
135 
     | 
    
         
             
                    options[:except] << self.class.otp_column_name
         
     | 
| 
       121 
136 
     | 
    
         
             
                    super(options)
         
     | 
| 
       122 
137 
     | 
    
         
             
                  end
         
     | 
| 
      
 138 
     | 
    
         
            +
             
     | 
| 
      
 139 
     | 
    
         
            +
                  def otp_regenerate_backup_codes
         
     | 
| 
      
 140 
     | 
    
         
            +
                    otp = ROTP::OTP.new(otp_column)
         
     | 
| 
      
 141 
     | 
    
         
            +
                    backup_codes = Array.new(self.class.otp_backup_codes_count) do
         
     | 
| 
      
 142 
     | 
    
         
            +
                      otp.generate_otp((SecureRandom.random_number(9e5) + 1e5).to_i)
         
     | 
| 
      
 143 
     | 
    
         
            +
                    end
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
                    public_send("#{self.class.otp_backup_codes_column_name}=", backup_codes)
         
     | 
| 
      
 146 
     | 
    
         
            +
                  end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                  def backup_codes_enabled?
         
     | 
| 
      
 149 
     | 
    
         
            +
                    self.class.attribute_method?(self.class.otp_backup_codes_column_name)
         
     | 
| 
      
 150 
     | 
    
         
            +
                  end
         
     | 
| 
      
 151 
     | 
    
         
            +
             
     | 
| 
      
 152 
     | 
    
         
            +
                  private
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
                  def authenticate_hotp(code, options = {})
         
     | 
| 
      
 155 
     | 
    
         
            +
                    hotp = ROTP::HOTP.new(otp_column, digits: otp_digits)
         
     | 
| 
      
 156 
     | 
    
         
            +
                    result = hotp.verify(code, otp_counter)
         
     | 
| 
      
 157 
     | 
    
         
            +
                    if result && options[:auto_increment]
         
     | 
| 
      
 158 
     | 
    
         
            +
                      self.otp_counter += 1
         
     | 
| 
      
 159 
     | 
    
         
            +
                      save if respond_to?(:changed?) && !new_record?
         
     | 
| 
      
 160 
     | 
    
         
            +
                    end
         
     | 
| 
      
 161 
     | 
    
         
            +
                    result
         
     | 
| 
      
 162 
     | 
    
         
            +
                  end
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                  def authenticate_totp(code, options = {})
         
     | 
| 
      
 165 
     | 
    
         
            +
                    totp = ROTP::TOTP.new(
         
     | 
| 
      
 166 
     | 
    
         
            +
                      otp_column,
         
     | 
| 
      
 167 
     | 
    
         
            +
                      digits: otp_digits,
         
     | 
| 
      
 168 
     | 
    
         
            +
                      interval: otp_interval
         
     | 
| 
      
 169 
     | 
    
         
            +
                    )
         
     | 
| 
      
 170 
     | 
    
         
            +
                    if (drift = options[:drift])
         
     | 
| 
      
 171 
     | 
    
         
            +
                      totp.verify(code, drift_behind: drift)
         
     | 
| 
      
 172 
     | 
    
         
            +
                    else
         
     | 
| 
      
 173 
     | 
    
         
            +
                      totp.verify(code)
         
     | 
| 
      
 174 
     | 
    
         
            +
                    end
         
     | 
| 
      
 175 
     | 
    
         
            +
                  end
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
                  def hotp_code(options = {})
         
     | 
| 
      
 178 
     | 
    
         
            +
                    if options[:auto_increment]
         
     | 
| 
      
 179 
     | 
    
         
            +
                      self.otp_counter += 1
         
     | 
| 
      
 180 
     | 
    
         
            +
                      save if respond_to?(:changed?) && !new_record?
         
     | 
| 
      
 181 
     | 
    
         
            +
                    end
         
     | 
| 
      
 182 
     | 
    
         
            +
                    ROTP::HOTP.new(otp_column, digits: otp_digits).at(otp_counter)
         
     | 
| 
      
 183 
     | 
    
         
            +
                  end
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
                  def totp_code(options = {})
         
     | 
| 
      
 186 
     | 
    
         
            +
                    time = if options.is_a?(Hash)
         
     | 
| 
      
 187 
     | 
    
         
            +
                             options.fetch(:time, Time.now)
         
     | 
| 
      
 188 
     | 
    
         
            +
                           else
         
     | 
| 
      
 189 
     | 
    
         
            +
                             options
         
     | 
| 
      
 190 
     | 
    
         
            +
                           end
         
     | 
| 
      
 191 
     | 
    
         
            +
                    ROTP::TOTP.new(
         
     | 
| 
      
 192 
     | 
    
         
            +
                      otp_column,
         
     | 
| 
      
 193 
     | 
    
         
            +
                      digits: otp_digits,
         
     | 
| 
      
 194 
     | 
    
         
            +
                      interval: otp_interval
         
     | 
| 
      
 195 
     | 
    
         
            +
                    ).at(time)
         
     | 
| 
      
 196 
     | 
    
         
            +
                  end
         
     | 
| 
      
 197 
     | 
    
         
            +
             
     | 
| 
      
 198 
     | 
    
         
            +
                  def authenticate_backup_code(code)
         
     | 
| 
      
 199 
     | 
    
         
            +
                    backup_codes_column_name = self.class.otp_backup_codes_column_name
         
     | 
| 
      
 200 
     | 
    
         
            +
                    backup_codes = public_send(backup_codes_column_name)
         
     | 
| 
      
 201 
     | 
    
         
            +
                    return false unless backup_codes.present? && backup_codes.include?(code)
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
      
 203 
     | 
    
         
            +
                    if self.class.otp_one_time_backup_codes
         
     | 
| 
      
 204 
     | 
    
         
            +
                      backup_codes.delete(code)
         
     | 
| 
      
 205 
     | 
    
         
            +
                      public_send("#{backup_codes_column_name}=", backup_codes)
         
     | 
| 
      
 206 
     | 
    
         
            +
                      save if respond_to?(:changed?) && !new_record?
         
     | 
| 
      
 207 
     | 
    
         
            +
                    end
         
     | 
| 
      
 208 
     | 
    
         
            +
             
     | 
| 
      
 209 
     | 
    
         
            +
                    true
         
     | 
| 
      
 210 
     | 
    
         
            +
                  end
         
     | 
| 
       123 
211 
     | 
    
         
             
                end
         
     | 
| 
       124 
212 
     | 
    
         
             
              end
         
     | 
| 
       125 
213 
     | 
    
         
             
            end
         
     | 
    
        data/test/models/user.rb
    CHANGED
    
    | 
         @@ -1,14 +1,14 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            class User
         
     | 
| 
       2 
2 
     | 
    
         
             
              extend ActiveModel::Callbacks
         
     | 
| 
       3 
3 
     | 
    
         
             
              include ActiveModel::Serializers::JSON
         
     | 
| 
       4 
     | 
    
         
            -
              include ActiveModel::Serializers::Xml
         
     | 
| 
       5 
4 
     | 
    
         
             
              include ActiveModel::Validations
         
     | 
| 
       6 
5 
     | 
    
         
             
              include ActiveModel::OneTimePassword
         
     | 
| 
       7 
6 
     | 
    
         | 
| 
       8 
7 
     | 
    
         
             
              define_model_callbacks :create
         
     | 
| 
       9 
     | 
    
         
            -
              attr_accessor :otp_secret_key, :email
         
     | 
| 
      
 8 
     | 
    
         
            +
              attr_accessor :otp_secret_key, :otp_backup_codes, :email
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
              has_one_time_password one_time_backup_codes: true
         
     | 
| 
       10 
11 
     | 
    
         | 
| 
       11 
     | 
    
         
            -
              has_one_time_password
         
     | 
| 
       12 
12 
     | 
    
         
             
              def attributes
         
     | 
| 
       13 
13 
     | 
    
         
             
                { "otp_secret_key" => otp_secret_key, "email" => email }
         
     | 
| 
       14 
14 
     | 
    
         
             
              end
         
     | 
| 
         @@ -1,6 +1,8 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
             
     | 
| 
      
 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'
         
     | 
| 
         @@ -31,6 +33,23 @@ class OtpTest < MiniTest::Unit::TestCase 
     | 
|
| 
       31 
33 
     | 
    
         
             
                assert @visitor.authenticate_otp(code)
         
     | 
| 
       32 
34 
     | 
    
         
             
              end
         
     | 
| 
       33 
35 
     | 
    
         | 
| 
      
 36 
     | 
    
         
            +
              def test_authenticate_with_otp_passing_false_or_empty_codes
         
     | 
| 
      
 37 
     | 
    
         
            +
                refute @user.authenticate_otp(nil)
         
     | 
| 
      
 38 
     | 
    
         
            +
                refute @user.authenticate_otp('')
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                refute @visitor.authenticate_otp(nil)
         
     | 
| 
      
 41 
     | 
    
         
            +
                refute @visitor.authenticate_otp('')
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                refute @member.authenticate_otp(nil)
         
     | 
| 
      
 44 
     | 
    
         
            +
                refute @member.authenticate_otp('')
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                refute @ar_user.authenticate_otp(nil)
         
     | 
| 
      
 47 
     | 
    
         
            +
                refute @ar_user.authenticate_otp('')
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                refute @opt_in.authenticate_otp(nil)
         
     | 
| 
      
 50 
     | 
    
         
            +
                refute @opt_in.authenticate_otp('')
         
     | 
| 
      
 51 
     | 
    
         
            +
              end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
       34 
53 
     | 
    
         
             
              def test_counter_based_otp
         
     | 
| 
       35 
54 
     | 
    
         
             
                code = @member.otp_code
         
     | 
| 
       36 
55 
     | 
    
         
             
                assert @member.authenticate_otp(code)
         
     | 
| 
         @@ -58,15 +77,30 @@ class OtpTest < MiniTest::Unit::TestCase 
     | 
|
| 
       58 
77 
     | 
    
         | 
| 
       59 
78 
     | 
    
         
             
                @opt_in.otp_regenerate_secret
         
     | 
| 
       60 
79 
     | 
    
         
             
                code = @opt_in.otp_code
         
     | 
| 
       61 
     | 
    
         
            -
                 
     | 
| 
      
 80 
     | 
    
         
            +
                assert_equal true, @opt_in.authenticate_otp(code)
         
     | 
| 
       62 
81 
     | 
    
         
             
              end
         
     | 
| 
       63 
82 
     | 
    
         | 
| 
       64 
83 
     | 
    
         
             
              def test_authenticate_with_otp_when_drift_is_allowed
         
     | 
| 
       65 
84 
     | 
    
         
             
                code = @user.otp_code(Time.now - 30)
         
     | 
| 
       66 
     | 
    
         
            -
                 
     | 
| 
      
 85 
     | 
    
         
            +
                assert_equal true, @user.authenticate_otp(code, drift: 60)
         
     | 
| 
       67 
86 
     | 
    
         | 
| 
       68 
87 
     | 
    
         
             
                code = @visitor.otp_code(Time.now - 30)
         
     | 
| 
       69 
     | 
    
         
            -
                 
     | 
| 
      
 88 
     | 
    
         
            +
                assert_equal true, @visitor.authenticate_otp(code, drift: 60)
         
     | 
| 
      
 89 
     | 
    
         
            +
              end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
              def test_authenticate_with_backup_code
         
     | 
| 
      
 92 
     | 
    
         
            +
                backup_code = @user.public_send(@user.otp_backup_codes_column_name).first
         
     | 
| 
      
 93 
     | 
    
         
            +
                assert_equal true, @user.authenticate_otp(backup_code)
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                backup_code = @user.public_send(@user.otp_backup_codes_column_name).last
         
     | 
| 
      
 96 
     | 
    
         
            +
                @user.otp_regenerate_backup_codes
         
     | 
| 
      
 97 
     | 
    
         
            +
                assert_equal true, !@user.authenticate_otp(backup_code)
         
     | 
| 
      
 98 
     | 
    
         
            +
              end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
              def test_authenticate_with_one_time_backup_code
         
     | 
| 
      
 101 
     | 
    
         
            +
                backup_code = @user.public_send(@user.otp_backup_codes_column_name).first
         
     | 
| 
      
 102 
     | 
    
         
            +
                assert_equal true, @user.authenticate_otp(backup_code)
         
     | 
| 
      
 103 
     | 
    
         
            +
                assert_equal true, !@user.authenticate_otp(backup_code)
         
     | 
| 
       70 
104 
     | 
    
         
             
              end
         
     | 
| 
       71 
105 
     | 
    
         | 
| 
       72 
106 
     | 
    
         
             
              def test_otp_code
         
     | 
| 
         @@ -85,22 +119,51 @@ class OtpTest < MiniTest::Unit::TestCase 
     | 
|
| 
       85 
119 
     | 
    
         
             
              end
         
     | 
| 
       86 
120 
     | 
    
         | 
| 
       87 
121 
     | 
    
         
             
              def test_provisioning_uri_with_provided_account
         
     | 
| 
       88 
     | 
    
         
            -
                 
     | 
| 
       89 
     | 
    
         
            -
                 
     | 
| 
       90 
     | 
    
         
            -
             
     | 
| 
      
 122 
     | 
    
         
            +
                totp = %r{^otpauth://totp/roberto\?secret=\w{32}$}
         
     | 
| 
      
 123 
     | 
    
         
            +
                hotp = %r{^otpauth://hotp/roberto\?secret=\w{32}&counter=1$}
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                assert_match totp, @user.provisioning_uri('roberto')
         
     | 
| 
      
 126 
     | 
    
         
            +
                assert_match totp, @visitor.provisioning_uri('roberto')
         
     | 
| 
      
 127 
     | 
    
         
            +
                assert_match hotp, @member.provisioning_uri('roberto')
         
     | 
| 
       91 
128 
     | 
    
         
             
              end
         
     | 
| 
       92 
129 
     | 
    
         | 
| 
       93 
130 
     | 
    
         
             
              def test_provisioning_uri_with_email_field
         
     | 
| 
       94 
     | 
    
         
            -
                 
     | 
| 
       95 
     | 
    
         
            -
                 
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
      
 131 
     | 
    
         
            +
                totp = %r{^otpauth://totp/roberto%40heapsource\.com\?secret=\w{32}$}
         
     | 
| 
      
 132 
     | 
    
         
            +
                hotp = %r{^otpauth://hotp/\?secret=\w{32}&counter=1$}
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                assert_match totp, @user.provisioning_uri
         
     | 
| 
      
 135 
     | 
    
         
            +
                assert_match totp, @visitor.provisioning_uri
         
     | 
| 
      
 136 
     | 
    
         
            +
                assert_match hotp, @member.provisioning_uri
         
     | 
| 
       97 
137 
     | 
    
         
             
              end
         
     | 
| 
       98 
138 
     | 
    
         | 
| 
       99 
139 
     | 
    
         
             
              def test_provisioning_uri_with_options
         
     | 
| 
       100 
     | 
    
         
            -
                 
     | 
| 
       101 
     | 
    
         
            -
             
     | 
| 
       102 
     | 
    
         
            -
                 
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
      
 140 
     | 
    
         
            +
                account = %r{
         
     | 
| 
      
 141 
     | 
    
         
            +
                  ^otpauth://totp/Example\:roberto\?secret=\w{32}&issuer=Example$
         
     | 
| 
      
 142 
     | 
    
         
            +
                }x
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                email = %r{
         
     | 
| 
      
 145 
     | 
    
         
            +
                  ^otpauth://totp/Example\:roberto%40heapsource\.com\?secret=\w{32}
         
     | 
| 
      
 146 
     | 
    
         
            +
                  &issuer=Example$
         
     | 
| 
      
 147 
     | 
    
         
            +
                }x
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
                assert_match(
         
     | 
| 
      
 150 
     | 
    
         
            +
                  account, @user.provisioning_uri('roberto', issuer: 'Example')
         
     | 
| 
      
 151 
     | 
    
         
            +
                )
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                assert_match(
         
     | 
| 
      
 154 
     | 
    
         
            +
                  account, @visitor.provisioning_uri('roberto', issuer: 'Example')
         
     | 
| 
      
 155 
     | 
    
         
            +
                )
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
                assert_match email, @user.provisioning_uri(nil, issuer: 'Example')
         
     | 
| 
      
 158 
     | 
    
         
            +
                assert_match email, @visitor.provisioning_uri(nil, issuer: 'Example')
         
     | 
| 
      
 159 
     | 
    
         
            +
              end
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
              def test_provisioning_uri_with_incremented_counter
         
     | 
| 
      
 162 
     | 
    
         
            +
                2.times { @member.otp_code(auto_increment: true) }
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                hotp = %r{^otpauth://hotp/\?secret=\w{32}&counter=3$}
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
                assert_match hotp, @member.provisioning_uri
         
     | 
| 
       104 
167 
     | 
    
         
             
              end
         
     | 
| 
       105 
168 
     | 
    
         | 
| 
       106 
169 
     | 
    
         
             
              def test_regenerate_otp
         
     | 
| 
         @@ -111,10 +174,29 @@ class OtpTest < MiniTest::Unit::TestCase 
     | 
|
| 
       111 
174 
     | 
    
         | 
| 
       112 
175 
     | 
    
         
             
              def test_hide_secret_key_in_serialize
         
     | 
| 
       113 
176 
     | 
    
         
             
                refute_match(/otp_secret_key/, @user.to_json)
         
     | 
| 
       114 
     | 
    
         
            -
                refute_match(/otp_secret_key/, @user.to_xml)
         
     | 
| 
       115 
177 
     | 
    
         
             
              end
         
     | 
| 
       116 
178 
     | 
    
         | 
| 
       117 
179 
     | 
    
         
             
              def test_otp_random_secret
         
     | 
| 
       118 
     | 
    
         
            -
                assert_match 
     | 
| 
      
 180 
     | 
    
         
            +
                assert_match(/^.{32}$/, @user.class.otp_random_secret)
         
     | 
| 
      
 181 
     | 
    
         
            +
              end
         
     | 
| 
      
 182 
     | 
    
         
            +
             
     | 
| 
      
 183 
     | 
    
         
            +
              def test_otp_interval
         
     | 
| 
      
 184 
     | 
    
         
            +
                @interval_user = IntervalUser.new
         
     | 
| 
      
 185 
     | 
    
         
            +
                @interval_user.email = 'roberto@heapsource.com'
         
     | 
| 
      
 186 
     | 
    
         
            +
                @interval_user.run_callbacks :create
         
     | 
| 
      
 187 
     | 
    
         
            +
                otp_code = @interval_user.otp_code
         
     | 
| 
      
 188 
     | 
    
         
            +
                2.times { assert_match(otp_code, @interval_user.otp_code) }
         
     | 
| 
      
 189 
     | 
    
         
            +
                sleep 5
         
     | 
| 
      
 190 
     | 
    
         
            +
                refute_match(otp_code, @interval_user.otp_code)
         
     | 
| 
      
 191 
     | 
    
         
            +
              end
         
     | 
| 
      
 192 
     | 
    
         
            +
             
     | 
| 
      
 193 
     | 
    
         
            +
              def test_otp_default_interval
         
     | 
| 
      
 194 
     | 
    
         
            +
                @default_interval_user = DefaultIntervalUser.new
         
     | 
| 
      
 195 
     | 
    
         
            +
                @default_interval_user.email = 'roberto@heapsource.com'
         
     | 
| 
      
 196 
     | 
    
         
            +
                @default_interval_user.run_callbacks :create
         
     | 
| 
      
 197 
     | 
    
         
            +
                otp_code = @default_interval_user.otp_code
         
     | 
| 
      
 198 
     | 
    
         
            +
                2.times { assert_match(otp_code, @default_interval_user.otp_code) }
         
     | 
| 
      
 199 
     | 
    
         
            +
                sleep 5
         
     | 
| 
      
 200 
     | 
    
         
            +
                assert_match(otp_code, @default_interval_user.otp_code)
         
     | 
| 
       119 
201 
     | 
    
         
             
              end
         
     | 
| 
       120 
202 
     | 
    
         
             
            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.1 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 2.3.1
         
     | 
| 
       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:  
     | 
| 
      
 13 
     | 
    
         
            +
            date: 2021-10-22 00:00:00.000000000 Z
         
     | 
| 
       14 
14 
     | 
    
         
             
            dependencies:
         
     | 
| 
       15 
15 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       16 
16 
     | 
    
         
             
              name: activemodel
         
     | 
| 
         @@ -32,14 +32,14 @@ dependencies: 
     | 
|
| 
       32 
32 
     | 
    
         
             
                requirements:
         
     | 
| 
       33 
33 
     | 
    
         
             
                - - "~>"
         
     | 
| 
       34 
34 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       35 
     | 
    
         
            -
                    version:  
     | 
| 
      
 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:  
     | 
| 
      
 42 
     | 
    
         
            +
                    version: 6.2.0
         
     | 
| 
       43 
43 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       44 
44 
     | 
    
         
             
              name: activerecord
         
     | 
| 
       45 
45 
     | 
    
         
             
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
         @@ -100,16 +100,16 @@ dependencies: 
     | 
|
| 
       100 
100 
     | 
    
         
             
              name: sqlite3
         
     | 
| 
       101 
101 
     | 
    
         
             
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
       102 
102 
     | 
    
         
             
                requirements:
         
     | 
| 
       103 
     | 
    
         
            -
                - - " 
     | 
| 
      
 103 
     | 
    
         
            +
                - - ">="
         
     | 
| 
       104 
104 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       105 
     | 
    
         
            -
                    version:  
     | 
| 
      
 105 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
       106 
106 
     | 
    
         
             
              type: :development
         
     | 
| 
       107 
107 
     | 
    
         
             
              prerelease: false
         
     | 
| 
       108 
108 
     | 
    
         
             
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
       109 
109 
     | 
    
         
             
                requirements:
         
     | 
| 
       110 
     | 
    
         
            -
                - - " 
     | 
| 
      
 110 
     | 
    
         
            +
                - - ">="
         
     | 
| 
       111 
111 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       112 
     | 
    
         
            -
                    version:  
     | 
| 
      
 112 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
       113 
113 
     | 
    
         
             
            description: Adds methods to set and authenticate against one time passwords 2FA(Two
         
     | 
| 
       114 
114 
     | 
    
         
             
              factor Authentication). Inspired in AM::SecurePassword"
         
     | 
| 
       115 
115 
     | 
    
         
             
            email:
         
     | 
| 
         @@ -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
         
     | 
| 
         @@ -134,10 +134,13 @@ files: 
     | 
|
| 
       134 
134 
     | 
    
         
             
            - gemfiles/rails_5.1.gemfile
         
     | 
| 
       135 
135 
     | 
    
         
             
            - gemfiles/rails_5.2.gemfile
         
     | 
| 
       136 
136 
     | 
    
         
             
            - gemfiles/rails_6.0.gemfile
         
     | 
| 
      
 137 
     | 
    
         
            +
            - gemfiles/rails_6.1.gemfile
         
     | 
| 
       137 
138 
     | 
    
         
             
            - lib/active_model/one_time_password.rb
         
     | 
| 
       138 
139 
     | 
    
         
             
            - lib/active_model/otp/version.rb
         
     | 
| 
       139 
140 
     | 
    
         
             
            - lib/active_model_otp.rb
         
     | 
| 
       140 
141 
     | 
    
         
             
            - test/models/activerecord_user.rb
         
     | 
| 
      
 142 
     | 
    
         
            +
            - test/models/default_interval_user.rb
         
     | 
| 
      
 143 
     | 
    
         
            +
            - test/models/interval_user.rb
         
     | 
| 
       141 
144 
     | 
    
         
             
            - test/models/member.rb
         
     | 
| 
       142 
145 
     | 
    
         
             
            - test/models/opt_in_two_factor.rb
         
     | 
| 
       143 
146 
     | 
    
         
             
            - test/models/user.rb
         
     | 
| 
         @@ -170,6 +173,8 @@ specification_version: 4 
     | 
|
| 
       170 
173 
     | 
    
         
             
            summary: Adds methods to set and authenticate against one time passwords.
         
     | 
| 
       171 
174 
     | 
    
         
             
            test_files:
         
     | 
| 
       172 
175 
     | 
    
         
             
            - test/models/activerecord_user.rb
         
     | 
| 
      
 176 
     | 
    
         
            +
            - test/models/default_interval_user.rb
         
     | 
| 
      
 177 
     | 
    
         
            +
            - test/models/interval_user.rb
         
     | 
| 
       173 
178 
     | 
    
         
             
            - test/models/member.rb
         
     | 
| 
       174 
179 
     | 
    
         
             
            - test/models/opt_in_two_factor.rb
         
     | 
| 
       175 
180 
     | 
    
         
             
            - test/models/user.rb
         
     | 
    
        data/.travis.yml
    DELETED
    
    | 
         @@ -1,26 +0,0 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            rvm:
         
     | 
| 
       2 
     | 
    
         
            -
              - 2.3
         
     | 
| 
       3 
     | 
    
         
            -
              - 2.4
         
     | 
| 
       4 
     | 
    
         
            -
              - 2.5
         
     | 
| 
       5 
     | 
    
         
            -
              - 2.6
         
     | 
| 
       6 
     | 
    
         
            -
              - ruby-head
         
     | 
| 
       7 
     | 
    
         
            -
            gemfile:
         
     | 
| 
       8 
     | 
    
         
            -
              - gemfiles/rails_4.2.gemfile
         
     | 
| 
       9 
     | 
    
         
            -
              - gemfiles/rails_5.0.gemfile
         
     | 
| 
       10 
     | 
    
         
            -
              - gemfiles/rails_5.1.gemfile
         
     | 
| 
       11 
     | 
    
         
            -
              - gemfiles/rails_5.2.gemfile
         
     | 
| 
       12 
     | 
    
         
            -
              - gemfiles/rails_6.0.gemfile
         
     | 
| 
       13 
     | 
    
         
            -
            matrix:
         
     | 
| 
       14 
     | 
    
         
            -
              exclude:
         
     | 
| 
       15 
     | 
    
         
            -
                - rvm: 2.3
         
     | 
| 
       16 
     | 
    
         
            -
                  gemfile: gemfiles/rails_6.0.gemfile
         
     | 
| 
       17 
     | 
    
         
            -
                - rvm: 2.4
         
     | 
| 
       18 
     | 
    
         
            -
                  gemfile: gemfiles/rails_6.0.gemfile
         
     | 
| 
       19 
     | 
    
         
            -
              fast_finish: true
         
     | 
| 
       20 
     | 
    
         
            -
              allow_failures:
         
     | 
| 
       21 
     | 
    
         
            -
                - rvm: ruby-head
         
     | 
| 
       22 
     | 
    
         
            -
            # include:
         
     | 
| 
       23 
     | 
    
         
            -
            #   - rvm: jruby
         
     | 
| 
       24 
     | 
    
         
            -
            #     env: JRUBY_OPTS="--1.9 --server -Xcext.enabled=true"
         
     | 
| 
       25 
     | 
    
         
            -
            notifications:
         
     | 
| 
       26 
     | 
    
         
            -
              email: false
         
     |