two_factor_authentication 0.2 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
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: