action_auth 1.8.7 → 2.0.0

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/README.md +485 -36
  4. data/Rakefile +0 -2
  5. data/app/assets/javascripts/action_auth/application.js +72 -71
  6. data/app/assets/stylesheets/action_auth/application.css +38 -1
  7. data/app/controllers/action_auth/application_controller.rb +132 -1
  8. data/app/controllers/action_auth/connected_accounts_controller.rb +16 -0
  9. data/app/controllers/action_auth/identity/email_verifications_controller.rb +36 -5
  10. data/app/controllers/action_auth/identity/emails_controller.rb +16 -4
  11. data/app/controllers/action_auth/identity/password_resets_controller.rb +20 -22
  12. data/app/controllers/action_auth/magics/requests_controller.rb +27 -6
  13. data/app/controllers/action_auth/magics/sign_ins_controller.rb +22 -7
  14. data/app/controllers/action_auth/passwords_controller.rb +7 -18
  15. data/app/controllers/action_auth/provider_callbacks_controller.rb +147 -0
  16. data/app/controllers/action_auth/registrations_controller.rb +7 -25
  17. data/app/controllers/action_auth/sessions/passkeys_controller.rb +17 -9
  18. data/app/controllers/action_auth/sessions_controller.rb +27 -25
  19. data/app/controllers/action_auth/sms_auths/requests_controller.rb +40 -14
  20. data/app/controllers/action_auth/sms_auths/sign_ins_controller.rb +40 -15
  21. data/app/controllers/action_auth/users_controller.rb +35 -1
  22. data/app/controllers/action_auth/webauthn_credential_authentications_controller.rb +36 -51
  23. data/app/controllers/action_auth/webauthn_credentials_controller.rb +63 -51
  24. data/app/helpers/action_auth/application_helper.rb +83 -0
  25. data/app/mailers/action_auth/application_mailer.rb +1 -1
  26. data/app/mailers/action_auth/user_mailer.rb +7 -0
  27. data/app/models/action_auth/connected_account.rb +18 -0
  28. data/app/models/action_auth/session.rb +9 -1
  29. data/app/models/action_auth/user.rb +197 -4
  30. data/app/models/action_auth/webauthn_credential.rb +2 -0
  31. data/app/views/action_auth/identity/email_verifications/show.html.erb +23 -0
  32. data/app/views/action_auth/identity/emails/edit.html.erb +41 -42
  33. data/app/views/action_auth/identity/password_resets/edit.html.erb +10 -11
  34. data/app/views/action_auth/identity/password_resets/new.html.erb +4 -9
  35. data/app/views/action_auth/magics/requests/new.html.erb +4 -14
  36. data/app/views/action_auth/magics/sign_ins/show.html.erb +18 -0
  37. data/app/views/action_auth/passwords/edit.html.erb +9 -21
  38. data/app/views/action_auth/registrations/new.html.erb +5 -27
  39. data/app/views/action_auth/sessions/_account.html.erb +30 -0
  40. data/app/views/action_auth/sessions/_connected_accounts.html.erb +32 -0
  41. data/app/views/action_auth/sessions/_security_keys.html.erb +32 -0
  42. data/app/views/action_auth/sessions/_sessions.html.erb +28 -0
  43. data/app/views/action_auth/sessions/index.html.erb +10 -62
  44. data/app/views/action_auth/sessions/new.html.erb +3 -21
  45. data/app/views/action_auth/sessions/passkeys/new.html.erb +10 -4
  46. data/app/views/action_auth/shared/_errors.html.erb +10 -0
  47. data/app/views/action_auth/shared/_flash.html.erb +11 -0
  48. data/app/views/action_auth/shared/_links.html.erb +3 -0
  49. data/app/views/action_auth/shared/_oauth_buttons.html.erb +5 -0
  50. data/app/views/action_auth/shared/_webauthn_error.html.erb +1 -0
  51. data/app/views/action_auth/sms_auths/requests/new.html.erb +4 -14
  52. data/app/views/action_auth/sms_auths/sign_ins/show.html.erb +6 -2
  53. data/app/views/action_auth/user_mailer/account_locked.html.erb +17 -0
  54. data/app/views/action_auth/user_mailer/account_locked.text.erb +12 -0
  55. data/app/views/action_auth/user_mailer/email_verification.html.erb +5 -0
  56. data/app/views/action_auth/user_mailer/email_verification.text.erb +3 -0
  57. data/app/views/action_auth/user_mailer/magic_link.html.erb +4 -0
  58. data/app/views/action_auth/user_mailer/magic_link.text.erb +5 -0
  59. data/app/views/action_auth/user_mailer/password_reset.text.erb +1 -1
  60. data/app/views/action_auth/webauthn_credential_authentications/new.html.erb +9 -3
  61. data/app/views/action_auth/webauthn_credentials/new.html.erb +10 -3
  62. data/app/views/layouts/action_auth/application.html.erb +2 -7
  63. data/config/routes.rb +13 -3
  64. data/db/migrate/20261105120000_add_magic_link_used_at_to_users.rb +5 -0
  65. data/db/migrate/20261105130000_add_account_lockout_and_suspension_to_users.rb +7 -0
  66. data/db/migrate/20261105130100_create_action_auth_connected_accounts.rb +19 -0
  67. data/lib/action_auth/configuration.rb +96 -0
  68. data/lib/action_auth/controllers/helpers.rb +44 -11
  69. data/lib/action_auth/engine.rb +27 -1
  70. data/lib/action_auth/routing/helpers.rb +8 -0
  71. data/lib/action_auth/version.rb +1 -1
  72. data/lib/generators/action_auth/views/USAGE +8 -0
  73. data/lib/generators/action_auth/views/views_generator.rb +59 -0
  74. data/lib/tasks/action_auth_tasks.rake +19 -1
  75. metadata +28 -5
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ActionAuth
2
2
  ActionAuth is an authentication Rails engine crafted to integrate seamlessly
