two_factor_authentication 0.2 → 1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 39274f1841e29f847f6c2900a9228493e6103243
4
+ data.tar.gz: c0c3e1aebe06d8981301dfa2ccd85ab6f347b6e0
5
+ SHA512:
6
+ metadata.gz: 3e4bf30aa6f90afd6ee31400b4acd5e75dfc6fa49162b9ceb18042e406c55b956a0841466b9881c289b842870cb56ca985005e639578861259862c9c04209e3c
7
+ data.tar.gz: 91f57824cfc4ea2588cd7658c50eb8ee0b221b6dff7c45362d2d2542c51911c61f3f88483e9d79951d14c1c61fb3a4c80a11065bf77a513a0cc5f15d41bd713f
data/.gitignore CHANGED
@@ -18,3 +18,4 @@ patches-*
18
18
  capybara-*.html
19
19
  dump.rdb
20
20
  *.ids
21
+ .rbenv-version
data/README.md CHANGED
@@ -32,6 +32,20 @@ Finally, run the migration with:
32
32
 
33
33
  bundle exec rake db:migrate
34
34
 
35
+ Add the following line to your model to fully enable two-factor auth:
36
+
37
+ has_one_time_password
38
+
39
+ Set config values if desired for maximum second factor attempts count and allowed time drift for one-time passwords:
40
+
41
+ config.max_login_attempts = 3
42
+ config.allowed_otp_drift_seconds = 30
43
+
44
+ Override the method to send one-time passwords in your model, this is automatically called when a user logs in:
45
+
46
+ def send_two_factor_authentication_code
47
+ # use Model#otp_code and send via SMS, etc.
48
+ end
35
49
 
36
50
  ### Manual installation
37
51
 
@@ -42,23 +56,22 @@ To manually enable two factor authentication for the User model, you should add
42
56
  :recoverable, :rememberable, :trackable, :validatable, :two_factor_authenticatable
