active_model_otp 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -1
- data/README.md +64 -14
- data/lib/active_model/one_time_password.rb +59 -17
- data/lib/active_model/otp/version.rb +1 -1
- data/test/one_time_password_test.rb +7 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a53fb4e15fa9408222f26928a84d9b1f67919b86
|
4
|
+
data.tar.gz: f436275d42c9e8e78c074809be7cbc06da3e9c16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4c409cf8191c2511f7f7c984d0b2623463f65a835e5b86d6cec3dd59b9f91836f8a5d5308b164d328c773dbebe76c51e0c9ae0503048bc81e8326280d57a2379
|
7
|
+
data.tar.gz: 2b69f18d93545ea5e0e41bc3bc10930887f2f7f1b95dbee947b6e9eed256cc5b78f80014fa7e11e2f3d271f204ccf5dad9b412f9c1193404dcda64478e1d116a
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
#
|
1
|
+
#v1.2.0
|
2
|
+
- Added Counter based OTP (HOTP) (@ResultsMayVary ) https://github.com/heapsource/active_model_otp/pull/19
|
3
|
+
- Adding options to provisioning uri, so we can include issuer (@doon) https://github.com/heapsource/active_model_otp/pull/15
|
4
|
+
|
5
|
+
#v1.1.0
|
6
|
+
- Add function to re-geterante the OTP secret (@TikiTDO) https://github.com/heapsource/active_model_otp/pull/14
|
7
|
+
- Added option to pass OTP length (@shivanibhanwal) https://github.com/heapsource/active_model_otp/pull/13
|
8
|
+
|
2
9
|
#v1.0.0
|
3
10
|
- Avoid overriding predefined otp_column value when initializing resource (Ilan Stern) https://github.com/heapsource/active_model_otp/pull/10
|
4
11
|
- Pad OTP codes with less than 6 digits (Johan Brissmyr) https://github.com/heapsource/active_model_otp/pull/7
|
data/README.md
CHANGED
@@ -1,8 +1,12 @@
|
|
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
|
+
[![Dependency Status](https://gemnasium.com/heapsource/active_model_otp.svg)](https://gemnasium.com/heapsource/active_model_otp)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/heapsource/active_model_otp/badges/gpa.svg)](https://codeclimate.com/github/heapsource/active_model_otp)
|
5
|
+
|
2
6
|
|
3
7
|
# ActiveModel::Otp
|
4
8
|
|
5
|
-
**ActiveModel::Otp** makes adding **Two Factor Authentication**(TFA) to a model simple
|
9
|
+
**ActiveModel::Otp** makes adding **Two Factor Authentication** (TFA) to a model simple. Let's see what's required to get AMo::Otp working in our Application, using Rails 4.0 (AMo::Otp is also compatible with Rails 3.x versions). We're going to use a User model and some authentication to do it. Inspired by AM::SecurePassword
|
6
10
|
|
7
11
|
## Installation
|
8
12
|
|
@@ -14,7 +18,7 @@ And then execute:
|
|
14
18
|
|
15
19
|
$ bundle
|
16
20
|
|
17
|
-
Or install it yourself as:
|
21
|
+
Or install it yourself as follows:
|
18
22
|
|
19
23
|
$ gem install active_model_otp
|
20
24
|
|
@@ -29,7 +33,7 @@ rails g migration AddOtpSecretKeyToUsers otp_secret_key:string
|
|
29
33
|
create db/migrate/20130707010931_add_otp_secret_key_to_users.rb
|
30
34
|
```
|
31
35
|
|
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
|
36
|
+
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
37
|
|
34
38
|
```ruby
|
35
39
|
class User < ActiveRecord::Base
|
@@ -42,20 +46,19 @@ Note: If you're adding this to an existing user model you'll need to generate *o
|
|
42
46
|
User.all.each { |user| user.update_attribute(:otp_secret_key, ROTP::Base32.random_base32) }
|
43
47
|
```
|
44
48
|
|
45
|
-
|
49
|
+
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.
|
46
50
|
|
47
51
|
```ruby
|
48
52
|
class User < ActiveRecord::Base
|
49
|
-
has_one_time_password column_name: :my_otp_secret_column
|
53
|
+
has_one_time_password column_name: :my_otp_secret_column, length: 4
|
50
54
|
end
|
51
55
|
```
|
52
56
|
|
57
|
+
## Usage
|
53
58
|
|
54
|
-
|
55
|
-
|
56
|
-
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.
|
59
|
+
The has_one_time_password statement provides to the model some useful methods in order to implement our TFA system. AMo:Otp generates one time passwords according to [TOTP RFC 6238](http://tools.ietf.org/html/rfc4226) and the [HOTP RFC 4226](http://tools.ietf.org/html/rfc4226). This is compatible with Google Authenticator apps available for Android and iPhone, and now in use on GMail.
|
57
60
|
|
58
|
-
The otp_secret_key is saved automatically when
|
61
|
+
The otp_secret_key is saved automatically when an object is created,
|
59
62
|
|
60
63
|
```ruby
|
61
64
|
user = User.create(email: "hello@heapsource.com")
|
@@ -63,9 +66,9 @@ user.otp_secret_key
|
|
63
66
|
=> "jt3gdd2qm6su5iqh"
|
64
67
|
```
|
65
68
|
|
66
|
-
**Note:** You can fork the applications for [iPhone](https://github.com/heapsource/google-authenticator) & [Android](https://github.com/heapsource/google-authenticator.android) and customize
|
69
|
+
**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
|
67
70
|
|
68
|
-
### Getting current code (
|
71
|
+
### Getting current code (e.g. to send via SMS)
|
69
72
|
```ruby
|
70
73
|
user.otp_code # => '186522'
|
71
74
|
sleep 30
|
@@ -94,19 +97,66 @@ sleep 30 # lets wait again
|
|
94
97
|
user.authenticate_otp('186522', drift: 60) # => true
|
95
98
|
```
|
96
99
|
|
100
|
+
## Counter based OTP
|
101
|
+
|
102
|
+
An additonal counter field is required in our ``User`` Model
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
rails g migration AddCounterForOtpToUsers otp_counter:integer
|
106
|
+
=>
|
107
|
+
invoke active_record
|
108
|
+
create db/migrate/20130707010931_add_counter_for_otp_to_users.rb
|
109
|
+
```
|
110
|
+
|
111
|
+
In addition set the counter flag option to true
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
class User < ActiveRecord::Base
|
115
|
+
has_one_time_password counter_based: true
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
And for a custom counter column
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
class User < ActiveRecord::Base
|
123
|
+
has_one_time_password counter_based: true, counter_column_name: :my_otp_secret_counter_column
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
Authentication is done the same. You can manually adjust the counter for your usage or set auto_increment on success to true.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
user.authenticate_otp('186522') # => true
|
131
|
+
user.authenticate_otp('186522', auto_increment: true) # => true
|
132
|
+
user.authenticate_otp('186522') # => false
|
133
|
+
user.otp_counter -= 1
|
134
|
+
user.authenticate_otp('186522') # => true
|
135
|
+
```
|
136
|
+
|
137
|
+
When retrieving an ```otp_code``` you can also pass the ```auto_increment``` option.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
user.otp_code # => '186522'
|
141
|
+
user.otp_code # => '186522'
|
142
|
+
user.otp_code(auto_increment: true) # => '768273'
|
143
|
+
user.otp_code(auto_increment: true) # => '002811'
|
144
|
+
user.otp_code # => '002811'
|
145
|
+
```
|
146
|
+
|
97
147
|
## Google Authenticator Compatible
|
98
148
|
|
99
149
|
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.
|
100
150
|
|
101
151
|
```ruby
|
102
|
-
# Use
|
152
|
+
# Use your user's email address to generate the provisioning_url
|
103
153
|
user.provisioning_uri # => 'otpauth://totp/hello@heapsource.com?secret=2z6hxkdwi3uvrnpn'
|
104
154
|
|
105
|
-
# Use a custom
|
155
|
+
# Use a custom field to generate the provisioning_url
|
106
156
|
user.provisioning_uri("hello") # => 'otpauth://totp/hello?secret=2z6hxkdwi3uvrnpn'
|
107
157
|
```
|
108
158
|
|
109
|
-
This can then be rendered as a QR Code which can
|
159
|
+
This can then be rendered as a QR Code which can be scanned and added to the users list of OTP credentials.
|
110
160
|
|
111
161
|
### Working example
|
112
162
|
|
@@ -5,20 +5,27 @@ module ActiveModel
|
|
5
5
|
module ClassMethods
|
6
6
|
|
7
7
|
def has_one_time_password(options = {})
|
8
|
-
|
9
|
-
|
10
|
-
class_attribute :otp_digits
|
8
|
+
cattr_accessor :otp_column_name, :otp_counter_column_name
|
9
|
+
class_attribute :otp_digits, :otp_counter_based
|
11
10
|
|
12
11
|
self.otp_column_name = (options[:column_name] || "otp_secret_key").to_s
|
13
12
|
self.otp_digits = options[:length] || 6
|
14
13
|
|
14
|
+
self.otp_counter_based = (options[:counter_based] || false)
|
15
|
+
self.otp_counter_column_name = (
|
16
|
+
options[:counter_column_name] || "otp_counter"
|
17
|
+
).to_s
|
18
|
+
|
15
19
|
include InstanceMethodsOnActivation
|
16
20
|
|
17
|
-
before_create
|
21
|
+
before_create do
|
22
|
+
self.otp_regenerate_secret if !otp_column
|
23
|
+
self.otp_regenerate_counter if otp_counter_based && !otp_counter
|
24
|
+
end
|
18
25
|
|
19
26
|
if respond_to?(:attributes_protected_by_default)
|
20
27
|
def self.attributes_protected_by_default #:nodoc:
|
21
|
-
super + [
|
28
|
+
super + [otp_column_name, otp_counter_column_name]
|
22
29
|
end
|
23
30
|
end
|
24
31
|
end
|
@@ -29,29 +36,56 @@ module ActiveModel
|
|
29
36
|
self.otp_column = ROTP::Base32.random_base32
|
30
37
|
end
|
31
38
|
|
39
|
+
def otp_regenerate_counter
|
40
|
+
self.otp_counter = 1
|
41
|
+
end
|
42
|
+
|
32
43
|
def authenticate_otp(code, options = {})
|
33
|
-
|
34
|
-
|
35
|
-
|
44
|
+
if otp_counter_based
|
45
|
+
hotp = ROTP::HOTP.new(otp_column, digits: otp_digits)
|
46
|
+
result = hotp.verify(code, otp_counter)
|
47
|
+
if result && options[:auto_increment]
|
48
|
+
self.otp_counter += 1
|
49
|
+
save if !new_record?
|
50
|
+
end
|
51
|
+
result
|
36
52
|
else
|
37
|
-
totp.
|
53
|
+
totp = ROTP::TOTP.new(otp_column, digits: otp_digits)
|
54
|
+
if drift = options[:drift]
|
55
|
+
totp.verify_with_drift(code, drift)
|
56
|
+
else
|
57
|
+
totp.verify(code)
|
58
|
+
end
|
38
59
|
end
|
39
60
|
end
|
40
61
|
|
41
62
|
def otp_code(options = {})
|
42
|
-
if
|
43
|
-
|
44
|
-
|
63
|
+
if otp_counter_based
|
64
|
+
if options[:auto_increment]
|
65
|
+
self.otp_counter += 1
|
66
|
+
save if !new_record?
|
67
|
+
end
|
68
|
+
ROTP::HOTP.new(otp_column, digits: otp_digits).at(self.otp_counter)
|
45
69
|
else
|
46
|
-
|
47
|
-
|
70
|
+
if options.is_a? Hash
|
71
|
+
time = options.fetch(:time, Time.now)
|
72
|
+
padding = options.fetch(:padding, true)
|
73
|
+
else
|
74
|
+
time = options
|
75
|
+
padding = true
|
76
|
+
end
|
77
|
+
ROTP::TOTP.new(otp_column, digits: otp_digits).at(time, padding)
|
48
78
|
end
|
49
|
-
ROTP::TOTP.new(self.otp_column, {digits: self.otp_digits}).at(time, padding)
|
50
79
|
end
|
51
80
|
|
52
|
-
def provisioning_uri(account = nil)
|
81
|
+
def provisioning_uri(account = nil, options = {})
|
53
82
|
account ||= self.email if self.respond_to?(:email)
|
54
|
-
|
83
|
+
|
84
|
+
if otp_counter_based
|
85
|
+
ROTP::HOTP.new(otp_column, options).provisioning_uri(account)
|
86
|
+
else
|
87
|
+
ROTP::TOTP.new(otp_column, options).provisioning_uri(account)
|
88
|
+
end
|
55
89
|
end
|
56
90
|
|
57
91
|
def otp_column
|
@@ -61,6 +95,14 @@ module ActiveModel
|
|
61
95
|
def otp_column=(attr)
|
62
96
|
self.send("#{self.class.otp_column_name}=", attr)
|
63
97
|
end
|
98
|
+
|
99
|
+
def otp_counter
|
100
|
+
self.send(self.class.otp_counter_column_name)
|
101
|
+
end
|
102
|
+
|
103
|
+
def otp_counter=(attr)
|
104
|
+
self.send("#{self.class.otp_counter_column_name}=", attr)
|
105
|
+
end
|
64
106
|
end
|
65
107
|
end
|
66
108
|
end
|
@@ -61,6 +61,13 @@ class OtpTest < MiniTest::Unit::TestCase
|
|
61
61
|
assert_match %r{otpauth://totp/roberto@heapsource\.com\?secret=\w{16}}, @visitor.provisioning_uri
|
62
62
|
end
|
63
63
|
|
64
|
+
def test_provisioning_uri_with_options
|
65
|
+
assert_match %r{otpauth://totp/roberto@heapsource\.com\?issuer=Example&secret=\w{16}},@user.provisioning_uri(nil,issuer: "Example")
|
66
|
+
assert_match %r{otpauth://totp/roberto@heapsource\.com\?issuer=Example&secret=\w{16}}, @visitor.provisioning_uri(nil,issuer: "Example")
|
67
|
+
assert_match %r{otpauth://totp/roberto\?issuer=Example&secret=\w{16}}, @user.provisioning_uri("roberto", issuer: "Example")
|
68
|
+
assert_match %r{otpauth://totp/roberto\?issuer=Example&secret=\w{16}}, @visitor.provisioning_uri("roberto", issuer: "Example")
|
69
|
+
end
|
70
|
+
|
64
71
|
def test_regenerate_otp
|
65
72
|
secret = @user.otp_column
|
66
73
|
@user.otp_regenerate_secret
|
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: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillermo Iguaran
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2015-02-26 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activemodel
|