devise_xfactor_authentication 2.2.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +21 -0
  3. data/.gitignore +23 -0
  4. data/.rubocop.yml +295 -0
  5. data/.travis.yml +28 -0
  6. data/CHANGELOG.md +119 -0
  7. data/Gemfile +31 -0
  8. data/LICENSE +19 -0
  9. data/README.md +405 -0
  10. data/Rakefile +14 -0
  11. data/app/controllers/devise/devise_xfactor_authentication_controller.rb +84 -0
  12. data/app/views/devise/devise_xfactor_authentication/max_login_attempts_reached.html.erb +3 -0
  13. data/app/views/devise/devise_xfactor_authentication/show.html.erb +19 -0
  14. data/config/locales/de.yml +8 -0
  15. data/config/locales/en.yml +8 -0
  16. data/config/locales/es.yml +8 -0
  17. data/config/locales/fr.yml +8 -0
  18. data/config/locales/ru.yml +8 -0
  19. data/devise_xfactor_authentication.gemspec +39 -0
  20. data/lib/devise_xfactor_authentication/controllers/helpers.rb +49 -0
  21. data/lib/devise_xfactor_authentication/hooks/devise_xfactor_authenticatable.rb +17 -0
  22. data/lib/devise_xfactor_authentication/models/devise_xfactor_authenticatable.rb +203 -0
  23. data/lib/devise_xfactor_authentication/orm/active_record.rb +14 -0
  24. data/lib/devise_xfactor_authentication/rails.rb +7 -0
  25. data/lib/devise_xfactor_authentication/routes.rb +11 -0
  26. data/lib/devise_xfactor_authentication/schema.rb +31 -0
  27. data/lib/devise_xfactor_authentication/version.rb +3 -0
  28. data/lib/devise_xfactor_authentication.rb +52 -0
  29. data/lib/generators/active_record/devise_xfactor_authentication_generator.rb +14 -0
  30. data/lib/generators/active_record/templates/migration.rb +14 -0
  31. data/lib/generators/devise_xfactor_authentication/devise_xfactor_authentication_generator.rb +17 -0
  32. data/spec/controllers/devise_xfactor_authentication_controller_spec.rb +41 -0
  33. data/spec/features/devise_xfactor_authenticatable_spec.rb +237 -0
  34. data/spec/generators/active_record/devise_xfactor_authentication_generator_spec.rb +36 -0
  35. data/spec/lib/devise_xfactor_authentication/models/devise_xfactor_authenticatable_spec.rb +326 -0
  36. data/spec/rails_app/.gitignore +3 -0
  37. data/spec/rails_app/README.md +3 -0
  38. data/spec/rails_app/Rakefile +7 -0
  39. data/spec/rails_app/app/assets/javascripts/application.js +1 -0
  40. data/spec/rails_app/app/assets/stylesheets/application.css +4 -0
  41. data/spec/rails_app/app/controllers/application_controller.rb +3 -0
  42. data/spec/rails_app/app/controllers/home_controller.rb +10 -0
  43. data/spec/rails_app/app/helpers/application_helper.rb +8 -0
  44. data/spec/rails_app/app/mailers/.gitkeep +0 -0
  45. data/spec/rails_app/app/models/.gitkeep +0 -0
  46. data/spec/rails_app/app/models/admin.rb +6 -0
  47. data/spec/rails_app/app/models/encrypted_user.rb +15 -0
  48. data/spec/rails_app/app/models/guest_user.rb +17 -0
  49. data/spec/rails_app/app/models/user.rb +14 -0
  50. data/spec/rails_app/app/views/home/dashboard.html.erb +11 -0
  51. data/spec/rails_app/app/views/home/index.html.erb +3 -0
  52. data/spec/rails_app/app/views/layouts/application.html.erb +20 -0
  53. data/spec/rails_app/config/application.rb +63 -0
  54. data/spec/rails_app/config/boot.rb +10 -0
  55. data/spec/rails_app/config/database.yml +19 -0
  56. data/spec/rails_app/config/environment.rb +5 -0
  57. data/spec/rails_app/config/environments/development.rb +28 -0
  58. data/spec/rails_app/config/environments/production.rb +68 -0
  59. data/spec/rails_app/config/environments/test.rb +41 -0
  60. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  61. data/spec/rails_app/config/initializers/cookies_serializer.rb +3 -0
  62. data/spec/rails_app/config/initializers/devise.rb +258 -0
  63. data/spec/rails_app/config/initializers/inflections.rb +15 -0
  64. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  65. data/spec/rails_app/config/initializers/secret_token.rb +7 -0
  66. data/spec/rails_app/config/initializers/session_store.rb +8 -0
  67. data/spec/rails_app/config/initializers/wrap_parameters.rb +14 -0
  68. data/spec/rails_app/config/locales/devise.en.yml +59 -0
  69. data/spec/rails_app/config/locales/en.yml +5 -0
  70. data/spec/rails_app/config/routes.rb +65 -0
  71. data/spec/rails_app/config.ru +4 -0
  72. data/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +42 -0
  73. data/spec/rails_app/db/migrate/20140407172619_devise_xfactor_authentication_add_to_users.rb +15 -0
  74. data/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb +7 -0
  75. data/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb +9 -0
  76. data/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb +19 -0
  77. data/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb +5 -0
  78. data/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +42 -0
  79. data/spec/rails_app/db/schema.rb +55 -0
  80. data/spec/rails_app/lib/assets/.gitkeep +0 -0
  81. data/spec/rails_app/lib/sms_provider.rb +17 -0
  82. data/spec/rails_app/public/404.html +26 -0
  83. data/spec/rails_app/public/422.html +26 -0
  84. data/spec/rails_app/public/500.html +25 -0
  85. data/spec/rails_app/public/favicon.ico +0 -0
  86. data/spec/rails_app/script/rails +6 -0
  87. data/spec/spec_helper.rb +26 -0
  88. data/spec/support/authenticated_model_helper.rb +59 -0
  89. data/spec/support/capybara.rb +3 -0
  90. data/spec/support/controller_helper.rb +16 -0
  91. data/spec/support/features_spec_helper.rb +42 -0
  92. data/spec/support/sms_provider.rb +5 -0
  93. data/spec/support/totp_helper.rb +11 -0
  94. metadata +293 -0
data/README.md ADDED
@@ -0,0 +1,405 @@
1
+ # Two factor authentication for Devise
2
+
3
+ [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Houdini/two_factor_authentication?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4
+
5
+ [![Build Status](https://travis-ci.org/Houdini/two_factor_authentication.svg?branch=master)](https://travis-ci.org/Houdini/two_factor_authentication)
6
+ [![Code Climate](https://codeclimate.com/github/Houdini/two_factor_authentication.svg)](https://codeclimate.com/github/Houdini/two_factor_authentication)
7
+
8
+ ## Features
9
+
10
+ * Support for 2 types of OTP codes
11
+ 1. Codes delivered directly to the user
12
+ 2. TOTP (Google Authenticator) codes based on a shared secret (HMAC)
13
+ * Configurable OTP code digit length
14
+ * Configurable max login attempts
15
+ * Customizable logic to determine if a user needs two factor authentication
16
+ * Configurable period where users won't be asked for 2FA again
17
+ * Option to encrypt the TOTP secret in the database, with iv and salt
18
+
19
+ ## Configuration
20
+
21
+ ### Initial Setup
22
+
23
+ In a Rails environment, require the gem in your Gemfile:
24
+
25
+ gem 'two_factor_authentication'
26
+
27
+ Once that's done, run:
28
+
29
+ bundle install
30
+
31
+ Note that Ruby 2.1 or greater is required.
32
+
33
+ ### Installation
34
+
35
+ #### Automatic initial setup
36
+
37
+ To set up the model and database migration file automatically, run the
38
+ following command:
39
+
40
+ bundle exec rails g two_factor_authentication MODEL
41
+
42
+ Where MODEL is your model name (e.g. User or Admin). This generator will add
43
+ `:two_factor_authenticatable` to your model's Devise options and create a
44
+ migration in `db/migrate/`, which will add the following columns to your table:
45
+
46
+ - `:second_factor_attempts_count`
47
+ - `:encrypted_otp_secret_key`
48
+ - `:encrypted_otp_secret_key_iv`
49
+ - `:encrypted_otp_secret_key_salt`
50
+ - `:direct_otp`
51
+ - `:direct_otp_sent_at`
52
+ - `:totp_timestamp`
53
+
54
+ #### Manual initial setup
55
+
56
+ If you prefer to set up the model and migration manually, add the
57
+ `:two_factor_authenticatable` option to your existing devise options, such as:
58
+
59
+ ```ruby
60
+ devise :database_authenticatable, :registerable, :recoverable, :rememberable,
61
+ :trackable, :validatable, :two_factor_authenticatable
62
+ ```
63
+
64
+ Then create your migration file using the Rails generator, such as:
65
+
66
+ ```
67
+ rails g migration AddTwoFactorFieldsToUsers second_factor_attempts_count:integer encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp
68
+ ```
69
+
70
+ Open your migration file (it will be in the `db/migrate` directory and will be
71
+ named something like `20151230163930_add_two_factor_fields_to_users.rb`), and
72
+ add `unique: true` to the `add_index` line so that it looks like this:
73
+
74
+ ```ruby
75
+ add_index :users, :encrypted_otp_secret_key, unique: true
76
+ ```
77
+ Save the file.
78
+
79
+ #### Complete the setup
80
+
81
+ Run the migration with:
82
+
83
+ bundle exec rake db:migrate
84
+
85
+ Add the following line to your model to fully enable two-factor auth:
86
+
87
+ has_one_time_password(encrypted: true)
88
+
89
+ Set config values in `config/initializers/devise.rb`:
90
+
91
+ ```ruby
92
+ config.max_login_attempts = 3 # Maximum second factor attempts count.
93
+ config.allowed_otp_drift_seconds = 30 # Allowed TOTP time drift between client and server.
94
+ config.otp_length = 6 # TOTP code length
95
+ config.direct_otp_valid_for = 5.minutes # Time before direct OTP becomes invalid
96
+ config.direct_otp_length = 6 # Direct OTP code length
97
+ config.remember_otp_session_for_seconds = 30.days # Time before browser has to perform 2fA again. Default is 0.
98
+ config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY']
99
+ config.second_factor_resource_id = 'id' # Field or method name used to set value for 2fA remember cookie
100
+ config.delete_cookie_on_logout = false # Delete cookie when user signs out, to force 2fA again on login
101
+ ```
102
+ The `otp_secret_encryption_key` must be a random key that is not stored in the
103
+ DB, and is not checked in to your repo. It is recommended to store it in an
104
+ environment variable, and you can generate it with `bundle exec rake secret`.
105
+
106
+ Override the method in your model in order to send direct OTP codes. This is
107
+ automatically called when a user logs in unless they have TOTP enabled (see
108
+ below):
109
+
110
+ ```ruby
111
+ def send_two_factor_authentication_code(code)
112
+ # Send code via SMS, etc.
113
+ end
114
+ ```
115
+
116
+ ### Customisation and Usage
117
+
118
+ By default, second factor authentication is required for each user. You can
119
+ change that by overriding the following method in your model:
120
+
121
+ ```ruby
122
+ def need_two_factor_authentication?(request)
123
+ request.ip != '127.0.0.1'
124
+ end
125
+ ```
126
+
127
+ In the example above, two factor authentication will not be required for local
128
+ users.
129
+
130
+ This gem is compatible with [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en).
131
+ To enable this a shared secret must be generated by invoking the following
132
+ method on your model:
133
+
134
+ ```ruby
135
+ user.generate_totp_secret
136
+ ```
137
+
138
+ This must then be shared via a provisioning uri:
139
+
140
+ ```ruby
141
+ user.provisioning_uri # This assumes a user model with an email attribute
142
+ ```
143
+
144
+ This provisioning uri can then be turned in to a QR code if desired so that
145
+ users may add the app to Google Authenticator easily. Once this is done, they
146
+ may retrieve a one-time password directly from the Google Authenticator app.
147
+
148
+ #### Overriding the view
149
+
150
+ The default view that shows the form can be overridden by adding a
151
+ file named `show.html.erb` (or `show.html.haml` if you prefer HAML)
152
+ inside `app/views/devise/two_factor_authentication/` and customizing it.
153
+ Below is an example using ERB:
154
+
155
+
156
+ ```html
157
+ <h2>Hi, you received a code by email, please enter it below, thanks!</h2>
158
+
159
+ <%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %>
160
+ <%= text_field_tag :code %>
161
+ <%= submit_tag "Log in!" %>
162
+ <% end %>
163
+
164
+ <%= link_to "Sign out", destroy_user_session_path, :method => :delete %>
165
+ ```
166
+
167
+ #### Upgrading from version 1.X to 2.X
168
+
169
+ The following database fields are new in version 2.
170
+
171
+ - `direct_otp`
172
+ - `direct_otp_sent_at`
173
+ - `totp_timestamp`
174
+
175
+ To add them, generate a migration such as:
176
+
177
+ $ rails g migration AddTwoFactorFieldsToUsers direct_otp:string direct_otp_sent_at:datetime totp_timestamp:timestamp
178
+
179
+ The `otp_secret_key` is only required for users who use TOTP (Google Authenticator) codes,
180
+ so unless it has been shared with the user it should be set to `nil`. The
181
+ following pseudo-code is an example of how this might be done:
182
+
183
+ ```ruby
184
+ User.find_each do |user| do
185
+ if !uses_authenticator_app(user)
186
+ user.otp_secret_key = nil
187
+ user.save!
188
+ end
189
+ end
190
+ ```
191
+
192
+ #### Adding the TOTP encryption option to an existing app
193
+
194
+ If you've already been using this gem, and want to start encrypting the OTP
195
+ secret key in the database (recommended), you'll need to perform the following
196
+ steps:
197
+
198
+ 1. Generate a migration to add the necessary columns to your model's table:
199
+
200
+ ```
201
+ rails g migration AddEncryptionFieldsToUsers encrypted_otp_secret_key:string:index encrypted_otp_secret_key_iv:string encrypted_otp_secret_key_salt:string
202
+ ```
203
+
204
+ Open your migration file (it will be in the `db/migrate` directory and will be
205
+ named something like `20151230163930_add_encryption_fields_to_users.rb`), and
206
+ add `unique: true` to the `add_index` line so that it looks like this:
207
+
208
+ ```ruby
209
+ add_index :users, :encrypted_otp_secret_key, unique: true
210
+ ```
211
+ Save the file.
212
+
213
+ 2. Run the migration: `bundle exec rake db:migrate`
214
+
215
+ 2. Update the gem: `bundle update two_factor_authentication`
216
+
217
+ 3. Add `encrypted: true` to `has_one_time_password` in your model.
218
+ For example: `has_one_time_password(encrypted: true)`
219
+
220
+ 4. Generate a migration to populate the new encryption fields:
221
+ ```
222
+ rails g migration PopulateEncryptedOtpFields
223
+ ```
224
+
225
+ Open the generated file, and replace its contents with the following:
226
+ ```ruby
227
+ class PopulateEncryptedOtpFields < ActiveRecord::Migration
228
+ def up
229
+ User.reset_column_information
230
+
231
+ User.find_each do |user|
232
+ user.otp_secret_key = user.read_attribute('otp_secret_key')
233
+ user.save!
234
+ end
235
+ end
236
+
237
+ def down
238
+ User.reset_column_information
239
+
240
+ User.find_each do |user|
241
+ user.otp_secret_key = ROTP::Base32.random_base32
242
+ user.save!
243
+ end
244
+ end
245
+ end
246
+ ```
247
+
248
+ 5. Generate a migration to remove the `:otp_secret_key` column:
249
+ ```
250
+ rails g migration RemoveOtpSecretKeyFromUsers otp_secret_key:string
251
+ ```
252
+
253
+ 6. Run the migrations: `bundle exec rake db:migrate`
254
+
255
+ If, for some reason, you want to switch back to the old non-encrypted version,
256
+ use these steps:
257
+
258
+ 1. Remove `(encrypted: true)` from `has_one_time_password`
259
+
260
+ 2. Roll back the last 3 migrations (assuming you haven't added any new ones
261
+ after them):
262
+ ```
263
+ bundle exec rake db:rollback STEP=3
264
+ ```
265
+
266
+ #### Critical Security Note! Add before_action to your user registration controllers
267
+
268
+ You should have a file registrations_controller.rb in your controllers folder
269
+ to overwrite/customize user registrations. It should include the lines below, for 2FA protection of user model updates, meaning that users can only access the users/edit page after confirming 2FA fully, not simply by logging in. Otherwise the entire 2FA system can be bypassed!
270
+
271
+ ```ruby
272
+ class RegistrationsController < Devise::RegistrationsController
273
+ before_action :confirm_two_factor_authenticated, except: [:new, :create, :cancel]
274
+
275
+ protected
276
+
277
+ def confirm_two_factor_authenticated
278
+ return if is_fully_authenticated?
279
+
280
+ flash[:error] = t('devise.errors.messages.user_not_authenticated')
281
+ redirect_to user_two_factor_authentication_url
282
+ end
283
+ end
284
+ ```
285
+
286
+ #### Critical Security Note! Add 2FA validation to your custom user actions
287
+
288
+ Make sure you are passing the 2FA secret codes securely and checking for them upon critical user actions, such as API key updates, user email or pgp pubkey updates, or any other changess to private/secure account-related details. Validate the secret during the initial 2FA key/secret verification by the user also, of course.
289
+
290
+ For example, a simple account_controller.rb may look something like this:
291
+
292
+ ```
293
+ require 'json'
294
+
295
+ class AccountController < ApplicationController
296
+ before_action :require_signed_in!
297
+ before_action :authenticate_user!
298
+ respond_to :html, :json
299
+
300
+ def account_API
301
+ resp = {}
302
+ begin
303
+ if(account_params["twoFAKey"] && account_params["twoFASecret"])
304
+ current_user.otp_secret_key = account_params["twoFAKey"]
305
+ if(current_user.authenticate_totp(account_params["twoFASecret"]))
306
+ # user has validated their temporary 2FA code, save it to their account, enable 2FA on this account
307
+ current_user.save!
308
+ resp['success'] = "passed 2FA validation!"
309
+ else
310
+ resp['error'] = "failed 2FA validation!"
311
+ end
312
+ elsif(param[:userAccountStuff] && param[:userAccountWidget])
313
+ #before updating important user account stuff and widgets,
314
+ #check to see that the 2FA secret has also been passed in, and verify it...
315
+ if(account_params["twoFASecret"] && current_user.totp_enabled? && current_user.authenticate_totp(account_params["twoFASecret"]))
316
+ # user has passed 2FA checks, do cool user account stuff here
317
+ ...
318
+ else
319
+ # user failed 2FA check! No cool user stuff happens!
320
+ resp[error] = 'You failed 2FA validation!'
321
+ end
322
+
323
+ ...
324
+ end
325
+ else
326
+ resp['error'] = 'unknown format error, not saved!'
327
+ end
328
+ rescue Exception => e
329
+ puts "WARNING: account api threw error : '#{e}' for user #{current_user.username}"
330
+ #print "error trace: #{e.backtrace}\n"
331
+ resp['error'] = "unanticipated server response"
332
+ end
333
+ render json: resp.to_json
334
+ end
335
+
336
+ def account_params
337
+ params.require(:twoFA).permit(:userAccountStuff, :userAcountWidget, :twoFAKey, :twoFASecret)
338
+ end
339
+ end
340
+ ```
341
+
342
+
343
+ ### Example App
344
+
345
+ [TwoFactorAuthenticationExample](https://github.com/Houdini/TwoFactorAuthenticationExample)
346
+
347
+
348
+ ### Example user actions
349
+
350
+ to use an ENV VAR for the 2FA encryption key:
351
+
352
+ config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY']
353
+
354
+ to set up TOTP for Google Authenticator for user:
355
+
356
+ ```
357
+ current_user.otp_secret_key = current_user.generate_totp_secret
358
+ current_user.save!
359
+ ```
360
+
361
+ ( encrypted db fields are set upon user model save action,
362
+ rails c access relies on setting env var: OTP_SECRET_ENCRYPTION_KEY )
363
+
364
+ to check if user has input the correct code (from the QR display page)
365
+ before saving the user model:
366
+
367
+ ```
368
+ current_user.authenticate_totp('123456')
369
+ ```
370
+
371
+ additional note:
372
+
373
+ ```
374
+ current_user.otp_secret_key
375
+ ```
376
+
377
+ This returns the OTP secret key in plaintext for the user (if you have set the env var) in the console
378
+ the string used for generating the QR given to the user for their Google Auth is something like:
379
+
380
+ otpauth://totp/LABEL?secret=p6wwetjnkjnrcmpd (example secret used here)
381
+
382
+ where LABEL should be something like "example.com (Username)", which shows up in their GA app to remind them the code is for example.com
383
+
384
+ this returns true or false with an allowed_otp_drift_seconds 'grace period'
385
+
386
+ to set TOTP to DISABLED for a user account:
387
+
388
+ ```
389
+ current_user.second_factor_attempts_count=nil
390
+ current_user.encrypted_otp_secret_key=nil
391
+ current_user.encrypted_otp_secret_key_iv=nil
392
+ current_user.encrypted_otp_secret_key_salt=nil
393
+ current_user.direct_otp=nil
394
+ current_user.direct_otp_sent_at=nil
395
+ current_user.totp_timestamp=nil
396
+ current_user.direct_otp=nil
397
+ current_user.otp_secret_key=nil
398
+ current_user.otp_confirmed=nil
399
+ current_user.save! (if in ruby code instead of console)
400
+ current_user.direct_otp? => false
401
+ current_user.totp_enabled? => false
402
+ ```
403
+
404
+
405
+
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ APP_RAKEFILE = File.expand_path("../spec/rails_app/Rakefile", __FILE__)
4
+ load 'rails/tasks/engine.rake'
5
+
6
+ require 'rspec/core/rake_task'
7
+
8
+ desc "Run all specs in spec directory (excluding plugin specs)"
9
+ RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
10
+
11
+ task :default => :spec
12
+
13
+ # To test against a specific version of Rails
14
+ # export RAILS_VERSION=3.2.0; bundle update; rake
@@ -0,0 +1,84 @@
1
+ require 'devise/version'
2
+
3
+ class Devise::DeviseXfactorAuthenticationController < DeviseController
4
+ prepend_before_action :authenticate_scope!
5
+ before_action :prepare_and_validate, :handle_devise_xfactor_authentication
6
+
7
+ def show
8
+ end
9
+
10
+ def update
11
+ render :show and return if params[:code].nil?
12
+
13
+ if resource.authenticate_otp(params[:code])
14
+ after_devise_xfactor_success_for(resource)
15
+ else
16
+ after_devise_xfactor_fail_for(resource)
17
+ end
18
+ end
19
+
20
+ def resend_code
21
+ resource.send_new_otp
22
+ redirect_to send("#{resource_name}_devise_xfactor_authentication_path"), notice: I18n.t('devise.devise_xfactor_authentication.code_has_been_sent')
23
+ end
24
+
25
+ private
26
+
27
+ def after_devise_xfactor_success_for(resource)
28
+ set_remember_devise_xfactor_cookie(resource)
29
+
30
+ warden.session(resource_name)[DeviseXfactorAuthentication::NEED_AUTHENTICATION] = false
31
+ # For compatability with devise versions below v4.2.0
32
+ # https://github.com/plataformatec/devise/commit/2044fffa25d781fcbaf090e7728b48b65c854ccb
33
+ if respond_to?(:bypass_sign_in)
34
+ bypass_sign_in(resource, scope: resource_name)
35
+ else
36
+ sign_in(resource_name, resource, bypass: true)
37
+ end
38
+ set_flash_message!(:notice, :success)
39
+ resource.update(second_factor_attempts_count: 0)
40
+
41
+ redirect_to after_devise_xfactor_success_path_for(resource)
42
+ end
43
+
44
+ def set_remember_devise_xfactor_cookie(resource)
45
+ expires_seconds = resource.class.remember_otp_session_for_seconds
46
+
47
+ if expires_seconds && expires_seconds > 0
48
+ cookies.signed[DeviseXfactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = {
49
+ value: "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}",
50
+ expires: expires_seconds.seconds.from_now
51
+ }
52
+ end
53
+ end
54
+
55
+ def after_devise_xfactor_success_path_for(resource)
56
+ stored_location_for(resource_name) || :root
57
+ end
58
+
59
+ def after_devise_xfactor_fail_for(resource)
60
+ resource.second_factor_attempts_count += 1
61
+ resource.save
62
+ set_flash_message :alert, :attempt_failed, now: true
63
+
64
+ if resource.max_login_attempts?
65
+ sign_out(resource)
66
+ render :max_login_attempts_reached
67
+ else
68
+ render :show
69
+ end
70
+ end
71
+
72
+ def authenticate_scope!
73
+ self.resource = send("current_#{resource_name}")
74
+ end
75
+
76
+ def prepare_and_validate
77
+ redirect_to :root and return if resource.nil?
78
+ @limit = resource.max_login_attempts
79
+ if resource.max_login_attempts?
80
+ sign_out(resource)
81
+ render :max_login_attempts_reached and return
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,3 @@
1
+ <h2><%= I18n.t("devise.devise_xfactor_authentication.max_login_attempts_reached") %> = <%= @limit %>.</h2>
2
+ <p><%= I18n.t("devise.devise_xfactor_authentication.contact_administrator") %></p>
3
+
@@ -0,0 +1,19 @@
1
+ <% if resource.direct_otp %>
2
+ <h2>Enter the code that was sent to you</h2>
3
+ <% else %>
4
+ <h2>Enter the code from your authenticator app</h2>
5
+ <% end %>
6
+
7
+ <p><%= flash[:notice] %></p>
8
+
9
+ <%= form_tag([resource_name, :devise_xfactor_authentication], :method => :put) do %>
10
+ <%= text_field_tag :code %>
11
+ <%= submit_tag "Submit" %>
12
+ <% end %>
13
+
14
+ <% if resource.direct_otp %>
15
+ <%= link_to "Resend Code", send("resend_code_#{resource_name}_devise_xfactor_authentication_path"), action: :get %>
16
+ <% else %>
17
+ <%= link_to "Send me a code instead", send("resend_code_#{resource_name}_devise_xfactor_authentication_path"), action: :get %>
18
+ <% end %>
19
+ <%= link_to "Sign out", send("destroy_#{resource_name}_session_path"), data: { turbo: false }, method: :delete %>
@@ -0,0 +1,8 @@
1
+ de:
2
+ devise:
3
+ devise_xfactor_authentication:
4
+ success: "Ihre Zwei-Faktor-Authentifizierung war erfolgreich."
5
+ attempt_failed: "Authentifizierungsversuch fehlgeschlagen."
6
+ max_login_attempts_reached: "Ihr Zugang wurde ganz verweigert, da Sie Ihr Versuchslimit erreicht haben."
7
+ contact_administrator: "Kontaktieren Sie bitte einen Ihrer Administratoren."
8
+ code_has_been_sent: "Ihr Einmal-Passwort wurde verschickt."
@@ -0,0 +1,8 @@
1
+ en:
2
+ devise:
3
+ devise_xfactor_authentication:
4
+ success: "Two factor authentication successful."
5
+ attempt_failed: "Attempt failed."
6
+ max_login_attempts_reached: "Access completely denied as you have reached your attempts limit"
7
+ contact_administrator: "Please contact your system administrator."
8
+ code_has_been_sent: "Your authentication code has been sent."
@@ -0,0 +1,8 @@
1
+ es:
2
+ devise:
3
+ devise_xfactor_authentication:
4
+ success: "Autenticación multi-factor realizada exitosamente."
5
+ attempt_failed: "La autenticación ha fallado."
6
+ max_login_attempts_reached: "Has llegado al límite de intentos fallidos, acceso denegado."
7
+ contact_administrator: "Contacte a su administrador de sistema."
8
+ code_has_been_sent: "El código de autenticación ha sido enviado."
@@ -0,0 +1,8 @@
1
+ fr:
2
+ devise:
3
+ devise_xfactor_authentication:
4
+ success: "Validation en deux étapes effectuée avec succès."
5
+ attempt_failed: "La connexion a échoué."
6
+ max_login_attempts_reached: "Limite de tentatives atteinte, accès refusé."
7
+ contact_administrator: "Merci de contacter votre administrateur système."
8
+ code_has_been_sent: "Votre code de validation envoyé."
@@ -0,0 +1,8 @@
1
+ ru:
2
+ devise:
3
+ devise_xfactor_authentication:
4
+ success: "Двухфакторная авторизация успешно пройдена."
5
+ attempt_failed: "Неверный код."
6
+ max_login_attempts_reached: "Доступ заблокирован. Превышено число попыток авторизации"
7
+ contact_administrator: "Пожалуйста, свяжитесь с системным администратором."
8
+ code_has_been_sent: "Ваш персональный код был отправлен."
@@ -0,0 +1,39 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "devise_xfactor_authentication/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "devise_xfactor_authentication"
7
+ s.version = DeviseXfactorAuthentication::VERSION.dup
8
+ s.authors = ["Jonathon Pickett"]
9
+ s.email = ["jpickett76@gmail.com"]
10
+ s.homepage = "https://github.com/jpickett76/devise_xfactor_authentication"
11
+ s.summary = %q{Two factor authentication plugin for devise forked from Houdini/two_factor_authentication}
12
+ s.description = <<-EOF
13
+ ### Features ###
14
+ * control sms code pattern
15
+ * configure max login attempts
16
+ * per user level control if he really need two factor authentication
17
+ * your own sms logic
18
+ EOF
19
+
20
+ #s.rubyforge_project = "devise_xfactor_authentication"
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.require_paths = ["lib"]
26
+
27
+ s.add_runtime_dependency 'rails', '>= 5.0'
28
+ s.add_runtime_dependency 'devise', '>= 4'
29
+ s.add_runtime_dependency 'randexp', '>= 0.1'
30
+ s.add_runtime_dependency 'rotp', '>= 6.0.0'
31
+ s.add_runtime_dependency 'encryptor', '>= 3.0.0'
32
+
33
+ s.add_development_dependency 'bundler', '>= 2'
34
+ s.add_development_dependency 'rake', '>= 13'
35
+ s.add_development_dependency 'rspec-rails', '>= 6.0'
36
+ s.add_development_dependency 'capybara', '>= 3'
37
+ s.add_development_dependency 'pry', '>= 0.14'
38
+ s.add_development_dependency 'timecop', '>= 0.9'
39
+ end
@@ -0,0 +1,49 @@
1
+ module DeviseXfactorAuthentication
2
+ module Controllers
3
+ module Helpers
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_action :handle_devise_xfactor_authentication
8
+ end
9
+
10
+ private
11
+
12
+ def handle_devise_xfactor_authentication
13
+ unless devise_controller?
14
+ Devise.mappings.keys.flatten.any? do |scope|
15
+ if signed_in?(scope) and warden.session(scope)[DeviseXfactorAuthentication::NEED_AUTHENTICATION]
16
+ handle_failed_second_factor(scope)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def handle_failed_second_factor(scope)
23
+ if request.format.present? and request.format.html?
24
+ session["#{scope}_return_to"] = request.original_fullpath if request.get?
25
+ redirect_to devise_xfactor_authentication_path_for(scope)
26
+ else
27
+ head :unauthorized
28
+ end
29
+ end
30
+
31
+ def devise_xfactor_authentication_path_for(resource_or_scope = nil)
32
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
33
+ change_path = "#{scope}_devise_xfactor_authentication_path"
34
+ send(change_path)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+
41
+ module Devise
42
+ module Controllers
43
+ module Helpers
44
+ def is_fully_authenticated?
45
+ !session["warden.user.user.session"].try(:[], DeviseXfactorAuthentication::NEED_AUTHENTICATION)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ Warden::Manager.after_authentication do |user, auth, options|
2
+ if auth.env["action_dispatch.cookies"]
3
+ expected_cookie_value = "#{user.class}-#{user.public_send(Devise.second_factor_resource_id)}"
4
+ actual_cookie_value = auth.env["action_dispatch.cookies"].signed[DeviseXfactorAuthentication::REMEMBER_TFA_COOKIE_NAME]
5
+ bypass_by_cookie = actual_cookie_value == expected_cookie_value
6
+ end
7
+
8
+ if user.respond_to?(:need_devise_xfactor_authentication?) && !bypass_by_cookie
9
+ if auth.session(options[:scope])[DeviseXfactorAuthentication::NEED_AUTHENTICATION] = user.need_devise_xfactor_authentication?(auth.request)
10
+ user.send_new_otp if user.send_new_otp_after_login?
11
+ end
12
+ end
13
+ end
14
+
15
+ Warden::Manager.before_logout do |user, auth, _options|
16
+ auth.cookies.delete DeviseXfactorAuthentication::REMEMBER_TFA_COOKIE_NAME if Devise.delete_cookie_on_logout
17
+ end