43
57
  ```
44
58
 
45
- Two default parameters
59
+ Add the following line to your model to fully enable two-factor auth:
46
60
 
47
- ```ruby
48
- config.login_code_random_pattern = /\w+/
49
- config.max_login_attempts = 3
50
- ```
61
+ has_one_time_password
51
62
 
52
- Possible random patterns
63
+ Set config values if desired for maximum second factor attempts count and allowed time drift for one-time passwords:
53
64
 
54
- ```ruby
55
- /\d{5}/
56
- /\w{4,8}/
57
- ```
65
+ config.max_login_attempts = 3
66
+ config.allowed_otp_drift_seconds = 30
67
+
68
+ Override the method to send one-time passwords in your model, this is automatically called when a user logs in:
58
69
 
59
- see more https://github.com/benburkert/randexp
70
+ def send_two_factor_authentication_code
71
+ # use Model#otp_code and send via SMS, etc.
72
+ end
60
73
 
61
- ### Customisation
74
+ ### Customisation and Usage
62
75
 
63
76
  By default second factor authentication enabled for each user, you can change it with this method in your User model:
64
77
 
@@ -70,12 +83,12 @@ By default second factor authentication enabled for each user, you can change it
70
83
 
71
84
  this will disable two factor authentication for local users
72
85
 
73
- Your send sms logic should be in this method in your User model:
86
+ This gem is compatible with Google Authenticator (https://support.google.com/accounts/answer/1066447?hl=en). You can generate provisioning uris by invoking the following method on your model:
74
87
 
75
- ```ruby
76
- def send_two_factor_authentication_code(code)
77
- puts code
78
- end
79
- ```
88
+ user.provisioning_uri #This assumes a user model with an email attributes
89
+
90
+ This provisioning uri can then be turned in to a QR code if desired so that users may add the app to Google Authenticator easily. Once this is done they may retrieve a one-time password directly from the Google Authenticator app as well as through whatever method you define in `send_two_factor_authentication_code`
91
+
92
+ ### Example
80
93
 
81
- This example just puts the code in the logs.
94
+ [TwoFactorAuthenticationExample](https://github.com/Houdini/TwoFactorAuthenticationExample)
@@ -7,8 +7,8 @@ class Devise::TwoFactorAuthenticationController < DeviseController
7
7
 
8
8
  def update
9
9
  render :show and return if params[:code].nil?
10
- md5 = Digest::MD5.hexdigest(params[:code])
11
- if md5.eql?(resource.second_factor_pass_code)
10
+
11
+ if resource.authenticate_otp(params[:code])
12
12
  warden.session(resource_name)[:need_two_factor_authentication] = false
13
13
  sign_in resource_name, resource, :bypass => true
14
14
  redirect_to stored_location_for(resource_name) || :root
@@ -16,7 +16,7 @@ class Devise::TwoFactorAuthenticationController < DeviseController
16
16
  else
17
17
  resource.second_factor_attempts_count += 1
18
18
  resource.save
19
- set_flash_message :notice, :attempt_failed
19
+ set_flash_message :error, :attempt_failed
20
20
  if resource.max_login_attempts?
21
21
  sign_out(resource)
22
22
  render :template => 'devise/two_factor_authentication/max_login_attempts_reached' and return
@@ -1,3 +1,3 @@
1
- <h2>Access completly denied as you have reached your attempts limit = <%= @limit %></h2>
2
- <p>Please contact your system administrator</p>
1
+ <h2>Access completely denied as you have reached your attempts limit = <%= @limit %>.</h2>
2
+ <p>Please contact your system administrator.</p>
3
3
 
@@ -1,4 +1,4 @@
1
1
  en:
2
2
  devise:
3
3
  two_factor_authentication:
4
- attempt_failed: "Attemp failed"
4
+ attempt_failed: "Attempt failed."
@@ -1,8 +1,15 @@
1
1
  class TwoFactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Migration
2
- def change
2
+ def up
3
3
  change_table :<%= table_name %> do |t|
4
- t.string :second_factor_pass_code , :limit => 32
4
+ t.string :otp_secret_key
5
5
  t.integer :second_factor_attempts_count, :default => 0
6
6
  end
7
+
8
+ add_index :<%= table_name %>, :otp_secret_key, :unique => true
9
+ end
10
+
11
+ def down
12
+ remove_column :<%= table_name %>, :otp_secret_key
13
+ remove_column :<%= table_name %>, :second_factor_attempts_count
7
14
  end
8
15
  end
@@ -1,15 +1,18 @@
1
1
  require 'two_factor_authentication/version'
2
- require 'randexp'
3
2
  require 'devise'
4
- require 'digest'
5
3
  require 'active_support/concern'
4
+ require "active_model"
5
+ require "active_record"
6
+ require "active_support/core_ext/class/attribute_accessors"
7
+ require "cgi"
8
+ require "rotp"
6
9
 
7
10
  module Devise
8
- mattr_accessor :login_code_random_pattern
9
- @@login_code_random_pattern = /\w+/
10
-
11
11
  mattr_accessor :max_login_attempts
12
12
  @@max_login_attempts = 3
13
+
14
+ mattr_accessor :allowed_otp_drift_seconds
15
+ @@allowed_otp_drift_seconds = 30
13
16
  end
14
17
 
15
18
  module TwoFactorAuthentication
@@ -10,17 +10,24 @@ module TwoFactorAuthentication
10
10
  private
11
11
 
12
12
  def handle_two_factor_authentication
13
- if not request.format.nil? and request.format.html? and not devise_controller?
13
+ unless devise_controller?
14
14
  Devise.mappings.keys.flatten.any? do |scope|
15
15
  if signed_in?(scope) and warden.session(scope)[:need_two_factor_authentication]
16
- session["#{scope}_return_tor"] = request.path if request.get?
17
- redirect_to two_factor_authentication_path_for(scope)
18
- return
16
+ handle_failed_second_factor(scope)
19
17
  end
20
18
  end
21
19
  end
22
20
  end
23
21
 
22
+ def handle_failed_second_factor(scope)
23
+ if request.format.present? and request.format.html?
24
+ session["#{scope}_return_tor"] = request.path if request.get?
25
+ redirect_to two_factor_authentication_path_for(scope)
26
+ else
27
+ render nothing: true, status: :unauthorized
28
+ end
29
+ end
30
+
24
31
  def two_factor_authentication_path_for(resource_or_scope = nil)
25
32
  scope = Devise::Mapping.find_scope!(resource_or_scope)
26
33
  change_path = "#{scope}_two_factor_authentication_path"
@@ -1,10 +1,7 @@
1
1
  Warden::Manager.after_authentication do |user, auth, options|
2
2
  if user.respond_to?(:need_two_factor_authentication?)
3
3
  if auth.session(options[:scope])[:need_two_factor_authentication] = user.need_two_factor_authentication?(auth.request)
4
- code = user.generate_two_factor_code
5
- user.second_factor_pass_code = Digest::MD5.hexdigest(code)
6
- user.save
7
- user.send_two_factor_authentication_code(code)
4
+ user.send_two_factor_authentication_code
8
5
  end
9
6
  end
10
7
  end
@@ -5,23 +5,61 @@ module Devise
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  module ClassMethods
8
- ::Devise::Models.config(self, :login_code_random_pattern, :max_login_attempts)
9
- end
8
+ def has_one_time_password(options = {})
10
9
 
11
- def need_two_factor_authentication?(request)
12
- true
13
- end
10
+ cattr_accessor :otp_column_name
11
+ self.otp_column_name = "otp_secret_key"
14
12
 
15
- def generate_two_factor_code
16
- self.class.login_code_random_pattern.gen
17
- end
13
+ include InstanceMethodsOnActivation
14
+
15
+ before_create { self.otp_column = ROTP::Base32.random_base32 }
18
16
 
19
- def send_two_factor_authentication_code(code)
20
- p "Code is #{code}"
17
+ if respond_to?(:attributes_protected_by_default)
18
+ def self.attributes_protected_by_default #:nodoc:
19
+ super + [self.otp_column_name]
20
+ end
21
+ end
22
+ end
23
+ ::Devise::Models.config(self, :max_login_attempts, :allowed_otp_drift_seconds)
21
24
  end
22
25
 
23
- def max_login_attempts?
24
- second_factor_attempts_count >= self.class.max_login_attempts
26
+ module InstanceMethodsOnActivation
27
+ def authenticate_otp(code, options = {})
28
+ totp = ROTP::TOTP.new(self.otp_column)
29
+ drift = options[:drift] || self.class.allowed_otp_drift_seconds
30
+
31
+ totp.verify_with_drift(code, drift)
32
+ end
33
+
34
+ def otp_code(time = Time.now)
35
+ ROTP::TOTP.new(self.otp_column).at(time)
36
+ end
37
+
38
+ def provisioning_uri(account = nil)
39
+ account ||= self.email if self.respond_to?(:email)
40
+ ROTP::TOTP.new(self.otp_column).provisioning_uri(account)
41
+ end
42
+
43
+ def otp_column
44
+ self.send(self.class.otp_column_name)
45
+ end
46
+
47
+ def otp_column=(attr)
48
+ self.send("#{self.class.otp_column_name}=", attr)
49
+ end
50
+
51
+ def need_two_factor_authentication?(request)
52
+ true
53
+ end
54
+
55
+ def send_two_factor_authentication_code
56
+ raise NotImplementedError.new("No default implementation - please define in your class.")
57
+ end
58
+
59
+ def max_login_attempts?
60
+ second_factor_attempts_count >= self.class.max_login_attempts
61
+ end
62
+
25
63
  end
26
64
  end
27
65
  end
@@ -1,7 +1,7 @@
1
1
  module TwoFactorAuthentication
2
2
  module Schema
3
- def second_factor_pass_code
4
- apply_devise_schema :second_factor_pass_code, String, :limit => 32
3
+ def otp_secret_key
4
+ apply_devise_schema :otp_secret_key, String
5
5
  end
6
6
 
7
7
  def second_factor_attempts_count
@@ -1,3 +1,3 @@
1
1
  module TwoFactorAuthentication
2
- VERSION = "0.2".freeze
2
+ VERSION = "1.0".freeze
3
3
  end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+ include AuthenticatedModelHelper
3
+
4
+
5
+ describe Devise::Models::TwoFactorAuthenticatable, '#otp_code' do
6
+ let(:instance) { AuthenticatedModelHelper.create_new_user }
7
+ subject { instance.otp_code(time) }
8
+ let(:time) { 1392852456 }
9
+
10
+ it "should return an error if no secret is set" do
11
+ expect {
12
+ subject
13
+ }.to raise_error
14
+ end
15
+
16
+ context "secret is set" do
17
+ before :each do
18
+ instance.otp_secret_key = "2z6hxkdwi3uvrnpn"
19
+ end
20
+
21
+ it "should not return an error" do
22
+ subject
23
+ end
24
+
25
+ context "with a known time" do
26
+ let(:time) { 1392852756 }
27
+
28
+ it "should return a known result" do
29
+ expect(subject).to eq(562202)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ describe Devise::Models::TwoFactorAuthenticatable, '#authenticate_otp' do
36
+ let(:instance) { AuthenticatedModelHelper.create_new_user }
37
+
38
+ before :each do
39
+ instance.otp_secret_key = "2z6hxkdwi3uvrnpn"
40
+ end
41
+
42
+ def do_invoke code, options = {}
43
+ instance.authenticate_otp(code, options)
44
+ end
45
+
46
+ it "should be able to authenticate a recently created code" do
47
+ code = instance.otp_code
48
+ expect(do_invoke(code)).to eq(true)
49
+ end
50
+
51
+ it "should not authenticate an old code" do
52
+ code = instance.otp_code(1.minutes.ago.to_i)
53
+ expect(do_invoke(code)).to eq(false)
54
+ end
55
+ end
56
+
57
+ describe Devise::Models::TwoFactorAuthenticatable, '#send_two_factor_authentication_code' do
58
+
59
+ it "should raise an error by default" do
60
+ instance = AuthenticatedModelHelper.create_new_user
61
+ expect {
62
+ instance.send_two_factor_authentication_code
63
+ }.to raise_error(NotImplementedError)
64
+ end
65
+
66
+ it "should be overrideable" do
67
+ instance = AuthenticatedModelHelper.create_new_user_with_overrides
68
+ expect(instance.send_two_factor_authentication_code).to eq("Code sent")
69
+ end
70
+ end
@@ -0,0 +1,21 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+
4
+ require 'two_factor_authentication'
5
+
6
+
7
+ Dir["#{Dir.pwd}/spec/support/**/*.rb"].each {|f| require f}
8
+
9
+
10
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
11
+ RSpec.configure do |config|
12
+ config.treat_symbols_as_metadata_keys_with_true_values = true
13
+ config.run_all_when_everything_filtered = true
14
+ config.filter_run :focus
15
+
16
+ # Run specs in random order to surface order dependencies. If you find an
17
+ # order dependency and want to debug it, you can fix the order by providing
18
+ # the seed, which is printed after each run.
19
+ # --seed 1234
20
+ config.order = 'random'
21
+ end
@@ -0,0 +1,29 @@
1
+ module AuthenticatedModelHelper
2
+
3
+ class User
4
+ extend ActiveModel::Callbacks
5
+ include ActiveModel::Validations
6
+ include Devise::Models::TwoFactorAuthenticatable
7
+
8
+ define_model_callbacks :create
9
+ attr_accessor :otp_secret_key, :email
10
+
11
+ has_one_time_password
12
+ end
13
+
14
+ class UserWithOverrides < User
15
+
16
+ def send_two_factor_authentication_code
17
+ "Code sent"
18
+ end
19
+ end
20
+
21
+ def create_new_user
22
+ User.new
23
+ end
24
+
25
+ def create_new_user_with_overrides
26
+ UserWithOverrides.new
27
+ end
28
+
29
+ end
@@ -24,9 +24,11 @@ Gem::Specification.new do |s|
24
24
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
25
  s.require_paths = ["lib"]
26
26
 
27
+ s.add_development_dependency "rspec"
27
28
  s.add_runtime_dependency 'rails', '>= 3.1.1'
28
29
  s.add_runtime_dependency 'devise'
29
30
  s.add_runtime_dependency 'randexp'
31
+ s.add_runtime_dependency 'rotp'
30
32
 
31
33
  s.add_development_dependency 'bundler'
32
34
  end
metadata CHANGED
@@ -1,83 +1,105 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: two_factor_authentication
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
5
- prerelease:
4
+ version: '1.0'
6
5
  platform: ruby
7
6
  authors:
8
7
  - Dmitrii Golub
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-07-13 00:00:00.000000000 Z
11
+ date: 2014-03-28 00:00:00.000000000 Z
13
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
14
27
  - !ruby/object:Gem::Dependency
15
28
  name: rails
16
29
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
30
  requirements:
19
- - - ! '>='
31
+ - - '>='
20
32
  - !ruby/object:Gem::Version
21
33
  version: 3.1.1
22
34
  type: :runtime
23
35
  prerelease: false
24
36
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
37
  requirements:
27
- - - ! '>='
38
+ - - '>='
28
39
  - !ruby/object:Gem::Version
29
40
  version: 3.1.1
30
41
  - !ruby/object:Gem::Dependency
31
42
  name: devise
32
43
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
44
  requirements:
35
- - - ! '>='
45
+ - - '>='
36
46
  - !ruby/object:Gem::Version
37
47
  version: '0'
38
48
  type: :runtime
39
49
  prerelease: false
40
50
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
51
  requirements:
43
- - - ! '>='
52
+ - - '>='
44
53
  - !ruby/object:Gem::Version
45
54
  version: '0'
46
55
  - !ruby/object:Gem::Dependency
47
56
  name: randexp
48
57
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
58
  requirements:
51
- - - ! '>='
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rotp
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
52
74
  - !ruby/object:Gem::Version
53
75
  version: '0'
54
76
  type: :runtime
55
77
  prerelease: false
56
78
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
79
  requirements:
59
- - - ! '>='
80
+ - - '>='
60
81
  - !ruby/object:Gem::Version
61
82
  version: '0'
62
83
  - !ruby/object:Gem::Dependency
63
84
  name: bundler
64
85
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
86
  requirements:
67
- - - ! '>='
87
+ - - '>='
68
88
  - !ruby/object:Gem::Version
69
89
  version: '0'
70
90
  type: :development
71
91
  prerelease: false
72
92
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
93
  requirements:
75
- - - ! '>='
94
+ - - '>='
76
95
  - !ruby/object:Gem::Version
77
96
  version: '0'
78
- description: ! " ### Features ###\n * control sms code pattern\n * configure
79
- max login attempts\n * per user level control if he really need two factor authentication\n
80
- \ * your own sms logic\n"
97
+ description: |2
98
+ ### Features ###
99
+ * control sms code pattern
100
+ * configure max login attempts
101
+ * per user level control if he really need two factor authentication
102
+ * your own sms logic
81
103
  email:
82
104
  - dmitrii.golub@gmail.com
83
105
  executables: []
@@ -105,30 +127,35 @@ files:
105
127
  - lib/two_factor_authentication/routes.rb
106
128
  - lib/two_factor_authentication/schema.rb
107
129
  - lib/two_factor_authentication/version.rb
130
+ - spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb
131
+ - spec/spec_helper.rb
132
+ - spec/support/authenticated_model_helper.rb
108
133
  - two_factor_authentication.gemspec
109
134
  homepage: https://github.com/Houdini/two_factor_authentication
110
135
  licenses: []
136
+ metadata: {}
111
137
  post_install_message:
112
138
  rdoc_options: []
113
139
  require_paths:
114
140
  - lib
115
141
  required_ruby_version: !ruby/object:Gem::Requirement
116
- none: false
117
142
  requirements:
118
- - - ! '>='
143
+ - - '>='
119
144
  - !ruby/object:Gem::Version
120
145
  version: '0'
121
146
  required_rubygems_version: !ruby/object:Gem::Requirement
122
- none: false
123
147
  requirements:
124
- - - ! '>='
148
+ - - '>='
125
149
  - !ruby/object:Gem::Version
126
150
  version: '0'
127
151
  requirements: []
128
152
  rubyforge_project: two_factor_authentication
129
- rubygems_version: 1.8.24
153
+ rubygems_version: 2.1.11
130
154
  signing_key:
131
- specification_version: 3
155
+ specification_version: 4
132
156
  summary: Two factor authentication plugin for devise
133
- test_files: []
157
+ test_files:
158
+ - spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb
159
+ - spec/spec_helper.rb
160
+ - spec/support/authenticated_model_helper.rb
134
161
  has_rdoc: