devise-otp-rails5 0.2.4

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.
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