add_auth 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +61 -9
- data/ROADMAP.md +43 -31
- data/SECURITY.md +7 -1
- data/app/controllers/add_auth/accounts_controller.rb +10 -2
- data/app/controllers/add_auth/reauthentications_controller.rb +5 -0
- data/app/controllers/add_auth/sessions_controller.rb +1 -1
- data/app/views/add_auth/accounts/_register.html.erb +1 -1
- data/app/views/add_auth/accounts/_request.html.erb +3 -0
- data/lib/add_auth/configuration.rb +2 -2
- data/lib/add_auth/core/account_lifecycle.rb +51 -7
- data/lib/add_auth/core/account_policy.rb +21 -3
- data/lib/add_auth/core/session_cursor.rb +38 -0
- data/lib/add_auth/core/session_key.rb +32 -0
- data/lib/add_auth/core/sessions.rb +19 -8
- data/lib/add_auth/rails/authentication_pages.rb +1 -1
- data/lib/add_auth/rails/doctor.rb +10 -3
- data/lib/add_auth/rails/migration/key_type.rb +3 -3
- data/lib/add_auth/rails/runtime.rb +13 -4
- data/lib/add_auth/rails/stores/account_tokens.rb +10 -2
- data/lib/add_auth/rails/stores/sessions.rb +10 -2
- data/lib/add_auth/version.rb +1 -1
- data/lib/generators/add_auth/accounts/accounts_generator.rb +7 -1
- data/lib/generators/add_auth/accounts/templates/add_add_auth_accounts.rb.tt +2 -2
- data/lib/generators/add_auth/accounts/templates/add_add_auth_provisioning.rb.tt +9 -0
- data/lib/generators/add_auth/email_link/email_link_generator.rb +4 -1
- data/lib/generators/add_auth/email_tokens/email_tokens_generator.rb +2 -0
- data/lib/generators/add_auth/email_tokens/templates/create_add_auth_sign_in_tokens.rb.tt +1 -1
- data/lib/generators/add_auth/external_identities/external_identities_generator.rb +2 -0
- data/lib/generators/add_auth/external_identities/templates/create_add_auth_external_identities.rb.tt +6 -12
- data/lib/generators/add_auth/feature_configuration.rb +21 -0
- data/lib/generators/add_auth/install/templates/initializer.rb +154 -50
- data/lib/generators/add_auth/mobile_sessions/mobile_sessions_generator.rb +2 -0
- data/lib/generators/add_auth/mobile_sessions/templates/create_add_auth_mobile_handoffs.rb.tt +4 -4
- data/lib/generators/add_auth/notifications/notifications_generator.rb +5 -4
- data/lib/generators/add_auth/notifications/templates/create_add_auth_security_events.rb.tt +1 -1
- data/lib/generators/add_auth/passkeys/passkeys_generator.rb +5 -21
- data/lib/generators/add_auth/passkeys/templates/add_add_auth_passkeys.rb.tt +2 -2
- data/lib/generators/add_auth/primary_key.rb +19 -0
- data/lib/generators/add_auth/session_upgrade/session_upgrade_generator.rb +6 -1
- data/lib/generators/add_auth/session_upgrade/templates/add_add_auth_session_cursor_index.rb.tt +5 -0
- data/lib/generators/add_auth/step_up/step_up_generator.rb +5 -5
- metadata +21 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b83eabd84666412ae2a83ffc20ac54c9765ef0133e0e401cffebce9e3f73f444
|
|
4
|
+
data.tar.gz: b187c3b70d0c43139f870a50231e5e024f71885218ed68770c98218c257b4be3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65eb201de7ebebca70a786c9664a46df8c27404e74dcd2704b25ef33673ab0f42f7a9c5a865c707cb33a943a4a933c8903fc5dafd1479c32bf0ad0266263f7d6
|
|
7
|
+
data.tar.gz: a48427dfe1145c18be90ddb4ec027b70a7179f7e742d3806ee99ee2ddbf7e9f865abbb0486d96cce600e7e5284ee7487ea7a4b17c71639b94c1801dc95b1b8ee
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-09-18
|
|
4
|
+
|
|
5
|
+
- Honor Rails' `primary_key_type` generator setting for all AddAuth-owned tables.
|
|
6
|
+
Support PostgreSQL UUID sessions and mixed integer/UUID account/session keys,
|
|
7
|
+
retaining bigint defaults and deriving references from the existing schema.
|
|
8
|
+
- Use creation time and a typed ID in opaque session-page cursors; validate
|
|
9
|
+
revocation and explicitly enabled legacy session adoption for either key type.
|
|
10
|
+
For apps already using hardened sessions, re-run `add_auth:session_upgrade`
|
|
11
|
+
for the new pagination index. Existing tables retain their IDs; refresh ejected
|
|
12
|
+
controllers through the normal review process.
|
|
13
|
+
Follow the [upgrade guide](https://addauthgem.com/upgrading/#uuid-support) for
|
|
14
|
+
the release gate, migration review, opaque client cursors and rollback boundary.
|
|
15
|
+
|
|
16
|
+
## 0.4.0 — 2026-09-13
|
|
17
|
+
|
|
18
|
+
- Constrain JSON below 3.0 for the supported Rails decoding API; independently
|
|
19
|
+
installed hosts must be able to decrypt account and email proof deliveries.
|
|
20
|
+
|
|
21
|
+
- Add explicit optional password-signup confirmation with atomic provisioning and
|
|
22
|
+
initial session issuance. Unverified email stays unverified; required
|
|
23
|
+
confirmation remains the default.
|
|
24
|
+
- Add an independent opt-in for single-use password reset proofs to unconfirmed
|
|
25
|
+
password accounts, retaining account restrictions and authority revocation.
|
|
26
|
+
- Allow password-only lifecycle/step-up generation without enabling email sign-in.
|
|
27
|
+
- Expand the generated Rails initializer with executable defaults and commented
|
|
28
|
+
alternatives. Feature generators update their own setting in that file.
|
|
29
|
+
|
|
3
30
|
## 0.3.0 — 2026-09-09
|
|
4
31
|
|
|
5
32
|
Optional account lifecycle, bounded Devise migration and native authentication.
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AddAuth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Password, email-link and passkey sign-in extend
|
|
4
4
|
Rails' generated authentication. AddAuth adds hardened sessions, verification
|
|
5
5
|
for sensitive actions, passkey management and email recovery with an optional
|
|
6
6
|
strict policy. Turnstile and reCAPTCHA integrations are available. See
|
|
@@ -9,7 +9,7 @@ strict policy. Turnstile and reCAPTCHA integrations are available. See
|
|
|
9
9
|
AddAuth builds on top of Rails 8's built-in login system instead of
|
|
10
10
|
replacing it -- it keeps using your existing `User` and `Session` models.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Introduced in 0.3.0: optional account lifecycle, bounded Devise migration,
|
|
13
13
|
provider-library integration and finite Android/iOS sessions. See the
|
|
14
14
|
[migration guide](https://addauthgem.com/migrating-from-devise/) before changing
|
|
15
15
|
an existing Devise app; do not run the Rails generator over its custom files.
|
|
@@ -17,24 +17,38 @@ an existing Devise app; do not run the Rails generator over its custom files.
|
|
|
17
17
|
Read the [documentation](https://addauthgem.com) for setup guides, configuration
|
|
18
18
|
reference and troubleshooting.
|
|
19
19
|
|
|
20
|
+
## Configuration and optional signup confirmation
|
|
21
|
+
|
|
22
|
+
Version 0.4.0 adds optional password-signup confirmation and a
|
|
23
|
+
complete, commented [Rails initializer](lib/generators/add_auth/install/templates/initializer.rb).
|
|
24
|
+
Its default assignments are active; opt-in alternatives are commented out.
|
|
25
|
+
Required confirmation remains the default. See the [configuration guide](https://addauthgem.com/configuration/)
|
|
26
|
+
and [account setup](https://addauthgem.com/configuration-preview/), or the
|
|
27
|
+
[contributor rehearsal](CONTRIBUTING.md#optional-confirmation-integration-rehearsal)
|
|
28
|
+
for verification in a disposable host.
|
|
29
|
+
|
|
20
30
|
## Quickstart
|
|
21
31
|
|
|
32
|
+
These commands target 0.5.0 and are available after its
|
|
33
|
+
[RubyGems publication](https://rubygems.org/gems/add_auth/versions/0.5.0).
|
|
34
|
+
Check package availability before changing your app’s bundle.
|
|
35
|
+
|
|
22
36
|
Install the `add_auth` gem from RubyGems through your Rails app’s Gemfile,
|
|
23
37
|
then enable password and email-link sign-in. Use Ruby 3.3+ and Rails 8.0+
|
|
24
38
|
with Active Record, and run the commands below from your Rails app’s root.
|
|
25
39
|
|
|
26
|
-
**Release availability:** these commands require the published `0.
|
|
40
|
+
**Release availability:** these commands require the published `0.5.0` package.
|
|
27
41
|
If it is not yet listed on [RubyGems](https://rubygems.org/gems/add_auth/versions),
|
|
28
42
|
wait for publication; see [release status](https://addauthgem.com/release-status/).
|
|
29
43
|
Start in your app’s development environment; use the deployment settings
|
|
30
44
|
below before enabling sign-in for users.
|
|
31
45
|
|
|
32
46
|
1. Keep `source "https://rubygems.org"` in your app’s Gemfile and add the
|
|
33
|
-
0.
|
|
47
|
+
0.5 release line:
|
|
34
48
|
|
|
35
49
|
```ruby
|
|
36
50
|
# Gemfile
|
|
37
|
-
gem "add_auth", "~> 0.
|
|
51
|
+
gem "add_auth", "~> 0.5.0"
|
|
38
52
|
```
|
|
39
53
|
|
|
40
54
|
Bundler downloads the package from RubyGems and records the resolved
|
|
@@ -51,10 +65,19 @@ below before enabling sign-in for users.
|
|
|
51
65
|
bin/rails generate authentication
|
|
52
66
|
```
|
|
53
67
|
|
|
54
|
-
3.
|
|
68
|
+
3. Create your configuration file:
|
|
55
69
|
|
|
56
70
|
```sh
|
|
57
71
|
bin/rails generate add_auth:install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Open `config/initializers/add_auth.rb`. Defaults are active and optional
|
|
75
|
+
alternatives are commented out. This command enables no features and preserves
|
|
76
|
+
an existing initializer. Each feature section names its generator.
|
|
77
|
+
|
|
78
|
+
4. Enable password and email-link sign-in:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
58
81
|
bin/rails generate add_auth:email_link
|
|
59
82
|
bin/rails db:migrate
|
|
60
83
|
```
|
|
@@ -63,7 +86,7 @@ below before enabling sign-in for users.
|
|
|
63
86
|
in, plus a page where a signed-in user can see their active sessions and
|
|
64
87
|
sign out of one remotely.
|
|
65
88
|
|
|
66
|
-
|
|
89
|
+
5. Set these values in `config/initializers/add_auth.rb`:
|
|
67
90
|
|
|
68
91
|
```ruby
|
|
69
92
|
AddAuth.configure do |config|
|
|
@@ -83,7 +106,7 @@ below before enabling sign-in for users.
|
|
|
83
106
|
config.action_mailer.raise_delivery_errors = true
|
|
84
107
|
```
|
|
85
108
|
|
|
86
|
-
|
|
109
|
+
6. Create a trial account in `bin/rails console` using your own test address and
|
|
87
110
|
password, then start `bin/rails server` and visit `/sign-in`. Request a link
|
|
88
111
|
for that account and open the message written under `tmp/mail`. Treat these
|
|
89
112
|
local messages as credentials and delete them after testing. Raw sign-in
|
|
@@ -538,13 +561,42 @@ or recovery procedures work in production.
|
|
|
538
561
|
|
|
539
562
|
## Scope
|
|
540
563
|
|
|
541
|
-
AddAuth
|
|
564
|
+
AddAuth implements password/email/passkey sign-in, session adoption and
|
|
542
565
|
management, purpose-bound verification, passkey recovery and strict account
|
|
543
566
|
policy, durable security mail and challenge adapters. Optional account lifecycle
|
|
544
567
|
adds registration, confirmation/reconfirmation, password reset, lock/unlock and
|
|
545
568
|
account changes. Hosts retain account eligibility, resource authorization and
|
|
546
569
|
provider registrations.
|
|
547
570
|
|
|
571
|
+
### UUID and integer primary keys in 0.5.0
|
|
572
|
+
|
|
573
|
+
Version 0.5.0 adds PostgreSQL UUID sessions and configurable primary keys for all
|
|
574
|
+
AddAuth authentication tables. Integer apps retain the normal Rails defaults.
|
|
575
|
+
These instructions become available after the 0.5.0 package is published.
|
|
576
|
+
|
|
577
|
+
For a new PostgreSQL app, set Rails' generator preference in `config/application.rb`
|
|
578
|
+
before generating authentication or AddAuth features:
|
|
579
|
+
|
|
580
|
+
```ruby
|
|
581
|
+
config.generators do |g|
|
|
582
|
+
g.orm :active_record, primary_key_type: :uuid
|
|
583
|
+
end
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
AddAuth uses this setting for every table it creates. Leave it unset for Rails'
|
|
587
|
+
bigint default, or set `:bigint` or `:integer` explicitly. References follow the
|
|
588
|
+
actual target table, so UUID users can have bigint sessions and vice versa.
|
|
589
|
+
The generated migration records its chosen type; later configuration changes do
|
|
590
|
+
not change that migration or convert existing rows. Other key types are unsupported.
|
|
591
|
+
|
|
592
|
+
For an existing installation, follow the
|
|
593
|
+
[version-gated package upgrade](https://addauthgem.com/upgrading/#uuid-support).
|
|
594
|
+
It covers the additive pagination index, ejected controller review, opaque
|
|
595
|
+
pagination cursors, verification and rollback from older gems. Integer apps need
|
|
596
|
+
no ID conversion. The [primary-key guide](https://addauthgem.com/uuid-primary-keys/)
|
|
597
|
+
explains mixed schemas and the separate, app-owned migration needed to convert
|
|
598
|
+
existing IDs. The published 0.4.0 package cannot manage UUID sessions.
|
|
599
|
+
|
|
548
600
|
## Optional integrations in 0.3.0
|
|
549
601
|
|
|
550
602
|
Enable bounded [Devise migration](https://addauthgem.com/migrating-from-devise/),
|
data/ROADMAP.md
CHANGED
|
@@ -4,16 +4,17 @@ AddAuth extends Rails 8's authentication generator with email-link sign-in,
|
|
|
4
4
|
passkeys, purpose-bound reauthentication, hardened sessions and pluggable captcha.
|
|
5
5
|
It reuses the host's accounts and Session model.
|
|
6
6
|
|
|
7
|
-
**Reviewed 2026-09-
|
|
8
|
-
|
|
9
|
-
and
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
**Reviewed 2026-09-13.** Check [published releases](https://github.com/taimoorq/add_auth/releases) for package availability and provenance. This checklist distinguishes verified implementation from publication.
|
|
8
|
+
It adds the bounded Devise migration, optional account lifecycle, provider-library
|
|
9
|
+
integration and finite native authentication below. Required PR and exact
|
|
10
|
+
release-commit CI/CodeQL, protected OIDC publication, downloaded-package comparison
|
|
11
|
+
and fresh-host installation passed. The matching migration and native guides are
|
|
12
|
+
published in [the manual](https://addauthgem.com/).
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
The original strategy baseline shipped in 0.2.1; 0.2.2 supplied the counter-store
|
|
15
|
+
correction. The separate 1.0 support commitment remains under review. Milestones
|
|
16
|
+
describe priorities, not promised dates. See [release status](https://addauthgem.com/release-status/)
|
|
17
|
+
and [the changelog](CHANGELOG.md).
|
|
17
18
|
|
|
18
19
|
An unchecked item is remaining work; deferred candidates need a scope decision
|
|
19
20
|
before implementation. Checked features have passing relevant acceptance specs.
|
|
@@ -22,7 +23,19 @@ can describe tested local work; release/publication remains a separate gate. Det
|
|
|
22
23
|
design and decision records remain in the companion private planning workspace;
|
|
23
24
|
this public checklist is derived from its section 14 and stands on its own.
|
|
24
25
|
|
|
25
|
-
## Now — maintain
|
|
26
|
+
## Now — maintain AddAuth and learn from adoption
|
|
27
|
+
|
|
28
|
+
- [x] Honor the Rails UUID/bigint generator setting across all authentication
|
|
29
|
+
tables, including UUID sessions, mixed references and three-mode bundled/ejected
|
|
30
|
+
session management. Local PostgreSQL and browser acceptance passes on Rails
|
|
31
|
+
8.0/8.1. Added in 0.5.0; see
|
|
32
|
+
[release availability](https://github.com/taimoorq/add_auth/releases).
|
|
33
|
+
|
|
34
|
+
- [x] Optional password-signup confirmation, immediate transactional provisioning
|
|
35
|
+
and session issuance, explicit unconfirmed-reset policy, password-only lifecycle
|
|
36
|
+
and the complete commented Rails initializer. Local supported-matrix, stock/
|
|
37
|
+
adopted-host, UUID PostgreSQL and three-mode bundled/ejected acceptance passes.
|
|
38
|
+
Added in the 0.4.0 source; see [release availability and provenance](https://github.com/taimoorq/add_auth/releases).
|
|
26
39
|
|
|
27
40
|
- [x] **R1 · Resolve the outstanding dependency update.**
|
|
28
41
|
[PR #9](https://github.com/taimoorq/add_auth/pull/9) updates the immutable
|
|
@@ -52,7 +65,7 @@ passwordless-fixture isolation failure is fixed in
|
|
|
52
65
|
[PR #8](https://github.com/taimoorq/add_auth/pull/8); it is not an open runtime
|
|
53
66
|
defect or a reason by itself to republish 0.2.1.
|
|
54
67
|
|
|
55
|
-
##
|
|
68
|
+
## Roadmap — migration delivery and the future 1.0 contract
|
|
56
69
|
|
|
57
70
|
- [x] **R3 · Rehearse an upgrade from published 0.2.1.** Start with a populated
|
|
58
71
|
host, active sessions, passkeys, strict accounts, pending mail and customized
|
|
@@ -114,26 +127,25 @@ defect or a reason by itself to republish 0.2.1.
|
|
|
114
127
|
supported database/runtime, ordinary HTML with JavaScript and Turbo absent,
|
|
115
128
|
Turbo enhancement and permitted no-JS journeys. Draft step-by-step guides
|
|
116
129
|
and prove the destination works with Devise removed.
|
|
117
|
-
- [
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
- [x] **R8 · Publish migration and native guides with the feature release.**
|
|
131
|
+
The 0.3.0 package, installation commands, HTTPS and deep links are verified;
|
|
132
|
+
the six guides now describe the published release.
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
Version 0.3.0 verifies preflight, optional account lifecycle and
|
|
122
135
|
provider-library integration across the supported Ruby/Rails matrix, including
|
|
123
|
-
ordinary HTML, Turbo and permitted no-JS journeys. These additions
|
|
124
|
-
unpublished. Migration/checkpoint/compatible-rollback tests and native client
|
|
136
|
+
ordinary HTML, Turbo and permitted no-JS journeys. These additions ship in 0.3.0. Migration/checkpoint/compatible-rollback tests and native client
|
|
125
137
|
acceptance are complete locally. The full adopter suite passes 1,330 examples
|
|
126
138
|
with zero failures and three existing empty scaffold examples pending. Six
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
published guides pass build, browser, keyboard and no-JS checks. The proposed
|
|
140
|
+
1.0 commitment remains a separate gate.
|
|
129
141
|
|
|
130
|
-
##
|
|
142
|
+
## Shipped in 0.3.0 — Android and iOS login
|
|
131
143
|
|
|
132
|
-
Mobile support
|
|
144
|
+
Mobile support ships as an optional integration in 0.3.0. These capabilities extend the
|
|
133
145
|
same account and session policies. The verified finite profile uses explicit
|
|
134
146
|
30-day absolute and 14-day idle limits, with fresh sign-in after expiry. Renewable
|
|
135
|
-
refresh-token families remain a distinct optional capability,
|
|
136
|
-
The
|
|
147
|
+
refresh-token families remain a distinct optional capability, not implemented in this release.
|
|
148
|
+
The published 0.3.0 package and installed native-consumer contracts are verified.
|
|
137
149
|
|
|
138
150
|
- [x] **R8 · Password and provider login for native clients.** Return credentials
|
|
139
151
|
over authenticated HTTPS responses; preserve supported client contracts.
|
|
@@ -152,15 +164,15 @@ The owner selected 0.3.0; publication and package verification remain pending.
|
|
|
152
164
|
Each change owns its tests and keeps intermediate releases usable. R3/R4 findings
|
|
153
165
|
feed R5. A patch may address compatible corrections; another 0.x minor is possible
|
|
154
166
|
if integration contracts change. A 0.3 release is not a prerequisite for 1.0.
|
|
155
|
-
R8
|
|
167
|
+
R8 ships in 0.3.0 after the published 0.2.2 correction; its supported source profiles
|
|
156
168
|
are bounded to the verified Devise 5.0.4 fixtures. The selected delivery version is 0.3.0. Its pre-1.0 integration contracts
|
|
157
|
-
remain subject to review before R5 freezes. Devise migration
|
|
169
|
+
remain subject to review before R5 freezes. The bounded Devise migration guide is available for 0.3.0.
|
|
158
170
|
|
|
159
171
|
## Delivery — 0.3.0 feature release, then the proposed 1.0 gate
|
|
160
172
|
|
|
161
173
|
- [x] Select 0.3.0 for the locally verified migration, lifecycle, provider and native scope.
|
|
162
|
-
- [
|
|
163
|
-
- [
|
|
174
|
+
- [x] Pass required PR and exact default-branch CI/CodeQL for 0.3.0.
|
|
175
|
+
- [x] Publish 0.3.0 through protected OIDC and verify the downloaded package and manual.
|
|
164
176
|
|
|
165
177
|
- [x] **R6 · Deliver the 0.2.2 correction.**
|
|
166
178
|
[PR #10](https://github.com/taimoorq/add_auth/pull/10) passed all required
|
|
@@ -172,7 +184,7 @@ remain subject to review before R5 freezes. Devise migration support is not avai
|
|
|
172
184
|
- [ ] **R6 · Finish the adoption and compatibility review.** R1–R5 findings are
|
|
173
185
|
completed or explicitly dispositioned, the supported API is accepted, and
|
|
174
186
|
the published-package upgrade and documented operations profile pass.
|
|
175
|
-
- [ ] **R6 · Verify the exact candidate.** Preserve the complete local
|
|
187
|
+
- [ ] **R6 · Verify the exact 1.0 candidate.** Preserve the complete local
|
|
176
188
|
Ruby/Rails, SQLite/PostgreSQL, generated/ejected browser and operations
|
|
177
189
|
acceptance; resolve release-blocking security and upgrade findings.
|
|
178
190
|
Recheck dependency currency and pass required GitHub CI/CodeQL controls.
|
|
@@ -213,13 +225,13 @@ compatible extension could ship in 1.x after an explicit scope decision.
|
|
|
213
225
|
are promoted to R8 above; broader module parity remains outside this scope.
|
|
214
226
|
- [ ] **F4 · Multiple realms.** Require a concrete identity/session-isolation need
|
|
215
227
|
and migration design before adding models, cookies or routing APIs.
|
|
216
|
-
- [ ] **F4 · API/token authentication.** Android/iOS login
|
|
217
|
-
|
|
228
|
+
- [ ] **F4 · API/token authentication.** Android/iOS login shipped under R8
|
|
229
|
+
above; multiple realms and general API-key management remain separate.
|
|
218
230
|
- [ ] **F5 · Standalone test helpers or challenge adapters.** Extract only if
|
|
219
231
|
independent demand and maintenance capacity justify another package;
|
|
220
232
|
retain one implementation per concern.
|
|
221
233
|
|
|
222
|
-
Optional provider-library integration
|
|
234
|
+
Optional provider-library integration ships under R8 in 0.3.0. SMS/TOTP and enterprise
|
|
223
235
|
or cross-origin WebAuthn remain outside the current roadmap. Account roles,
|
|
224
236
|
invitations, authorization and email branding
|
|
225
237
|
stay with the host.
|
data/SECURITY.md
CHANGED
|
@@ -36,7 +36,9 @@ mailing list, or social media) until a fix has been released.
|
|
|
36
36
|
scoping, eligibility rechecks and active-delivery-lease protection.
|
|
37
37
|
- Optional account lifecycle and migration boundaries: credential
|
|
38
38
|
profile selection, source-write fencing, address ownership, account proofs,
|
|
39
|
-
policy and revocation. These capabilities are introduced in 0.3.0.
|
|
39
|
+
policy and revocation. These capabilities are introduced in 0.3.0; 0.4.0 adds
|
|
40
|
+
optional confirmation, atomic signup provisioning/session issuance and the
|
|
41
|
+
independent unconfirmed-password-reset policy.
|
|
40
42
|
- Optional provider identity binding and mobile sessions/handoffs, including
|
|
41
43
|
nonce/client/purpose binding, exact callbacks, single use and bearer/cookie
|
|
42
44
|
separation. Provider protocol cryptography remains library-owned.
|
|
@@ -52,6 +54,10 @@ the currency policy).
|
|
|
52
54
|
|
|
53
55
|
## Supported versions
|
|
54
56
|
|
|
57
|
+
UUID/integer session lookup, account-scoped pagination and revocation are also
|
|
58
|
+
in scope. Version 0.5.0 adds UUID sessions and UUID authentication-table generation;
|
|
59
|
+
primary keys must never substitute for browser/mobile bearer secrets.
|
|
60
|
+
|
|
55
61
|
Until a 1.0 is released, only the latest published version receives security
|
|
56
62
|
fixes. This table will be expanded once there are stable release lines to
|
|
57
63
|
support.
|
|
@@ -19,9 +19,17 @@ module AddAuth
|
|
|
19
19
|
def create
|
|
20
20
|
admitted = intake(:register)
|
|
21
21
|
return intake_failure(admitted) if admitted.is_a?(Symbol)
|
|
22
|
-
|
|
22
|
+
accounts = Rails::Runtime.accounts
|
|
23
|
+
result = accounts.register(identifier: admitted, password: params[:password], profile: registration_profile,
|
|
24
|
+
replacing: add_auth_replacement_session, **add_auth_session_hints)
|
|
23
25
|
if result.success?
|
|
24
|
-
|
|
26
|
+
if result.grant
|
|
27
|
+
destination = after_authentication_url
|
|
28
|
+
add_auth_accept(result.grant)
|
|
29
|
+
redirect_to destination, status: :see_other
|
|
30
|
+
else
|
|
31
|
+
redirect_to accounts.registration_next_path, status: :see_other
|
|
32
|
+
end
|
|
25
33
|
else
|
|
26
34
|
@error = "Check your email and choose a password that meets this application's requirements."
|
|
27
35
|
page("register", status: :unprocessable_entity)
|
|
@@ -8,6 +8,7 @@ module AddAuth
|
|
|
8
8
|
skip_before_action :require_authentication, raise: false
|
|
9
9
|
before_action :private_response
|
|
10
10
|
before_action :enabled_feature
|
|
11
|
+
before_action :enabled_email_feature, only: %i[request_link check_email link confirm]
|
|
11
12
|
before_action :require_add_auth_authentication, except: %i[link confirm]
|
|
12
13
|
before_action :load_purpose, only: %i[new password request_link check_email]
|
|
13
14
|
protect_from_forgery with: :exception
|
|
@@ -54,6 +55,10 @@ module AddAuth
|
|
|
54
55
|
head :not_found unless Rails::Runtime.config.step_up.enabled
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
def enabled_email_feature
|
|
59
|
+
head :not_found unless Core::Intake.email_available?(Rails::Runtime.config)
|
|
60
|
+
end
|
|
61
|
+
|
|
57
62
|
def load_purpose
|
|
58
63
|
@rule = Rails::Runtime.step_up_policy.reauthentication_rule_for(params[:purpose])
|
|
59
64
|
return head :not_found unless @rule
|
|
@@ -25,7 +25,7 @@ module AddAuth
|
|
|
25
25
|
|
|
26
26
|
def destroy
|
|
27
27
|
target_id = params[:id].to_s
|
|
28
|
-
if
|
|
28
|
+
if Rails::Runtime.sessions.current_session?(session: Current.session, session_id: target_id)
|
|
29
29
|
terminate_session
|
|
30
30
|
return redirect_to Rails::Runtime.sign_in_path, status: :see_other
|
|
31
31
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% content_for :title, "Create an account" %>
|
|
2
2
|
<h1 id="add_auth-title">Create an account</h1>
|
|
3
|
-
<p class="<%= add_auth_class(:muted) %>"
|
|
3
|
+
<p class="<%= add_auth_class(:muted) %>"><%= AddAuth::Rails::Runtime.accounts.registration_instructions %></p>
|
|
4
4
|
<% if @error %><p role="alert" class="<%= add_auth_class(:notice) %>"><%= @error %></p><% end %>
|
|
5
5
|
<%= form_with url: "/account/sign-up", data: add_auth_challenge_form_data(:register) do |form| %>
|
|
6
6
|
<div class="<%= add_auth_class(:field) %>">
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
<% content_for :title, title %>
|
|
3
3
|
<h1 id="add_auth-title"><%= title %></h1>
|
|
4
4
|
<p class="<%= add_auth_class(:muted) %>">Enter your account email address to request a secure link.</p>
|
|
5
|
+
<% if @purpose == "reset_password" %>
|
|
6
|
+
<p>You can also <%= link_to "request email confirmation", "/account/requests/confirm" %> if you haven’t confirmed your address.</p>
|
|
7
|
+
<% end %>
|
|
5
8
|
<% if @error %><p role="alert" class="<%= add_auth_class(:notice) %>"><%= @error %></p><% end %>
|
|
6
9
|
<%= form_with url: "/account/requests/#{@purpose}", data: add_auth_challenge_form_data(:account_request) do |form| %>
|
|
7
10
|
<div class="<%= add_auth_class(:field) %>">
|
|
@@ -15,7 +15,7 @@ module AddAuth
|
|
|
15
15
|
PasskeyOptions = Struct.new(:enabled, :rp_id, :origins, :name, :anonymous_limit)
|
|
16
16
|
NotificationOptions = Struct.new(:enabled)
|
|
17
17
|
MaintenanceOptions = Struct.new(:batch_size, :session_retention, :email_retention, :notification_retention, :account_retention)
|
|
18
|
-
LifecycleOptions = Struct.new(:enabled, :password_policy, :eligible, :provision, :profile_attributes, :maximum_attempts, :unlock_in, :proof_lifetime, :remember_lifetime, :remember_idle_timeout, :deletion_allowed, :delete_account)
|
|
18
|
+
LifecycleOptions = Struct.new(:enabled, :password_policy, :eligible, :provision, :profile_attributes, :maximum_attempts, :unlock_in, :proof_lifetime, :remember_lifetime, :remember_idle_timeout, :deletion_allowed, :delete_account, :confirmation_required, :reset_unconfirmed)
|
|
19
19
|
class ExternalProvider
|
|
20
20
|
attr_reader :id, :label, :middleware_name, :configuration, :apple_form_post, :reauthentication
|
|
21
21
|
|
|
@@ -88,7 +88,7 @@ module AddAuth
|
|
|
88
88
|
@lifecycle = LifecycleOptions.new(enabled: false, password_policy: ->(password) { password.length >= 12 && password.bytesize <= 72 },
|
|
89
89
|
eligible: ->(_user) { true }, provision: ->(_user) {}, profile_attributes: ->(_profile) { {} }, maximum_attempts: 20, unlock_in: 3600, proof_lifetime: 3600,
|
|
90
90
|
remember_lifetime: 14 * 86_400, remember_idle_timeout: 7 * 86_400,
|
|
91
|
-
deletion_allowed: ->(_user) { true }, delete_account: ->(user) { user.destroy! })
|
|
91
|
+
deletion_allowed: ->(_user) { true }, delete_account: ->(user) { user.destroy! }, confirmation_required: true, reset_unconfirmed: false)
|
|
92
92
|
@session = SessionOptions.new(enabled: false, lifetime: 43_200, idle_timeout: 1800)
|
|
93
93
|
@mobile = MobileOptions.new(enabled: false, clients: [], callbacks: {}, apple_providers: {})
|
|
94
94
|
@email_link = EmailOptions.new(enabled: false, token_lifetime: 1200, same_browser: false)
|
|
@@ -16,6 +16,7 @@ module AddAuth
|
|
|
16
16
|
class Conflict < StandardError; end
|
|
17
17
|
class InvalidPassword < StandardError; end
|
|
18
18
|
class DeletionRejected < StandardError; end
|
|
19
|
+
class RegistrationRejected < StandardError; end
|
|
19
20
|
|
|
20
21
|
class NewAccount
|
|
21
22
|
attr_reader :user, :user_id
|
|
@@ -44,9 +45,21 @@ module AddAuth
|
|
|
44
45
|
@external_identities = external_identities
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
def register(identifier:, password:, profile: {})
|
|
48
|
+
def register(identifier:, password:, profile: {}, replacing: nil, user_agent: nil, ip_address: nil, remember: false)
|
|
48
49
|
return failure unless valid_new_password?(password)
|
|
49
|
-
register_account(identifier: identifier, password: password, profile: profile
|
|
50
|
+
register_account(identifier: identifier, password: password, profile: profile,
|
|
51
|
+
immediate: !@policy.confirmation_required?, replacing: replacing,
|
|
52
|
+
session_hints: {user_agent: user_agent, ip_address: ip_address, remember: remember})
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generic non-grant result mapping; never authenticate a duplicate result.
|
|
56
|
+
def registration_next_path
|
|
57
|
+
@policy.confirmation_required? ? "/account/check-email" : "/sign-in"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def registration_instructions
|
|
61
|
+
@policy.confirmation_required? ? "We’ll email you a link to confirm your address before you sign in." :
|
|
62
|
+
"Create your account and sign in right away. Your email address stays unverified until you confirm it."
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
def register_external(identifier:, evidence:, profile: {})
|
|
@@ -98,7 +111,7 @@ module AddAuth
|
|
|
98
111
|
@notify.call(user: user, kind: :email_changed, at: now, recipient: pending)
|
|
99
112
|
else
|
|
100
113
|
@store.update_account(user: user, confirmed_at: now)
|
|
101
|
-
|
|
114
|
+
provision_account(user)
|
|
102
115
|
end
|
|
103
116
|
when "reset_password"
|
|
104
117
|
next failure unless valid_new_password?(password)
|
|
@@ -165,19 +178,47 @@ module AddAuth
|
|
|
165
178
|
|
|
166
179
|
private
|
|
167
180
|
|
|
168
|
-
def register_account(identifier:, password:, profile:)
|
|
181
|
+
def register_account(identifier:, password:, profile:, immediate: false, replacing: nil, session_hints: {})
|
|
169
182
|
email = normalize(identifier)
|
|
170
183
|
return failure unless email && profile.is_a?(Hash) && profile.length <= 20
|
|
171
184
|
attributes = @profile_attributes.call(profile)
|
|
172
185
|
profile_valid = attributes.is_a?(Hash) && attributes.keys.all? { |name| (name.is_a?(String) || name.is_a?(Symbol)) && !SECURITY_ATTRIBUTES.include?(name.to_s) && !name.to_s.start_with?("add_auth_") }
|
|
173
186
|
raise AddAuth::Error, "registration profile must contain host profile fields only" unless profile_valid
|
|
174
|
-
|
|
187
|
+
registered = nil
|
|
188
|
+
outcome = @store.create_account(email: email, password: password, profile: attributes, replacing: replacing) do |user|
|
|
175
189
|
raise Conflict unless @policy.allowed?(user, purpose: :confirm)
|
|
176
190
|
yield user if block_given?
|
|
177
191
|
@store.claim_address(user: user, digest: address_digest(email), address: email, state: "current")
|
|
178
|
-
|
|
192
|
+
if immediate
|
|
193
|
+
raise AddAuth::Error, "optional confirmation requires sessions and the provisioning migration" unless @sessions && user.respond_to?(:add_auth_provisioned_at)
|
|
194
|
+
raise RegistrationRejected unless @policy.allowed?(user)
|
|
195
|
+
password_version = user.password_digest
|
|
196
|
+
provision_account(user)
|
|
197
|
+
unless user.email_address == email && user.password_digest == password_version && !user.confirmed_at && user.unconfirmed_email.to_s.empty?
|
|
198
|
+
raise RegistrationRejected
|
|
199
|
+
end
|
|
200
|
+
grant = nil
|
|
201
|
+
@store.finalize_session(user: user) do |writer|
|
|
202
|
+
grant = @sessions.create_in_transaction(user: user, method: :password, persist: writer, replacing: replacing, **session_hints)
|
|
203
|
+
raise RegistrationRejected unless grant
|
|
204
|
+
grant.session
|
|
205
|
+
end
|
|
206
|
+
registered = Result.success(user: user, strategy: :password, session: grant.session, grant: grant)
|
|
207
|
+
else
|
|
208
|
+
issue_in_transaction(user: user, purpose: "confirm", recipient: email)
|
|
209
|
+
end
|
|
179
210
|
end
|
|
180
|
-
|
|
211
|
+
return failure if outcome == :invalid
|
|
212
|
+
(outcome == :created && registered) || accepted
|
|
213
|
+
rescue RegistrationRejected
|
|
214
|
+
failure
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def provision_account(user)
|
|
218
|
+
tracked = user.respond_to?(:add_auth_provisioned_at)
|
|
219
|
+
return if tracked && user.add_auth_provisioned_at
|
|
220
|
+
@store.provision(user: user)
|
|
221
|
+
@store.update_account(user: user, add_auth_provisioned_at: @clock.now) if tracked
|
|
181
222
|
end
|
|
182
223
|
|
|
183
224
|
def with_account_change(user:, session:, purpose:)
|
|
@@ -203,7 +244,10 @@ module AddAuth
|
|
|
203
244
|
def recipient_for(user, purpose)
|
|
204
245
|
if purpose == "confirm"
|
|
205
246
|
normalize(user.unconfirmed_email.to_s.empty? ? user.email_address : user.unconfirmed_email)
|
|
247
|
+
elsif purpose == "reset_password" && @policy.unconfirmed_reset?(user)
|
|
248
|
+
normalize(user.email_address)
|
|
206
249
|
else
|
|
250
|
+
return unless @policy.trusted_address?(user)
|
|
207
251
|
address = normalize(@trusted_address.call(user))
|
|
208
252
|
address if address == normalize(user.email_address)
|
|
209
253
|
end
|
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
module AddAuth
|
|
4
4
|
module Core
|
|
5
5
|
class AccountPolicy
|
|
6
|
-
def initialize(enabled:, eligible: ->(_user) { true }, clock: Time)
|
|
6
|
+
def initialize(enabled:, eligible: ->(_user) { true }, clock: Time, confirmation_required: true, reset_unconfirmed: false)
|
|
7
|
+
unless [confirmation_required, reset_unconfirmed].all? { |value| value == true || value == false }
|
|
8
|
+
raise ArgumentError, "confirmation_required and reset_unconfirmed must be true or false"
|
|
9
|
+
end
|
|
10
|
+
raise ArgumentError, "reset_unconfirmed requires optional confirmation" if confirmation_required && reset_unconfirmed
|
|
7
11
|
@enabled, @eligible, @clock = enabled, eligible, clock
|
|
12
|
+
@confirmation_required, @reset_unconfirmed = confirmation_required, reset_unconfirmed
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def confirmation_required? = @confirmation_required
|
|
16
|
+
|
|
17
|
+
def unconfirmed_reset?(user)
|
|
18
|
+
@enabled && @reset_unconfirmed && !value(user, :confirmed_at) &&
|
|
19
|
+
password_account?(user)
|
|
8
20
|
end
|
|
9
21
|
|
|
22
|
+
def trusted_address?(user) = !@enabled || !!value(user, :confirmed_at)
|
|
23
|
+
|
|
10
24
|
def allowed?(user, purpose: :sign_in)
|
|
11
25
|
return denial(user).nil? if %i[sign_in manage].include?(purpose.to_sym)
|
|
12
26
|
return false unless available?(user)
|
|
@@ -17,7 +31,7 @@ module AddAuth
|
|
|
17
31
|
when :unlock
|
|
18
32
|
locked?(user) && !value(user, :add_auth_manual_lock)
|
|
19
33
|
when :reset_password
|
|
20
|
-
!!value(user, :confirmed_at) && !value(user, :add_auth_strict)
|
|
34
|
+
(!!value(user, :confirmed_at) || unconfirmed_reset?(user)) && !value(user, :add_auth_strict)
|
|
21
35
|
else false
|
|
22
36
|
end
|
|
23
37
|
end
|
|
@@ -26,7 +40,7 @@ module AddAuth
|
|
|
26
40
|
def denial(user)
|
|
27
41
|
return :disabled unless available?(user)
|
|
28
42
|
return unless @enabled
|
|
29
|
-
return :unconfirmed
|
|
43
|
+
return :unconfirmed if !value(user, :confirmed_at) && (confirmation_required? || !password_account?(user))
|
|
30
44
|
:locked if locked?(user)
|
|
31
45
|
end
|
|
32
46
|
|
|
@@ -36,6 +50,10 @@ module AddAuth
|
|
|
36
50
|
|
|
37
51
|
private
|
|
38
52
|
|
|
53
|
+
def password_account?(user)
|
|
54
|
+
value(user, :password_digest).is_a?(String) && !user.password_digest.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
39
57
|
def available?(user)
|
|
40
58
|
user && @eligible.call(user) == true &&
|
|
41
59
|
(!user.respond_to?(:add_auth_authority) || user.add_auth_authority == "add_auth") &&
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module AddAuth
|
|
8
|
+
module Core
|
|
9
|
+
class SessionCursor
|
|
10
|
+
def initialize(key:)
|
|
11
|
+
@key = key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def encode(row)
|
|
15
|
+
"sc1:" + Base64.urlsafe_encode64(JSON.generate([row.created_at.utc.iso8601(6), row.id]), padding: false)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def decode(value)
|
|
19
|
+
# Retain links issued by releases with integer-only pagination.
|
|
20
|
+
legacy = @key.parse(value)
|
|
21
|
+
return {legacy_id: legacy} if legacy.is_a?(Integer)
|
|
22
|
+
return unless value.is_a?(String) && value.ascii_only? && value.bytesize <= 256 && value.match?(/\Asc1:[A-Za-z0-9_-]+\z/)
|
|
23
|
+
|
|
24
|
+
fields = JSON.parse(Base64.urlsafe_decode64(value.delete_prefix("sc1:")))
|
|
25
|
+
return unless fields.is_a?(Array) && fields.length == 2
|
|
26
|
+
timestamp, id = fields
|
|
27
|
+
return unless timestamp.is_a?(String) && timestamp.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z\z/)
|
|
28
|
+
id = @key.parse(id)
|
|
29
|
+
return unless id
|
|
30
|
+
time = Time.iso8601(timestamp)
|
|
31
|
+
return unless time.year.positive? && time.utc.iso8601(6) == timestamp
|
|
32
|
+
{created_at: time, id: id}
|
|
33
|
+
rescue ArgumentError, JSON::ParserError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|