quo_vadis 2.0.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +59 -33
  4. data/app/controllers/quo_vadis/confirmations_controller.rb +47 -1
  5. data/app/controllers/quo_vadis/password_resets_controller.rb +11 -8
  6. data/app/controllers/quo_vadis/passwords_controller.rb +4 -2
  7. data/app/controllers/quo_vadis/recovery_codes_controller.rb +1 -1
  8. data/app/controllers/quo_vadis/sessions_controller.rb +3 -3
  9. data/app/controllers/quo_vadis/totps_controller.rb +1 -1
  10. data/app/models/quo_vadis/account.rb +14 -0
  11. data/app/models/quo_vadis/log.rb +4 -2
  12. data/app/models/quo_vadis/password.rb +16 -0
  13. data/{test/dummy/app → app}/views/quo_vadis/confirmations/edit.html.erb +0 -0
  14. data/app/views/quo_vadis/confirmations/edit_email.html.erb +14 -0
  15. data/app/views/quo_vadis/confirmations/index.html.erb +14 -0
  16. data/{test/dummy/app → app}/views/quo_vadis/confirmations/new.html.erb +0 -0
  17. data/{test/dummy/app → app}/views/quo_vadis/logs/index.html.erb +3 -1
  18. data/{test/dummy/app → app}/views/quo_vadis/mailer/account_confirmation.text.erb +0 -0
  19. data/{test/dummy/app → app}/views/quo_vadis/mailer/email_change_notification.text.erb +0 -0
  20. data/{test/dummy/app → app}/views/quo_vadis/mailer/identifier_change_notification.text.erb +0 -0
  21. data/{test/dummy/app → app}/views/quo_vadis/mailer/password_change_notification.text.erb +0 -0
  22. data/{test/dummy/app → app}/views/quo_vadis/mailer/password_reset_notification.text.erb +0 -0
  23. data/{test/dummy/app → app}/views/quo_vadis/mailer/recovery_codes_generation_notification.text.erb +0 -0
  24. data/{test/dummy/app → app}/views/quo_vadis/mailer/reset_password.text.erb +0 -0
  25. data/{test/dummy/app → app}/views/quo_vadis/mailer/totp_reuse_notification.text.erb +0 -0
  26. data/{test/dummy/app → app}/views/quo_vadis/mailer/totp_setup_notification.text.erb +0 -0
  27. data/{test/dummy/app → app}/views/quo_vadis/mailer/twofa_deactivated_notification.text.erb +0 -0
  28. data/{test/dummy/app → app}/views/quo_vadis/password_resets/edit.html.erb +1 -9
  29. data/{test/dummy/app → app}/views/quo_vadis/password_resets/index.html.erb +0 -0
  30. data/{test/dummy/app → app}/views/quo_vadis/password_resets/new.html.erb +0 -0
  31. data/{test/dummy/app → app}/views/quo_vadis/passwords/edit.html.erb +1 -9
  32. data/{test/dummy/app → app}/views/quo_vadis/recovery_codes/challenge.html.erb +0 -0
  33. data/{test/dummy/app → app}/views/quo_vadis/recovery_codes/index.html.erb +0 -0
  34. data/{test/dummy/app → app}/views/quo_vadis/sessions/index.html.erb +0 -0
  35. data/{test/dummy/app → app}/views/quo_vadis/sessions/new.html.erb +0 -0
  36. data/{test/dummy/app → app}/views/quo_vadis/totps/challenge.html.erb +0 -0
  37. data/{test/dummy/app → app}/views/quo_vadis/totps/new.html.erb +0 -0
  38. data/{test/dummy/app → app}/views/quo_vadis/twofas/show.html.erb +0 -0
  39. data/config/locales/quo_vadis.en.yml +30 -1
  40. data/config/routes.rb +11 -5
  41. data/lib/generators/quo_vadis/install_generator.rb +1 -1
  42. data/lib/quo_vadis/controller.rb +3 -3
  43. data/lib/quo_vadis/defaults.rb +1 -1
  44. data/lib/quo_vadis/model.rb +8 -11
  45. data/lib/quo_vadis/version.rb +1 -1
  46. data/lib/quo_vadis.rb +5 -1
  47. data/test/dummy/app/controllers/sign_ups_controller.rb +1 -1
  48. data/test/dummy/config/initializers/quo_vadis.rb +0 -4
  49. data/test/integration/account_confirmation_test.rb +35 -2
  50. data/test/integration/logging_test.rb +14 -5
  51. data/test/integration/password_change_test.rb +16 -10
  52. data/test/integration/password_login_test.rb +14 -2
  53. data/test/integration/password_reset_test.rb +6 -6
  54. data/test/integration/totps_test.rb +1 -1
  55. data/test/models/account_test.rb +16 -0
  56. data/test/models/password_test.rb +16 -0
  57. data/test/models/recovery_code_test.rb +7 -1
  58. data/test/models/token_test.rb +1 -1
  59. metadata +29 -28
  60. data/test/dummy/app/views/quo_vadis/confirmations/index.html.erb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ed3f9c506465f26124edf6207abe8bd40d8a87587d779c9789a882eb3894153
