add_auth 0.2.1
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 +7 -0
- data/CHANGELOG.md +125 -0
- data/CODE_OF_CONDUCT.md +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +545 -0
- data/ROADMAP.md +244 -0
- data/SECURITY.md +75 -0
- data/app/controllers/add_auth/assets_controller.rb +54 -0
- data/app/controllers/add_auth/passkeys_controller.rb +144 -0
- data/app/controllers/add_auth/reauthentications_controller.rb +81 -0
- data/app/controllers/add_auth/recoveries_controller.rb +50 -0
- data/app/controllers/add_auth/sessions_controller.rb +62 -0
- data/app/controllers/add_auth/sign_ins_controller.rb +65 -0
- data/app/helpers/add_auth/sign_ins_helper.rb +42 -0
- data/app/javascript/controllers/.keep +0 -0
- data/app/jobs/add_auth/delivery_job.rb +31 -0
- data/app/jobs/add_auth/email_delivery_job.rb +15 -0
- data/app/jobs/add_auth/email_request_job.rb +21 -0
- data/app/jobs/add_auth/security_notification_job.rb +13 -0
- data/app/mailers/add_auth/security_mailer.rb +16 -0
- data/app/mailers/add_auth/sign_in_mailer.rb +36 -0
- data/app/models/concerns/.keep +0 -0
- data/app/views/.keep +0 -0
- data/app/views/add_auth/passkeys/_controls.html.erb +17 -0
- data/app/views/add_auth/passkeys/_list.html.erb +32 -0
- data/app/views/add_auth/reauthentications/_check_email.html.erb +3 -0
- data/app/views/add_auth/reauthentications/_confirmation.html.erb +7 -0
- data/app/views/add_auth/reauthentications/_different_browser.html.erb +3 -0
- data/app/views/add_auth/reauthentications/_form.html.erb +27 -0
- data/app/views/add_auth/reauthentications/_invalid_link.html.erb +3 -0
- data/app/views/add_auth/recoveries/_check_email.html.erb +3 -0
- data/app/views/add_auth/recoveries/_confirmation.html.erb +9 -0
- data/app/views/add_auth/recoveries/_form.html.erb +11 -0
- data/app/views/add_auth/recoveries/_invalid_link.html.erb +3 -0
- data/app/views/add_auth/security_mailer/notice.text.erb +3 -0
- data/app/views/add_auth/sessions/_list.html.erb +25 -0
- data/app/views/add_auth/sessions/_revoke_all.html.erb +18 -0
- data/app/views/add_auth/sessions/index.html.erb +12 -0
- data/app/views/add_auth/sign_in_mailer/link.text.erb +5 -0
- data/app/views/add_auth/sign_ins/_check_email.html.erb +5 -0
- data/app/views/add_auth/sign_ins/_confirmation.html.erb +9 -0
- data/app/views/add_auth/sign_ins/_different_browser.html.erb +3 -0
- data/app/views/add_auth/sign_ins/_form.html.erb +34 -0
- data/app/views/add_auth/sign_ins/_invalid_link.html.erb +3 -0
- data/app/views/add_auth/sign_ins/show.html.erb +1 -0
- data/app/views/layouts/add_auth/authentication.html.erb +19 -0
- data/lib/add_auth/configuration.rb +61 -0
- data/lib/add_auth/core/access_policy.rb +55 -0
- data/lib/add_auth/core/browser_binding.rb +28 -0
- data/lib/add_auth/core/challenge/base.rb +100 -0
- data/lib/add_auth/core/challenge/http.rb +130 -0
- data/lib/add_auth/core/challenge/null.rb +21 -0
- data/lib/add_auth/core/challenge/recaptcha.rb +76 -0
- data/lib/add_auth/core/challenge/test.rb +32 -0
- data/lib/add_auth/core/challenge/turnstile.rb +46 -0
- data/lib/add_auth/core/delivery.rb +55 -0
- data/lib/add_auth/core/digest/base.rb +34 -0
- data/lib/add_auth/core/digest/hmac.rb +41 -0
- data/lib/add_auth/core/intake.rb +59 -0
- data/lib/add_auth/core/maintenance.rb +45 -0
- data/lib/add_auth/core/rate_limit.rb +30 -0
- data/lib/add_auth/core/security_events.rb +48 -0
- data/lib/add_auth/core/sessions.rb +287 -0
- data/lib/add_auth/core/step_up.rb +129 -0
- data/lib/add_auth/core/strategies/email_link.rb +201 -0
- data/lib/add_auth/core/strategies/passkey.rb +286 -0
- data/lib/add_auth/rails/authentication.rb +67 -0
- data/lib/add_auth/rails/authentication_pages.rb +66 -0
- data/lib/add_auth/rails/delivery_cipher.rb +33 -0
- data/lib/add_auth/rails/doctor.rb +154 -0
- data/lib/add_auth/rails/ejection.rb +138 -0
- data/lib/add_auth/rails/elevation.rb +35 -0
- data/lib/add_auth/rails/engine.rb +34 -0
- data/lib/add_auth/rails/password_entry.rb +34 -0
- data/lib/add_auth/rails/runtime.rb +216 -0
- data/lib/add_auth/rails/stores/account_lock.rb +40 -0
- data/lib/add_auth/rails/stores/delivery_state.rb +21 -0
- data/lib/add_auth/rails/stores/email_tokens.rb +92 -0
- data/lib/add_auth/rails/stores/maintenance.rb +46 -0
- data/lib/add_auth/rails/stores/passkeys.rb +63 -0
- data/lib/add_auth/rails/stores/security_events.rb +40 -0
- data/lib/add_auth/rails/stores/sessions.rb +62 -0
- data/lib/add_auth/rails/user_lifecycle.rb +35 -0
- data/lib/add_auth/result.rb +55 -0
- data/lib/add_auth/testing.rb +30 -0
- data/lib/add_auth/version.rb +5 -0
- data/lib/add_auth.rb +52 -0
- data/lib/generators/add_auth/challenge/challenge_generator.rb +48 -0
- data/lib/generators/add_auth/challenge/templates/recaptcha.rb.tt +28 -0
- data/lib/generators/add_auth/challenge/templates/turnstile.rb.tt +24 -0
- data/lib/generators/add_auth/controllers/controllers_generator.rb +14 -0
- data/lib/generators/add_auth/ejection.rb +18 -0
- data/lib/generators/add_auth/email_link/email_link_generator.rb +56 -0
- data/lib/generators/add_auth/email_link/templates/add_add_auth_email_binding.rb.tt +5 -0
- data/lib/generators/add_auth/email_link/templates/add_add_auth_email_delivery.rb.tt +10 -0
- data/lib/generators/add_auth/email_link/templates/add_auth.css +28 -0
- data/lib/generators/add_auth/email_link/templates/add_auth_challenge.js +129 -0
- data/lib/generators/add_auth/email_tokens/email_tokens_generator.rb +35 -0
- data/lib/generators/add_auth/email_tokens/templates/add_auth_sign_in_token.rb +4 -0
- data/lib/generators/add_auth/email_tokens/templates/create_add_auth_sign_in_tokens.rb.tt +18 -0
- data/lib/generators/add_auth/install/install_generator.rb +27 -0
- data/lib/generators/add_auth/install/templates/initializer.rb +46 -0
- data/lib/generators/add_auth/javascript/javascript_generator.rb +14 -0
- data/lib/generators/add_auth/javascript/templates/application.js +2 -0
- data/lib/generators/add_auth/javascript/templates/codec.js +28 -0
- data/lib/generators/add_auth/javascript/templates/passkey.js +81 -0
- data/lib/generators/add_auth/mailer_views/mailer_views_generator.rb +14 -0
- data/lib/generators/add_auth/notifications/notifications_generator.rb +32 -0
- data/lib/generators/add_auth/notifications/templates/add_auth_security_event.rb +4 -0
- data/lib/generators/add_auth/notifications/templates/create_add_auth_security_events.rb.tt +18 -0
- data/lib/generators/add_auth/passkeys/passkeys_generator.rb +75 -0
- data/lib/generators/add_auth/passkeys/templates/add_add_auth_passkeys.rb.tt +45 -0
- data/lib/generators/add_auth/passkeys/templates/add_auth_ceremony.rb +4 -0
- data/lib/generators/add_auth/passkeys/templates/add_auth_credential.rb +4 -0
- data/lib/generators/add_auth/session_upgrade/session_upgrade_generator.rb +79 -0
- data/lib/generators/add_auth/session_upgrade/templates/add_add_auth_elevation.rb.tt +12 -0
- data/lib/generators/add_auth/session_upgrade/templates/extend_sessions_for_add_auth.rb.tt +14 -0
- data/lib/generators/add_auth/step_up/step_up_generator.rb +44 -0
- data/lib/generators/add_auth/step_up/templates/add_add_auth_reauthentication.rb.tt +9 -0
- data/lib/generators/add_auth/views/views_generator.rb +16 -0
- data/lib/tasks/add_auth.rake +38 -0
- metadata +361 -0
data/README.md
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# AddAuth
|
|
2
|
+
|
|
3
|
+
**0.2.1:** password, email-link and passkey sign-in extend
|
|
4
|
+
Rails' generated authentication. AddAuth adds hardened sessions, verification
|
|
5
|
+
for sensitive actions, passkey management and email recovery with an optional
|
|
6
|
+
strict policy. Turnstile and reCAPTCHA integrations are available. See
|
|
7
|
+
[features](https://addauthgem.com/features/) for the capabilities your app can enable.
|
|
8
|
+
|
|
9
|
+
AddAuth builds on top of Rails 8's built-in login system instead of
|
|
10
|
+
replacing it -- it keeps using your existing `User` and `Session` models.
|
|
11
|
+
|
|
12
|
+
Read the [documentation](https://addauthgem.com) for setup guides, configuration
|
|
13
|
+
reference and troubleshooting.
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
Install the `add_auth` gem from RubyGems through your Rails app’s Gemfile,
|
|
18
|
+
then enable password and email-link sign-in. Use Ruby 3.3+ and Rails 8.0+
|
|
19
|
+
with Active Record, and run the commands below from your Rails app’s root.
|
|
20
|
+
|
|
21
|
+
**Release availability:** these commands require the published `0.2.1` package.
|
|
22
|
+
If it is not yet listed on [RubyGems](https://rubygems.org/gems/add_auth/versions),
|
|
23
|
+
wait for publication; see [release status](https://addauthgem.com/release-status/).
|
|
24
|
+
Start in your app’s development environment; use the deployment settings
|
|
25
|
+
below before enabling sign-in for users.
|
|
26
|
+
|
|
27
|
+
1. Keep `source "https://rubygems.org"` in your app’s Gemfile and add the
|
|
28
|
+
0.2 release line:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Gemfile
|
|
32
|
+
gem "add_auth", "~> 0.2.1"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Bundler downloads the package from RubyGems and records the resolved
|
|
36
|
+
version in `Gemfile.lock`.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
bundle install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. If your app doesn't already have Rails' built-in login system, add it
|
|
43
|
+
first:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
bin/rails generate authentication
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
3. Add AddAuth:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
bin/rails generate add_auth:install
|
|
53
|
+
bin/rails generate add_auth:email_link
|
|
54
|
+
bin/rails db:migrate
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This adds password sign-in *and* "email me a sign-in link" as ways to log
|
|
58
|
+
in, plus a page where a signed-in user can see their active sessions and
|
|
59
|
+
sign out of one remotely.
|
|
60
|
+
|
|
61
|
+
4. Set these values in `config/initializers/add_auth.rb`:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
AddAuth.configure do |config|
|
|
65
|
+
config.base_url = "http://localhost:3000"
|
|
66
|
+
config.mail_from = "AddAuth <sign-in@example.test>"
|
|
67
|
+
config.rate_limit_store = ActiveSupport::Cache::MemoryStore.new
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For a local trial, add this to `config/environments/development.rb`:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
config.active_job.queue_adapter = :inline
|
|
75
|
+
config.action_mailer.delivery_method = :file
|
|
76
|
+
config.action_mailer.file_settings = {location: Rails.root.join("tmp/mail")}
|
|
77
|
+
config.action_mailer.perform_deliveries = true
|
|
78
|
+
config.action_mailer.raise_delivery_errors = true
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
5. Create a trial account in `bin/rails console` using your own test address and
|
|
82
|
+
password, then start `bin/rails server` and visit `/sign-in`. Request a link
|
|
83
|
+
for that account and open the message written under `tmp/mail`. Treat these
|
|
84
|
+
local messages as credentials and delete them after testing. Raw sign-in
|
|
85
|
+
links are deliberately excluded from application logs.
|
|
86
|
+
|
|
87
|
+
These memory/inline/file adapters are for a local trial. Use the shared cache,
|
|
88
|
+
durable queue and real mail transport described below for deployment.
|
|
89
|
+
|
|
90
|
+
## Set it up for real use
|
|
91
|
+
|
|
92
|
+
`add_auth:install` just writes the configuration file and runs a quick health
|
|
93
|
+
check -- it doesn't turn anything on by itself. `add_auth:email_link` is the
|
|
94
|
+
one that does the real work: it also sets up hardened sessions, adds the
|
|
95
|
+
sign-in routes, and creates the database tables sign-in links are stored in.
|
|
96
|
+
If you only want hardened sessions and don't need email sign-in yet, run
|
|
97
|
+
`bin/rails generate add_auth:session_upgrade` instead. It's safe to run these
|
|
98
|
+
generators again later -- they won't overwrite changes you've already made.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
AddAuth.configure do |config|
|
|
102
|
+
config.base_url = "https://accounts.example.com"
|
|
103
|
+
config.mail_from = "Accounts <sign-in@example.com>"
|
|
104
|
+
config.rate_limit_store = Rails.cache
|
|
105
|
+
# Only let certain accounts sign in, e.g. skip unconfirmed or banned users:
|
|
106
|
+
# config.eligible = ->(user) { user.confirmed? && !user.disabled? }
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Before real users touch this, make sure of three things:
|
|
111
|
+
|
|
112
|
+
- **Email actually sends.** Configure Action Mailer's SMTP settings for
|
|
113
|
+
whatever provider you use.
|
|
114
|
+
- **Background jobs survive a restart.** Sign-in emails are sent as a
|
|
115
|
+
background job, so use a real Active Job backend like Sidekiq or Solid
|
|
116
|
+
Queue -- not Rails' default in-memory one, which forgets everything on
|
|
117
|
+
deploy.
|
|
118
|
+
- **Your cache is shared across servers**, e.g. Redis, Memcached or Solid
|
|
119
|
+
Cache -- not each server's own memory. Otherwise sign-in rate limits only
|
|
120
|
+
apply per-server instead of across your whole app.
|
|
121
|
+
|
|
122
|
+
Then schedule this to run at least once a minute, however you run scheduled
|
|
123
|
+
jobs (cron, `whenever`, your platform's scheduler):
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
bin/rails add_auth:deliver_pending
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
It retries pending sign-in emails and erases expired delivery ciphertext. Keep an eye on failed background jobs -- a stuck one
|
|
130
|
+
means an email that never went out.
|
|
131
|
+
|
|
132
|
+
If a mail callback or interceptor intentionally cancels a message, AddAuth
|
|
133
|
+
cancels that link too and emits `delivery_cancelled.add_auth` with its issuance
|
|
134
|
+
ID. The sweep will not resend cancelled mail. Transport failures remain retryable;
|
|
135
|
+
keep delivery errors enabled so failures can be detected.
|
|
136
|
+
|
|
137
|
+
Run `bin/rails add_auth:doctor` any time after changing configuration. It
|
|
138
|
+
double-checks your migrations, your `base_url`, your mail and background-job
|
|
139
|
+
setup, and your CAPTCHA setup (if you turned one on), and tells you exactly
|
|
140
|
+
what's missing. It also checks enabled passkey, reauthentication, recovery,
|
|
141
|
+
notification and ejection wiring. Verify your host integrations using the
|
|
142
|
+
[deployment checklist](https://addauthgem.com/production/).
|
|
143
|
+
|
|
144
|
+
Visit `/sign-in` to sign in with a password, or to request an email link
|
|
145
|
+
instead when email is enabled. Clicking the emailed link opens a confirmation page -- you still
|
|
146
|
+
have to click a button there to actually sign in. That extra click matters:
|
|
147
|
+
it stops email scanners and link-preview bots from signing you in just by
|
|
148
|
+
opening your inbox. The original `/session/new` and every route to `SessionsController#create`
|
|
149
|
+
use the same protected sign-in flow. The host controller file is preserved, but
|
|
150
|
+
its `new`/`create` actions are handled by AddAuth once session adoption is enabled.
|
|
151
|
+
Move custom sign-in presentation into AddAuth's ejected views and verify custom
|
|
152
|
+
controller hooks before adoption. Password reset remains owned by Rails. Changing a password or email address automatically signs
|
|
153
|
+
out other sessions and cancels any pending sign-in links, as long as the
|
|
154
|
+
change goes through Rails and not a direct database update.
|
|
155
|
+
Password and address changes cancel existing email links even while email sign-in
|
|
156
|
+
is temporarily disabled.
|
|
157
|
+
For email/passkey-only sign-in, set `config.passwords_enabled = false` in the
|
|
158
|
+
initializer and restart. Password verification and password proof are rejected;
|
|
159
|
+
the shared sign-in page hides password controls. Existing `SessionsController`
|
|
160
|
+
aliases stay guarded. A host that removes that controller owns its replacement
|
|
161
|
+
routes. Review Rails' password-reset routes separately: AddAuth does not replace
|
|
162
|
+
account provisioning or password reset. Keep the conventional `User`/`Session`
|
|
163
|
+
models, Rails email normalization and one authentication database connection pool.
|
|
164
|
+
|
|
165
|
+
Signing in again on the same browser retires its previous session; sessions on
|
|
166
|
+
other browsers remain available until they expire or are revoked.
|
|
167
|
+
|
|
168
|
+
Signed-in users can visit `/sessions` to see their active sessions and sign one
|
|
169
|
+
of them out remotely. It shows a rough description of each one (like browser
|
|
170
|
+
and device) and never anything that could be used to impersonate it. The
|
|
171
|
+
“Sign out everywhere” path requires fresh password proof by default. With
|
|
172
|
+
reauthentication enabled it also accepts other methods allowed for that purpose.
|
|
173
|
+
Confirmation revokes every active session, including this browser, and redirects
|
|
174
|
+
to sign-in.
|
|
175
|
+
|
|
176
|
+
If your app already has real logged-in users, deploying this will sign all of
|
|
177
|
+
them out -- unless you set `config.session.legacy_bridge_until` to a cutoff
|
|
178
|
+
date, which gives existing sessions a grace period before they're required to
|
|
179
|
+
sign in again. New sign-ins work fine either way. Once hardened sessions are
|
|
180
|
+
on, don't roll that change back -- it would weaken security for anyone who
|
|
181
|
+
already upgraded. If your app has a heavily customized login setup already,
|
|
182
|
+
or a database other than SQLite or PostgreSQL, test this
|
|
183
|
+
carefully before relying on it in production.
|
|
184
|
+
|
|
185
|
+
Session management uses up to 50 candidates per page, with the current browser
|
|
186
|
+
pinned on the first page and older pages ordered by session creation ID.
|
|
187
|
+
`Core::Sessions#list` returns the first page; hosts building custom lists use
|
|
188
|
+
`list_page(user:, current_session_id:, before:)` and its `entries`/`next_cursor`.
|
|
189
|
+
Cursors do not authorize access to another account.
|
|
190
|
+
|
|
191
|
+
## Rate limits and maintenance
|
|
192
|
+
|
|
193
|
+
Configure these operating limits and scheduled tasks for your app:
|
|
194
|
+
|
|
195
|
+
- Authentication budgets remain five attempts per identifier and 30 per IP,
|
|
196
|
+
per action. Overlapping counters prevent a fresh burst at a five-minute
|
|
197
|
+
boundary; a limit can last up to six minutes. Attempts that are denied also
|
|
198
|
+
count. The shared cache must support atomic increments and retain counters
|
|
199
|
+
for six minutes. Keep server clocks synchronized and monitor cache eviction.
|
|
200
|
+
Upgrade every web worker to apply the new bound; changing the counter format
|
|
201
|
+
resets existing rate budgets once during deployment.
|
|
202
|
+
- Anonymous passkey sign-in options share an additional budget across IPs:
|
|
203
|
+
`config.passkeys.anonymous_limit = 1000`. Set a positive integer appropriate
|
|
204
|
+
to your traffic. Exhaustion returns 429 before creating a ceremony; bound
|
|
205
|
+
reauthentication, registration and completion retain their separate gates.
|
|
206
|
+
This limits creation rate, not total retained rows during a cleanup outage.
|
|
207
|
+
- Schedule `bin/rails add_auth:deliver_pending` every minute with the same
|
|
208
|
+
configuration and shared cache as the web processes. It removes expired
|
|
209
|
+
ceremonies and records completion using the cache's read/write operations.
|
|
210
|
+
In production, `bin/rails add_auth:doctor`
|
|
211
|
+
reports missing cleanup after two minutes without a success. If it reports
|
|
212
|
+
that problem, inspect the scheduler's errors, run the task, then rerun doctor.
|
|
213
|
+
A single manual success does not verify that the recurring schedule works.
|
|
214
|
+
- Invalid passkey RP/origin/support settings return a generic 503 with
|
|
215
|
+
`Retry-After: 60`; doctor identifies the configuration problem. Removing a
|
|
216
|
+
step-up purpose during verification returns the browser safely to `/`, and
|
|
217
|
+
subsequent protected actions still require a currently allowed purpose.
|
|
218
|
+
|
|
219
|
+
Rate limiting complements the independent IP/account budgets described in
|
|
220
|
+
[OWASP's authentication guidance](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#login-throttling).
|
|
221
|
+
Configure Rails' allowed hosts and trusted proxies at deployment; request origin
|
|
222
|
+
checks still require valid CSRF tokens. Live captcha providers and physical or
|
|
223
|
+
hybrid passkey devices retain the acceptance gates in [ROADMAP.md](ROADMAP.md).
|
|
224
|
+
|
|
225
|
+
## Block bots with a CAPTCHA (optional)
|
|
226
|
+
|
|
227
|
+
AddAuth can show a Cloudflare Turnstile or Google reCAPTCHA check before
|
|
228
|
+
someone can sign in or request an email link. This is entirely optional --
|
|
229
|
+
skip it if you don't need it yet.
|
|
230
|
+
|
|
231
|
+
1. Sign up for Turnstile or reCAPTCHA (whichever you'd rather use) and get a
|
|
232
|
+
site key and a secret key from them.
|
|
233
|
+
2. Run one command:
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
bin/rails generate add_auth:challenge turnstile
|
|
237
|
+
# or, for reCAPTCHA:
|
|
238
|
+
bin/rails generate add_auth:challenge recaptcha --version=v2
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
3. Set the two keys as environment variables (in your `.env` file locally, or
|
|
242
|
+
your hosting provider's environment settings in production):
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
TURNSTILE_SITE_KEY=...
|
|
246
|
+
TURNSTILE_SECRET_KEY=...
|
|
247
|
+
TURNSTILE_ALLOWED_HOSTNAMES=accounts.example.com
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
That's it. The sign-in and email-link forms already know how to show the
|
|
251
|
+
widget -- you don't need to change any views. Outside production, missing keys leave the check off with a warning.
|
|
252
|
+
In production, a generated provider initializer requires both keys and an
|
|
253
|
+
explicit hostname allowlist; missing configuration prevents boot. For Google,
|
|
254
|
+
use `RECAPTCHA_SITE_KEY`, `RECAPTCHA_SECRET_KEY` and
|
|
255
|
+
`RECAPTCHA_ALLOWED_HOSTNAMES`. Test with keys matching the selected v2/v3 mode.
|
|
256
|
+
Add `:reauthenticate` to `challenge_on` if revoke-all should also require captcha.
|
|
257
|
+
|
|
258
|
+
One thing worth knowing: if Turnstile or reCAPTCHA itself ever goes down,
|
|
259
|
+
AddAuth's default is to block sign-in rather than let everyone through
|
|
260
|
+
unchecked. If you'd rather let people sign in during that kind of outage than
|
|
261
|
+
lock everyone out, set `config.challenge_when_unavailable = :open` in
|
|
262
|
+
`config/initializers/add_auth.rb`.
|
|
263
|
+
|
|
264
|
+
## Change how long sessions and links last (optional)
|
|
265
|
+
|
|
266
|
+
AddAuth ships with sensible defaults, but you can adjust them:
|
|
267
|
+
|
|
268
|
+
- A signed-in session lasts **12 hours**, or **30 minutes of no activity**,
|
|
269
|
+
whichever comes first.
|
|
270
|
+
- An emailed sign-in link stays valid for **20 minutes**.
|
|
271
|
+
|
|
272
|
+
To change any of these, edit `config/initializers/add_auth.rb`:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
AddAuth.configure do |config|
|
|
276
|
+
config.session.lifetime = 24.hours # how long a session lasts, total
|
|
277
|
+
config.session.idle_timeout = 1.hour # how long before inactivity signs someone out
|
|
278
|
+
config.email_link.token_lifetime = 10.minutes # how long an emailed link stays clickable
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
There's no need to touch these unless your app has specific requirements --
|
|
283
|
+
the defaults follow common security guidance.
|
|
284
|
+
|
|
285
|
+
## Style the login pages
|
|
286
|
+
|
|
287
|
+
The default CSS uses scoped `add_auth-*` classes and `--add_auth-*` properties.
|
|
288
|
+
It has no global reset or framework dependency. The gem serves its CSS and Turbo
|
|
289
|
+
from fixed same-origin routes, including in hosts without an asset pipeline.
|
|
290
|
+
Token pages use a minimal layout without analytics or third-party assets.
|
|
291
|
+
|
|
292
|
+
To use Bootstrap, point to your compiled, same-origin stylesheet and replace
|
|
293
|
+
semantic classes:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
config.stylesheet = "/stylesheets/authentication.css"
|
|
297
|
+
config.css_classes = {
|
|
298
|
+
body: "bg-body-tertiary p-3", panel: "container bg-white p-4",
|
|
299
|
+
field: "mb-3", input: "form-control", button: "btn btn-primary w-100",
|
|
300
|
+
notice: "alert alert-danger", muted: "text-body-secondary", link: "link-primary"
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Tailwind uses the same interface:
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
config.stylesheet = "/stylesheets/authentication.css"
|
|
308
|
+
config.css_classes = {
|
|
309
|
+
body: "bg-slate-50 p-4 text-slate-900",
|
|
310
|
+
panel: "mx-auto mt-8 max-w-md bg-white p-6",
|
|
311
|
+
field: "my-5", input: "block w-full rounded border border-slate-500 p-3",
|
|
312
|
+
button: "w-full rounded bg-emerald-800 p-3 font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
313
|
+
notice: "my-4 border-l-4 p-3", muted: "text-slate-600", link: "text-emerald-800 underline"
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Include the initializer and any ejected views in Tailwind's class detection.
|
|
318
|
+
For Tailwind v4, use `@source` when they fall outside automatic detection; v3
|
|
319
|
+
uses the `content` configuration. See [Tailwind's source detection guide](https://tailwindcss.com/docs/detecting-classes-in-source-files).
|
|
320
|
+
AddAuth does not install either framework. Verify contrast/focus in your theme.
|
|
321
|
+
|
|
322
|
+
Set `config.stylesheet = nil` to render without a stylesheet. To own the markup,
|
|
323
|
+
run `bin/rails generate add_auth:views --only=email_link` or
|
|
324
|
+
`bin/rails generate add_auth:views --only=sessions`; customize the copied
|
|
325
|
+
partials and dedicated layout, keeping form actions, CSRF fields, cache directives
|
|
326
|
+
and secret-free assets. Prefer class overrides when markup can stay shared.
|
|
327
|
+
See the ejection and upgrade instructions below.
|
|
328
|
+
|
|
329
|
+
## Browser-bound email and reauthentication
|
|
330
|
+
|
|
331
|
+
Ordinary email links work across devices by default. After running the current
|
|
332
|
+
`add_auth:email_link` generator and its additive migration, set
|
|
333
|
+
`config.email_link.same_browser = true` to require the requesting browser. A
|
|
334
|
+
wrong-browser attempt does not consume the link. Existing bound links stay bound
|
|
335
|
+
when the option is disabled; enabling it rejects outstanding unbound links.
|
|
336
|
+
Do not roll back to a reader that ignores browser binding while bound links live.
|
|
337
|
+
|
|
338
|
+
Run `bin/rails generate add_auth:step_up` and `bin/rails db:migrate` to install
|
|
339
|
+
password and email reauthentication, including the session/email prerequisites.
|
|
340
|
+
Declare the purposes your host exposes:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
AddAuth.configure do |config|
|
|
344
|
+
config.step_up.purposes = {
|
|
345
|
+
manage_profile: {
|
|
346
|
+
methods: [:password, :email_link],
|
|
347
|
+
label: "update your profile",
|
|
348
|
+
return_to: "/account/security"
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`return_to` is a fixed local GET confirmation page. Successful verification rotates
|
|
355
|
+
the existing session bearer; an email verification is always tied to the initiating
|
|
356
|
+
account, session, browser and purpose and expires after five minutes. Neither path
|
|
357
|
+
replays a submitted mutation. Ordinary sign-in links cannot be used for elevation.
|
|
358
|
+
|
|
359
|
+
In a host controller, `require_elevated_session purpose: :manage_profile,
|
|
360
|
+
only: :update` provides navigation to the verification page. At the actual write,
|
|
361
|
+
use `with_elevated_session(purpose: :manage_profile) { |account| ... }` and check its
|
|
362
|
+
`AddAuth::Result`. Perform your resource ownership, authorization and target/version
|
|
363
|
+
checks inside that database block; do not make network calls while it holds the
|
|
364
|
+
account lock. It must own the transaction and cannot run inside an outer one.
|
|
365
|
+
The authentication proof is reusable for the configured freshness window; the
|
|
366
|
+
host owns single-use business confirmation and idempotency. Reset, revocation,
|
|
367
|
+
expiry and credential changes invalidate the appropriate evidence. An unknown
|
|
368
|
+
purpose or unavailable required method denies access.
|
|
369
|
+
|
|
370
|
+
`bin/rails generate add_auth:views --only=step_up` ejects the shared templates.
|
|
371
|
+
Passkey-only purposes require verified browser user verification. A method label
|
|
372
|
+
or recent password/email timestamp cannot satisfy that requirement.
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
## Passkeys, recovery and security mail
|
|
376
|
+
|
|
377
|
+
Run the feature generator and review its additive migrations before enabling traffic:
|
|
378
|
+
|
|
379
|
+
```sh
|
|
380
|
+
bin/rails generate add_auth:passkeys
|
|
381
|
+
bin/rails db:migrate
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
It installs the session, email, step-up and notification prerequisites. Configure
|
|
385
|
+
stable deployment identity and your host's verified recovery address explicitly:
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
AddAuth.configure do |config|
|
|
389
|
+
config.passkeys.rp_id = "example.com"
|
|
390
|
+
config.passkeys.origins = ["https://accounts.example.com"]
|
|
391
|
+
config.passkeys.name = "Your app"
|
|
392
|
+
config.trusted_recovery_address = ->(user) { user.email_address if user.confirmed? }
|
|
393
|
+
config.support_url = "/support"
|
|
394
|
+
end
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`confirmed?` is a host example; return an address only when the host has verified
|
|
398
|
+
that it belongs to the account. Without this callback, email replacement is
|
|
399
|
+
unavailable. HTTP is accepted only for local development loopback origins. Keep
|
|
400
|
+
RP ID stable across deploys; changing it makes existing credentials unusable.
|
|
401
|
+
Origins must be exact HTTPS origins within that RP ID and cannot be public suffixes.
|
|
402
|
+
The support path must lead to your real, documented recovery process.
|
|
403
|
+
|
|
404
|
+
Visit `/passkeys` after sign-in. Adding or removing credentials requires fresh
|
|
405
|
+
proof for `manage_passkeys`; the page directs users to `/reauthenticate` when
|
|
406
|
+
needed. The native browser prompt supports available device and security-key
|
|
407
|
+
choices. Sign-in supports explicit passkey selection and conditional autofill.
|
|
408
|
+
Registration requires a discoverable credential and user verification; all
|
|
409
|
+
assertions require server-verified user verification too. JavaScript is required.
|
|
410
|
+
A browser without it displays an unavailable state and permitted alternatives.
|
|
411
|
+
|
|
412
|
+
Default recovery starts at `/recover`. A delivered recovery link expires in
|
|
413
|
+
20 minutes and requires explicit confirmation. Its replacement grant uses the configured freshness window (ten minutes by
|
|
414
|
+
default), independently of ordinary sign-in or email reauthentication. Only
|
|
415
|
+
successful replacement revokes other sessions and outstanding proofs, rotates the
|
|
416
|
+
current bearer and sends a security notice. Existing credentials remain listed
|
|
417
|
+
for deliberate removal. The last usable method cannot be removed.
|
|
418
|
+
|
|
419
|
+
Strict policy is per account and must be explicitly acknowledged after fresh
|
|
420
|
+
passkey verification. It disables password, ordinary email and email recovery;
|
|
421
|
+
password resets or feature toggles cannot turn those methods back on. Activating
|
|
422
|
+
or relaxing it revokes other sessions and pending proofs. Strict accounts need a
|
|
423
|
+
remaining passkey or the host's documented support process; AddAuth does not
|
|
424
|
+
supply recovery codes. Keep strict enforcement installed during maintenance and
|
|
425
|
+
rollback.
|
|
426
|
+
|
|
427
|
+
`sign_out_everywhere` also accepts the configured allowed verification methods.
|
|
428
|
+
A successful proof returns to a separate confirmation page; it does not replay
|
|
429
|
+
the sign-out request. Hosts may declare their own purposes using the same API.
|
|
430
|
+
|
|
431
|
+
`add_auth:notifications` can also be installed independently with hardened
|
|
432
|
+
sessions. It sends notices for password/address changes, credential addition or
|
|
433
|
+
removal, policy changes and completed recovery. Address changes notify both old
|
|
434
|
+
and new addresses. Notifications use an encrypted durable outbox and the same
|
|
435
|
+
lease/retry/cancellation machinery as sign-in mail; they contain no secret links.
|
|
436
|
+
Schedule `bin/rails add_auth:deliver_pending` at least every minute for interrupted
|
|
437
|
+
queue handoffs, retries and expired-secret cleanup. Ambiguous transport failures
|
|
438
|
+
can duplicate the same message; they do not create a new authentication proof.
|
|
439
|
+
Keep Action Mailer's delivery errors enabled.
|
|
440
|
+
|
|
441
|
+
All account, session, credential, ceremony, email-proof and notification tables
|
|
442
|
+
must use the same database connection pool. Cross-database authentication writes
|
|
443
|
+
are rejected. SQLite and PostgreSQL have real-store concurrency coverage; other
|
|
444
|
+
adapters need their own contracts before adoption.
|
|
445
|
+
|
|
446
|
+
## Ejection and upgrades
|
|
447
|
+
|
|
448
|
+
```sh
|
|
449
|
+
bin/rails generate add_auth:views
|
|
450
|
+
bin/rails generate add_auth:controllers
|
|
451
|
+
bin/rails generate add_auth:javascript
|
|
452
|
+
bin/rails generate add_auth:mailer_views
|
|
453
|
+
bin/rails add_auth:doctor
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Generators preserve existing files. New copies carry a version/source fingerprint;
|
|
457
|
+
`config/add_auth-ejections.json` retains their pristine upstream baseline. Commit
|
|
458
|
+
that manifest with your host customizations. Doctor identifies customized or
|
|
459
|
+
missing files and prints upstream changes after a gem upgrade. Apply and review
|
|
460
|
+
those changes manually. To accept a reviewed upstream baseline, remove only its
|
|
461
|
+
reviewed file entries from the manifest and rerun the relevant ejection generator;
|
|
462
|
+
existing host files stay intact. Keep unreviewed entries so doctor continues to
|
|
463
|
+
report them. Rerunning a generator never silently advances
|
|
464
|
+
an old baseline or overwrites your code. Keep controller policy calls, browser
|
|
465
|
+
cleanup, CSRF and cache protections intact.
|
|
466
|
+
|
|
467
|
+
`add_auth:views --only=passkeys` includes management and recovery; `--only=step_up`
|
|
468
|
+
includes reauthentication. Ejected JavaScript lives in `app/javascript/add_auth`
|
|
469
|
+
and is served by the fixed asset routes. Mailers resolve host template overrides.
|
|
470
|
+
The same browser suite runs with engine files and with all four surfaces ejected.
|
|
471
|
+
|
|
472
|
+
For host specs, `require "add_auth/testing"` provides framework-neutral
|
|
473
|
+
`AddAuth::Testing.delivered_link(mail, purpose: :sign_in)` (also
|
|
474
|
+
`:reauthentication` and `:recovery`) and
|
|
475
|
+
`AddAuth::Testing.with_virtual_authenticator(selenium_driver) { |authenticator| ... }`.
|
|
476
|
+
The latter removes the virtual authenticator even if the block raises; install
|
|
477
|
+
Selenium in the host test bundle. AddAuth itself uses RSpec.
|
|
478
|
+
|
|
479
|
+
## Operations and rollback
|
|
480
|
+
|
|
481
|
+
Run doctor after migrations and template upgrades. Expand schemas before enabling
|
|
482
|
+
features; generators preserve existing sessions and credentials. Keep the new
|
|
483
|
+
reader during rollback while browser-bound proofs or strict accounts exist.
|
|
484
|
+
Rolling back to code that ignores their policy can restore forbidden access.
|
|
485
|
+
Do not drop credential/policy columns to disable a feature.
|
|
486
|
+
|
|
487
|
+
Monitor `passkey_failure.add_auth` (reason only),
|
|
488
|
+
`notification_enqueue_failed.add_auth` (event ID), delivery failures/cancellations
|
|
489
|
+
and durable pending-outbox age. Filter credentials, transactions, token URLs and
|
|
490
|
+
mail bodies in proxy/APM logs as well as Rails; application filtering cannot
|
|
491
|
+
configure upstream infrastructure. Investigate counter-regression events as
|
|
492
|
+
possible cloned or reset authenticators without treating backup flags as proof
|
|
493
|
+
of safety. For an incident, block affected accounts through the host eligibility
|
|
494
|
+
policy, revoke sessions/proofs through an authorized account workflow, and retain
|
|
495
|
+
strict enforcement. Rotating digest or encryption keys invalidates associated
|
|
496
|
+
sessions/proofs or undelivered ciphertext; coordinate this with users and queues.
|
|
497
|
+
Validate SMTP, the durable queue, shared cache, TLS/proxy trust, retention and
|
|
498
|
+
support recovery in the actual deployment before serving users.
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
Disabling `config.email_link.enabled` rejects new email requests and hides the
|
|
502
|
+
email form while retaining hardened session reading and revocation. Expired
|
|
503
|
+
outbox ciphertext is scrubbed and expired WebAuthn ceremonies are deleted by
|
|
504
|
+
`deliver_pending`. Each pass handles at most `config.maintenance.batch_size`
|
|
505
|
+
rows per operation and model (default 100; range 1–1000). Configure
|
|
506
|
+
`maintenance.session_retention`, `maintenance.email_retention` and
|
|
507
|
+
`maintenance.notification_retention` as nonnegative seconds or Rails durations.
|
|
508
|
+
They default to `nil`, preserving history until the host chooses its retention
|
|
509
|
+
policy. Session retention starts at expiry or revocation; receipt retention starts
|
|
510
|
+
at expiry. Active delivery leases and live sessions are protected. Monitor
|
|
511
|
+
`maintenance.add_auth` counts and pending age: a successful bounded pass does not
|
|
512
|
+
mean the backlog is empty. Repeated sweeps can enqueue duplicate jobs, handled by
|
|
513
|
+
the existing delivery lease and delivered-state checks. Keep proxy trust configured in
|
|
514
|
+
Rails, test Secure/HttpOnly/SameSite cookies through your TLS terminator, and
|
|
515
|
+
verify atomic cache increments across every app instance. Doctor checks local
|
|
516
|
+
configuration and schema; it cannot prove your mail provider, proxy, cache cluster
|
|
517
|
+
or recovery procedures work in production.
|
|
518
|
+
|
|
519
|
+
## Scope
|
|
520
|
+
|
|
521
|
+
AddAuth 0.2 implements password/email/passkey sign-in, session
|
|
522
|
+
adoption and management, purpose-bound verification, passkey recovery and
|
|
523
|
+
strict account policy, durable security mail, and challenge adapters. Account
|
|
524
|
+
provisioning, address confirmation and password reset remain host responsibilities.
|
|
525
|
+
Release acceptance uses local real-database, generated-host, browser and
|
|
526
|
+
SMTP/queue/cache tests. Hosts verify their own live providers and supported
|
|
527
|
+
physical devices before deployment. Recovery codes and AddAuth-owned password
|
|
528
|
+
policy remain deferred.
|
|
529
|
+
|
|
530
|
+
## Roadmap
|
|
531
|
+
|
|
532
|
+
Track development progress in [ROADMAP.md](ROADMAP.md). Check
|
|
533
|
+
[release status](https://addauthgem.com/release-status/) for package availability.
|
|
534
|
+
|
|
535
|
+
## Contributing
|
|
536
|
+
|
|
537
|
+
Public issues are open for bug reports and feedback; pull requests are currently
|
|
538
|
+
restricted to collaborators. See the
|
|
539
|
+
[contributor guide](https://github.com/taimoorq/add_auth/blob/master/CONTRIBUTING.md)
|
|
540
|
+
for checkout setup, source layout and test commands. Report vulnerabilities
|
|
541
|
+
privately through [SECURITY.md](SECURITY.md).
|
|
542
|
+
|
|
543
|
+
## License
|
|
544
|
+
|
|
545
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|