active_model_otp 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +131 -0
- data/Rakefile +27 -0
- data/active_model_otp.gemspec +27 -0
- data/lib/active_model/one_time_password.rb +41 -0
- data/lib/active_model/otp/version.rb +5 -0
- data/lib/active_model_otp.rb +8 -0
- data/test/models/user.rb +11 -0
- data/test/one_time_password_test.rb +33 -0
- data/test/test_helper.rb +12 -0
- metadata +146 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Guillermo Iguaran, Roberto Miranda, Firebase.co
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# ActiveModel::Otp
|
2
|
+
|
3
|
+
**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
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'active_model_otp'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install active_model_otp
|
18
|
+
|
19
|
+
## Setting your Model
|
20
|
+
|
21
|
+
We're going to add a field to our ``User`` Model, so each user can have an otp secret key. The next step is to run the migration generator in order to add the secret key field.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
rails g migration AddOtpSecretKeyToUsers otp_secret_key:string
|
25
|
+
=>
|
26
|
+
invoke active_record
|
27
|
+
create db/migrate/20130707010931_add_otp_secret_key_to_users.rb
|
28
|
+
```
|
29
|
+
|
30
|
+
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.
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class User < ActiveRecord::Base
|
34
|
+
has_one_time_password
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
|
39
|
+
##Usage
|
40
|
+
|
41
|
+
The has_one_time_password sentence provides to the model some useful methods in order to implement our TFA system.
|
42
|
+
The otp_secret_key is saved automatically when a object is created, otp_secret_key is generated 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.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
user = User.create(email: "hello@heapsource.com")
|
46
|
+
user.otp_secret_key
|
47
|
+
=> "jt3gdd2qm6su5iqh"
|
48
|
+
```
|
49
|
+
|
50
|
+
**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
|
51
|
+
|
52
|
+
### Getting current code (ex. to send via SMS)
|
53
|
+
```ruby
|
54
|
+
user.otp_code # => '186522'
|
55
|
+
sleep 30
|
56
|
+
user.otp_code # => '850738'
|
57
|
+
```
|
58
|
+
|
59
|
+
### Authenticating using a code
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
user.authenticate_otp('186522') # => true
|
63
|
+
sleep 30 # let's wait 30 secs
|
64
|
+
user.authenticate_otp('186522') # => false
|
65
|
+
```
|
66
|
+
|
67
|
+
### Authenticating using a slightly old code
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
user.authenticate_otp('186522') # => true
|
71
|
+
sleep 30 # lets wait again
|
72
|
+
user.authenticate_otp('186522', drift: 60) # => true
|
73
|
+
```
|
74
|
+
|
75
|
+
## Google Authenticator Compatible
|
76
|
+
|
77
|
+
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.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# Use you user's emails for generate the provision_url
|
81
|
+
user.provision_uri # => 'otpauth://totp/hello@heapsource.com?secret=2z6hxkdwi3uvrnpn'
|
82
|
+
|
83
|
+
# Use a custom fied for generate the provision_url
|
84
|
+
user.provision_uri("hello") # => 'otpauth://totp/hello?secret=2z6hxkdwi3uvrnpn'
|
85
|
+
```
|
86
|
+
|
87
|
+
This can then be rendered as a QR Code which can then be scanned and added to the users list of OTP credentials.
|
88
|
+
|
89
|
+
### Working example
|
90
|
+
|
91
|
+
Scan the following barcode with your phone, using Google Authenticator
|
92
|
+
|
93
|
+
![QRCODE](http://qrfree.kaywa.com/?l=1&s=8&d=otpauth%3A%2F%2Ftotp%2Froberto%40heapsource.com%3Fsecret%3D2z6hxkdwi3uvrnpn)
|
94
|
+
|
95
|
+
Now run the following and compare the output
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
require "active_model_otp"
|
99
|
+
class User
|
100
|
+
extend ActiveModel::Callbacks
|
101
|
+
include ActiveModel::Validations
|
102
|
+
include ActiveModel::OneTimePassword
|
103
|
+
|
104
|
+
define_model_callbacks :create
|
105
|
+
attr_accessor :otp_secret_key, :email
|
106
|
+
|
107
|
+
has_one_time_password
|
108
|
+
end
|
109
|
+
user = User.new
|
110
|
+
user.email = 'roberto@heapsource.com'
|
111
|
+
user.otp_secret_key = "2z6hxkdwi3uvrnpn"
|
112
|
+
puts "Current code #{user.otp_code}"
|
113
|
+
```
|
114
|
+
|
115
|
+
### Useful Examples
|
116
|
+
|
117
|
+
#### Generating QR Code with Google Charts API
|
118
|
+
|
119
|
+
#### Generating QR Code with rqrcode and chunky_png
|
120
|
+
|
121
|
+
#### Sendind code via email with Twilio
|
122
|
+
|
123
|
+
#### Using with Mongoid
|
124
|
+
|
125
|
+
## Contributing
|
126
|
+
|
127
|
+
1. Fork it
|
128
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
129
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
130
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
131
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
Bundler::GemHelper.install_tasks
|
4
|
+
|
5
|
+
task :console do
|
6
|
+
puts "Loading development console..."
|
7
|
+
system("irb -r active_model_otp")
|
8
|
+
end
|
9
|
+
|
10
|
+
task :help do
|
11
|
+
puts "Available rake tasks: "
|
12
|
+
puts "rake console - Run a IRB console with all enviroment loaded"
|
13
|
+
puts "rake test - Run tests"
|
14
|
+
end
|
15
|
+
|
16
|
+
task :test do
|
17
|
+
Dir.chdir('test')
|
18
|
+
end
|
19
|
+
|
20
|
+
Rake::TestTask.new(:test) do |t|
|
21
|
+
t.libs << '../lib'
|
22
|
+
t.libs << '../test'
|
23
|
+
t.test_files = FileList['*_test.rb']
|
24
|
+
t.verbose = false
|
25
|
+
end
|
26
|
+
|
27
|
+
task :default => :test
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_model/otp/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "active_model_otp"
|
8
|
+
spec.version = ActiveModel::Otp::VERSION
|
9
|
+
spec.authors = ["Guillermo Iguaran", "Roberto Miranda", "Firebase.co"]
|
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"}
|
12
|
+
spec.summary = "Adds methods to set and authenticate against one time passwords."
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activemodel"
|
22
|
+
spec.add_dependency "rotp"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "minitest"
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
module OneTimePassword
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def has_one_time_password(options = {})
|
7
|
+
|
8
|
+
include InstanceMethodsOnActivation
|
9
|
+
|
10
|
+
before_create { self.otp_secret_key = ROTP::Base32.random_base32 }
|
11
|
+
|
12
|
+
if respond_to?(:attributes_protected_by_default)
|
13
|
+
def self.attributes_protected_by_default #:nodoc:
|
14
|
+
super + ["otp_secret_key"]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module InstanceMethodsOnActivation
|
21
|
+
def authenticate_otp(code, options = {})
|
22
|
+
totp = ROTP::TOTP.new(self.otp_secret_key)
|
23
|
+
if drift = options[:drift]
|
24
|
+
totp.verify_with_drift(code, drift)
|
25
|
+
else
|
26
|
+
totp.verify(code)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def otp_code(time = Time.now)
|
31
|
+
ROTP::TOTP.new(self.otp_secret_key).at(time)
|
32
|
+
end
|
33
|
+
|
34
|
+
def provisioning_uri(account = nil)
|
35
|
+
account ||= self.email if self.respond_to?(:email)
|
36
|
+
ROTP::TOTP.new(self.otp_secret_key).provisioning_uri(account)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/test/models/user.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class OtpTest < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@user = User.new
|
6
|
+
@user.email = 'roberto@heapsource.com'
|
7
|
+
@user.run_callbacks :create
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_authenticate_with_otp
|
11
|
+
code = @user.otp_code
|
12
|
+
|
13
|
+
assert @user.authenticate_otp(code)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_authenticate_with_otp_when_drift_is_allowed
|
17
|
+
code = @user.otp_code(Time.now - 30)
|
18
|
+
|
19
|
+
assert @user.authenticate_otp(code, drift: 60)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_otp_code
|
23
|
+
assert_match(/\d{6}/, @user.otp_code.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_provisioning_uri_with_provided_account
|
27
|
+
assert_match %r{otpauth://totp/roberto\?secret=\w{16}}, @user.provisioning_uri("roberto")
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_provisioning_uri_with_email_field
|
31
|
+
assert_match %r{otpauth://totp/roberto@heapsource\.com\?secret=\w{16}}, @user.provisioning_uri
|
32
|
+
end
|
33
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
testdir = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift testdir unless $LOAD_PATH.include?(testdir)
|
3
|
+
|
4
|
+
libdir = File.dirname(File.dirname(__FILE__)) + '/lib'
|
5
|
+
$LOAD_PATH.unshift libdir unless $LOAD_PATH.include?(libdir)
|
6
|
+
|
7
|
+
require "rubygems"
|
8
|
+
require "active_model_otp"
|
9
|
+
require "minitest/unit"
|
10
|
+
require "minitest/autorun"
|
11
|
+
|
12
|
+
Dir["models/*.rb"].each {|file| require file }
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_model_otp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Guillermo Iguaran
|
9
|
+
- Roberto Miranda
|
10
|
+
- Firebase.co
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2013-07-11 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: activemodel
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '0'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ! '>='
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '0'
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: rotp
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: bundler
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '1.3'
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ~>
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '1.3'
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: rake
|
66
|
+
requirement: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
type: :development
|
73
|
+
prerelease: false
|
74
|
+
version_requirements: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: minitest
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
description: Adds methods to set and authenticate against one time passwords. Inspired
|
97
|
+
in AM::SecurePassword"
|
98
|
+
email:
|
99
|
+
- guilleiguaran@gmail.com
|
100
|
+
- rjmaltamar@gmail.com
|
101
|
+
- hello@firebase.co
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- .gitignore
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- active_model_otp.gemspec
|
112
|
+
- lib/active_model/one_time_password.rb
|
113
|
+
- lib/active_model/otp/version.rb
|
114
|
+
- lib/active_model_otp.rb
|
115
|
+
- test/models/user.rb
|
116
|
+
- test/one_time_password_test.rb
|
117
|
+
- test/test_helper.rb
|
118
|
+
homepage: ''
|
119
|
+
licenses:
|
120
|
+
- MIT
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
none: false
|
133
|
+
requirements:
|
134
|
+
- - ! '>='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
requirements: []
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 1.8.25
|
140
|
+
signing_key:
|
141
|
+
specification_version: 3
|
142
|
+
summary: Adds methods to set and authenticate against one time passwords.
|
143
|
+
test_files:
|
144
|
+
- test/models/user.rb
|
145
|
+
- test/one_time_password_test.rb
|
146
|
+
- test/test_helper.rb
|