4
- data.tar.gz: 9fb0294a48aef63132e359c97529e9547d3f6e6425ebcd734b2eff08a385fe2f
3
+ metadata.gz: 5b81cb83f9cd0b6bb6e9af4015dba59b5cef0f5c3fccd5a82ee6d7e1bbc5768a
4
+ data.tar.gz: 8a9f5f0c013ec89de6849e9ddb3393fee46b84a2d08b20e606126cf97bba8032
5
5
  SHA512:
6
- metadata.gz: a40cb588843f7e78d186e3203ac082e7238ceb21ced56393b26822b353797f214cd4d0bc77242a1d51d4e197a6ca95f2d0b6eebd97a8f730bda48eb2f86c9c3b
7
- data.tar.gz: 826424470205327161484deff2aa1a031fd7426bf189c493488a9223fbe542b32ea47c89faa6b732fc3fe9d02da6bcd59cd7d31d042b9a30b775e3fae9229c34
6
+ metadata.gz: a7b5e766f22e55e5bc269d00e6fb9132cc54ba2523a72996d0d7da478dbf0662764d29265c591f5847d5f2cfaeaca1e78fb4d48fb8fecb24af0bee9e9c2047e2
7
+ data.tar.gz: 47bf8bba82c90f3b7527be6a316ec8f9a4b229077a6846454abecebd6d2ae29910f7e416635d9cc1d54e3b99fe87d4c7d0ca871741907f1987d1ee42bf79994b
data/CHANGELOG.md CHANGED
@@ -1,6 +1,37 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## HEAD
5
+
6
+
7
+ ## 2.1.1 (8 July 2021)
8
+
9
+ * Remove unnecessary route names.
10
+ * Add user revocation.
11
+ * Ensure password is only updated via #change or #reset.
12
+ * Move views into gem's app/views/ directory.
13
+
14
+
15
+ ## 2.1.0 (25 June 2021)
16
+
17
+ * Do not require password on create.
18
+ * Fix incorrect assignment of built association.
19
+ * Add i18n translations for log actions.
20
+ * Use model instance in change-password form.
21
+ * Ensure password-reset flash notice not displayed when emailed link is clicked.
22
+ * Use model instance in password-reset form.
23
+ * Give no indication of unknown account on request of password reset email.
24
+ * Use 422 status code for form submission error responses.
25
+ * Make default cookie name depend on Rails environment.
26
+
27
+
28
+ ## 2.0.2 (24 May 2021)
29
+
30
+ * Account confirmation: enable updating of email address.
31
+ * Account confirmation: enable direct resending of email.
32
+ * Log unknown identifier in metadata.
33
+
34
+
4
35
  ## 2.0.1 (18 May 2021)
5
36
 
6
37
  * Remove Gemfile.lock from repo.
data/README.md CHANGED
@@ -86,9 +86,9 @@ class Person < ApplicationRecord
86
86
  end
87
87
  ```
88
88
 
89
- When __creating__ a model instance, include a `:password` attribute and, optionally, `:password_confirmation` attribute.
89
+ You can create and update your models as before. When you want to set a password for the first time, just include `:password` and, optionally, `:password_confirmation` in the attributes to `#create` or `#update`.
90
90
 
91
- When __updating__ a model instance, do not include a `:password` attribute. To change someone's password, use the Change Password feature (see below).
91
+ If you want to change an existing password, use the Change Password feature (see below). If you update a model (that already has a password) with a `:password` attribute, it will raise a `QuoVadis::PasswordExistsError`.
92
92
 
93
93
  The minimum password length is configured by `QuoVadis.password_minimum_length` (12 by default).
94
94
 
@@ -232,7 +232,7 @@ class UsersController < ApplicationController
232
232
  @user = User.new user_params
233
233
  if @user.save
234
234
  request_confirmation @user
235
- redirect_to qv.confirmations_path # a page where you advise the user to check their email
235
+ redirect_to quo_vadis.confirmations_path # a page where you advise the user to check their email
236
236
  else