3
- with your Rails application. Optimized for Rails 7.2.0, it employs the most modern authentication
3
+ with your Rails application. It supports Rails 7.2 and later, and employs the most modern authentication
4
4
  techniques and streamlined token reset processes. Its simplicity and ease of use let you concentrate
5
5
  on developing your application, while its reliance on ActiveSupport::CurrentAttributes ensures a
6
6
  user experience akin to that offered by the well-regarded Devise gem.
@@ -10,26 +10,30 @@ user experience akin to that offered by the well-regarded Devise gem.
10
10
  ## Table of Contents
11
11
  1. [Introduction](#introduction)
12
12
  2. [Installation](#installation)
13
- 3. [Features](#features)
14
- 4. [Security Features](#security-features)
13
+ 3. [Upgrading](#upgrading)
14
+ 4. [Features](#features)
15
+ 5. [Security Features](#security-features)
15
16
  - [Password Security](#password-security)
16
17
  - [Session Security](#session-security)
17
18
  - [Rate Limiting](#rate-limiting)
18
19
  - [Multi-Factor Authentication](#multi-factor-authentication)
19
- 5. [Usage](#usage)
20
+ 6. [Usage](#usage)
20
21
  - [Routes](#routes)
21
22
  - [Helper Methods](#helper-methods)
22
23
  - [Restricting and Changing Routes](#restricting-and-changing-routes)
23
- 6. [Have I Been Pwned](#have-i-been-pwned)
24
- 7. [Magic Links](#magic-links)
25
- 8. [SMS Authentication](#sms-authentication)
26
- 9. [Account Deletion](#account-deletion)
27
- 10. [WebAuthn](#webauthn)
28
- 11. [Within Your Application](#within-your-application)
29
- 12. Customizing
30
- - [Sign In Page](https://github.com/kobaltz/action_auth/wiki/Overriding-Sign-In-page-view)
31
- 13. [License](#license)
32
- 14. [Credits](#credits)
24
+ 7. [Have I Been Pwned](#have-i-been-pwned)
25
+ 8. [Email Verification](#email-verification)
26
+ 9. [Magic Links](#magic-links)
27
+ 10. [SMS Authentication](#sms-authentication)
28
+ 11. [OAuth](#oauth)
29
+ 12. [Account Lockout](#account-lockout)
30
+ 13. [Account Suspension](#account-suspension)
31
+ 14. [Account Deletion](#account-deletion)
32
+ 15. [WebAuthn](#webauthn)
33
+ 16. [Within Your Application](#within-your-application)
34
+ 17. [Customizing](#customizing)
35
+ 18. [License](#license)
36
+ 19. [Credits](#credits)
33
37
 
34
38
 
35
39
  ## Minimum Requirements
@@ -80,13 +84,6 @@ In your view layout
80
84
  <% end %>
81
85
  ```
82
86
 
83
- If you're using something like importmaps and plain css, then you may need to add the lines below to your `app/assets/config/manifest.js` file.
84
-
85
- ```javascript
86
- //= link action_auth/application.css
87
- //= link action_auth/application.js
88
- ```
89
-
90
87
  See [WebAuthn](#webauthn) for additional configuration steps if you want to enable WebAuthn.
91
88
  In your `config/initializers/action_auth.rb` file, you can add the following configuration
92
89
  settings.
@@ -107,6 +104,21 @@ ActionAuth.configure do |config|
107
104
  config.webauthn_rp_name = Rails.application.class.to_s.deconstantize
108
105
 
109
106
  config.insert_cookie_domain = false
107
+
108
+ # On by default. See "Account Lockout" and "Account Suspension" below
109
+ config.account_lockout_enabled = true
110
+ config.account_lockout_max_attempts = 10
111
+ config.account_lockout_duration = 30.minutes
112
+ config.account_suspension_enabled = true
113
+
114
+ # Needs the omniauth gems. See "OAuth" below
115
+ config.oauth_provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"], scope: "user:email"
116
+
117
+ # See "Customizing" below
118
+ config.layout = "application" # Use your app's layout
119
+ config.parent_controller = "ApplicationController" # Lets your layout use your app's helpers
120
+ config.after_sign_in_path = "/dashboard" # or -> { main_app.dashboard_path }
121
+ config.after_sign_out_path = "/"
110
122
  end
111
123
 
112
124
  Rails.application.config.after_initialize do
@@ -116,6 +128,97 @@ Rails.application.config.after_initialize do
116
128
  end
117
129
  ```
118
130
 
131
+ ## Upgrading
132
+
133
+ ### Every upgrade
134
+
135
+ Update the gem, then copy and run any new migrations:
136
+
137
+ ```bash
138
+ bundle update action_auth
139
+ bin/rails action_auth:install:migrations
140
+ bin/rails db:migrate
141
+ ```
142
+
143
+ `action_auth:install:migrations` only copies migrations your app doesn't have yet. It's safe to run on every
144
+ upgrade, and prints nothing when there's nothing new.
145
+
146
+ You don't need to run `bin/rails action_auth:install` again, though it's safe to. It leaves your initializer and
147
+ your `mount` line alone.
148
+
149
+ Then check the [changelog](CHANGELOG.md) for anything else that needs a change in your app.
150
+
151
+ ### New settings
152
+
153
+ Upgrading never changes `config/initializers/action_auth.rb`, so that file won't list settings added after you
154
+ installed. Every setting has a default, which means your app keeps working without them. Only add one if you
155
+ want something other than its default:
156
+
157
+ ```ruby
158
+ # config/initializers/action_auth.rb
159
+ ActionAuth.configure do |config|
160
+ config.layout = "application"
161
+ config.session_timeout = 1.week
162
+ end
163
+ ```
164
+
165
+ If your file is still all comments, as `action_auth:install` generates it, also uncomment the
166
+ `ActionAuth.configure do |config|` and `end` lines, or your settings won't take effect.
167
+
168
+ These settings were added in 1.7.0 or later:
169
+
170
+ | Setting | Added in | Default | What it does |
171
+ | --- | --- | --- | --- |
172
+ | `account_lockout_enabled` | After 1.8.9 | `true` | Turns on [Account Lockout](#account-lockout). Does nothing until the new migration has run. |
173
+ | `account_lockout_max_attempts` | After 1.8.9 | `10` | How many failures in a row lock an account. |
174
+ | `account_lockout_duration` | After 1.8.9 | `30.minutes` | How long a lock lasts. `nil` keeps it until it's lifted. |
175
+ | `account_suspension_enabled` | After 1.8.9 | `true` | Turns on [Account Suspension](#account-suspension). Nobody is suspended unless your app says so. |
176
+ | `oauth_provider` | After 1.8.9 | None | Adds a provider for [OAuth](#oauth). Nothing changes until you add one. Set it in the `ActionAuth.configure` block, not in `after_initialize`. |
177
+ | `oauth_enabled` | After 1.8.9 | `defined?(OmniAuth)` | Turns [OAuth](#oauth) off even with providers configured. |
178
+ | `layout` | After 1.8.9 | `nil` (ActionAuth's layout) | The layout for ActionAuth's screens. See [Customizing](#customizing). |
179
+ | `parent_controller` | After 1.8.9 | `"ActionController::Base"` | What ActionAuth's controllers inherit from. Use `"ApplicationController"` if your layout calls your app's helpers. Set it in the `ActionAuth.configure` block, not in `after_initialize`. |
180
+ | `after_sign_in_path` | After 1.8.9 | `nil` (your root path) | Where people go after signing in. A path or a Proc. |
181
+ | `after_sign_out_path` | After 1.8.9 | `nil` (your root path) | Where people go after signing out. A path or a Proc. |
182
+ | `password_complexity_check` | 1.8.0 | `true` | Requires new passwords to mix upper and lower case letters, numbers and special characters. |
183
+ | `session_timeout` | 1.8.0 | `2.weeks` | How long a session can go unused before it expires. `nil` turns this off. |
184
+ | `insert_cookie_domain` | 1.7.1 | `false` | Shares the session cookie across subdomains. |
185
+ | `sms_auth_enabled` | 1.7.0 | `false` | Turns on [SMS Authentication](#sms-authentication). |
186
+ | `sms_send_class` | 1.7.0 | `nil` | The class that sends SMS codes. Set it in an `after_initialize` block. |
187
+
188
+ ### From 1.8.9 or earlier
189
+
190
+ No new settings are required, but these may need something from you:
191
+
192
+ - **Rails 7.2 or later is required.**
193
+ - **There are new migrations**, which add columns to `users` and a new `action_auth_connected_accounts` table. Run
194
+ them as part of your deploy, before the new code takes requests. Until they have run, requesting a magic link
195
+ raises an error that tells you to run them. Everything else keeps working, but account lockout and suspension do
196
+ nothing yet.
197
+ - **Accounts now lock after 10 failed sign ins in a row**, for 30 minutes, and their owner is sent an email. See
198
+ [Account Lockout](#account-lockout) to change this or turn it off. If your tests fail a sign in that many times
199
+ for one user, they'll need `config.account_lockout_enabled = false`.
200
+ - **Verifying an email now asks for the password.** The link opens a page with a password field. If your tests open
201
+ the link and expect the address to be verified, they'll need to `PATCH` the token and password instead. See
202
+ [Email Verification](#email-verification).
203
+ - **The first magic link on an unverified account replaces its password**, and ends its other sessions. See
204
+ [Magic Links](#magic-links) for why. If you run with `verify_email_on_sign_in = false`, expect it to reach most of your
205
+ password users, because nothing else verifies them. The link's "Sign in" page warns them first, and they can use Reset
206
+ Password afterwards.
207
+ - **Your tests may need a setting.** Password complexity is no longer skipped in the test environment. If your
208
+ tests create users with simple passwords, add this to your initializer, or use stronger passwords:
209
+
210
+ ```ruby
211
+ config.password_complexity_check = !Rails.env.test?
212
+ ```
213
+
214
+ - **Your delete account button may need a password field.** A `button_to` copied from an earlier version of this
215
+ README no longer deletes the account. See [Account Deletion](#account-deletion) for a form to use.
216
+ - **Rate limits need a working cache.** They use `config.cache_store`, and `:null_store` turns them off. With
217
+ several server processes, use a shared store such as Solid Cache or Redis.
218
+
219
+ If you've copied views or a layout into your app, or have your own WebAuthn JavaScript, the
220
+ [changelog](CHANGELOG.md#upgrading) lists what to change, along with the other changes in this release.
221
+
119
222
  ## Features
120
223
 
121
224
  These are the planned features for ActionAuth. The ones that are checked off are currently implemented. The ones that are not checked off are planned for future releases.
@@ -138,7 +241,7 @@ These are the planned features for ActionAuth. The ones that are checked off are
138
241
 
139
242
  ✅ - Magic Links
140
243
 
141
- ⏳ - OAuth with Google, Facebook, Github, Twitter, etc.
244
+ ✅ - OAuth with Google, Facebook, Github, Twitter, etc.
142
245
 
143
246
  ✅ - SMS Authentication
144
247
 
@@ -154,9 +257,9 @@ These are the planned features for ActionAuth. The ones that are checked off are
154
257
 
155
258
  ✅ - HTTPS-only cookies in production
156
259
 
157
- ⏳ - Account Lockout
260
+ ✅ - Account Lockout
158
261
 
159
- ⏳ - Account Suspension
262
+ ✅ - Account Suspension
160
263
 
161
264
  ⏳ - Account Impersonation
162
265
 
@@ -184,11 +287,23 @@ ActionAuth comes with a robust set of security features designed to protect user
184
287
  - Rate limiting on registration attempts
185
288
  - Rate limiting on password reset attempts
186
289
  - Rate limiting on WebAuthn authentication
290
+ - Rate limiting on magic link, SMS and password reset requests, on SMS code guesses, and on passwords entered to verify an email
291
+
292
+ Rate limits are stored in `config.cache_store`, so `:null_store` turns them off. If you run several
293
+ server processes, use a store they share, such as Solid Cache or Redis.
294
+
295
+ ### Account Protection
296
+ - [Account lockout](#account-lockout) after repeated failed passwords or SMS codes. It's stored in the database, so it
297
+ holds even without a working cache, and doesn't reveal who has an account.
298
+ - [Account suspension](#account-suspension) blocks every way of signing in and ends existing sessions.
299
+ - [Email verification](#email-verification) takes both the emailed link and the password, so nobody can get a verified
300
+ account on an address they don't own, and that address's owner can always take the account over.
187
301
 
188
302
  ### Multi-Factor Authentication
189
303
  - Support for WebAuthn/passkeys as a second factor
190
304
  - Modern security key and biometric authentication support
191
- - Magic link authentication as an alternative authentication method
305
+ - Magic link authentication as an alternative authentication method. An account with a security key
306
+ still has to use it after a magic link or an SMS code.
192
307
 
193
308
  ### Configuration Options
194
309
  ```ruby
@@ -214,8 +329,8 @@ Within your application, you'll have access to these routes. They have been styl
214
329
  user_session_path DELETE [:id] Log Out
215
330
  new_user_session_path GET Log in
216
331
  new_user_registration_path GET Sign Up
217
- edit_password_path GET Change Password
218
- password_path PATCH Update Password
332
+ edit_user_password_path GET Change Password
333
+ user_password_path PATCH Update Password
219
334
 
220
335
  ### Helper Methods
221
336
 
@@ -242,7 +357,7 @@ can create a constraint to restrict access to these routes.
242
357
  session_token = request.cookie_jar.signed[:session_token]
243
358
  session = ActionAuth::Session.find_by(id: session_token)
244
359
  return nil unless session.present?
245
- session.action_auth_user&.becomes(User)
360
+ session.user&.becomes(User)
246
361
  end
247
362
  end
248
363
 
@@ -279,6 +394,18 @@ Add the `pwned` gem to your Gemfile. That's all you'll have to do to enable this
279
394
  bundle add pwned
280
395
  ```
281
396
 
397
+ ## Email Verification
398
+
399
+ Verifying an address takes both the link in the email and the account's password. The link shows that someone has the
400
+ inbox, and the password shows that they're whoever set up the account. Neither is enough alone, because anyone can sign
401
+ up with someone else's address and know the password, and that address's owner or their mail scanner can open the link
402
+ without having signed up. Opening the link only shows a page that asks for the password.
403
+
404
+ Someone who has the inbox but not the password can take over the account in either of two ways, a password reset or
405
+ a [magic link](#magic-links). Both verify the address, end every session on the account and remove its connected OAuth
406
+ accounts, so that nothing from before can still get in. A password reset on an account that's already verified only
407
+ changes the password and signs out other devices, as it always has.
408
+
282
409
  ## Magic Links
283
410
 
284
411
  Magic Links are a way to authenticate a user without requiring a password. This is done by sending
@@ -286,6 +413,20 @@ an email to the user with a link that will log them in. This is a great way to a
286
413
  without having to remember a password. This is especially useful for users who may not have a password
287
414
  manager or have a hard time remembering passwords.
288
415
 
416
+ A link expires after 20 minutes and only works once. Using one also uses up any others already sent to that
417
+ account. Opening a link shows a page with a "Sign in" button rather than signing in straight away, because
418
+ mail scanners and link previews open the links in an email and would otherwise use them up first.
419
+
420
+ The first time a link is used on an account whose email isn't verified, it also replaces the password, ends every
421
+ session on the account and removes its connected OAuth accounts. Until then nobody has shown that the address is
422
+ theirs. Anyone can sign up with someone else's address, or change their own account to it, and this stops them from
423
+ keeping access once the address's real owner starts using the account. The page with the "Sign in" button says this
424
+ first, and the person can use Reset Password afterwards to choose a new password.
425
+
426
+ Accounts that are already verified aren't affected. Security keys are kept and are still asked for, because removing them
427
+ would let someone holding only the inbox get past a second factor. If someone else's key is on the account, that
428
+ means its owner can't get past that step.
429
+
289
430
  ## SMS Authentication
290
431
 
291
432
  SMS Authentication is disabled by default. The purpose of this is to allow users to authenticate
@@ -330,22 +471,189 @@ Rails.application.config.after_initialize do
330
471
  end
331
472
  ```
332
473
 
474
+ ## OAuth
475
+
476
+ OAuth lets people sign in with an account they have on another site, such as Google, Facebook, GitHub or X. It's built on
477
+ [OmniAuth](https://github.com/omniauth/omniauth), so any OmniAuth strategy works.
478
+
479
+ Like WebAuthn, this is opt in. The gems aren't dependencies of ActionAuth, so apps that don't want this don't carry them,
480
+ and nothing changes until you add a provider. If your app already uses OmniAuth for something else, it's left alone.
481
+
482
+ #### Add the gems
483
+
484
+ ```bash
485
+ bundle add omniauth omniauth-rails_csrf_protection
486
+ ```
487
+
488
+ Then add a gem for each provider you want.
489
+
490
+ | Provider | Gem | Name for `oauth_provider` |
491
+ | --- | --- | --- |
492
+ | Google | `omniauth-google-oauth2` | `:google_oauth2` |
493
+ | GitHub | `omniauth-github` | `:github` |
494
+ | Facebook | `omniauth-facebook` | `:facebook` |
495
+ | X (Twitter) | `omniauth-twitter2` | `:twitter2` |
496
+
497
+ `omniauth-rails_csrf_protection` isn't optional. Without it every sign in fails, because OmniAuth's own CSRF check
498
+ doesn't understand the tokens Rails puts in forms.
499
+
500
+ #### Run the migration
501
+
502
+ ```bash
503
+ bin/rails action_auth:install:migrations
504
+ bin/rails db:migrate
505
+ ```
506
+
507
+ #### Add your providers
508
+
509
+ `oauth_provider` takes the same arguments as OmniAuth's `provider`. Set these in the `ActionAuth.configure` block, not in
510
+ `after_initialize`, because the routes depend on them.
511
+
512
+ ```ruby
513
+ # config/initializers/action_auth.rb
514
+ ActionAuth.configure do |config|
515
+ config.oauth_provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"]
516
+ config.oauth_provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"], scope: "user:email"
517
+ config.oauth_provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"]
518
+ config.oauth_provider :twitter2, ENV["TWITTER_CLIENT_ID"], ENV["TWITTER_CLIENT_SECRET"],
519
+ scope: "tweet.read users.read",
520
+ callback_url: "https://example.com/action_auth/auth/twitter2/callback"
521
+ end
522
+
523
+ # Callback URLs otherwise come from the request's Host header, which can be forged.
524
+ OmniAuth.config.full_host = "https://example.com"
525
+ ```
526
+
527
+ - GitHub needs `scope: "user:email"` to see the address of someone who keeps theirs private.
528
+ - X needs a `scope`, and `callback_url` too, unless ActionAuth is mounted at `/`. `omniauth-twitter2` otherwise adds the
529
+ mount path twice, and X refuses the sign in.
530
+ - The buttons say "Continue with" and the provider's name. To change that name, add `label: "Work Account"`.
531
+
532
+ Each provider needs to know the callback URL, which is your mount path, then `auth`, the name, and `callback`. For example,
533
+ `https://example.com/action_auth/auth/github/callback`.
534
+
535
+ Providers appear on the sign in and sign up screens, and in a new section of the management screen at `user_sessions_path`.
536
+
537
+ #### What happens when someone signs in
538
+
539
+ | Situation | Result |
540
+ | --- | --- |
541
+ | They've connected this account before | They're signed in, whatever email address the provider now reports. A security key is still required if they have one. |
542
+ | They're new here | They get a new account using the provider's email address. It has a random password, which they can reset if they want one. |
543
+ | They're new here, and the provider doesn't share an email address, or says it's unverified | They get a new account with a placeholder address, as with SMS. They can only sign in through the provider. X usually works like this. |
544
+ | Their email address matches an existing account | They **aren't** signed in. They're told to sign in another way, then connect the provider from the management screen. |
545
+ | They're already signed in | The provider is connected to the account they're signed in to. |
546
+ | The provider is connected to someone else | They're told, and nothing changes. |
547
+
548
+ An email address match never signs anyone in, because that would rely on every provider having verified the address, and
549
+ on nobody having signed up here with an address they don't own. For the same reason, only add providers that verify
550
+ email addresses, or that don't share them. New accounts take the address the provider gives.
551
+
552
+ Disconnecting from the management screen works unless it's the only way left to sign in to that account. Only the
553
+ provider's name for the account is stored, not its access tokens. If you need those, for example to call the provider's
554
+ API, that's currently something your app has to do itself.
555
+
556
+ #### Your own views
557
+
558
+ If you've copied `sessions/new` or `registrations/new` into your app, add the buttons where you want them.
559
+
560
+ ```erb
561
+ <%= render "action_auth/shared/oauth_buttons" %>
562
+ ```
563
+
564
+ To style them yourself, use `action_auth_oauth_providers`. These have to be POST requests, and Turbo has to stay out of
565
+ them, because the response redirects to another site.
566
+
567
+ ```erb
568
+ <% action_auth_oauth_providers.each do |label, path| %>
569
+ <%= button_to "Continue with #{label}", path, form: { data: { turbo: false } }, class: "btn" %>
570
+ <% end %>
571
+ ```
572
+
573
+ Similarly, add `render "action_auth/sessions/connected_accounts"` to a copied `sessions/index`.
574
+
575
+ #### Good to know
576
+
577
+ - If your app has a Content Security Policy that sets `form-action`, it needs to allow your providers' sites, or browsers
578
+ will block the redirect after the button is pressed.
579
+ - In development, OmniAuth raises an error when something goes wrong at the provider, such as pressing cancel on its page.
580
+ Elsewhere it goes back to the sign in screen with a message. See `OmniAuth.config.failure_raise_out_environments`.
581
+ - To test your app without a real provider, use [OmniAuth's test mode](https://github.com/omniauth/omniauth/wiki/Integration-Testing).
582
+
583
+ ## Account Lockout
584
+
585
+ Account lockout is on by default. After 10 failed passwords or SMS codes in a row, an account is locked for 30 minutes.
586
+
587
+ ```ruby
588
+ ActionAuth.configure do |config|
589
+ config.account_lockout_enabled = true
590
+ config.account_lockout_max_attempts = 10
591
+ config.account_lockout_duration = 30.minutes # nil keeps it locked until it's lifted
592
+ end
593
+ ```
594
+
595
+ While an account is locked, the sign in form gives the same answer it gives to a wrong password, even if the password is
596
+ right. Saying "locked" only when the password is right would let someone carry on guessing, and saying it to everyone would
597
+ reveal who has an account. So that they aren't left wondering, the account's owner is sent an email when the lock
598
+ starts, with a link to reset their password.
599
+
600
+ A lock ends by itself, or as soon as they reset their password or sign in another way. Signing in also restarts the count.
601
+
602
+ Only things that can be guessed are locked out. A magic link, passkey or connected OAuth account still works, and devices
603
+ that are already signed in stay signed in. Without this, anyone who knew an email address could keep its owner out by
604
+ failing on purpose. They can still lock password sign in that way, which is why locks are temporary. It does mean that an
605
+ account that only has SMS, with no other way in, has to wait.
606
+
607
+ From your app, you can use `user.account_locked?` and `user.unlock_account!`. To change the email, generate the
608
+ `user_mailer` views. If the email can't be sent, the problem is logged, and the sign in form still answers as usual.
609
+
610
+ This adds to [rate limiting](#rate-limiting), which is per client and depends on your cache. Lockout is per account and
611
+ depends on your database.
612
+
613
+ ## Account Suspension
614
+
615
+ Account suspension is on by default, but nothing happens until your app suspends someone. ActionAuth has no admin screen,
616
+ since it doesn't know who your admins are, so call this from your own, behind your own authorization.
617
+
618
+ ```ruby
619
+ user.suspend_account! # Blocks every way of signing in, and signs them out everywhere
620
+ user.account_suspended?
621
+ user.unsuspend_account!
622
+ ```
623
+
624
+ A suspended account can't sign in with a password, magic link, SMS code, passkey, security key or OAuth, and can't reset its
625
+ password. They're only told it's suspended after proving who they are, such as with the right password. Everyone else gets
626
+ the usual answers, so this doesn't reveal who has been suspended, and forms that would send something just don't send it.
627
+
628
+ `suspend_account!` raises an error if it can't take effect, such as before the migration has run or with
629
+ `config.account_suspension_enabled = false`. Setting `account_suspended_at` directly also blocks the account and ends its
630
+ sessions on their next request, but use `suspend_account!`, so that a route constraint that reads sessions itself, like the
631
+ one [above](#restricting-and-changing-routes-with-constraints), sees it straight away.
632
+
333
633
  ## Account Deletion
334
634
 
335
635
  Account deletion is a feature that is enabled by default. When a user deletes their account, the account
336
- is marked as deleted and the user is logged out. The user will no longer be able to log in with their
337
- email and password. The user will need to create a new account if they wish to continue using the application.
636
+ and its sessions and security keys are permanently deleted, and the user is logged out. The user will need
637
+ to create a new account if they wish to continue using the application.
338
638
 
339
- Here's an example of how you may want to add a delete account button to your application. Obviously, you
340
- will want to style this to fit your application and have some kind of confirmation dialog.
639
+ Deleting an account requires the current password. The management screen at `user_sessions_path` includes a
640
+ form for this, so the simplest option is to link there. Here's an example of how you may want to add the form
641
+ somewhere else in your application. Obviously, you will want to style this to fit your application.
341
642
 
342
643
  ```
343
- <p>
344
- Unhappy with the service?
345
- <%= button_to "Delete Account", action_auth.users_path, method: :delete %>
346
- </p>
644
+ <%= form_with url: action_auth.users_path, method: :delete do |form| %>
645
+ <p>Unhappy with the service?</p>
646
+ <%= form.label :password_challenge, "Current password" %>
647
+ <%= form.password_field :password_challenge, required: true, autocomplete: "current-password" %>
648
+ <%= form.submit "Delete Account" %>
649
+ <% end %>
347
650
  ```
348
651
 
652
+ If the password is missing or wrong, the account is kept and the user is sent to the management screen with a
653
+ message. An account that signed in with a magic link or OAuth has a random password, so it has to reset the password
654
+ first. An account created through SMS, or through an OAuth provider that didn't share an email address, has no password or
655
+ email to reset, so instead it can delete itself within 10 minutes of signing in again. It doesn't need the password field.
656
+
349
657
  ## WebAuthn
350
658
 
351
659
  ActionAuth's approach for WebAuthn is simplicity. It is used as a multifactor authentication step,
@@ -354,6 +662,9 @@ they can add a Passkey to their account. The Passkey could be an iCloud Keychain
354
662
  key like a Yubikey, or a mobile device. If enabled and configured, the user will be prompted to use
355
663
  their Passkey after they log in.
356
664
 
665
+ The screens that use WebAuthn load Stimulus and `@github/webauthn-json` from unpkg.com, and no other screen
666
+ loads them. If your app has a Content Security Policy, it needs to allow that host on those screens.
667
+
357
668
  #### Configuration
358
669
 
359
670
  The migrations are already copied over to your application when you run
@@ -441,6 +752,144 @@ end
441
752
  Now, you'll be able to do things like `Current.user` and `Current.user.posts` within your application. However, I recommend that you still use
442
753
  the helpers around `user_signed_in?` to verify that the `ActionAuth::Current.user` is not nil (or nil if they are signed out). This will help ensure that any thread safety issues are avoided.
443
754
 
755
+ ## Customizing
756
+
757
+ There are several ways to make ActionAuth's screens your own. Start at the top, and only go as far as you need.
758
+
759
+ ### Use your app's layout
760
+
761
+ This is one line, and is often all you need.
762
+
763
+ ```ruby
764
+ # config/initializers/action_auth.rb
765
+ ActionAuth.configure do |config|
766
+ config.layout = "application"
767
+ end
768
+ ```
769
+
770
+ Your layout's route helpers, like `root_path`, keep working on these screens. If your app has a route with the
771
+ same name as one of ActionAuth's, such as `session_path`, ActionAuth's wins here, so use `main_app.session_path`
772
+ for yours.
773
+
774
+ If your layout calls your own helpers, also make ActionAuth's controllers inherit from your `ApplicationController`.
775
+
776
+ ```ruby
777
+ ActionAuth.configure do |config|
778
+ config.layout = "application"
779
+ config.parent_controller = "ApplicationController"
780
+ end
781
+ ```
782
+
783
+ This has to be set in the initializer's `ActionAuth.configure` block, not inside `after_initialize`.
784
+
785
+ If your `ApplicationController` has `before_action :authenticate_user!`, there's nothing more to do, because
786
+ ActionAuth skips it on the screens that have to be public. For any other filter that shouldn't run on these screens,
787
+ such as checking for a subscription, use the `action_auth_controller?` predicate.
788
+
789
+ ```ruby
790
+ class ApplicationController < ActionController::Base
791
+ before_action :authenticate_user!
792
+ before_action :require_subscription, unless: :action_auth_controller?
793
+ end
794
+ ```
795
+
796
+ Only a few screens need a different layout? Create one named after the screens, and leave `config.layout` unset. For
797
+ example, `app/views/layouts/action_auth/sessions.html.erb` is used for the sign in page and the management screen.
798
+ To replace the default layout without changing the setting, create `app/views/layouts/action_auth/application.html.erb`.
799
+
800
+ ### Change the views
801
+
802
+ Copy the views into your app, then edit them. Your copies take priority over the ones in the gem.
803
+
804
+ ```bash
805
+ bin/rails generate action_auth:views # Everything
806
+ bin/rails generate action_auth:views sessions registrations # Only these
807
+ bin/rails generate action_auth:views sessions --layout # Also the default layout
808
+ ```
809
+
810
+ Scope Screens
811
+ sessions Sign in, and the management screen for sessions and security keys
812
+ registrations Sign up
813
+ passwords Change password
814
+ identity Change email, verify email and reset password
815
+ magics Request and confirm a magic link
816
+ sms_auths Request and enter an SMS code
817
+ webauthn_credentials Add a security key
818
+ webauthn_credential_authentications Use a security key while signing in
819
+ user_mailer Emails
820
+ shared Flash messages, form errors and links between screens
821
+
822
+ Copied views don't pick up fixes from future versions of ActionAuth, so only keep the ones you've changed. Delete the
823
+ rest and they'll go back to following the gem.
824
+
825
+ Inside a view, your app's routes are available as usual, and ActionAuth's helpers are there too.
826
+
827
+ Helper Description
828
+ action_auth_links [label, path] pairs for the other auth screens, leaving out disabled features and the current page
829
+ action_auth_oauth_providers [label, path] pairs for OAuth providers, which need a button rather than a link. See OAuth
830
+ after_sign_in_path Where signing in goes
831
+ after_sign_out_path Where signing out goes
832
+ current_user, current_session The signed in user and their session
833
+
834
+ For example, to style the links between screens without checking which features are on:
835
+
836
+ ```erb
837
+ <% action_auth_links.each do |label, path| %>
838
+ <%= link_to label, path, class: "underline" %>
839
+ <% end %>
840
+ ```
841
+
842
+ See [examples](examples) for some Tailwind CSS versions of these screens.
843
+
844
+ ### Change one piece
845
+
846
+ If you only want to change part of a screen, override the partial that renders it. Generate the `shared` or `sessions`
847
+ scope and delete the files you don't need, or create the file yourself.
848
+
849
+ Partial in app/views/action_auth Used for
850
+ shared/_flash.html.erb Flash messages
851
+ shared/_errors.html.erb Form errors
852
+ shared/_links.html.erb Links between the sign in, sign up and similar screens
853
+ shared/_oauth_buttons.html.erb OAuth buttons on the sign in and sign up screens
854
+ sessions/_sessions.html.erb List of signed in devices on the management screen
855
+ sessions/_security_keys.html.erb List of security keys on the management screen
856
+ sessions/_connected_accounts.html.erb List of connected OAuth accounts on the management screen
857
+ sessions/_account.html.erb Change email, change password and delete account
858
+
859
+ #### Flash messages
860
+
861
+ Each screen displays its own flash messages, so they appear in any layout. They're removed once displayed, so a layout
862
+ that also displays flash won't show them twice. To use your layout's flash instead, make
863
+ `app/views/action_auth/shared/_flash.html.erb` an empty file.
864
+
865
+ #### WebAuthn screens
866
+
867
+ If you write your own screens for adding or using a security key, keep these parts from the generated views, since the
868
+ JavaScript looks for them.
869
+
870
+ - `<%= action_auth_webauthn_tags %>`, which loads the scripts and URLs they need, in any layout.
871
+ - The `id="webauthn_credential_form"` element and its `data-controller` attributes.
872
+ - `<%= render "action_auth/shared/webauthn_error" %>`, or any element with `id="webauthn_error"`, to show failures.
873
+
874
+ ### Change where people go
875
+
876
+ ```ruby
877
+ ActionAuth.configure do |config|
878
+ config.after_sign_in_path = "/dashboard"
879
+ config.after_sign_out_path = "/"
880
+
881
+ # A Proc runs in the controller, so it can use routes and the request
882
+ config.after_sign_in_path = -> { main_app.dashboard_path }
883
+ end
884
+ ```
885
+
886
+ Both default to your app's root path.
887
+
888
+ ### Change the emails
889
+
890
+ Generate the `user_mailer` scope to change their content. They use your app's `layouts/mailer` layout, and come from
891
+ `config.default_from_email`.
892
+
444
893
  ## License
445
894
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
446
895
 
data/Rakefile CHANGED
@@ -3,6 +3,4 @@ require "bundler/setup"
3
3
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
- load "rails/tasks/statistics.rake"
7
-
8
6
  require "bundler/gem_tasks"