devise-otp-rails5 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +42 -0
  3. data/.travis.yml +12 -0
  4. data/Gemfile +25 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +140 -0
  7. data/Rakefile +42 -0
  8. data/app/assets/javascripts/devise-otp.js +1 -0
  9. data/app/assets/javascripts/qrcode.js +609 -0
  10. data/app/controllers/devise_otp/credentials_controller.rb +106 -0
  11. data/app/controllers/devise_otp/tokens_controller.rb +111 -0
  12. data/app/views/devise_otp/credentials/refresh.html.erb +20 -0
  13. data/app/views/devise_otp/credentials/show.html.erb +23 -0
  14. data/app/views/devise_otp/tokens/_token_secret.html.erb +19 -0
  15. data/app/views/devise_otp/tokens/_trusted_devices.html.erb +10 -0
  16. data/app/views/devise_otp/tokens/recovery.html.erb +21 -0
  17. data/app/views/devise_otp/tokens/recovery_codes.text.erb +3 -0
  18. data/app/views/devise_otp/tokens/show.html.erb +19 -0
  19. data/config/locales/en.yml +66 -0
  20. data/devise-otp.gemspec +25 -0
  21. data/lib/devise-otp.rb +83 -0
  22. data/lib/devise-otp/version.rb +5 -0
  23. data/lib/devise_otp_authenticatable/controllers/helpers.rb +168 -0
  24. data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +33 -0
  25. data/lib/devise_otp_authenticatable/engine.rb +23 -0
  26. data/lib/devise_otp_authenticatable/hooks.rb +13 -0
  27. data/lib/devise_otp_authenticatable/hooks/sessions.rb +59 -0
  28. data/lib/devise_otp_authenticatable/mapping.rb +19 -0
  29. data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +137 -0
  30. data/lib/devise_otp_authenticatable/routes.rb +32 -0
  31. data/lib/generators/active_record/devise_otp_generator.rb +13 -0
  32. data/lib/generators/active_record/templates/migration.rb +27 -0
  33. data/lib/generators/devise_otp/devise_otp_generator.rb +17 -0
  34. data/lib/generators/devise_otp/install_generator.rb +53 -0
  35. data/lib/generators/devise_otp/views_generator.rb +19 -0
  36. data/test/dummy/README.rdoc +261 -0
  37. data/test/dummy/Rakefile +7 -0
  38. data/test/dummy/app/assets/javascripts/application.js +13 -0
  39. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  40. data/test/dummy/app/controllers/application_controller.rb +4 -0
  41. data/test/dummy/app/controllers/posts_controller.rb +83 -0
  42. data/test/dummy/app/helpers/application_helper.rb +2 -0
  43. data/test/dummy/app/helpers/posts_helper.rb +2 -0
  44. data/test/dummy/app/mailers/.gitkeep +0 -0
  45. data/test/dummy/app/models/post.rb +2 -0
  46. data/test/dummy/app/models/user.rb +20 -0
  47. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  48. data/test/dummy/app/views/posts/_form.html.erb +25 -0
  49. data/test/dummy/app/views/posts/edit.html.erb +6 -0
  50. data/test/dummy/app/views/posts/index.html.erb +25 -0
  51. data/test/dummy/app/views/posts/new.html.erb +5 -0
  52. data/test/dummy/app/views/posts/show.html.erb +15 -0
  53. data/test/dummy/config.ru +4 -0
  54. data/test/dummy/config/application.rb +67 -0
  55. data/test/dummy/config/boot.rb +10 -0
  56. data/test/dummy/config/database.yml +25 -0
  57. data/test/dummy/config/environment.rb +5 -0
  58. data/test/dummy/config/environments/development.rb +30 -0
  59. data/test/dummy/config/environments/production.rb +69 -0
  60. data/test/dummy/config/environments/test.rb +36 -0
  61. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  62. data/test/dummy/config/initializers/devise.rb +253 -0
  63. data/test/dummy/config/initializers/inflections.rb +15 -0
  64. data/test/dummy/config/initializers/mime_types.rb +5 -0
  65. data/test/dummy/config/initializers/secret_token.rb +8 -0
  66. data/test/dummy/config/initializers/session_store.rb +8 -0
  67. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  68. data/test/dummy/config/locales/en.yml +5 -0
  69. data/test/dummy/config/routes.rb +6 -0
  70. data/test/dummy/db/migrate/20130125101430_create_users.rb +9 -0
  71. data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +53 -0
  72. data/test/dummy/db/migrate/20130131142320_create_posts.rb +10 -0
  73. data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +28 -0
  74. data/test/dummy/lib/assets/.gitkeep +0 -0
  75. data/test/dummy/public/404.html +26 -0
  76. data/test/dummy/public/422.html +26 -0
  77. data/test/dummy/public/500.html +25 -0
  78. data/test/dummy/public/favicon.ico +0 -0
  79. data/test/dummy/script/rails +6 -0
  80. data/test/integration/persistence_test.rb +65 -0
  81. data/test/integration/refresh_test.rb +106 -0
  82. data/test/integration/sign_in_test.rb +87 -0
  83. data/test/integration/token_test.rb +34 -0
  84. data/test/integration_tests_helper.rb +66 -0
  85. data/test/model_tests_helper.rb +22 -0
  86. data/test/models/otp_authenticatable_test.rb +122 -0
  87. data/test/orm/active_record.rb +4 -0
  88. data/test/test_helper.rb +22 -0
  89. metadata +253 -0
@@ -0,0 +1,5 @@
1
+ module Devise
2
+ module Otp
3
+ VERSION = "0.2.4"
4
+ end
5
+ end
@@ -0,0 +1,168 @@
1
+ module DeviseOtpAuthenticatable
2
+
3
+ module Controllers
4
+ module Helpers
5
+
6
+
7
+ def authenticate_scope!
8
+ send(:"authenticate_#{resource_name}!", :force => true)
9
+ self.resource = send("current_#{resource_name}")
10
+ end
11
+
12
+ #
13
+ # similar to DeviseController#set_flash_message, but sets the scope inside
14
+ # the otp controller
15
+ #
16
+ def otp_set_flash_message(key, kind, options={})
17
+ options[:scope] ||= "devise.otp.#{controller_name}"
18
+ options[:default] = Array(options[:default]).unshift(kind.to_sym)
19
+ options[:resource_name] = resource_name
20
+ options = devise_i18n_options(options) if respond_to?(:devise_i18n_options, true)
21
+ message = I18n.t("#{options[:resource_name]}.#{kind}", options)
22
+ flash[key] = message if message.present?
23
+ end
24
+
25
+ def otp_t()
26
+
27
+ end
28
+
29
+
30
+ def trusted_devices_enabled?
31
+ resource.class.otp_trust_persistence && (resource.class.otp_trust_persistence > 0)
32
+ end
33
+
34
+ def recovery_enabled?
35
+ resource_class.otp_recovery_tokens && (resource_class.otp_recovery_tokens > 0)
36
+ end
37
+
38
+ #
39
+ # Sanity check for resource validity
40
+ #
41
+ def ensure_resource!
42
+ if resource.nil?
43
+ raise ArgumentError, "Should not happen"
44
+ end
45
+ end
46
+
47
+
48
+ # fixme do cookies and persistence need to be scoped? probably
49
+
50
+ #
51
+ # check if the resource needs a credentials refresh. IE, they need to be asked a password again to access
52
+ # this resource.
53
+ #
54
+ def needs_credentials_refresh?(resource)
55
+ return false unless resource.class.otp_credentials_refresh
56
+
57
+ (!session[otp_scoped_refresh_property].present? ||
58
+ (session[otp_scoped_refresh_property] < DateTime.now)).tap { |need| otp_set_refresh_return_url if need }
59
+ end
60
+
61
+ #
62
+ # credentials are refreshed
63
+ #
64
+ def otp_refresh_credentials_for(resource)
65
+ return false unless resource.class.otp_credentials_refresh
66
+ session[otp_scoped_refresh_property] = (Time.now + resource.class.otp_credentials_refresh)
67
+ end
68
+
69
+
70
+ #
71
+ # is the current browser trusted?
72
+ #
73
+ def is_otp_trusted_device_for?(resource)
74
+ return false unless resource.class.otp_trust_persistence
75
+ if cookies[otp_scoped_persistence_cookie].present?
76
+ cookies.signed[otp_scoped_persistence_cookie] ==
77
+ [resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ #
84
+ # make the current browser trusted
85
+ #
86
+ def otp_set_trusted_device_for(resource)
87
+ return unless resource.class.otp_trust_persistence
88
+ cookies.signed[otp_scoped_persistence_cookie] = {
89
+ :httponly => true,
90
+ :expires => Time.now + resource.class.otp_trust_persistence,
91
+ :value => [resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
92
+ }
93
+ end
94
+
95
+ def otp_set_refresh_return_url
96
+ session[otp_scoped_refresh_return_url_property] = request.fullpath
97
+ end
98
+
99
+ def otp_fetch_refresh_return_url
100
+ session.delete(otp_scoped_refresh_return_url_property) { :root }
101
+
102
+ end
103
+
104
+ def otp_scoped_refresh_return_url_property
105
+ "otp_#{resource_name}refresh_return_url".to_sym
106
+ end
107
+
108
+ def otp_scoped_refresh_property
109
+ "otp_#{resource_name}refresh_after".to_sym
110
+ end
111
+
112
+ def otp_scoped_persistence_cookie
113
+ "otp_#{resource_name}_device_trusted"
114
+ end
115
+
116
+ #
117
+ # make the current browser NOT trusted
118
+ #
119
+ def otp_clear_trusted_device_for(resource)
120
+ cookies.delete(otp_scoped_persistence_cookie)
121
+ end
122
+
123
+
124
+ #
125
+ # clears the persistence list for this kind of resource
126
+ #
127
+ def otp_reset_persistence_for(resource)
128
+ otp_clear_trusted_device_for(resource)
129
+ resource.reset_otp_persistence!
130
+ end
131
+
132
+ #
133
+ # returns the URL for the QR Code to initialize the Authenticator device
134
+ #
135
+ def otp_authenticator_token_image(resource)
136
+ otp_authenticator_token_image_js(resource.otp_provisioning_uri)
137
+ end
138
+
139
+ private
140
+
141
+ def otp_authenticator_token_image_js(otp_url)
142
+
143
+ content_tag(:div, :class => 'qrcode-container') do
144
+ tag(:div, :id => 'qrcode', :class => 'qrcode') +
145
+ javascript_tag(%Q[
146
+
147
+ new QRCode("qrcode", {
148
+ text: "#{otp_url}",
149
+ width: 256,
150
+ height: 256,
151
+ colorDark : "#000000",
152
+ colorLight : "#ffffff",
153
+ correctLevel : QRCode.CorrectLevel.H
154
+ });
155
+ ]) + tag("/div")
156
+ end
157
+ end
158
+
159
+
160
+ def otp_authenticator_token_image_google(otp_url)
161
+ otp_url = Rack::Utils.escape(otp_url)
162
+ url = "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=#{otp_url}"
163
+ image_tag(url, :alt => 'OTP Url QRCode')
164
+ end
165
+
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,33 @@
1
+ module DeviseOtpAuthenticatable
2
+ module Controllers
3
+
4
+ module UrlHelpers
5
+
6
+ def recovery_otp_token_for(resource_or_scope, opts = {})
7
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
8
+ send("recovery_#{scope}_otp_token_path", opts)
9
+ end
10
+
11
+ def refresh_otp_credential_path_for(resource_or_scope, opts = {})
12
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
13
+ send("refresh_#{scope}_otp_credential_path", opts)
14
+ end
15
+
16
+ def persistence_otp_token_path_for(resource_or_scope, opts = {})
17
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
18
+ send("persistence_#{scope}_otp_token_path", opts)
19
+ end
20
+
21
+ def otp_token_path_for(resource_or_scope, opts = {})
22
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
23
+ send("#{scope}_otp_token_path", opts)
24
+ end
25
+
26
+ def otp_credential_path_for(resource_or_scope, opts = {})
27
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
28
+ send("#{scope}_otp_credential_path", opts)
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ module DeviseOtpAuthenticatable
2
+ class Engine < ::Rails::Engine
3
+
4
+ ActiveSupport.on_load(:action_controller) do
5
+ include DeviseOtpAuthenticatable::Controllers::UrlHelpers
6
+ include DeviseOtpAuthenticatable::Controllers::Helpers
7
+ end
8
+ ActiveSupport.on_load(:action_view) do
9
+ include DeviseOtpAuthenticatable::Controllers::UrlHelpers
10
+ include DeviseOtpAuthenticatable::Controllers::Helpers
11
+ end
12
+
13
+ # We use to_prepare instead of after_initialize here because Devise is a Rails engine;
14
+ config.to_prepare do
15
+ DeviseOtpAuthenticatable::Hooks.apply
16
+ end
17
+
18
+ # extend mapping with after_initialize because is not reloaded
19
+ config.after_initialize do
20
+ Devise::Mapping.send :include, DeviseOtpAuthenticatable::Mapping
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ module DeviseOtpAuthenticatable
2
+ module Hooks
3
+
4
+ autoload :Sessions, 'devise_otp_authenticatable/hooks/sessions.rb'
5
+
6
+ class << self
7
+ def apply
8
+ Devise::SessionsController.send(:include, Hooks::Sessions)
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,59 @@
1
+ module DeviseOtpAuthenticatable::Hooks
2
+ module Sessions
3
+ extend ActiveSupport::Concern
4
+ include DeviseOtpAuthenticatable::Controllers::UrlHelpers
5
+
6
+ included do
7
+ alias_method_chain :create, :otp
8
+ end
9
+
10
+ #
11
+ # replaces Devise::SessionsController#create
12
+ #
13
+ def create_with_otp
14
+
15
+ resource = warden.authenticate!(auth_options)
16
+
17
+ devise_stored_location = stored_location_for(resource) # Grab the current stored location before it gets lost by warden.logout
18
+
19
+ otp_refresh_credentials_for(resource)
20
+
21
+ if otp_challenge_required_on?(resource)
22
+ challenge = resource.generate_otp_challenge!
23
+ warden.logout
24
+ store_location_for(resource, devise_stored_location) # restore the stored location
25
+ respond_with resource, :location => otp_credential_path_for(resource, {:challenge => challenge})
26
+ elsif otp_mandatory_on?(resource) # if mandatory, log in user but send him to the must activate otp
27
+ set_flash_message(:notice, :signed_in_but_otp) if is_navigational_format?
28
+ sign_in(resource_name, resource)
29
+ respond_with resource, :location => otp_token_path_for(resource)
30
+ else
31
+
32
+ set_flash_message(:notice, :signed_in) if is_navigational_format?
33
+ sign_in(resource_name, resource)
34
+ respond_with resource, :location => after_sign_in_path_for(resource)
35
+ end
36
+ end
37
+
38
+
39
+ private
40
+
41
+ #
42
+ # resource should be challenged for otp
43
+ #
44
+ def otp_challenge_required_on?(resource)
45
+ return false unless resource.respond_to?(:otp_enabled) && resource.respond_to?(:otp_auth_secret)
46
+ resource.otp_enabled && !is_otp_trusted_device_for?(resource)
47
+ end
48
+
49
+ #
50
+ # the resource -should- have otp turned on, but it isn't
51
+ #
52
+ def otp_mandatory_on?(resource)
53
+ return true if resource.class.otp_mandatory
54
+ return false unless resource.respond_to?(:otp_mandatory)
55
+
56
+ resource.otp_mandatory && !resource.otp_enabled
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,19 @@
1
+ module DeviseOtpAuthenticatable
2
+
3
+ module Mapping
4
+
5
+ def self.included(base)
6
+ base.alias_method_chain :default_controllers, :otp
7
+ end
8
+
9
+ private
10
+ def default_controllers_with_otp(options)
11
+ options[:controllers] ||= {}
12
+
13
+ options[:controllers][:otp_tokens] ||= "tokens"
14
+ options[:controllers][:otp_credentials] ||= "credentials"
15
+
16
+ default_controllers_without_otp(options)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,137 @@
1
+ require 'rotp'
2
+
3
+ module Devise::Models
4
+ module OtpAuthenticatable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_validation :generate_otp_auth_secret, :on => :create
9
+ before_validation :generate_otp_persistence_seed, :on => :create
10
+ scope :with_valid_otp_challenge, lambda { |time| where('otp_challenge_expires > ?', time) }
11
+ end
12
+
13
+ module ClassMethods
14
+ ::Devise::Models.config(self, :otp_authentication_timeout, :otp_drift_window, :otp_trust_persistence,
15
+ :otp_mandatory, :otp_credentials_refresh, :otp_issuer, :otp_recovery_tokens)
16
+
17
+ def find_valid_otp_challenge(challenge)
18
+ with_valid_otp_challenge(Time.now).where(:otp_session_challenge => challenge).first
19
+ end
20
+ end
21
+
22
+ def time_based_otp
23
+ @time_based_otp ||= ROTP::TOTP.new(otp_auth_secret, issuer: "#{self.class.otp_issuer || Rails.application.class.parent_name}")
24
+ end
25
+
26
+ def recovery_otp
27
+ @recovery_otp ||= ROTP::HOTP.new(otp_recovery_secret)
28
+ end
29
+
30
+ def otp_provisioning_uri
31
+ time_based_otp.provisioning_uri(otp_provisioning_identifier)
32
+ end
33
+
34
+ def otp_provisioning_identifier
35
+ email
36
+ end
37
+
38
+
39
+ def reset_otp_credentials
40
+ @time_based_otp = nil
41
+ @recovery_otp = nil
42
+ generate_otp_auth_secret
43
+ reset_otp_persistence
44
+ update_attributes!(:otp_enabled => false,
45
+ :otp_session_challenge => nil, :otp_challenge_expires => nil,
46
+ :otp_recovery_counter => 0)
47
+ end
48
+
49
+ def reset_otp_credentials!
50
+ reset_otp_credentials
51
+ save!
52
+ end
53
+
54
+ def reset_otp_persistence
55
+ generate_otp_persistence_seed
56
+ end
57
+
58
+ def reset_otp_persistence!
59
+ reset_otp_persistence
60
+ save!
61
+ end
62
+
63
+ def enable_otp!
64
+ if otp_persistence_seed.nil?
65
+ reset_otp_credentials!
66
+ end
67
+
68
+ update_attributes!(:otp_enabled => true, :otp_enabled_on => Time.now)
69
+ end
70
+
71
+ def disable_otp!
72
+ update_attributes!(:otp_enabled => false, :otp_enabled_on => nil)
73
+ end
74
+
75
+ def generate_otp_challenge!(expires = nil)
76
+ update_attributes!(:otp_session_challenge => SecureRandom.hex,
77
+ :otp_challenge_expires => DateTime.now + (expires || self.class.otp_authentication_timeout))
78
+ otp_session_challenge
79
+ end
80
+
81
+ def otp_challenge_valid?
82
+ (otp_challenge_expires.nil? || otp_challenge_expires > Time.now)
83
+ end
84
+
85
+
86
+ def validate_otp_token(token, recovery = false)
87
+ if recovery
88
+ validate_otp_recovery_token token
89
+ else
90
+ validate_otp_time_token token
91
+ end
92
+ end
93
+ alias_method :valid_otp_token?, :validate_otp_token
94
+
95
+ def validate_otp_time_token(token)
96
+ return false if token.blank?
97
+ validate_otp_token_with_drift(token)
98
+ end
99
+ alias_method :valid_otp_time_token?, :validate_otp_time_token
100
+
101
+ def next_otp_recovery_tokens(number = self.class.otp_recovery_tokens)
102
+ (otp_recovery_counter..otp_recovery_counter + number).inject({}) do |h, index|
103
+ h[index] = recovery_otp.at(index)
104
+ h
105
+ end
106
+ end
107
+
108
+ def validate_otp_recovery_token(token)
109
+ recovery_otp.verify(token, otp_recovery_counter).tap do
110
+ self.otp_recovery_counter += 1
111
+ save!
112
+ end
113
+ end
114
+ alias_method :valid_otp_recovery_token?, :validate_otp_recovery_token
115
+
116
+
117
+
118
+ private
119
+
120
+ def validate_otp_token_with_drift(token)
121
+
122
+ # should be centered around saved drift
123
+ (-self.class.otp_drift_window..self.class.otp_drift_window).any? {|drift|
124
+ (time_based_otp.verify(token, Time.now.ago(30 * drift))) }
125
+ end
126
+
127
+ def generate_otp_persistence_seed
128
+ self.otp_persistence_seed = SecureRandom.hex
129
+ end
130
+
131
+ def generate_otp_auth_secret
132
+ self.otp_auth_secret = ROTP::Base32.random_base32
133
+ self.otp_recovery_secret = ROTP::Base32.random_base32
134
+ end
135
+
136
+ end
137
+ end