237
237
  # ...
238
238
  end
@@ -246,17 +246,19 @@ class UsersController < ApplicationController
246
246
  end
247
247
  ```
248
248
 
249
- QuoVadis will send the user an email with a link. Write the email view ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/mailer/account_confirmation.text.erb)). It must be in `app/views/quo_vadis/mailer/account_confirmation.{text,html}.erb` and output the `@url` variable.
249
+ QuoVadis will send the user an email with a link. Write the email view ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/account_confirmation.text.erb)). It must be in `app/views/quo_vadis/mailer/account_confirmation.{text,html}.erb` and output the `@url` variable.
250
250
 
251
251
  See the Configuration section below for how to set QuoVadis's emails' from addresses, headers, etc.
252
252
 
253
- Now write the page to where the user is redirected while they wait for the email ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/confirmations/index.html.erb)). It must be in `app/views/quo_vadis/confirmations/index.html.:format`.
253
+ Now write the page to where the user is redirected while they wait for the email ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/confirmations/index.html.erb)). It must be in `app/views/quo_vadis/confirmations/index.html.:format`.
254
254
 
255
- It's a good idea for that page to link to `new_confirmation_path` where the user can request another email if need be.
255
+ On that page you can show the user the address the email was sent to, enable them to update their email address if they make a mistake on the sign-up form, and provide a button to resend another email directly. If the sign-up occurred in a different browser session, you can instead link to `new_confirmation_path` where the user can request another email if need be.
256
256
 
257
- Next, write the page to which the link in the email points ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/confirmations/edit.html.erb)). It must be in `app/views/quo_vadis/confirmations/edit.html.:format`.
257
+ Next, write the page to which the link in the email points ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/confirmations/edit.html.erb)). It must be in `app/views/quo_vadis/confirmations/edit.html.:format`.
258
258
 
259
- Finally, write the page where people can put in their identifier (not their email, unless the identifier is email) again to request another confirmation email ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/confirmations/new.html.erb)). It must be in `app/views/quo_vadis/confirmations/new.html.:format`.
259
+ Next, write the page where the user can amend their email address if they made a mistake when signing up ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/confirmations/edit_email.html.erb)). It must be in `app/views/quo_vadis/confirmations/edit_email.html.:format`.
260
+
261
+ Finally, write the page where people can put in their identifier (not their email, unless the identifier is email) again to request another confirmation email ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/confirmations/new.html.erb)). It must be in `app/views/quo_vadis/confirmations/new.html.:format`.
260
262
 
261
263
  After the user has confirmed their account, they will be logged in and redirected to the first of these that exists:
262
264
 
@@ -270,7 +272,7 @@ So add whichever works best for you.
270
272
 
271
273
  Use `before_action :require_password_authentication` or `before_action :require_authentication` in your controllers.
272
274
 
273
- Write the login view ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/sessions/new.html.erb)). Your login form must be in `app/views/quo_vadis/sessions/new.html.:format`. Note it must capture the user's identifier (not email, unless the identifier is email).
275
+ Write the login view ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/sessions/new.html.erb)). Your login form must be in `app/views/quo_vadis/sessions/new.html.:format`. Note it must capture the user's identifier (not email, unless the identifier is email).
274
276
 
275
277
  If you include a `remember` checkbox in your login form:
276
278
 
@@ -290,7 +292,7 @@ After authenticating the user will be redirected to the first of these that exis
290
292
 
291
293
  If you do not want 2FA at all, set `QuoVadis.two_factor_authentication_mandatory false` in your configuration and skip the rest of this section.
292
294
 
293
- If you do want 2FA, you can choose whether it is optional or mandatory for your users by setting `QuoVadis.two_factor_authentication_mandatory <true|false>` in your configuration.
295
+ If you do want 2FA, you can choose whether it is mandatory or optional for your users by setting `QuoVadis.two_factor_authentication_mandatory <true|false>` in your configuration.
294
296
 
295
297
  Use `before_action :require_two_factor_authentication` in your controllers (which supersedes `:require_password_authentication`). This will require the user, after authenticating with their password, to authenticate with 2FA – when 2FA is mandatory, or when it is optional and the user has set up 2FA.
296
298
 
@@ -310,22 +312,22 @@ In your views, have a link where users can manage their 2FA:
310
312
  link_to '2FA', quo_vadis.twofa_path
311
313
  ```
312
314
 
313
- Write the 2FA overview page ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/twofas/show.html.erb)). It must be in `app/views/quo_vadis/twofas/show.html.:format`. This page allows the user to set up 2FA, deactivate or reset it, and generate new recovery codes.
315
+ Write the 2FA overview page ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/twofas/show.html.erb)). It must be in `app/views/quo_vadis/twofas/show.html.:format`. This page allows the user to set up 2FA, deactivate or reset it, and generate new recovery codes.
314
316
 
315
- Next, write the TOTP setup page ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/totps/new.html.erb)). It must be in `app/views/quo_vadis/totps/new.html.:format`. This page shows the user a QR code (and the key as text) which they scan with their authenticator.
317
+ Next, write the TOTP setup page ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/totps/new.html.erb)). It must be in `app/views/quo_vadis/totps/new.html.:format`. This page shows the user a QR code (and the key as text) which they scan with their authenticator.
316
318
 
317
- Next, write the recovery codes page ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/recovery_codes/index.html.erb)). It must be in `app/views/quo_vadis/recovery_codes/index.html.:format`. This shows the recovery codes immediately after TOTP is setup, and immediately after generating fresh recovery codes, but not otherwise.
319
+ Next, write the recovery codes page ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/recovery_codes/index.html.erb)). It must be in `app/views/quo_vadis/recovery_codes/index.html.:format`. This shows the recovery codes immediately after TOTP is setup, and immediately after generating fresh recovery codes, but not otherwise.
318
320
 
319
- Next, write the TOTP challenge page where a user inputs their 6-digit TOTP ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/totps/challenge.html.erb)). It must be in `app/views/quo_vadis/totps/challenge.html.:format`. It's a good idea to link to the recovery code page (`challenge_recovery_codes_path`) for any user who has lost their authenticator.
321
+ Next, write the TOTP challenge page where a user inputs their 6-digit TOTP ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/totps/challenge.html.erb)). It must be in `app/views/quo_vadis/totps/challenge.html.:format`. It's a good idea to link to the recovery code page (`challenge_recovery_codes_path`) for any user who has lost their authenticator.
320
322
 
321
- Finally, write the recovery code challenge page where a user inputs one of their recovery codes ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/recovery_codes/challenge.html.erb)). It must be in `app/views/quo_vadis/recovery_codes/challenge.html.:format`. A recovery code can only be used once, and using one deactivates TOTP – so the user will have to set it up again next time.
323
+ Finally, write the recovery code challenge page where a user inputs one of their recovery codes ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/recovery_codes/challenge.html.erb)). It must be in `app/views/quo_vadis/recovery_codes/challenge.html.:format`. A recovery code can only be used once, and using one deactivates TOTP – so the user will have to set it up again next time.
322
324
 
323
325
 
324
326
  ### Change password
325
327
 
326
328
  To change their password, the user must provide their current one as well as the new one.
327
329
 
328
- Write the change-password form ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/passwords/edit.html.erb)). It must be in `app/views/quo_vadis/passwords/edit.html.:format`.
330
+ Write the change-password form ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/passwords/edit.html.erb)). It must be in `app/views/quo_vadis/passwords/edit.html.:format`.
329
331
 
330
332
  After the password has been changed, the user is redirected to the first of:
331
333
 
@@ -345,17 +347,17 @@ The user can reset their password if they lose it. The flow is:
345
347
  4. [Password-reset confirmation page] The user enters their new password and clicks a button.
346
348
  5. QuoVadis sets the user's password and logs them in.
347
349
 
348
- First, write the page where the user requests a password-reset ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/password_resets/new.html.erb)). It must be in `app/views/quo_vadis/password_resets/new.html.:format`. Note it must capture the user's identifier (not email, unless the identifier is email).
350
+ First, write the page where the user requests a password-reset ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/password_resets/new.html.erb)). It must be in `app/views/quo_vadis/password_resets/new.html.:format`. Note it must capture the user's identifier (not email, unless the identifier is email).
349
351
 
350
352
  See the Configuration section below for how to set QuoVadis's emails' from addresses, headers, etc.
351
353
 
352
- Now write the page to where the user is redirected while they wait for the email ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/password_resets/index.html.erb)). It must be in `app/views/quo_vadis/password_resets/index.html.:format`.
354
+ Now write the page to where the user is redirected while they wait for the email ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/password_resets/index.html.erb)). It must be in `app/views/quo_vadis/password_resets/index.html.:format`.
353
355
 
354
356
  It's a good idea for that page to link to `new_password_reset_path` where the user can request another email if need be.
355
357
 
356
- Next, write the page to which the link in the email points ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/password_resets/edit.html.erb)). It must be in `app/views/quo_vadis/password_resets/edit.html.:format`.
358
+ Now write the email view ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/reset_password.text.erb)). It must be in `app/views/quo_vadis/mailer/reset_password.{text,html}.erb` and output the `@url` variable.
357
359
 
358
- Finally, write the page where people can put in their identifier (not their email, unless the identifier is email) again to request another password-reset email ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/password_resets/new.html.erb)). It must be in `app/views/quo_vadis/password_resets/new.html.:format`.
360
+ Next, write the page to which the link in the email points ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/password_resets/edit.html.erb)). It must be in `app/views/quo_vadis/password_resets/edit.html.:format`.
359
361
 
360
362
  After the user has reset their password, they will be logged in and redirected to the first of these that exists:
361
363
 
@@ -369,14 +371,14 @@ A logged-in session lasts for either the browser session or `QuoVadis.session_li
369
371
 
370
372
  A user can view their active sessions and log out of any of them.
371
373
 
372
- Write the view showing the sessions ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/sessions/index.html.erb)). It must be in `app/views/quo_vadis/sessions/index.html.:format`.
374
+ Write the view showing the sessions ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/sessions/index.html.erb)). It must be in `app/views/quo_vadis/sessions/index.html.:format`.
373
375
 
374
376
 
375
377
  ### Audit trail
376
378
 
377
379
  An audit trail is kept of authentication events. You can see the full list in the [`Log`](https://github.com/airblade/quo_vadis/blob/master/app/models/quo_vadis/log.rb) class.
378
380
 
379
- Write the view showing the events ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/logs/index.html.erb)). It must be in `app/views/quo_vadis/logs/index.html.:format`.
381
+ Write the view showing the events ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/logs/index.html.erb)). It must be in `app/views/quo_vadis/logs/index.html.:format`.
380
382
 
381
383
 
382
384
  ### Notifications
@@ -385,18 +387,23 @@ QuoVadis notifies users by email whenever their authentication details are chang
385
387
 
386
388
  Write the corresponding mailer views:
387
389
 
388
- - change of email ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/mailer/email_change_notification.text.erb))
389
- - change of identifier (unless the identifier is email) ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/mailer/identifier_change_notification.text.erb))
390
- - change of password ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/mailer/password_change_notification.text.erb))
391
- - reset of password ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/mailer/password_reset_notification.text.erb))
392
- - TOTP setup ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/mailer/totp_setup_notification.text.erb))
393
- - TOTP code used a second time ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/mailer/totp_reuse_notification.text.erb))
394
- - 2FA deactivated ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/mailer/twofa_deactivated_notification.text.erb))
395
- - recovery codes generated ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/quo_vadis/mailer/recovery_codes_generation_notification.text.erb))
390
+ - change of email ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/email_change_notification.text.erb))
391
+ - change of identifier (unless the identifier is email) ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/identifier_change_notification.text.erb))
392
+ - change of password ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/password_change_notification.text.erb))
393
+ - reset of password ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/password_reset_notification.text.erb))
394
+ - TOTP setup ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/totp_setup_notification.text.erb))
395
+ - TOTP code used a second time ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/totp_reuse_notification.text.erb))
396
+ - 2FA deactivated ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/twofa_deactivated_notification.text.erb))
397
+ - recovery codes generated ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/recovery_codes_generation_notification.text.erb))
396
398
 
397
399
  They must be in `app/views/quo_vadis/mailer/NAME.{text,html}.erb`.
398
400
 
399
401
 
402
+ ### Revocation
403
+
404
+ You can revoke a user's access by calling `#revoke_authentication_credentials` on the model instance. This deletes the user's password, TOTP credential, recovery codes, and active sessions. Their authentication logs, or audit trail, are preserved.
405
+
406
+
400
407
  ## Configuration
401
408
 
402
409
  This is QuoVadis' [default configuration](https://github.com/airblade/quo_vadis/blob/master/lib/quo_vadis/defaults.rb):
@@ -405,7 +412,7 @@ This is QuoVadis' [default configuration](https://github.com/airblade/quo_vadis/
405
412
  QuoVadis.configure do
406
413
  password_minimum_length 12
407
414
  mask_ips false
408
- cookie_name '__Host-qv'
415
+ cookie_name (Rails.env.production? ? '__Host-qv' : 'qv')
409
416
  session_lifetime :session
410
417
  session_lifetime_extend_to_end_of_day false
411
418
  session_idle_timeout :lifetime
@@ -436,7 +443,7 @@ Masking means setting the last octet (IPv4) or the last 80 bits (IPv6) to 0.
436
443
 
437
444
  __`cookie_name`__ (string)
438
445
 
