action_auth 1.8.9 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -0
- data/README.md +485 -36
- data/Rakefile +0 -2
- data/app/assets/javascripts/action_auth/application.js +72 -71
- data/app/assets/stylesheets/action_auth/application.css +38 -1
- data/app/controllers/action_auth/application_controller.rb +122 -1
- data/app/controllers/action_auth/connected_accounts_controller.rb +16 -0
- data/app/controllers/action_auth/identity/email_verifications_controller.rb +36 -5
- data/app/controllers/action_auth/identity/emails_controller.rb +16 -4
- data/app/controllers/action_auth/identity/password_resets_controller.rb +20 -22
- data/app/controllers/action_auth/magics/requests_controller.rb +27 -6
- data/app/controllers/action_auth/magics/sign_ins_controller.rb +22 -7
- data/app/controllers/action_auth/passwords_controller.rb +7 -18
- data/app/controllers/action_auth/provider_callbacks_controller.rb +147 -0
- data/app/controllers/action_auth/registrations_controller.rb +7 -25
- data/app/controllers/action_auth/sessions/passkeys_controller.rb +17 -9
- data/app/controllers/action_auth/sessions_controller.rb +27 -25
- data/app/controllers/action_auth/sms_auths/requests_controller.rb +40 -14
- data/app/controllers/action_auth/sms_auths/sign_ins_controller.rb +40 -15
- data/app/controllers/action_auth/users_controller.rb +35 -1
- data/app/controllers/action_auth/webauthn_credential_authentications_controller.rb +36 -51
- data/app/controllers/action_auth/webauthn_credentials_controller.rb +63 -51
- data/app/helpers/action_auth/application_helper.rb +83 -0
- data/app/mailers/action_auth/application_mailer.rb +1 -1
- data/app/mailers/action_auth/user_mailer.rb +7 -0
- data/app/models/action_auth/connected_account.rb +18 -0
- data/app/models/action_auth/session.rb +9 -1
- data/app/models/action_auth/user.rb +197 -4
- data/app/models/action_auth/webauthn_credential.rb +2 -0
- data/app/views/action_auth/identity/email_verifications/show.html.erb +23 -0
- data/app/views/action_auth/identity/emails/edit.html.erb +41 -42
- data/app/views/action_auth/identity/password_resets/edit.html.erb +10 -11
- data/app/views/action_auth/identity/password_resets/new.html.erb +4 -9
- data/app/views/action_auth/magics/requests/new.html.erb +4 -14
- data/app/views/action_auth/magics/sign_ins/show.html.erb +18 -0
- data/app/views/action_auth/passwords/edit.html.erb +9 -21
- data/app/views/action_auth/registrations/new.html.erb +5 -27
- data/app/views/action_auth/sessions/_account.html.erb +30 -0
- data/app/views/action_auth/sessions/_connected_accounts.html.erb +32 -0
- data/app/views/action_auth/sessions/_security_keys.html.erb +32 -0
- data/app/views/action_auth/sessions/_sessions.html.erb +28 -0
- data/app/views/action_auth/sessions/index.html.erb +10 -62
- data/app/views/action_auth/sessions/new.html.erb +3 -21
- data/app/views/action_auth/sessions/passkeys/new.html.erb +10 -4
- data/app/views/action_auth/shared/_errors.html.erb +10 -0
- data/app/views/action_auth/shared/_flash.html.erb +11 -0
- data/app/views/action_auth/shared/_links.html.erb +3 -0
- data/app/views/action_auth/shared/_oauth_buttons.html.erb +5 -0
- data/app/views/action_auth/shared/_webauthn_error.html.erb +1 -0
- data/app/views/action_auth/sms_auths/requests/new.html.erb +4 -14
- data/app/views/action_auth/sms_auths/sign_ins/show.html.erb +6 -2
- data/app/views/action_auth/user_mailer/account_locked.html.erb +17 -0
- data/app/views/action_auth/user_mailer/account_locked.text.erb +12 -0
- data/app/views/action_auth/user_mailer/email_verification.html.erb +5 -0
- data/app/views/action_auth/user_mailer/email_verification.text.erb +3 -0
- data/app/views/action_auth/user_mailer/magic_link.html.erb +4 -0
- data/app/views/action_auth/user_mailer/magic_link.text.erb +5 -0
- data/app/views/action_auth/user_mailer/password_reset.text.erb +1 -1
- data/app/views/action_auth/webauthn_credential_authentications/new.html.erb +9 -3
- data/app/views/action_auth/webauthn_credentials/new.html.erb +10 -3
- data/app/views/layouts/action_auth/application.html.erb +2 -7
- data/config/routes.rb +13 -3
- data/db/migrate/20261105120000_add_magic_link_used_at_to_users.rb +5 -0
- data/db/migrate/20261105130000_add_account_lockout_and_suspension_to_users.rb +7 -0
- data/db/migrate/20261105130100_create_action_auth_connected_accounts.rb +19 -0
- data/lib/action_auth/configuration.rb +96 -0
- data/lib/action_auth/controllers/helpers.rb +44 -11
- data/lib/action_auth/engine.rb +27 -1
- data/lib/action_auth/routing/helpers.rb +8 -0
- data/lib/action_auth/version.rb +1 -1
- data/lib/generators/action_auth/views/USAGE +8 -0
- data/lib/generators/action_auth/views/views_generator.rb +59 -0
- data/lib/tasks/action_auth_tasks.rake +19 -1
- 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.
|
|
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. [
|
|
14
|
-
4. [
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
+
✅ - Account Lockout
|
|
158
261
|
|
|
159
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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.
|
|
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
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
Unhappy with the service
|
|
345
|
-
<%=
|
|
346
|
-
|
|
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
|
|