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.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +125 -0
  3. data/CODE_OF_CONDUCT.md +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +545 -0
  6. data/ROADMAP.md +244 -0
  7. data/SECURITY.md +75 -0
  8. data/app/controllers/add_auth/assets_controller.rb +54 -0
  9. data/app/controllers/add_auth/passkeys_controller.rb +144 -0
  10. data/app/controllers/add_auth/reauthentications_controller.rb +81 -0
  11. data/app/controllers/add_auth/recoveries_controller.rb +50 -0
  12. data/app/controllers/add_auth/sessions_controller.rb +62 -0
  13. data/app/controllers/add_auth/sign_ins_controller.rb +65 -0
  14. data/app/helpers/add_auth/sign_ins_helper.rb +42 -0
  15. data/app/javascript/controllers/.keep +0 -0
  16. data/app/jobs/add_auth/delivery_job.rb +31 -0
  17. data/app/jobs/add_auth/email_delivery_job.rb +15 -0
  18. data/app/jobs/add_auth/email_request_job.rb +21 -0
  19. data/app/jobs/add_auth/security_notification_job.rb +13 -0
  20. data/app/mailers/add_auth/security_mailer.rb +16 -0
  21. data/app/mailers/add_auth/sign_in_mailer.rb +36 -0
  22. data/app/models/concerns/.keep +0 -0
  23. data/app/views/.keep +0 -0
  24. data/app/views/add_auth/passkeys/_controls.html.erb +17 -0
  25. data/app/views/add_auth/passkeys/_list.html.erb +32 -0
  26. data/app/views/add_auth/reauthentications/_check_email.html.erb +3 -0
  27. data/app/views/add_auth/reauthentications/_confirmation.html.erb +7 -0
  28. data/app/views/add_auth/reauthentications/_different_browser.html.erb +3 -0
  29. data/app/views/add_auth/reauthentications/_form.html.erb +27 -0
  30. data/app/views/add_auth/reauthentications/_invalid_link.html.erb +3 -0
  31. data/app/views/add_auth/recoveries/_check_email.html.erb +3 -0
  32. data/app/views/add_auth/recoveries/_confirmation.html.erb +9 -0
  33. data/app/views/add_auth/recoveries/_form.html.erb +11 -0
  34. data/app/views/add_auth/recoveries/_invalid_link.html.erb +3 -0
  35. data/app/views/add_auth/security_mailer/notice.text.erb +3 -0
  36. data/app/views/add_auth/sessions/_list.html.erb +25 -0
  37. data/app/views/add_auth/sessions/_revoke_all.html.erb +18 -0
  38. data/app/views/add_auth/sessions/index.html.erb +12 -0
  39. data/app/views/add_auth/sign_in_mailer/link.text.erb +5 -0
  40. data/app/views/add_auth/sign_ins/_check_email.html.erb +5 -0
  41. data/app/views/add_auth/sign_ins/_confirmation.html.erb +9 -0
  42. data/app/views/add_auth/sign_ins/_different_browser.html.erb +3 -0
  43. data/app/views/add_auth/sign_ins/_form.html.erb +34 -0
  44. data/app/views/add_auth/sign_ins/_invalid_link.html.erb +3 -0
  45. data/app/views/add_auth/sign_ins/show.html.erb +1 -0
  46. data/app/views/layouts/add_auth/authentication.html.erb +19 -0
  47. data/lib/add_auth/configuration.rb +61 -0
  48. data/lib/add_auth/core/access_policy.rb +55 -0
  49. data/lib/add_auth/core/browser_binding.rb +28 -0
  50. data/lib/add_auth/core/challenge/base.rb +100 -0
  51. data/lib/add_auth/core/challenge/http.rb +130 -0
  52. data/lib/add_auth/core/challenge/null.rb +21 -0
  53. data/lib/add_auth/core/challenge/recaptcha.rb +76 -0
  54. data/lib/add_auth/core/challenge/test.rb +32 -0
  55. data/lib/add_auth/core/challenge/turnstile.rb +46 -0
  56. data/lib/add_auth/core/delivery.rb +55 -0
  57. data/lib/add_auth/core/digest/base.rb +34 -0
  58. data/lib/add_auth/core/digest/hmac.rb +41 -0
  59. data/lib/add_auth/core/intake.rb +59 -0
  60. data/lib/add_auth/core/maintenance.rb +45 -0
  61. data/lib/add_auth/core/rate_limit.rb +30 -0
  62. data/lib/add_auth/core/security_events.rb +48 -0
  63. data/lib/add_auth/core/sessions.rb +287 -0
  64. data/lib/add_auth/core/step_up.rb +129 -0
  65. data/lib/add_auth/core/strategies/email_link.rb +201 -0
  66. data/lib/add_auth/core/strategies/passkey.rb +286 -0
  67. data/lib/add_auth/rails/authentication.rb +67 -0
  68. data/lib/add_auth/rails/authentication_pages.rb +66 -0
  69. data/lib/add_auth/rails/delivery_cipher.rb +33 -0
  70. data/lib/add_auth/rails/doctor.rb +154 -0
  71. data/lib/add_auth/rails/ejection.rb +138 -0
  72. data/lib/add_auth/rails/elevation.rb +35 -0
  73. data/lib/add_auth/rails/engine.rb +34 -0
  74. data/lib/add_auth/rails/password_entry.rb +34 -0
  75. data/lib/add_auth/rails/runtime.rb +216 -0
  76. data/lib/add_auth/rails/stores/account_lock.rb +40 -0
  77. data/lib/add_auth/rails/stores/delivery_state.rb +21 -0
  78. data/lib/add_auth/rails/stores/email_tokens.rb +92 -0
  79. data/lib/add_auth/rails/stores/maintenance.rb +46 -0
  80. data/lib/add_auth/rails/stores/passkeys.rb +63 -0
  81. data/lib/add_auth/rails/stores/security_events.rb +40 -0
  82. data/lib/add_auth/rails/stores/sessions.rb +62 -0
  83. data/lib/add_auth/rails/user_lifecycle.rb +35 -0
  84. data/lib/add_auth/result.rb +55 -0
  85. data/lib/add_auth/testing.rb +30 -0
  86. data/lib/add_auth/version.rb +5 -0
  87. data/lib/add_auth.rb +52 -0
  88. data/lib/generators/add_auth/challenge/challenge_generator.rb +48 -0
  89. data/lib/generators/add_auth/challenge/templates/recaptcha.rb.tt +28 -0
  90. data/lib/generators/add_auth/challenge/templates/turnstile.rb.tt +24 -0
  91. data/lib/generators/add_auth/controllers/controllers_generator.rb +14 -0
  92. data/lib/generators/add_auth/ejection.rb +18 -0
  93. data/lib/generators/add_auth/email_link/email_link_generator.rb +56 -0
  94. data/lib/generators/add_auth/email_link/templates/add_add_auth_email_binding.rb.tt +5 -0
  95. data/lib/generators/add_auth/email_link/templates/add_add_auth_email_delivery.rb.tt +10 -0
  96. data/lib/generators/add_auth/email_link/templates/add_auth.css +28 -0
  97. data/lib/generators/add_auth/email_link/templates/add_auth_challenge.js +129 -0
  98. data/lib/generators/add_auth/email_tokens/email_tokens_generator.rb +35 -0
  99. data/lib/generators/add_auth/email_tokens/templates/add_auth_sign_in_token.rb +4 -0
  100. data/lib/generators/add_auth/email_tokens/templates/create_add_auth_sign_in_tokens.rb.tt +18 -0
  101. data/lib/generators/add_auth/install/install_generator.rb +27 -0
  102. data/lib/generators/add_auth/install/templates/initializer.rb +46 -0
  103. data/lib/generators/add_auth/javascript/javascript_generator.rb +14 -0
  104. data/lib/generators/add_auth/javascript/templates/application.js +2 -0
  105. data/lib/generators/add_auth/javascript/templates/codec.js +28 -0
  106. data/lib/generators/add_auth/javascript/templates/passkey.js +81 -0
  107. data/lib/generators/add_auth/mailer_views/mailer_views_generator.rb +14 -0
  108. data/lib/generators/add_auth/notifications/notifications_generator.rb +32 -0
  109. data/lib/generators/add_auth/notifications/templates/add_auth_security_event.rb +4 -0
  110. data/lib/generators/add_auth/notifications/templates/create_add_auth_security_events.rb.tt +18 -0
  111. data/lib/generators/add_auth/passkeys/passkeys_generator.rb +75 -0
  112. data/lib/generators/add_auth/passkeys/templates/add_add_auth_passkeys.rb.tt +45 -0
  113. data/lib/generators/add_auth/passkeys/templates/add_auth_ceremony.rb +4 -0
  114. data/lib/generators/add_auth/passkeys/templates/add_auth_credential.rb +4 -0
  115. data/lib/generators/add_auth/session_upgrade/session_upgrade_generator.rb +79 -0
  116. data/lib/generators/add_auth/session_upgrade/templates/add_add_auth_elevation.rb.tt +12 -0
  117. data/lib/generators/add_auth/session_upgrade/templates/extend_sessions_for_add_auth.rb.tt +14 -0
  118. data/lib/generators/add_auth/step_up/step_up_generator.rb +44 -0
  119. data/lib/generators/add_auth/step_up/templates/add_add_auth_reauthentication.rb.tt +9 -0
  120. data/lib/generators/add_auth/views/views_generator.rb +16 -0
  121. data/lib/tasks/add_auth.rake +38 -0
  122. 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).