439
- The name of the cookie QuoVadis uses to store the session identifier. The `__Host-` prefix is [recommended](https://developer.mozilla.org/en-US/docs/Web/API/document/cookie).
446
+ The name of the cookie QuoVadis uses to store the session identifier. The `__Host-` prefix is [recommended](https://developer.mozilla.org/en-US/docs/Web/API/document/cookie) in an SSL environment (but cannot be used in a non-SSL environment).
440
447
 
441
448
  __`session_lifetime`__ (`:session` | `ActiveSupport::Duration` | integer)
442
449
 
@@ -488,13 +495,32 @@ For example, the default login path is at `/login`. If you set `mount_point` to
488
495
 
489
496
  #### Rails configuration
490
497
 
498
+ __Mailer URLs__
499
+
491
500
  You must also configure the mailer host so URLs are generated correctly in emails:
492
501
 
493
502
  ```ruby
494
503
  config.action_mailer.default_url_options: { host: 'example.com' }
495
504
  ```
496
505
 
497
- Finally, you can set up your post-authentication and post-password-change routes. If you don't, you must have a root route. For example:
506
+ __Layouts__
507
+
508
+ You can specify QuoVadis's controllers' layouts in a `#to_prepare` block in your application configuration. For example:
509
+
510
+ ```ruby
511
+ # config/application.rb
512
+ module YourApp
513
+ class Application < Rails::Application
514
+ config.to_prepare do
515
+ QuoVadis::ConfirmationsController.layout 'your_layout'
516
+ end
517
+ end
518
+ end
519
+ ```
520
+
521
+ __Routes__
522
+
523
+ You can set up your post-authentication and post-password-change routes. If you don't, you must have a root route. For example:
498
524
 
499
525
  ```ruby
500
526
  # config/routes.rb
@@ -5,6 +5,7 @@ module QuoVadis
5
5
 
6
6
  # holding page
7
7
  def index
8
+ @account = find_pending_account_from_session
8
9
  end
9
10
 
10
11
 
@@ -45,12 +46,57 @@ module QuoVadis
45
46
  end
46
47
 
47
48
  account.confirmed!
48
-
49
49
  qv.log account, Log::ACCOUNT_CONFIRMATION
50
50
 
51
+ session.delete :account_pending_confirmation
52
+
51
53
  login account.model, true
52
54
  redirect_to qv.path_after_authentication, notice: QuoVadis.translate('flash.confirmation.confirmed')
53
55
  end
54
56
 
57
+
58
+ def edit_email
59
+ account = find_pending_account_from_session
60
+
61
+ unless account
62
+ redirect_to confirmations_path, alert: QuoVadis.translate('flash.confirmation.unknown') and return
63
+ end
64
+
65
+ @email = account.model.email
66
+ end
67
+
68
+
69
+ def update_email
70
+ account = find_pending_account_from_session
71
+
72
+ unless account
73
+ redirect_to confirmations_path, alert: QuoVadis.translate('flash.confirmation.unknown') and return
74
+ end
75
+
76
+ account.model.update email: params[:email]
77
+
78
+ request_confirmation account.model
79
+ redirect_to confirmations_path
80
+ end
81
+
82
+
83
+ def resend
84
+ account = find_pending_account_from_session
85
+
86
+ unless account
87
+ redirect_to confirmations_path, alert: QuoVadis.translate('flash.confirmation.unknown') and return
88
+ end
89
+
90
+ request_confirmation account.model
91
+ redirect_to confirmations_path
92
+ end
93
+
94
+
95
+ private
96
+
97
+ def find_pending_account_from_session
98
+ Account.find(session[:account_pending_confirmation]) if session[:account_pending_confirmation]
99
+ end
100
+
55
101
  end
56
102
  end
@@ -13,15 +13,14 @@ module QuoVadis
13
13
 
14
14
 
15
15
  def create
16
- flash[:notice] = QuoVadis.translate 'flash.password_reset.create'
17
-
18
16
  account = QuoVadis.find_account_by_identifier_in_params params
19
- return unless account
20
17
 
21
- token = QuoVadis::PasswordResetToken.generate account
22
- QuoVadis.deliver :reset_password, email: account.model.email, url: quo_vadis.edit_password_reset_url(token)
18
+ if account
19
+ token = QuoVadis::PasswordResetToken.generate account
20
+ QuoVadis.deliver :reset_password, email: account.model.email, url: quo_vadis.password_reset_url(token)
21
+ end
23
22
 
24
- redirect_to password_resets_path
23
+ redirect_to password_resets_path, notice: QuoVadis.translate('flash.password_reset.create')
25
24
  end
26
25
 
27
26
 
@@ -33,6 +32,10 @@ module QuoVadis
33
32
  redirect_to new_password_reset_path, alert: QuoVadis.translate('flash.password_reset.unknown') and return
34
33
  end
35
34
 
35
+ # Ensure the flash notice set in the create action does not appear when the user clicks the
36
+ # link in the email they were sent.
37
+ flash.delete :notice
38
+
36
39
  @password = QuoVadis::Password.new
37
40
  end
38
41
 
@@ -46,7 +49,7 @@ module QuoVadis
46
49
  end
47
50
 
48
51
  @password = account.password
49
- if @password.reset params[:password], params[:password_confirmation]
52
+ if @password.reset params[:password][:password], params[:password][:password_confirmation]
50
53
  # Logout account's sessions because password has changed.
51
54
  # Note model is not logged in here.
52
55
  @password.account.sessions.destroy_all
@@ -57,7 +60,7 @@ module QuoVadis
57
60
  login @password.account.model, true
58
61
  redirect_to qv.path_after_authentication, notice: QuoVadis.translate('flash.password_reset.reset')
59
62
  else
60
- render :edit
63
+ render :edit, status: :unprocessable_entity
61
64
  end
62
65
  end
63
66
 
@@ -11,14 +11,16 @@ module QuoVadis
11
11
 
12
12
  def update
13
13
  @password = authenticated_model.qv_account.password
14
- if @password.change params[:password], params[:new_password], params[:new_password_confirmation]
14
+ if @password.change(params[:password][:password],
15
+ params[:password][:new_password],
16
+ params[:password][:new_password_confirmation])
15
17
  qv.log authenticated_model.qv_account, Log::PASSWORD_CHANGE
16
18
  QuoVadis.notify :password_change_notification, email: authenticated_model.email
17
19
  qv.logout_other_sessions
18
20
  qv.replace_session
19
21
  redirect_to qv.path_after_password_change, notice: QuoVadis.translate('flash.password.update')
20
22
  else
21
- render :edit
23
+ render :edit, status: :unprocessable_entity
22
24
  end
23
25
  end
24
26
 
@@ -27,7 +27,7 @@ module QuoVadis
27
27
  else
28
28
  qv.log account, Log::RECOVERY_CODE_FAILURE
29
29
  flash.now[:alert] = QuoVadis.translate('flash.recovery_code.unverified')
30
- render :challenge
30
+ render :challenge, status: :unprocessable_entity
31
31
  end
32
32
  end
33
33
 
@@ -21,16 +21,16 @@ module QuoVadis
21
21
  account = QuoVadis.find_account_by_identifier_in_params params
22
22
 
23
23
  unless account
24
- qv.log nil, Log::LOGIN_UNKNOWN
24
+ qv.log nil, Log::LOGIN_UNKNOWN, identifier: QuoVadis.identifier_value_in_params(params)
25
25
  flash.now[:alert] = QuoVadis.translate 'flash.login.failed'
26
- render :new
26
+ render :new, status: :unprocessable_entity
27
27
  return
28
28
  end
29
29
 
30
30
  unless account.password.authenticate params[:password]
31
31
  qv.log account, Log::LOGIN_FAILURE
32
32
  flash.now[:alert] = QuoVadis.translate 'flash.login.failed'
33
- render :new
33
+ render :new, status: :unprocessable_entity
34
34
  return
35
35
  end
36
36
 
@@ -53,7 +53,7 @@ module QuoVadis
53
53
  qv.log authenticated_model.qv_account, Log::TOTP_FAILURE
54
54
  end
55
55
  flash.now[:alert] = QuoVadis.translate('flash.totp.unverified')
56
- render :challenge
56
+ render :challenge, status: :unprocessable_entity
57
57
  end
58
58
  end
59
59
 
@@ -32,9 +32,23 @@ module QuoVadis
32
32
 
33
33
  # Returns an array of the recovery codes' codes.
34
34
  def generate_recovery_codes
35
+ recovery_codes.delete_all
35
36
  Array.new(MAX_NUMBER_OF_RECOVERY_CODES) { recovery_codes.create }.map &:code
36
37
  end
37
38
 
39
+ def revoke
40
+ password&.destroy
41
+ totp&.destroy
42
+ recovery_codes.destroy_all
43
+ sessions.destroy_all
44
+
45
+ Log.create(
46
+ account: self,
47
+ action: Log::REVOKE,
48
+ ip: (CurrentRequestDetails.ip || '')
49
+ )
50
+ end
51
+
38
52
  private
39
53
 
40
54
  def log_identifier_change
@@ -21,7 +21,8 @@ module QuoVadis
21
21
  PASSWORD_RESET = 'password.reset'
22
22
  ACCOUNT_CONFIRMATION = 'account.confirmation'
23
23
  LOGOUT_OTHER = 'logout.other'
24
- LOGOUT = 'logout'
24
+ LOGOUT = 'logout.self'
25
+ REVOKE = 'revoke'
25
26
 
26
27
  ACTIONS = [
27
28
  LOGIN_SUCCESS,
@@ -41,7 +42,8 @@ module QuoVadis
41
42
  PASSWORD_RESET,
42
43
  ACCOUNT_CONFIRMATION,
43
44
  LOGOUT_OTHER,
44
- LOGOUT
45
+ LOGOUT,
46
+ REVOKE
45
47
  ]
46
48
 
47
49
  belongs_to :account, optional: true # optional only for LOGIN_UNKNOWN
@@ -7,6 +7,7 @@ module QuoVadis
7
7
  has_secure_password
8
8
 
9
9
  validates_length_of :password, minimum: QuoVadis.password_minimum_length, allow_blank: true
10
+ validate :password_updated_legitimately, on: :update
10
11
 
11
12
  attr_accessor :new_password
12
13
 
@@ -48,5 +49,20 @@ module QuoVadis
48
49
  save
49
50
  end
50
51
 
52
+ private
53
+
54
+ def password_updated_legitimately
55
+ return unless password_digest_changed?
56
+
57
+ unless change_or_reset_called?
58
+ errors.add :password, 'must be updated via #change or #reset'
59
+ end
60
+ end
61
+
62
+ def change_or_reset_called?
63
+ caller_locations.any? { |loc|
64
+ ['change', 'reset'].include?(loc.label) && Pathname.new(loc.path).basename.to_s == 'password.rb'
65
+ }
66
+ end
51
67
  end
52
68
  end
@@ -0,0 +1,14 @@
1
+ <h1>Account confirmation: change email</h1>
2
+
3
+ <p>Please update your email address.</p>
4
+
5
+ <%= form_with url: update_email_confirmations_path, method: :put do |f| %>
6
+ <p>
7
+ <%= f.label :email %>
8
+ <%= f.text_field :email, value: @email, inputmode: 'email', autocomplete: 'email' %>
9
+ </p>
10
+
11
+ <p>
12
+ <%= f.submit 'Update my email address and send me a new confirmation email' %>
13
+ </p>
14
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <h1>Account confirmation</h1>
2
+
3
+ <% if @account %>
4
+ <p>We have sent an email to <%= @account.model.email %>.</p>
5
+
6
+ <p>Wrong address? <%= link_to 'Change it', edit_email_confirmations_path %>.</p>
7
+
8
+ <p>Didn't receive it? <%= button_to 'Get another one', resend_confirmations_path %></p>
9
+
10
+ <% else %>
11
+ <p>We have sent an email to you.</p>
12
+
13
+ <p><%= link_to 'Request a new email', new_confirmation_path %></p>
14
+ <% end %>
@@ -3,6 +3,7 @@
3
3
  <table>
4
4
  <thead>
5
5
  <tr>
6
+ <th>Timestamp</th>
6
7
  <th>Action</th>
7
8
  <th>IP</th>
8
9
  <th>Metadata</th>
@@ -11,7 +12,8 @@
11
12
  <tbody>
12
13
  <% @logs.each do |log| %>
13
14
  <tr>
14
- <td><%= log.action %></td>
15
+ <td><%= log.created_at %></td>
16
+ <td><%= QuoVadis.translate "log.action.#{log.action}" %></td>
15
17
  <td><%= log.ip %></td>
16
18
  <td><%= log.metadata.empty? ? '' : log.metadata.map {|k,v| "#{k}: #{v}"}.join(', ') %></td>
17
19
  </tr>
@@ -1,14 +1,6 @@
1
1
  <h1>Reset password</h1>
2
2
 
3
- <% if @password.errors.any? %>
4
- <ul>
5
- <% @password.errors.full_messages.each do |msg| %>
6
- <li><%= msg %></li>
7
- <% end %>
8
- </ul>
9
- <% end %>
10
-
11
- <%= form_with url: password_reset_path(params[:token]), method: :put do |f| %>
3
+ <%= form_with model: @password, url: password_reset_path(params[:token]), method: :put do |f| %>
12
4
  <p>
13
5
  <%= f.label :password %>
14
6
  <%= f.password_field :password, autocomplete: 'new-password' %>