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/ROADMAP.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
AddAuth extends Rails 8's `bin/rails generate authentication` with email-link
|
|
4
|
+
sign-in, passkeys, purpose-bound reauthentication, session hardening and pluggable
|
|
5
|
+
captcha. It reuses the host's accounts and Session model.
|
|
6
|
+
|
|
7
|
+
The canonical design lives in the companion **private** planning workspace:
|
|
8
|
+
[scope](https://github.com/taimoorq/add_auth-workspace/blob/main/docs/authentication-gem-plan.md#14-scope-decision-and-roadmap)
|
|
9
|
+
and [user journeys, contracts and test plan](https://github.com/taimoorq/add_auth-workspace/blob/main/docs/authentication-gem-plan.md#16-integrated-user-journeys-and-implementation-plan).
|
|
10
|
+
Those links require workspace access. This public checklist stands on its own as
|
|
11
|
+
progress tracking; it does not duplicate the private design. Engineering guidance
|
|
12
|
+
stays only in that workspace's `AGENTS.md`.
|
|
13
|
+
|
|
14
|
+
Reviewed 2026-09-07. The 0.2.1 source implements the v1
|
|
15
|
+
strategy features: password/email/passkey sign-in, hardened sessions, reauthentication,
|
|
16
|
+
credential management, default recovery and strict policy, security notifications,
|
|
17
|
+
challenge adapters and fingerprinted ejection. Core policy, session finalization
|
|
18
|
+
and leased mail delivery are shared by engine and ejected flows.
|
|
19
|
+
|
|
20
|
+
The 0.2 release gate uses local real-database, generated-host, browser and
|
|
21
|
+
SMTP/queue/cache acceptance. The final supported matrix is recorded in the
|
|
22
|
+
[canonical evidence ledger](https://github.com/taimoorq/add_auth-workspace/blob/main/docs/authentication-gem-plan.md#20-release-020-execution--2026-09-07).
|
|
23
|
+
Checked feature items mean passing relevant local specs. GitHub's required
|
|
24
|
+
merge/release controls still apply. Hosts verify their own live providers,
|
|
25
|
+
physical authenticators and deployment operations; those checks do not block
|
|
26
|
+
the gem's 0.2 release. Email themes and branding are optional host presentation.
|
|
27
|
+
|
|
28
|
+
Generated pages use shared HTML/Turbo partials. Password/email paths support
|
|
29
|
+
ordinary no-JS navigation when permitted by policy; passkeys require browser
|
|
30
|
+
JavaScript, and a configured captcha may also require it. Strict policy must never
|
|
31
|
+
be weakened to simulate no-JS parity.
|
|
32
|
+
|
|
33
|
+
## 0. Project foundations
|
|
34
|
+
|
|
35
|
+
- [x] Gemspec with a capability-derived Rails floor (`>= 8.0`, the release
|
|
36
|
+
that shipped the authentication generator) and a security-patch-derived
|
|
37
|
+
Ruby floor (`>= 3.3.0`), not just whatever the newest Rails tolerates.
|
|
38
|
+
- [x] MIT license, Code of Conduct, Security policy (`SECURITY.md`).
|
|
39
|
+
- [x] RSpec test setup (`.rspec`, `spec/spec_helper.rb`).
|
|
40
|
+
- [x] `standard` for formatting/linting.
|
|
41
|
+
- [x] CI (GitHub Actions): RSpec + Standard across the Ruby support matrix,
|
|
42
|
+
plus a `bundler-audit` job.
|
|
43
|
+
- [x] Dependabot, grouped by ecosystem, with the Rails family grouped
|
|
44
|
+
together — the concrete mechanism behind the "stay current" mandate in
|
|
45
|
+
the workspace's `AGENTS.md`.
|
|
46
|
+
- [x] RubyGems release workflow configured for [Trusted
|
|
47
|
+
Publishing](https://guides.rubygems.org/trusted-publishing/) (OIDC from
|
|
48
|
+
GitHub Actions) instead of a long-lived API key, gated behind
|
|
49
|
+
`rubygems_mfa_required` and `allowed_push_host`.
|
|
50
|
+
- [x] `bin/setup` / `bin/console` dev scripts.
|
|
51
|
+
- [ ] First successful tagged release published via the Trusted Publishing
|
|
52
|
+
workflow, to lock in the gem name on RubyGems.
|
|
53
|
+
|
|
54
|
+
## 1. Core primitives
|
|
55
|
+
|
|
56
|
+
- [x] `AddAuth::Result` — closed success/failure type for auth outcomes.
|
|
57
|
+
- [x] `AddAuth::Configuration` / `AddAuth.configure`.
|
|
58
|
+
- [x] Challenge adapter contract (`AddAuth::Core::Challenge::Base`) with the
|
|
59
|
+
three-state result (success / rejected / unavailable).
|
|
60
|
+
- [x] `Challenge::Null` (default, always succeeds) and `Challenge::Test`
|
|
61
|
+
(configurable, for specs) adapters.
|
|
62
|
+
- [x] Purpose-separated HMAC digests with explicit strong key material,
|
|
63
|
+
real Rails key derivation, override tests and a framework-free Core check.
|
|
64
|
+
|
|
65
|
+
## 2. Shared contracts and a real host harness — slices A1/A2
|
|
66
|
+
|
|
67
|
+
- [x] One Core policy for eligibility, purpose, proof strength and freshness;
|
|
68
|
+
one session finalizer and public result presenter across all methods.
|
|
69
|
+
- [x] Ordinary email-token store and encrypted delivery-intent contracts,
|
|
70
|
+
shared examples against fake and real SQLite/PostgreSQL adapters, with clock/digest
|
|
71
|
+
injection, replay/race/rollback/address-binding coverage.
|
|
72
|
+
- [x] Passkey/recovery proof and host lifecycle contracts, with actual WebAuthn
|
|
73
|
+
cryptography, rollback and concurrency on SQLite and PostgreSQL.
|
|
74
|
+
- [x] Boot `spec/dummy` through RSpec; replace generated test stubs with
|
|
75
|
+
password sign-in/reset/sign-out requests and real database coverage.
|
|
76
|
+
- [x] Generate and boot Rails 8.0/8.1 hosts; exercise the persistence
|
|
77
|
+
generator and preserve host customizations. CI covers both Rails lines
|
|
78
|
+
on Ruby 3.3, 3.4 and 4.0. Commands are in CONTRIBUTING.md.
|
|
79
|
+
- [x] Browser/virtual-authenticator harness with the passkey slice.
|
|
80
|
+
|
|
81
|
+
## 3. Adopt the host's sessions and password flow — slice B, U1/U8
|
|
82
|
+
|
|
83
|
+
- [x] Inert `add_auth:install` configuration plus additive `session_upgrade`
|
|
84
|
+
migration and shared lifecycle hooks; repeat generation preserves edits.
|
|
85
|
+
- [x] Bounded signed-ID cookie transition to random digested bearers, with
|
|
86
|
+
real signature/tamper, race, cutoff and revocation tests.
|
|
87
|
+
- [x] Password/reset normalization and routes preserved; password/address
|
|
88
|
+
invalidation and account deletion integrated. Hosts supply eligibility.
|
|
89
|
+
- [x] Absolute/idle expiry, protected cookies, fresh session IDs and safe local
|
|
90
|
+
return destinations for implemented password/email flows.
|
|
91
|
+
- [x] Current-session sign-out revokes its bearer and clears browser state.
|
|
92
|
+
- [x] Session list and revoke-one, including next-request rejection in another
|
|
93
|
+
browser and cache/back-safe authenticated-page handling.
|
|
94
|
+
- [x] Sign-out-everywhere, guarded by fresh allowed proof and covering every
|
|
95
|
+
active browser, with old bearers rejected on their next request.
|
|
96
|
+
- [x] Account-scoped cursor pages and bounded maintenance, with optional history
|
|
97
|
+
retention, active-lease protection and completed-pass counts.
|
|
98
|
+
- [x] Explicit passwordless mode, guarded stock entry routes and authenticated
|
|
99
|
+
account-management pages in hosts that also serve public pages.
|
|
100
|
+
|
|
101
|
+
## 4. Complete email-link sign-in — slice C, U2
|
|
102
|
+
|
|
103
|
+
- [x] Internal `EmailLink#issue`/`#consume` lifecycle: atomic replacement and
|
|
104
|
+
session persistence, one-use proof, account/address eligibility rechecks,
|
|
105
|
+
expiring encrypted delivery handoff and tested concurrent use.
|
|
106
|
+
- [x] Wire the lifecycle to the hardened session finalizer and uniform
|
|
107
|
+
asynchronous/rate-limited public intake.
|
|
108
|
+
- [x] Additive token model/store generator and protected pending-delivery
|
|
109
|
+
payload, cleared on consumption/revocation; no Session schema changes.
|
|
110
|
+
- [x] Encrypted request jobs, idempotent issuance, leased mail delivery,
|
|
111
|
+
retry/cleanup sweep and delivered-link-to-browser integration.
|
|
112
|
+
- [x] Durable security notifications share the delivery lease/retry/cancellation
|
|
113
|
+
contract and recover interrupted queue handoffs.
|
|
114
|
+
- [x] Local SMTP, durable queue restart/retry and cross-process shared-cache
|
|
115
|
+
acceptance. Live transport and monitoring validation belongs to each host.
|
|
116
|
+
- [x] Shared IP + keyed identifier rate policy, normalization and generic
|
|
117
|
+
request/resend responses for unknown, disabled and throttled accounts.
|
|
118
|
+
- [x] Request → check-email → inert GET confirmation → explicit POST consume
|
|
119
|
+
→ session; masked account confirmation and deliberate account switching.
|
|
120
|
+
- [x] Resend limits, newest-link guidance, expired/used-link recovery and
|
|
121
|
+
cross-device sign-in by default.
|
|
122
|
+
- [x] Optional same-browser binding, including delivered links, wrong-browser denial,
|
|
123
|
+
Turbo/no-JS browsers and generated/ejected hosts.
|
|
124
|
+
- [x] Real DB concurrency and delivered-mail-to-session specs, plus HTML,
|
|
125
|
+
Turbo and no-JS request/system coverage for the complete flow.
|
|
126
|
+
|
|
127
|
+
- [x] Basic scoped CSS, semantic class overrides for host Bootstrap/Tailwind
|
|
128
|
+
builds, stylesheet opt-out and view ejection with custom-file preservation.
|
|
129
|
+
|
|
130
|
+
## 5. Reauthentication and recovery policy — slice D, U6/U7
|
|
131
|
+
|
|
132
|
+
- [x] Core purpose/freshness evaluator with account/session binding, generic
|
|
133
|
+
elevation failures and passkey UV requirements.
|
|
134
|
+
- [x] Additive session elevation metadata and bearer-rotation finalizer for a
|
|
135
|
+
previously authorized grant, now wired to public reauthentication routes.
|
|
136
|
+
- [x] Host reauthentication adapters persist purpose-bound grants and rotate the
|
|
137
|
+
existing session's bearer after password/email/passkey verification.
|
|
138
|
+
- [x] Password/email reauthentication adapters share policy and presentation;
|
|
139
|
+
email step-up is bound to the initiating browser/session/purpose.
|
|
140
|
+
- [x] Sensitive-action return goes to a safe confirmation page; final mutation
|
|
141
|
+
rechecks authorization/grant/target and never automatically replays a POST.
|
|
142
|
+
- [x] Default email recovery with explicit recovery purpose, replacement grant,
|
|
143
|
+
security notifications and post-recovery session/proof invalidation.
|
|
144
|
+
- [x] Stricter opt-in policy enforced across sign-in, fallback, credential
|
|
145
|
+
management, password reset and policy changes; no hidden weaker route.
|
|
146
|
+
- [x] Tests for fresh-but-insufficient proof, wrong account/session/purpose,
|
|
147
|
+
expiry, lost response, cancellation and attempted policy bypass.
|
|
148
|
+
|
|
149
|
+
## 6. Complete passkeys and credential management — slice E, U3–U7
|
|
150
|
+
|
|
151
|
+
- [x] Registration with discoverable credentials, server-enforced user
|
|
152
|
+
verification and transaction binding to an existing account.
|
|
153
|
+
- [x] Discoverable sign-in with credential/userHandle ownership checks;
|
|
154
|
+
explicit and conditional-autofill UI share the same verification path.
|
|
155
|
+
- [x] Native browser support for another device/security key, neutral cancel,
|
|
156
|
+
understandable retry/fallback and strict-policy unavailable states.
|
|
157
|
+
- [x] First-passkey bootstrap and additional-passkey enrollment require
|
|
158
|
+
appropriate fresh proof. The optional post-login invitation is a host product
|
|
159
|
+
choice; v1 supplies the authenticated `/passkeys` entry point.
|
|
160
|
+
- [x] Credential list, rename, remove, notifications and atomic last-usable-method
|
|
161
|
+
checks; default/strict recovery works with enrollment and lost-device flows.
|
|
162
|
+
- [x] Correct sign-counter anomaly/backup-flag handling, with atomic counter
|
|
163
|
+
updates and tests for zero, equal, increasing and decreasing counters.
|
|
164
|
+
- [x] Exact origin/RP policy, single-use server transactions, one shared codec,
|
|
165
|
+
payload bounds and cleanup of pending browser ceremonies.
|
|
166
|
+
- [x] Virtual-authenticator and real-store coverage of success, UV/signature/
|
|
167
|
+
origin/ownership failures, replay, races, management and recovery.
|
|
168
|
+
|
|
169
|
+
## 7. Challenge adapters and accessible failure paths — slice F, U9
|
|
170
|
+
|
|
171
|
+
- [x] Turnstile adapter with hostname/action checks, provider-owned lifetime,
|
|
172
|
+
bounded HTTPS timeouts and safe no-retry handling for single-use tokens.
|
|
173
|
+
- [x] reCAPTCHA v2/v3 adapters respecting their different verification fields
|
|
174
|
+
and configured score/action requirements where applicable.
|
|
175
|
+
- [x] Shared success/rejected/unavailable behavior, fail-closed default and
|
|
176
|
+
observable explicit fail-open policy; no implicit bypass without JS.
|
|
177
|
+
- [x] Retry/outage messages, preserved input, provider protocol fixtures and no
|
|
178
|
+
live-provider dependency in routine specs.
|
|
179
|
+
- [x] Local keyboard/focus/status, virtual-authenticator and provider-contract
|
|
180
|
+
browser coverage in bundled and ejected UI. Broader browser/device and
|
|
181
|
+
assistive-technology verification remains a host deployment responsibility.
|
|
182
|
+
|
|
183
|
+
## 8. Generators, ejection and integrated acceptance — slice G
|
|
184
|
+
|
|
185
|
+
- [x] `add_auth:install` writes inert configuration; session/email feature
|
|
186
|
+
generators add reviewable wiring and preserve edits on repeat runs.
|
|
187
|
+
- [x] `add_auth:views`, `add_auth:controllers`, `add_auth:javascript` and
|
|
188
|
+
`add_auth:mailer_views` reuse the same Core policy, presenter and templates.
|
|
189
|
+
- [x] `add_auth:challenge` writes environment-keyed Turnstile/reCAPTCHA config,
|
|
190
|
+
adds the fixed challenge route and preserves an existing initializer.
|
|
191
|
+
- [x] `add_auth:doctor` checks deployment origins, cookies/session metadata,
|
|
192
|
+
delivery, migrations and challenge policy/routes.
|
|
193
|
+
- [x] `add_auth:doctor` checks recovery policy and generated-file drift.
|
|
194
|
+
- [x] Every generated flow exercised before and after ejection, with Turbo
|
|
195
|
+
Drive/Frames/Streams, ordinary HTML, no-JS alternatives and strict denial.
|
|
196
|
+
- [x] Auth-page cache/referrer protections, redacted app/job telemetry,
|
|
197
|
+
CSRF, safe redirects and correct success/failure HTTP contracts.
|
|
198
|
+
- [x] RSpec strategy/store shared examples and host-facing test helpers,
|
|
199
|
+
including virtual authenticator lifecycle and delivered-link extraction.
|
|
200
|
+
- [x] Full RSpec, Standard and dependency audit pass on supported local matrices;
|
|
201
|
+
README/roadmap distinguish working development APIs from release gates.
|
|
202
|
+
- [ ] Exact-commit remote CI/CodeQL and required repository security checks;
|
|
203
|
+
refresh deployed public documentation when the gem is released.
|
|
204
|
+
|
|
205
|
+
## 9. Release readiness
|
|
206
|
+
|
|
207
|
+
- [x] README rewritten from skeleton status to verified usage and migration
|
|
208
|
+
instructions, linking to maintained public API docs as they ship.
|
|
209
|
+
- [x] Security policy updated for shipped strategies, recovery limits and
|
|
210
|
+
supported versions; redacted events and incident/rollback guidance documented.
|
|
211
|
+
- [ ] CHANGELOG entries and successful Trusted Publishing release; a tag or
|
|
212
|
+
configured workflow alone does not prove the gem was published.
|
|
213
|
+
- [x] Local enabled-journey, delivery-retry/restart, expiry and transaction rollback
|
|
214
|
+
evidence. Record 0.2 operational defaults and host deployment responsibilities.
|
|
215
|
+
- [x] Audit adopter-requested changes against stock Rails behavior; keep app roles,
|
|
216
|
+
invitations, authorization and email themes in the host.
|
|
217
|
+
- [ ] `v1.0.0` only after the integrated acceptance gates pass.
|
|
218
|
+
|
|
219
|
+
## v2 — reassess after v1 usage
|
|
220
|
+
|
|
221
|
+
- [ ] AddAuth-owned password registration/reset, confirmation, lockout and
|
|
222
|
+
password policy. Existing host password integration is part of v1.
|
|
223
|
+
- [ ] Password-hashing adapter seam, following the canonical cryptography
|
|
224
|
+
policy and Rails support available at implementation time.
|
|
225
|
+
- [ ] Recovery codes; they are not an implied fallback for v1 strict policy.
|
|
226
|
+
- [ ] Multiple realms/routing scopes.
|
|
227
|
+
|
|
228
|
+
## Decisions before they become dependencies
|
|
229
|
+
|
|
230
|
+
Owner and deadline details remain in the canonical plan's section 16.
|
|
231
|
+
|
|
232
|
+
- [x] Review the 0.2 configuration and public integration contract. The runtime
|
|
233
|
+
uses Rails-generator User/Session conventions; authentication models must
|
|
234
|
+
share one database connection pool and cross-pool writes are rejected.
|
|
235
|
+
- [x] Review lifetime/resend/legacy-bridge defaults and local failure evidence;
|
|
236
|
+
retention is opt-in, and key changes require a host deployment plan.
|
|
237
|
+
- [x] Framework-neutral mail and virtual-authenticator helpers; no
|
|
238
|
+
Minitest-specific integration DSL. AddAuth's own suite stays RSpec.
|
|
239
|
+
- [ ] Revisit API/token authentication after v1; outside current scope.
|
|
240
|
+
|
|
241
|
+
## Potential standalone libraries
|
|
242
|
+
|
|
243
|
+
- [ ] Virtual-authenticator test helpers.
|
|
244
|
+
- [ ] Challenge adapter contract and its three-state result.
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
AddAuth handles authentication credentials -- passwords (via the host app),
|
|
4
|
+
passkeys, session and sign-in tokens, and cryptographic digests. Please report
|
|
5
|
+
suspected vulnerabilities privately rather than opening a public issue.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Use GitHub's private vulnerability reporting for this repository:
|
|
10
|
+
https://github.com/taimoorq/add_auth/security/advisories/new
|
|
11
|
+
|
|
12
|
+
If that isn't available to you, email taimoorq@gmail.com with:
|
|
13
|
+
|
|
14
|
+
- A description of the vulnerability and its potential impact.
|
|
15
|
+
- Steps to reproduce, or a proof-of-concept if you have one.
|
|
16
|
+
- The AddAuth version, Rails version, and Ruby version involved.
|
|
17
|
+
|
|
18
|
+
Please do not disclose the issue publicly (including in a GitHub issue,
|
|
19
|
+
mailing list, or social media) until a fix has been released.
|
|
20
|
+
|
|
21
|
+
## What's in scope
|
|
22
|
+
|
|
23
|
+
- The `AddAuth::Core` cryptographic and authentication logic (token issuance
|
|
24
|
+
and consumption, session adoption/resume/revocation, session/sign-in
|
|
25
|
+
digesting, rate limiting and account eligibility).
|
|
26
|
+
- The encrypted email intake/outbox, worker retry/lease semantics and default
|
|
27
|
+
mail logging, plus the durable security-notification outbox.
|
|
28
|
+
- WebAuthn registration/assertion, UV, origin/RP and ownership verification,
|
|
29
|
+
single-use ceremonies, counters, last-credential concurrency and management.
|
|
30
|
+
- Purpose-bound password/email/passkey reauthentication, bearer rotation,
|
|
31
|
+
trusted-address recovery, strict policy and host mutation-guard contracts.
|
|
32
|
+
- The Turnstile and reCAPTCHA challenge adapters, their server-side verification
|
|
33
|
+
contract and scoped browser lifecycle, including failure, outage and Turbo
|
|
34
|
+
replacement handling.
|
|
35
|
+
- Bounded session listing and authentication-history cleanup, including account
|
|
36
|
+
scoping, eligibility rechecks and active-delivery-lease protection.
|
|
37
|
+
- The generated controllers and views this gem ships,
|
|
38
|
+
including their CSRF, enumeration-safety, and Turbo-Stream behavior.
|
|
39
|
+
- The GitHub Actions release pipeline (`.github/workflows/push_gem.yml`) and
|
|
40
|
+
its Trusted Publishing configuration.
|
|
41
|
+
|
|
42
|
+
Vulnerabilities in Rails itself, in `webauthn-ruby`, or in `bcrypt`/`argon2`
|
|
43
|
+
should be reported to those projects directly; AddAuth will pick up fixed
|
|
44
|
+
releases via Dependabot (see `AGENTS.md` in the companion workspace repo for
|
|
45
|
+
the currency policy).
|
|
46
|
+
|
|
47
|
+
## Supported versions
|
|
48
|
+
|
|
49
|
+
Until a 1.0 is released, only the latest published version receives security
|
|
50
|
+
fixes. This table will be expanded once there are stable release lines to
|
|
51
|
+
support.
|
|
52
|
+
|
|
53
|
+
| Version | Supported |
|
|
54
|
+
| ------- | --------- |
|
|
55
|
+
| latest 0.x | :white_check_mark: |
|
|
56
|
+
| older 0.x | :x: |
|
|
57
|
+
|
|
58
|
+
## Response expectations
|
|
59
|
+
|
|
60
|
+
This is currently a single-maintainer project. Please allow a reasonable
|
|
61
|
+
window for an initial response before following up, and understand that a fix
|
|
62
|
+
timeline depends on severity and complexity.
|
|
63
|
+
|
|
64
|
+
## Recovery and deployment boundaries
|
|
65
|
+
|
|
66
|
+
The 0.2 line includes default email replacement only when
|
|
67
|
+
an explicit host callback returns a verified recovery address. Strict accounts
|
|
68
|
+
cannot use password or email recovery; they require a remaining passkey or the
|
|
69
|
+
host's documented support process. Password reset and feature disablement must
|
|
70
|
+
not relax strict policy. Report any contrary behavior as a policy bypass.
|
|
71
|
+
|
|
72
|
+
See README's operations and rollback instructions for log filtering, encrypted
|
|
73
|
+
outbox handling, key rotation and migration safety. Hosts own account eligibility,
|
|
74
|
+
address confirmation, resource authorization, SMTP/queue/cache/proxy configuration
|
|
75
|
+
and support recovery. A local test suite does not certify those deployments.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "add_auth/rails/ejection"
|
|
4
|
+
|
|
5
|
+
module AddAuth
|
|
6
|
+
# Fixed gem assets only. This works even in hosts without an asset pipeline.
|
|
7
|
+
class AssetsController < ActionController::API
|
|
8
|
+
# Stateless public files: no cookies, sessions or authenticated response data.
|
|
9
|
+
before_action do
|
|
10
|
+
response.set_header("Cross-Origin-Resource-Policy", "same-origin")
|
|
11
|
+
unless request.get? || request.head?
|
|
12
|
+
response.set_header("Allow", "GET, HEAD")
|
|
13
|
+
head :method_not_allowed
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
def stylesheet
|
|
17
|
+
expires_in 1.hour, public: true
|
|
18
|
+
send_file Rails::Engine.root.join("lib/generators/add_auth/email_link/templates/add_auth.css"),
|
|
19
|
+
type: "text/css", disposition: "inline"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def boot
|
|
23
|
+
source = 'if (!window.Turbo) await import("/add_auth/turbo.js"); await import("/add_auth/challenge.js");'
|
|
24
|
+
source += ' await import("/add_auth/passkey.js");' if Rails::Runtime.config.passkeys.enabled
|
|
25
|
+
render body: source, content_type: "text/javascript"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def turbo
|
|
29
|
+
expires_in 1.hour, public: true
|
|
30
|
+
source = File.read(Gem.loaded_specs.fetch("turbo-rails").full_gem_path + "/app/assets/javascripts/turbo.min.js")
|
|
31
|
+
render body: source, content_type: "text/javascript"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def stimulus
|
|
35
|
+
expires_in 1.hour, public: true
|
|
36
|
+
source = File.read(Gem.loaded_specs.fetch("stimulus-rails").full_gem_path + "/app/assets/javascripts/stimulus.min.js")
|
|
37
|
+
render body: source, content_type: "text/javascript"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
%w[application codec passkey].each do |asset|
|
|
41
|
+
define_method(asset) do
|
|
42
|
+
expires_in 1.hour, public: true
|
|
43
|
+
send_file Rails::Ejection.new(host_root: ::Rails.root).asset(asset),
|
|
44
|
+
type: "text/javascript", disposition: "inline"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def challenge
|
|
49
|
+
expires_in 1.hour, public: true
|
|
50
|
+
send_file Rails::Ejection.new(host_root: ::Rails.root).asset("challenge"),
|
|
51
|
+
type: "text/javascript", disposition: "inline"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
class PasskeysController < ::ApplicationController
|
|
5
|
+
include Rails::AuthenticationPages
|
|
6
|
+
|
|
7
|
+
helper AddAuth::SignInsHelper
|
|
8
|
+
skip_before_action :require_authentication, raise: false
|
|
9
|
+
before_action :private_response
|
|
10
|
+
before_action :enabled_feature
|
|
11
|
+
before_action :require_add_auth_authentication, only: %i[index registration_options register rename remove change_policy reauthentication_options reauthenticate]
|
|
12
|
+
protect_from_forgery with: :exception
|
|
13
|
+
rescue_from AddAuth::Error, with: :service_unavailable
|
|
14
|
+
|
|
15
|
+
def index
|
|
16
|
+
@credentials = service.list(user: Current.user, session: Current.session)
|
|
17
|
+
@strict = Rails::Runtime.access_policy.strict?(Current.user)
|
|
18
|
+
page("list")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def registration_options
|
|
22
|
+
return unless admitted(:passkey_enrollment)
|
|
23
|
+
options(service.registration_options(user: Current.user, session: Current.session, browser_secret: browser_secret))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def register
|
|
27
|
+
return unless admitted(:passkey_finish)
|
|
28
|
+
result = service.register(transaction: params[:transaction], credential_response: credential_payload,
|
|
29
|
+
user: Current.user, session: Current.session, browser_secret: browser_secret, nickname: params[:nickname])
|
|
30
|
+
finish(result, destination: "/passkeys", grant: result.grant)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def authentication_options
|
|
34
|
+
return unless admitted(:sign_in)
|
|
35
|
+
options(service.authentication_options(browser_secret: browser_secret))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def authenticate
|
|
39
|
+
return unless admitted(:passkey_finish)
|
|
40
|
+
result = service.authenticate(transaction: params[:transaction], credential_response: credential_payload,
|
|
41
|
+
browser_secret: browser_secret, replacing: add_auth_replacement_session, **add_auth_session_hints)
|
|
42
|
+
finish(result, destination: result.success? ? after_authentication_url : "/", grant: result.success? ? result.credential : nil)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reauthentication_options
|
|
46
|
+
return unless admitted(:reauthenticate)
|
|
47
|
+
options(service.authentication_options(browser_secret: browser_secret, user: Current.user,
|
|
48
|
+
session: Current.session, purpose: params[:purpose]))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reauthenticate
|
|
52
|
+
return unless admitted(:passkey_finish)
|
|
53
|
+
result = service.authenticate(transaction: params[:transaction], credential_response: credential_payload,
|
|
54
|
+
browser_secret: browser_secret, session: Current.session)
|
|
55
|
+
destination = result.success? ? Rails::Runtime.step_up_policy.return_to(result.session.elevation_purpose) : "/"
|
|
56
|
+
finish(result, destination: destination, grant: result.success? ? result.credential : nil)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def rename
|
|
60
|
+
management(service.rename(user: Current.user, session: Current.session, id: params[:id], nickname: params[:nickname]))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def remove
|
|
64
|
+
management(service.remove(user: Current.user, session: Current.session, id: params[:id]))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def change_policy
|
|
68
|
+
result = service.change_policy(user: Current.user, session: Current.session,
|
|
69
|
+
strict: {"strict" => true, "default" => false}[params[:policy]], acknowledged: params[:acknowledged] == "1")
|
|
70
|
+
add_auth_accept(result.credential) if result.success?
|
|
71
|
+
management(result, purpose: :manage_policy)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cancel
|
|
75
|
+
service.cancel(transaction: params[:transaction], browser_secret: session[:add_auth_browser])
|
|
76
|
+
head :no_content
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def service = Rails::Runtime.passkeys
|
|
82
|
+
def browser_secret = session[:add_auth_browser] ||= Rails::Runtime.browser_binding.generate
|
|
83
|
+
|
|
84
|
+
def enabled_feature
|
|
85
|
+
head :not_found unless Rails::Runtime.config.passkeys.enabled
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def authentication_partial(partial) = "add_auth/passkeys/#{partial}"
|
|
89
|
+
|
|
90
|
+
def credential_payload
|
|
91
|
+
value = params[:credential]
|
|
92
|
+
value.to_unsafe_h if value.is_a?(ActionController::Parameters)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def admitted(action)
|
|
96
|
+
result = Rails::Runtime.intake.anonymous(ip: request.remote_ip, action: action, challenge_token: challenge_token)
|
|
97
|
+
return true if result == true
|
|
98
|
+
render json: {error: "Verification could not start. Try again shortly."}, status: if result == :rate_limited
|
|
99
|
+
429
|
|
100
|
+
else
|
|
101
|
+
(result == :challenge_unavailable) ? 503 : 422
|
|
102
|
+
end
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def options(result)
|
|
107
|
+
return render(json: result.credential) if result.success?
|
|
108
|
+
return render(json: {error: "Verification could not start. Try again shortly."}, status: :too_many_requests) if result.reason == :rate_limited
|
|
109
|
+
error(result)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def finish(result, destination:, grant:)
|
|
113
|
+
return error(result) unless result.success?
|
|
114
|
+
add_auth_accept(grant) if grant
|
|
115
|
+
render json: {redirect: Core::Sessions.safe_return(destination) || "/"}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def error(result)
|
|
119
|
+
payload = {error: "Verification did not complete. Try another passkey or an allowed recovery method."}
|
|
120
|
+
if result.reason == :elevation_required && %w[registration_options register].include?(action_name)
|
|
121
|
+
payload[:redirect] = "/reauthenticate?purpose=manage_passkeys"
|
|
122
|
+
end
|
|
123
|
+
render json: payload, status: :unprocessable_entity
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def management(result, purpose: :manage_passkeys)
|
|
127
|
+
if result.success?
|
|
128
|
+
redirect_to "/passkeys", status: :see_other
|
|
129
|
+
elsif result.reason == :elevation_required
|
|
130
|
+
redirect_to "/reauthenticate?#{URI.encode_www_form(purpose: purpose)}", status: :see_other
|
|
131
|
+
else
|
|
132
|
+
@error = "That change could not be made. Keep a usable sign-in method and check your selection."
|
|
133
|
+
@credentials = service.list(user: Current.user, session: Current.session)
|
|
134
|
+
@strict = Rails::Runtime.access_policy.strict?(Current.user)
|
|
135
|
+
page("list", status: :unprocessable_entity)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def service_unavailable
|
|
140
|
+
response.set_header("Retry-After", "60")
|
|
141
|
+
render json: {error: "Passkeys are temporarily unavailable. Try again shortly."}, status: :service_unavailable
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
class ReauthenticationsController < ::ApplicationController
|
|
5
|
+
include Rails::AuthenticationPages
|
|
6
|
+
|
|
7
|
+
helper AddAuth::SignInsHelper
|
|
8
|
+
skip_before_action :require_authentication, raise: false
|
|
9
|
+
before_action :private_response
|
|
10
|
+
before_action :enabled_feature
|
|
11
|
+
before_action :require_add_auth_authentication, except: %i[link confirm]
|
|
12
|
+
before_action :load_purpose, only: %i[new password request_link check_email]
|
|
13
|
+
protect_from_forgery with: :exception
|
|
14
|
+
rescue_from AddAuth::Error, "ActiveJob::EnqueueError", with: :service_unavailable
|
|
15
|
+
|
|
16
|
+
def new = page("form")
|
|
17
|
+
|
|
18
|
+
def password
|
|
19
|
+
result = Rails::Runtime.elevate_password(user: Current.user, session: Current.session,
|
|
20
|
+
purpose: @purpose, password: params[:password], ip: request.remote_ip, challenge_token: challenge_token)
|
|
21
|
+
finish(result)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def request_link
|
|
25
|
+
admitted = Rails::Runtime.intake.call(identifier: Current.user.email_address, ip: request.remote_ip,
|
|
26
|
+
action: :reauthenticate, challenge_token: challenge_token)
|
|
27
|
+
return intake_failure(admitted) if %i[challenge_rejected challenge_unavailable].include?(admitted)
|
|
28
|
+
unless admitted.is_a?(Symbol)
|
|
29
|
+
session[:add_auth_browser] ||= Rails::Runtime.browser_binding.generate
|
|
30
|
+
Rails::Runtime.enqueue_reauthentication(user: Current.user, session: Current.session,
|
|
31
|
+
purpose: @purpose, browser_secret: session[:add_auth_browser])
|
|
32
|
+
end
|
|
33
|
+
redirect_to "/reauthenticate/check-email?#{URI.encode_www_form(purpose: @purpose)}", status: :see_other
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check_email = page("check_email")
|
|
37
|
+
|
|
38
|
+
def link
|
|
39
|
+
@preview = service.preview(token: params[:token], browser_secret: session[:add_auth_browser])
|
|
40
|
+
page(service.confirmation_page(@preview))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def confirm
|
|
44
|
+
result = service.reauthenticate(token: params[:token], session: add_auth_replacement_session,
|
|
45
|
+
browser_secret: session[:add_auth_browser])
|
|
46
|
+
finish(result, failure_page: "invalid_link")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def service = Rails::Runtime.email(purpose: :reauthentication)
|
|
52
|
+
|
|
53
|
+
def enabled_feature
|
|
54
|
+
head :not_found unless Rails::Runtime.config.step_up.enabled
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def load_purpose
|
|
58
|
+
@rule = Rails::Runtime.step_up_policy.reauthentication_rule_for(params[:purpose])
|
|
59
|
+
return head :not_found unless @rule
|
|
60
|
+
@purpose = params[:purpose].to_s
|
|
61
|
+
@methods = Rails::Runtime.step_up_policy.methods_for(user: Current.user, purpose: @purpose)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def finish(result, failure_page: "form")
|
|
65
|
+
if result.success?
|
|
66
|
+
destination = Rails::Runtime.step_up_policy.return_to(result.session.elevation_purpose)
|
|
67
|
+
add_auth_accept(result.credential)
|
|
68
|
+
redirect_to destination, status: :see_other
|
|
69
|
+
else
|
|
70
|
+
@error = "We could not verify this request. Try again from the browser where you started."
|
|
71
|
+
if %i[rate_limited challenge_rejected challenge_unavailable].include?(result.reason)
|
|
72
|
+
intake_failure(result.reason)
|
|
73
|
+
else
|
|
74
|
+
page(failure_page, status: :unprocessable_entity)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def authentication_partial(partial) = "add_auth/reauthentications/#{partial}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
class RecoveriesController < ::ApplicationController
|
|
5
|
+
include Rails::AuthenticationPages
|
|
6
|
+
|
|
7
|
+
helper AddAuth::SignInsHelper
|
|
8
|
+
skip_before_action :require_authentication, raise: false
|
|
9
|
+
before_action :private_response
|
|
10
|
+
before_action :enabled_feature
|
|
11
|
+
protect_from_forgery with: :exception
|
|
12
|
+
rescue_from AddAuth::Error, "ActiveJob::EnqueueError", with: :service_unavailable
|
|
13
|
+
|
|
14
|
+
def new = page("form")
|
|
15
|
+
def check_email = page("check_email")
|
|
16
|
+
|
|
17
|
+
def request_link
|
|
18
|
+
value = Rails::Runtime.intake.call(identifier: params[:email_address], ip: request.remote_ip,
|
|
19
|
+
action: :email_link, challenge_token: challenge_token)
|
|
20
|
+
return intake_failure(value) if %i[challenge_rejected challenge_unavailable].include?(value)
|
|
21
|
+
Rails::Runtime.enqueue_recovery(value) unless value.is_a?(Symbol)
|
|
22
|
+
redirect_to "/recover/check-email", status: :see_other
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def link
|
|
26
|
+
@preview = service.preview(token: params[:token])
|
|
27
|
+
page(service.confirmation_page(@preview))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def confirm
|
|
31
|
+
result = service.consume(token: params[:token], current_session: add_auth_replacement_session,
|
|
32
|
+
switch_account: params[:switch_account] == "1")
|
|
33
|
+
if result.success?
|
|
34
|
+
add_auth_accept(result.grant)
|
|
35
|
+
redirect_to "/passkeys", status: :see_other
|
|
36
|
+
else
|
|
37
|
+
page("invalid_link", status: :unprocessable_entity)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def service = Rails::Runtime.email(purpose: :recovery)
|
|
44
|
+
def authentication_partial(partial) = "add_auth/recoveries/#{partial}"
|
|
45
|
+
|
|
46
|
+
def enabled_feature
|
|
47
|
+
head :not_found unless Rails::Runtime.config.passkeys.enabled
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|