active_model_otp 0.1.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 973b2897bf7c434844a9ec652c599705fbfe6def2da99b20302059047ba960d1
4
+ data.tar.gz: 4ac0735f61e0d74109ec39d4a6b7504028a76d2ab74f31bde8e90dbd9625a61a
5
+ SHA512:
6
+ metadata.gz: 53612a20dc401b03c051c48450e7161388fed1ab5d2c7d43ebcf12dbec82ca8f12e05d033237f505257d79c4e3ba9861fe5bdae943280e0b625ebb9a0bb95e2d
7
+ data.tar.gz: 2cbb80548cccb92ff3ed398671c65aa600adc32de84a95b91287de67a4a7fbac45cf97d2d1d311bc1fc141cdb630a1a4e803a3785e79449ec00fea8ce3db5f2c
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  .ruby-version
19
+ gemfiles/*.lock
@@ -1,11 +1,26 @@
1
1
  rvm:
2
- - 1.9.3
3
- - 2.0.0
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
4
13
  matrix:
5
- include:
6
- - rvm: jruby
7
- env: JRUBY_OPTS="--1.9 --server -Xcext.enabled=true"
8
- - rvm: jruby-head
9
- env: JRUBY_OPTS="--1.9 --server -Xcext.enabled=true"
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"
10
25
  notifications:
11
26
  email: false
@@ -0,0 +1,25 @@
1
+ appraise "rails-4.2" do
2
+ gem "activemodel", "~> 4.2"
3
+ end
4
+
5
+ appraise "rails-5.0" do
6
+ gem "activemodel", "~> 5.0"
7
+ gem "activemodel-serializers-xml"
8
+ end
9
+
10
+ appraise "rails-5.1" do
11
+ gem "activemodel", "~> 5.1"
12
+ gem "activemodel-serializers-xml"
13
+ end
14
+
15
+ appraise "rails-5.2" do
16
+ gem "activemodel", "~> 5.2"
17
+ gem "activemodel-serializers-xml"
18
+ end
19
+
20
+ appraise "rails-6.0" do
21
+ gem "activerecord", "~> 6.0"
22
+ gem "activemodel", "~> 6.0"
23
+ gem "activemodel-serializers-xml"
24
+ gem "sqlite3", "~> 1.4"
25
+ end
@@ -0,0 +1 @@
1
+ CHANGELOG it's been deprecated in favor of https://github.com/heapsource/active_model_otp/releases
@@ -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,8 +1,16 @@
1
1
  [![Build Status](https://travis-ci.org/heapsource/active_model_otp.png)](https://travis-ci.org/heapsource/active_model_otp)
2
+ [![Gem Version](https://badge.fury.io/rb/active_model_otp.svg)](http://badge.fury.io/rb/active_model_otp)
3
+ [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
4
+
2
5
 
3
6
  # ActiveModel::Otp
4
7
 
5
- **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 an User model and some authentication to it. Inspired in 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) 5.0 or higher
13
+ * Ruby 2.3 or greater
6
14
 
7
15
  ## Installation
8
16
 
@@ -14,7 +22,7 @@ And then execute:
14
22
 
15
23
  $ bundle
16
24
 
17
- Or install it yourself as:
25
+ Or install it yourself as follows:
18
26
 
19
27
  $ gem install active_model_otp
20
28
 
@@ -29,34 +37,49 @@ rails g migration AddOtpSecretKeyToUsers otp_secret_key:string
29
37
  create db/migrate/20130707010931_add_otp_secret_key_to_users.rb
30
38
  ```
31
39
 
32
- 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 tell it will be use TFA.
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.
33
41
 
34
42
  ```ruby
35
- class User < ActiveRecord::Base
43
+ class User < ApplicationRecord
36
44
  has_one_time_password
37
45
  end
38
46
  ```
39
47
 
48
+ Note: If you're adding this to an existing user model you'll need to generate *otp_secret_key* with a migration like:
49
+ ```ruby
50
+ User.find_each { |user| user.update_attribute(:otp_secret_key, User.otp_random_secret) }
51
+ ```
40
52
 
41
- ##Usage
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.
42
54
 
43
- The has_one_time_password sentence provides to the model some useful methods in order to implement our TFA system. AMo:Otp generates one time passwords according to [RFC 4226](http://tools.ietf.org/html/rfc4226) and the [HOTP RFC](http://tools.ietf.org/html/draft-mraihi-totp-timebased-00). This is compatible with Google Authenticator apps available for Android and iPhone, and now in use on GMail.
55
+ ```ruby
56
+ class User < ApplicationRecord
57
+ has_one_time_password column_name: :my_otp_secret_column, length: 4
58
+ end
59
+ ```
60
+
61
+ ## Usage
62
+
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.
44
64
 
45
- The otp_secret_key is saved automatically when a object is created,
65
+ The otp_secret_key is saved automatically when an object is created,
46
66
 
47
67
  ```ruby
48
68
  user = User.create(email: "hello@heapsource.com")
49
69
  user.otp_secret_key
50
- => "jt3gdd2qm6su5iqh"
70
+ => "jt3gdd2qm6su5iqh"
51
71
  ```
52
72
 
53
- **Note:** You can fork the applications for [iPhone](https://github.com/heapsource/google-authenticator) & [Android](https://github.com/heapsource/google-authenticator.android) and customize it
73
+ **Note:** You can fork the applications for [iPhone](https://github.com/heapsource/google-authenticator) & [Android](https://github.com/heapsource/google-authenticator.android) and customize them
54
74
 
55
- ### Getting current code (ex. to send via SMS)
75
+ ### Getting current code (e.g. to send via SMS)
56
76
  ```ruby
57
77
  user.otp_code # => '186522'
58
78
  sleep 30
59
79
  user.otp_code # => '850738'
80
+
81
+ # Override current time
82
+ user.otp_code(time: Time.now + 3600) # => '317438'
60
83
  ```
61
84
 
62
85
  ### Authenticating using a code
@@ -75,19 +98,75 @@ sleep 30 # lets wait again
75
98
  user.authenticate_otp('186522', drift: 60) # => true
76
99
  ```
77
100
 
101
+ ## Counter based OTP
102
+
103
+ An additonal counter field is required in our ``User`` Model
104
+
105
+ ```ruby
106
+ rails g migration AddCounterForOtpToUsers otp_counter:integer
107
+ =>
108
+ invoke active_record
109
+ create db/migrate/20130707010931_add_counter_for_otp_to_users.rb
110
+ ```
111
+
112
+ Set default value for otp_counter to 0.
113
+ ```ruby
114
+ change_column :users, :otp_counter, :integer, default: 0
115
+ ```
116
+
117
+ In addition set the counter flag option to true
118
+
119
+ ```ruby
120
+ class User < ApplicationRecord
121
+ has_one_time_password counter_based: true
122
+ end
123
+ ```
124
+
125
+ And for a custom counter column
126
+
127
+ ```ruby
128
+ class User < ApplicationRecord
129
+ has_one_time_password counter_based: true, counter_column_name: :my_otp_secret_counter_column
130
+ end
131
+ ```
132
+
133
+ Authentication is done the same. You can manually adjust the counter for your usage or set auto_increment on success to true.
134
+
135
+ ```ruby
136
+ user.authenticate_otp('186522') # => true
137
+ user.authenticate_otp('186522', auto_increment: true) # => true
138
+ user.authenticate_otp('186522') # => false
139
+ user.otp_counter -= 1
140
+ user.authenticate_otp('186522') # => true
141
+ ```
142
+
143
+ When retrieving an ```otp_code``` you can also pass the ```auto_increment``` option.
144
+
145
+ ```ruby
146
+ user.otp_code # => '186522'
147
+ user.otp_code # => '186522'
148
+ user.otp_code(auto_increment: true) # => '768273'
149
+ user.otp_code(auto_increment: true) # => '002811'
150
+ user.otp_code # => '002811'
151
+ ```
152
+
78
153
  ## Google Authenticator Compatible
79
154
 
80
155
  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.
81
156
 
82
157
  ```ruby
83
- # Use you user's emails for generate the provision_url
84
- user.provision_uri # => 'otpauth://totp/hello@heapsource.com?secret=2z6hxkdwi3uvrnpn'
158
+ # Use your user's email address to generate the provisioning_url
159
+ user.provisioning_uri # => 'otpauth://totp/hello@heapsource.com?secret=2z6hxkdwi3uvrnpn'
160
+
161
+ # Use a custom field to generate the provisioning_url
162
+ user.provisioning_uri("hello") # => 'otpauth://totp/hello?secret=2z6hxkdwi3uvrnpn'
85
163
 
86
- # Use a custom fied for generate the provision_url
87
- user.provision_uri("hello") # => 'otpauth://totp/hello?secret=2z6hxkdwi3uvrnpn'
164
+ # You can customize the generated url, by passing a hash of Options
165
+ # `:issuer` lets you set the Issuer name in Google Authenticator, so it doesn't show as a blank entry.
166
+ user.provisioning_uri(nil, issuer: 'MYAPP') #=> 'otpauth://totp/hello@heapsource.com?secret=2z6hxkdwi3uvrnpn&issuer=MYAPP'
88
167
  ```
89
168
 
90
- This can then be rendered as a QR Code which can then be scanned and added to the users list of OTP credentials.
169
+ This can then be rendered as a QR Code which can be scanned and added to the users list of OTP credentials.
91
170
 
92
171
  ### Working example
93
172
 
@@ -99,6 +178,7 @@ Now run the following and compare the output
99
178
 
100
179
  ```ruby
101
180
  require "active_model_otp"
181
+
102
182
  class User
103
183
  extend ActiveModel::Callbacks
104
184
  include ActiveModel::Validations
@@ -109,6 +189,7 @@ class User
109
189
 
110
190
  has_one_time_password
111
191
  end
192
+
112
193
  user = User.new
113
194
  user.email = 'roberto@heapsource.com'
114
195
  user.otp_secret_key = "2z6hxkdwi3uvrnpn"
@@ -118,14 +199,11 @@ puts "Current code #{user.otp_code}"
118
199
  **Note:** otp_secret_key must be generated using RFC 3548 base32 key strings (for compatilibity with google authenticator)
119
200
 
120
201
  ### Useful Examples
121
-
122
- #### Generating QR Code with Google Charts API
123
-
124
- #### Generating QR Code with rqrcode and chunky_png
125
-
126
- #### Sendind code via email with Twilio
127
-
128
- #### Using with Mongoid
202
+ - [Drifting Ruby Tutorial](https://www.driftingruby.com/episodes/two-factor-authentication)
203
+ - [Generate QR code with rqrcode gem](https://github.com/heapsource/active_model_otp/wiki/Generate-QR-code-with-rqrcode-gem)
204
+ - Generating QR Code with Google Charts API
205
+ - [Sending code via SMS with Twilio](https://github.com/heapsource/active_model_otp/wiki/Send-code-via-Twilio-SMS)
206
+ - [Using with Mongoid](https://github.com/heapsource/active_model_otp/wiki/Using-with-Mongoid)
129
207
 
130
208
  ## Contributing
131
209
 
@@ -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"
@@ -17,11 +17,20 @@ 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
+
21
+ spec.required_ruby_version = ">= 2.3"
20
22
 
21
23
  spec.add_dependency "activemodel"
22
- spec.add_dependency "rotp"
24
+ spec.add_dependency "rotp", "~> 5.0.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
- spec.add_development_dependency "minitest"
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", "~> 1.3.6"
35
+ end
27
36
  end
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", "~> 4.2"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,8 @@
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
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
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
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
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
+
8
+ 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: "../"
@@ -4,49 +4,122 @@ module ActiveModel
4
4
 
5
5
  module ClassMethods
6
6
  def has_one_time_password(options = {})
7
+ cattr_accessor :otp_column_name, :otp_counter_column_name
8
+ class_attribute :otp_digits, :otp_counter_based
7
9
 
8
- cattr_accessor :otp_column_name
9
10
  self.otp_column_name = (options[:column_name] || "otp_secret_key").to_s
11
+ self.otp_digits = options[:length] || 6
12
+
13
+ self.otp_counter_based = (options[:counter_based] || false)
14
+ self.otp_counter_column_name = (options[:counter_column_name] || "otp_counter").to_s
10
15
 
11
16
  include InstanceMethodsOnActivation
12
17
 
13
- before_create { self.otp_column = ROTP::Base32.random_base32 }
18
+ before_create(options.slice(:if, :unless)) do
19
+ self.otp_regenerate_secret if !otp_column
20
+ self.otp_regenerate_counter if otp_counter_based && !otp_counter
21
+ end
14
22
 
15
23
  if respond_to?(:attributes_protected_by_default)
16
24
  def self.attributes_protected_by_default #:nodoc:
17
- super + [self.otp_column_name]
25
+ super + [otp_column_name, otp_counter_column_name]
18
26
  end
19
27
  end
20
28
  end
29
+
30
+ # Defaults to 160 bit long secret
31
+ # (meaning a 32 character long base32 secret)
32
+ def otp_random_secret(length = 20)
33
+ ROTP::Base32.random(length)
34
+ end
21
35
  end
22
36
 
23
37
  module InstanceMethodsOnActivation
38
+ def otp_regenerate_secret
39
+ self.otp_column = self.class.otp_random_secret
40
+ end
41
+
42
+ def otp_regenerate_counter
43
+ self.otp_counter = 1
44
+ end
45
+
24
46
  def authenticate_otp(code, options = {})
25
- totp = ROTP::TOTP.new(self.otp_column)
26
- if drift = options[:drift]
27
- totp.verify_with_drift(code, drift)
47
+ if otp_counter_based
48
+ hotp = ROTP::HOTP.new(otp_column, digits: otp_digits)
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
28
55
  else
29
- totp.verify(code)
56
+ totp = ROTP::TOTP.new(otp_column, digits: otp_digits)
57
+ if drift = options[:drift]
58
+ totp.verify(code, drift_behind: drift)
59
+ else
60
+ totp.verify(code)
61
+ end
30
62
  end
31
63
  end
32
64
 
33
- def otp_code(time = Time.now)
34
- ROTP::TOTP.new(self.otp_column).at(time)
65
+ def otp_code(options = {})
66
+ if otp_counter_based
67
+ if options[:auto_increment]
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)
72
+ else
73
+ if options.is_a? Hash
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)
79
+ end
35
80
  end
36
81
 
37
- def provisioning_uri(account = nil)
82
+ def provisioning_uri(account = nil, options = {})
38
83
  account ||= self.email if self.respond_to?(:email)
39
- ROTP::TOTP.new(self.otp_column).provisioning_uri(account)
84
+ account ||= ""
85
+
86
+ if otp_counter_based
87
+ ROTP::HOTP.new(otp_column, options).provisioning_uri(account)
88
+ else
89
+ ROTP::TOTP.new(otp_column, options).provisioning_uri(account)
90
+ end
40
91
  end
41
92
 
42
93
  def otp_column
43
- self.send(self.class.otp_column_name)
94
+ self.public_send(self.class.otp_column_name)
44
95
  end
45
96
 
46
97
  def otp_column=(attr)
47
- self.send("#{self.class.otp_column_name}=", attr)
98
+ self.public_send("#{self.class.otp_column_name}=", attr)
48
99
  end
49
100
 
101
+ def otp_counter
102
+ if self.class.otp_counter_column_name != "otp_counter"
103
+ self.public_send(self.class.otp_counter_column_name)
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ def otp_counter=(attr)
110
+ if self.class.otp_counter_column_name != "otp_counter"
111
+ self.public_send("#{self.class.otp_counter_column_name}=", attr)
112
+ else
113
+ super
114
+ end
115
+ end
116
+
117
+ def serializable_hash(options = nil)
118
+ options ||= {}
119
+ options[:except] = Array(options[:except])
120
+ options[:except] << self.class.otp_column_name
121
+ super(options)
122
+ end
50
123
  end
51
124
  end
52
125
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  module Otp
3
- VERSION = "0.1.0"
3
+ VERSION = "2.1.0"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  require "active_model"
2
- require "active_support/core_ext/class/attribute_accessors"
2
+ require "active_support/core_ext/module/attribute_accessors"
3
3
  require "cgi"
4
4
  require "rotp"
5
5
  require "active_model/one_time_password"
@@ -0,0 +1,3 @@
1
+ class ActiverecordUser < ActiveRecord::Base
2
+ has_one_time_password counter_based: true
3
+ 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
@@ -1,5 +1,7 @@
1
1
  class User
2
2
  extend ActiveModel::Callbacks
3
+ include ActiveModel::Serializers::JSON
4
+ include ActiveModel::Serializers::Xml
3
5
  include ActiveModel::Validations
4
6
  include ActiveModel::OneTimePassword
5
7
 
@@ -7,5 +9,7 @@ class User
7
9
  attr_accessor :otp_secret_key, :email
8
10
 
9
11
  has_one_time_password
10
-
12
+ def attributes
13
+ { "otp_secret_key" => otp_secret_key, "email" => email }
14
+ end
11
15
  end
@@ -6,7 +6,6 @@ class Visitor
6
6
  define_model_callbacks :create
7
7
  attr_accessor :otp_token, :email
8
8
 
9
- has_one_time_password :column_name => :otp_token
10
-
9
+ has_one_time_password column_name: :otp_token, length: 4
11
10
  end
12
11
 
@@ -5,9 +5,22 @@ class OtpTest < MiniTest::Unit::TestCase
5
5
  @user = User.new
6
6
  @user.email = 'roberto@heapsource.com'
7
7
  @user.run_callbacks :create
8
+
8
9
  @visitor = Visitor.new
9
10
  @visitor.email = 'roberto@heapsource.com'
10
11
  @visitor.run_callbacks :create
12
+
13
+ @member = Member.new
14
+ @member.email = nil
15
+ @member.run_callbacks :create
16
+
17
+ @ar_user = ActiverecordUser.new
18
+ @ar_user.email = 'roberto@heapsource.com'
19
+ @ar_user.run_callbacks :create
20
+
21
+ @opt_in = OptInTwoFactor.new
22
+ @opt_in.email = 'roberto@heapsource.com'
23
+ @opt_in.run_callbacks :create
11
24
  end
12
25
 
13
26
  def test_authenticate_with_otp
@@ -18,6 +31,36 @@ class OtpTest < MiniTest::Unit::TestCase
18
31
  assert @visitor.authenticate_otp(code)
19
32
  end
20
33
 
34
+ def test_counter_based_otp
35
+ code = @member.otp_code
36
+ assert @member.authenticate_otp(code)
37
+ assert @member.authenticate_otp(code, auto_increment: true)
38
+ assert !@member.authenticate_otp(code)
39
+ @member.otp_counter -= 1
40
+ assert @member.authenticate_otp(code)
41
+ assert code == @member.otp_code
42
+ assert code != @member.otp_code(auto_increment: true)
43
+ end
44
+
45
+ def test_counter_based_otp_active_record
46
+ code = @ar_user.otp_code
47
+ assert @ar_user.authenticate_otp(code)
48
+ assert @ar_user.authenticate_otp(code, auto_increment: true)
49
+ assert !@ar_user.authenticate_otp(code)
50
+ @ar_user.otp_counter -= 1
51
+ assert @ar_user.authenticate_otp(code)
52
+ assert code == @ar_user.otp_code
53
+ assert code != @ar_user.otp_code(auto_increment: true)
54
+ end
55
+
56
+ def test_opt_in_two_factor
57
+ assert @opt_in.otp_column.nil?
58
+
59
+ @opt_in.otp_regenerate_secret
60
+ code = @opt_in.otp_code
61
+ assert @opt_in.authenticate_otp(code)
62
+ end
63
+
21
64
  def test_authenticate_with_otp_when_drift_is_allowed
22
65
  code = @user.otp_code(Time.now - 30)
23
66
  assert @user.authenticate_otp(code, drift: 60)
@@ -27,17 +70,51 @@ class OtpTest < MiniTest::Unit::TestCase
27
70
  end
28
71
 
29
72
  def test_otp_code
30
- assert_match(/\d{5,6}/, @user.otp_code.to_s)
31
- assert_match(/\d{5,6}/, @visitor.otp_code.to_s)
73
+ assert_match(/^\d{6}$/, @user.otp_code.to_s)
74
+ assert_match(/^\d{4}$/, @visitor.otp_code.to_s)
75
+ end
76
+
77
+ def test_otp_code_with_specific_length
78
+ assert_match(/^\d{4}$/, @visitor.otp_code(2160).to_s)
79
+ assert_operator(@visitor.otp_code(2160).to_s.length, :<=, 4)
80
+ end
81
+
82
+ def test_otp_code_without_specific_length
83
+ assert_match(/^\d{6}$/, @user.otp_code(2160).to_s)
84
+ assert_operator(@user.otp_code(2160).to_s.length, :<=, 6)
32
85
  end
33
86
 
34
87
  def test_provisioning_uri_with_provided_account
35
- assert_match %r{otpauth://totp/roberto\?secret=\w{16}}, @user.provisioning_uri("roberto")
36
- assert_match %r{otpauth://totp/roberto\?secret=\w{16}}, @visitor.provisioning_uri("roberto")
88
+ assert_match %r{^otpauth://totp/roberto\?secret=\w{32}$}, @user.provisioning_uri("roberto")
89
+ assert_match %r{^otpauth://totp/roberto\?secret=\w{32}$}, @visitor.provisioning_uri("roberto")
90
+ assert_match %r{^otpauth://hotp/roberto\?secret=\w{32}&counter=0$}, @member.provisioning_uri("roberto")
37
91
  end
38
92
 
39
93
  def test_provisioning_uri_with_email_field
40
- assert_match %r{otpauth://totp/roberto@heapsource\.com\?secret=\w{16}}, @user.provisioning_uri
41
- assert_match %r{otpauth://totp/roberto@heapsource\.com\?secret=\w{16}}, @visitor.provisioning_uri
94
+ assert_match %r{^otpauth://totp/roberto@heapsource\.com\?secret=\w{32}$}, @user.provisioning_uri
95
+ assert_match %r{^otpauth://totp/roberto@heapsource\.com\?secret=\w{32}$}, @visitor.provisioning_uri
96
+ assert_match %r{^otpauth://hotp/\?secret=\w{32}&counter=0$}, @member.provisioning_uri
97
+ end
98
+
99
+ def test_provisioning_uri_with_options
100
+ assert_match %r{^otpauth://totp/Example\:roberto@heapsource\.com\?secret=\w{32}&issuer=Example$}, @user.provisioning_uri(nil, issuer: "Example")
101
+ assert_match %r{^otpauth://totp/Example\:roberto@heapsource\.com\?secret=\w{32}&issuer=Example$}, @visitor.provisioning_uri(nil, issuer: "Example")
102
+ assert_match %r{^otpauth://totp/Example\:roberto\?secret=\w{32}&issuer=Example$}, @user.provisioning_uri("roberto", issuer: "Example")
103
+ assert_match %r{^otpauth://totp/Example\:roberto\?secret=\w{32}&issuer=Example$}, @visitor.provisioning_uri("roberto", issuer: "Example")
104
+ end
105
+
106
+ def test_regenerate_otp
107
+ secret = @user.otp_column
108
+ @user.otp_regenerate_secret
109
+ assert secret != @user.otp_column
110
+ end
111
+
112
+ def test_hide_secret_key_in_serialize
113
+ refute_match(/otp_secret_key/, @user.to_json)
114
+ refute_match(/otp_secret_key/, @user.to_xml)
115
+ end
116
+
117
+ def test_otp_random_secret
118
+ assert_match /^.{32}$/, @user.class.otp_random_secret
42
119
  end
43
120
  end
@@ -0,0 +1,11 @@
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
+ end
@@ -6,7 +6,16 @@ $LOAD_PATH.unshift libdir unless $LOAD_PATH.include?(libdir)
6
6
 
7
7
  require "rubygems"
8
8
  require "active_model_otp"
9
- require "minitest/unit"
10
9
  require "minitest/autorun"
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,100 +1,117 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_model_otp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
5
- prerelease:
4
+ version: 2.1.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Guillermo Iguaran
9
8
  - Roberto Miranda
10
9
  - Heapsource
11
- autorequire:
10
+ autorequire:
12
11
  bindir: bin
13
12
  cert_chain: []
14
- date: 2013-12-19 00:00:00.000000000 Z
13
+ date: 2020-12-15 00:00:00.000000000 Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
16
  name: activemodel
18
17
  requirement: !ruby/object:Gem::Requirement
19
- none: false
20
18
  requirements:
21
- - - ! '>='
19
+ - - ">="
22
20
  - !ruby/object:Gem::Version
23
21
  version: '0'
24
22
  type: :runtime
25
23
  prerelease: false
26
24
  version_requirements: !ruby/object:Gem::Requirement
27
- none: false
28
25
  requirements:
29
- - - ! '>='
26
+ - - ">="
30
27
  - !ruby/object:Gem::Version
31
28
  version: '0'
32
29
  - !ruby/object:Gem::Dependency
33
30
  name: rotp
34
31
  requirement: !ruby/object:Gem::Requirement
35
- none: false
36
32
  requirements:
37
- - - ! '>='
33
+ - - "~>"
38
34
  - !ruby/object:Gem::Version
39
- version: '0'
35
+ version: 5.0.0
40
36
  type: :runtime
41
37
  prerelease: false
42
38
  version_requirements: !ruby/object:Gem::Requirement
43
- none: false
44
39
  requirements:
45
- - - ! '>='
40
+ - - "~>"
46
41
  - !ruby/object:Gem::Version
47
- version: '0'
42
+ version: 5.0.0
48
43
  - !ruby/object:Gem::Dependency
49
- name: bundler
44
+ name: activerecord
50
45
  requirement: !ruby/object:Gem::Requirement
51
- none: false
52
46
  requirements:
53
- - - ~>
47
+ - - ">="
54
48
  - !ruby/object:Gem::Version
55
- version: '1.3'
49
+ version: '0'
56
50
  type: :development
57
51
  prerelease: false
58
52
  version_requirements: !ruby/object:Gem::Requirement
59
- none: false
60
53
  requirements:
61
- - - ~>
54
+ - - ">="
62
55
  - !ruby/object:Gem::Version
63
- version: '1.3'
56
+ version: '0'
64
57
  - !ruby/object:Gem::Dependency
65
58
  name: rake
66
59
  requirement: !ruby/object:Gem::Requirement
67
- none: false
68
60
  requirements:
69
- - - ! '>='
61
+ - - ">="
70
62
  - !ruby/object:Gem::Version
71
63
  version: '0'
72
64
  type: :development
73
65
  prerelease: false
74
66
  version_requirements: !ruby/object:Gem::Requirement
75
- none: false
76
67
  requirements:
77
- - - ! '>='
68
+ - - ">="
78
69
  - !ruby/object:Gem::Version
79
70
  version: '0'
80
71
  - !ruby/object:Gem::Dependency
81
72
  name: minitest
82
73
  requirement: !ruby/object:Gem::Requirement
83
- none: false
84
74
  requirements:
85
- - - ! '>='
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: 5.4.2
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: 5.4.2
85
+ - !ruby/object:Gem::Dependency
86
+ name: appraisal
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
86
90
  - !ruby/object:Gem::Version
87
91
  version: '0'
88
92
  type: :development
89
93
  prerelease: false
90
94
  version_requirements: !ruby/object:Gem::Requirement
91
- none: false
92
95
  requirements:
93
- - - ! '>='
96
+ - - ">="
94
97
  - !ruby/object:Gem::Version
95
98
  version: '0'
96
- description: Adds methods to set and authenticate against one time passwords. Inspired
97
- in AM::SecurePassword"
99
+ - !ruby/object:Gem::Dependency
100
+ name: sqlite3
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - "~>"
104
+ - !ruby/object:Gem::Version
105
+ version: 1.3.6
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: 1.3.6
113
+ description: Adds methods to set and authenticate against one time passwords 2FA(Two
114
+ factor Authentication). Inspired in AM::SecurePassword"
98
115
  email:
99
116
  - guilleiguaran@gmail.com
100
117
  - rjmaltamar@gmail.com
@@ -103,47 +120,60 @@ executables: []
103
120
  extensions: []
104
121
  extra_rdoc_files: []
105
122
  files:
106
- - .gitignore
107
- - .travis.yml
123
+ - ".gitignore"
124
+ - ".travis.yml"
125
+ - Appraisals
126
+ - CHANGELOG.md
108
127
  - Gemfile
109
128
  - LICENSE.txt
110
129
  - README.md
111
130
  - Rakefile
112
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
113
137
  - lib/active_model/one_time_password.rb
114
138
  - lib/active_model/otp/version.rb
115
139
  - lib/active_model_otp.rb
140
+ - test/models/activerecord_user.rb
141
+ - test/models/member.rb
142
+ - test/models/opt_in_two_factor.rb
116
143
  - test/models/user.rb
117
144
  - test/models/visitor.rb
118
145
  - test/one_time_password_test.rb
146
+ - test/schema.rb
119
147
  - test/test_helper.rb
120
148
  homepage: ''
121
149
  licenses:
122
150
  - MIT
123
- post_install_message:
151
+ metadata: {}
152
+ post_install_message:
124
153
  rdoc_options: []
125
154
  require_paths:
126
155
  - lib
127
156
  required_ruby_version: !ruby/object:Gem::Requirement
128
- none: false
129
157
  requirements:
130
- - - ! '>='
158
+ - - ">="
131
159
  - !ruby/object:Gem::Version
132
- version: '0'
160
+ version: '2.3'
133
161
  required_rubygems_version: !ruby/object:Gem::Requirement
134
- none: false
135
162
  requirements:
136
- - - ! '>='
163
+ - - ">="
137
164
  - !ruby/object:Gem::Version
138
165
  version: '0'
139
166
  requirements: []
140
- rubyforge_project:
141
- rubygems_version: 1.8.25
142
- signing_key:
143
- specification_version: 3
167
+ rubygems_version: 3.0.3
168
+ signing_key:
169
+ specification_version: 4
144
170
  summary: Adds methods to set and authenticate against one time passwords.
145
171
  test_files:
172
+ - test/models/activerecord_user.rb
173
+ - test/models/member.rb
174
+ - test/models/opt_in_two_factor.rb
146
175
  - test/models/user.rb
147
176
  - test/models/visitor.rb
148
177
  - test/one_time_password_test.rb
178
+ - test/schema.rb
149
179
  - test/test_helper.rb