@puzzle-ui/auth 0.0.0-stage → 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.
Files changed (95) hide show
  1. package/README.md +301 -2
  2. package/dist/LICENSE +21 -0
  3. package/dist/auth.lifecycle.manager.cjs +835 -0
  4. package/dist/auth.lifecycle.manager.cjs.map +1 -0
  5. package/dist/auth.lifecycle.manager.d.cts +115 -0
  6. package/dist/auth.lifecycle.manager.d.ts +115 -0
  7. package/dist/auth.lifecycle.manager.js +835 -0
  8. package/dist/auth.lifecycle.manager.js.map +1 -0
  9. package/dist/auth.manager.cjs +454 -0
  10. package/dist/auth.manager.cjs.map +1 -0
  11. package/dist/auth.manager.d.cts +54 -0
  12. package/dist/auth.manager.d.ts +54 -0
  13. package/dist/auth.manager.js +454 -0
  14. package/dist/auth.manager.js.map +1 -0
  15. package/dist/auth.mfa.manager.cjs +1488 -0
  16. package/dist/auth.mfa.manager.cjs.map +1 -0
  17. package/dist/auth.mfa.manager.d.cts +100 -0
  18. package/dist/auth.mfa.manager.d.ts +100 -0
  19. package/dist/auth.mfa.manager.js +1488 -0
  20. package/dist/auth.mfa.manager.js.map +1 -0
  21. package/dist/auth.rate-limit.cjs +78 -0
  22. package/dist/auth.rate-limit.cjs.map +1 -0
  23. package/dist/auth.rate-limit.d.cts +24 -0
  24. package/dist/auth.rate-limit.d.ts +24 -0
  25. package/dist/auth.rate-limit.js +78 -0
  26. package/dist/auth.rate-limit.js.map +1 -0
  27. package/dist/auth.step-up.manager.cjs +269 -0
  28. package/dist/auth.step-up.manager.cjs.map +1 -0
  29. package/dist/auth.step-up.manager.d.cts +74 -0
  30. package/dist/auth.step-up.manager.d.ts +74 -0
  31. package/dist/auth.step-up.manager.js +268 -0
  32. package/dist/auth.step-up.manager.js.map +1 -0
  33. package/dist/auth.storage.cjs +164 -0
  34. package/dist/auth.storage.cjs.map +1 -0
  35. package/dist/auth.storage.d.cts +29 -0
  36. package/dist/auth.storage.d.ts +29 -0
  37. package/dist/auth.storage.js +148 -0
  38. package/dist/auth.storage.js.map +1 -0
  39. package/dist/errors/auth.errors.cjs +101 -0
  40. package/dist/errors/auth.errors.cjs.map +1 -0
  41. package/dist/errors/auth.errors.d.cts +62 -0
  42. package/dist/errors/auth.errors.d.ts +62 -0
  43. package/dist/errors/auth.errors.js +90 -0
  44. package/dist/errors/auth.errors.js.map +1 -0
  45. package/dist/index.cjs +54 -0
  46. package/dist/index.d.cts +21 -0
  47. package/dist/index.d.ts +21 -0
  48. package/dist/index.js +13 -0
  49. package/dist/providers/mfa-provider.types.d.cts +66 -0
  50. package/dist/providers/mfa-provider.types.d.ts +66 -0
  51. package/dist/providers/provider.types.d.cts +34 -0
  52. package/dist/providers/provider.types.d.ts +34 -0
  53. package/dist/providers/simplewebauthn-provider.cjs +185 -0
  54. package/dist/providers/simplewebauthn-provider.cjs.map +1 -0
  55. package/dist/providers/simplewebauthn-provider.d.cts +35 -0
  56. package/dist/providers/simplewebauthn-provider.d.ts +35 -0
  57. package/dist/providers/simplewebauthn-provider.js +185 -0
  58. package/dist/providers/simplewebauthn-provider.js.map +1 -0
  59. package/dist/providers/webcrypto-secret-protector.cjs +118 -0
  60. package/dist/providers/webcrypto-secret-protector.cjs.map +1 -0
  61. package/dist/providers/webcrypto-secret-protector.d.cts +14 -0
  62. package/dist/providers/webcrypto-secret-protector.d.ts +14 -0
  63. package/dist/providers/webcrypto-secret-protector.js +118 -0
  64. package/dist/providers/webcrypto-secret-protector.js.map +1 -0
  65. package/dist/types/auth-lifecycle.types.d.cts +53 -0
  66. package/dist/types/auth-lifecycle.types.d.ts +53 -0
  67. package/dist/types/auth-manager.types.d.cts +106 -0
  68. package/dist/types/auth-manager.types.d.ts +106 -0
  69. package/dist/types/credential.types.d.cts +9 -0
  70. package/dist/types/credential.types.d.ts +9 -0
  71. package/dist/types/mfa.types.d.cts +244 -0
  72. package/dist/types/mfa.types.d.ts +244 -0
  73. package/dist/types/state.types.d.cts +33 -0
  74. package/dist/types/state.types.d.ts +33 -0
  75. package/dist/types/user.types.d.cts +9 -0
  76. package/dist/types/user.types.d.ts +9 -0
  77. package/dist/utils/auth-crypto.utils.cjs +38 -0
  78. package/dist/utils/auth-crypto.utils.cjs.map +1 -0
  79. package/dist/utils/auth-crypto.utils.d.cts +4 -0
  80. package/dist/utils/auth-crypto.utils.d.ts +4 -0
  81. package/dist/utils/auth-crypto.utils.js +35 -0
  82. package/dist/utils/auth-crypto.utils.js.map +1 -0
  83. package/dist/utils/email.utils.cjs +20 -0
  84. package/dist/utils/email.utils.cjs.map +1 -0
  85. package/dist/utils/email.utils.d.cts +2 -0
  86. package/dist/utils/email.utils.d.ts +2 -0
  87. package/dist/utils/email.utils.js +19 -0
  88. package/dist/utils/email.utils.js.map +1 -0
  89. package/dist/utils/totp.utils.cjs +145 -0
  90. package/dist/utils/totp.utils.cjs.map +1 -0
  91. package/dist/utils/totp.utils.d.cts +26 -0
  92. package/dist/utils/totp.utils.d.ts +26 -0
  93. package/dist/utils/totp.utils.js +140 -0
  94. package/dist/utils/totp.utils.js.map +1 -0
  95. package/package.json +45 -4
package/README.md CHANGED
@@ -1,3 +1,302 @@
1
- # Temporary Holding Version
1
+ ## Multi-factor authentication, passkeys and step-up
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The authentication package includes a framework-independent MFA implementation.
4
+
5
+ Supported authentication factors:
6
+
7
+ - TOTP authenticators.
8
+ - WebAuthn passkeys.
9
+ - Single-use recovery codes.
10
+
11
+ The MFA manager supports:
12
+
13
+ - TOTP factor enrollment and verification.
14
+ - Passkey registration and authentication.
15
+ - Discoverable passkey sign-in.
16
+ - Password-backed MFA enrollment.
17
+ - Mandatory first-factor enrollment.
18
+ - Recovery code generation, verification and regeneration.
19
+ - Factor listing and removal.
20
+ - Challenge expiration and attempt limits.
21
+ - TOTP replay protection.
22
+ - WebAuthn challenge, origin and RP ID verification.
23
+ - Authenticator counter validation.
24
+ - MFA audit events.
25
+ - MFA-specific rate limiting.
26
+
27
+ ### MFA policy
28
+
29
+ MFA is optional by default.
30
+
31
+ Configure a global requirement through `AuthManagerOptions`:
32
+
33
+ ```ts
34
+ const auth = new StorageAuthManager(
35
+ {
36
+ storage,
37
+ passwordHasher,
38
+ rateLimitProvider,
39
+ sessions,
40
+ mfa,
41
+ },
42
+ {
43
+ rateLimitKeySecret,
44
+ mfaPolicy: {
45
+ required: true,
46
+ },
47
+ },
48
+ );
49
+ ```
50
+
51
+ For user-specific requirements, configure `requiredFor`:
52
+
53
+ ```ts
54
+ const auth = new StorageAuthManager(
55
+ {
56
+ storage,
57
+ passwordHasher,
58
+ rateLimitProvider,
59
+ sessions,
60
+ mfa,
61
+ },
62
+ {
63
+ rateLimitKeySecret,
64
+ mfaPolicy: {
65
+ requiredFor: async (user) => {
66
+ const account = await roleRepository.findByUserId(user.id);
67
+
68
+ return account?.role === "admin";
69
+ },
70
+ },
71
+ },
72
+ );
73
+ ```
74
+
75
+ The callback receives the public `AuthUser`. It can return a boolean or a promise.
76
+
77
+ When neither `required` nor `requiredFor` requires MFA, users without enrolled factors can sign in normally. Existing active factors are still challenged during sign-in.
78
+
79
+ ### Mandatory first-factor enrollment
80
+
81
+ When MFA is required and the user has no active factor:
82
+
83
+ 1. Password authentication succeeds.
84
+ 2. The manager creates an enrollment challenge.
85
+ 3. No normal authenticated session is issued.
86
+ 4. The user enrolls either a TOTP authenticator or a passkey.
87
+ 5. The first factor is verified.
88
+ 6. Recovery codes are generated.
89
+ 7. The factor, recovery codes and challenge consumption are committed.
90
+ 8. Only then is an authenticated session issued.
91
+
92
+ The temporary enrollment token is server-only and must be transported through a short-lived, secure, HttpOnly cookie.
93
+
94
+ The browser must never receive the raw challenge token in JSON, local storage or session storage.
95
+
96
+ The authentication state is `mfa-enrollment-pending` until the enrollment has completed.
97
+
98
+ ### Existing sessions and policy changes
99
+
100
+ The MFA policy is evaluated when an existing session is inspected.
101
+
102
+ If a policy now requires MFA and the user has no active factor, the authentication state becomes `mfa-enrollment-required`.
103
+
104
+ This is a restricted state, not an authenticated application session:
105
+
106
+ - `getCurrentUser()` returns `null`.
107
+ - `getAuthState()` returns `mfa-enrollment-required`.
108
+ - Applications must not authorize protected operations for this state.
109
+ - The user can use the enrollment flow after confirming their current password.
110
+ - Normal authentication is restored after an active factor exists.
111
+
112
+ Removing the last active factor therefore cannot silently preserve full application access when MFA is required.
113
+
114
+ ### TOTP enrollment
115
+
116
+ For an authenticated user with a normal session:
117
+
118
+ ```ts
119
+ const enrollment = await mfa.beginTotpEnrollment({
120
+ sessionToken,
121
+ currentPassword,
122
+ ipAddress,
123
+ label: "Authenticator",
124
+ });
125
+ ```
126
+
127
+ Display the returned `otpauthUrl` as a QR code or allow the user to enter the secret manually.
128
+
129
+ Verify the first code:
130
+
131
+ ```ts
132
+ const completed = await mfa.verifyTotpEnrollment({
133
+ sessionToken,
134
+ factorId: enrollment.factorId,
135
+ code,
136
+ ipAddress,
137
+ });
138
+ ```
139
+
140
+ The result includes the public factor and recovery codes. Display recovery codes once and ask the user to store them securely.
141
+
142
+ ### Passkey enrollment
143
+
144
+ Passkey registration is a two-step WebAuthn ceremony:
145
+
146
+ 1. Generate registration options.
147
+ 2. Verify the browser's registration response on the server.
148
+
149
+ The server must validate the expected challenge, origin, RP ID and user verification result.
150
+
151
+ Never trust browser-provided credential metadata without server-side verification.
152
+
153
+ ### MFA sign-in
154
+
155
+ Password-backed sign-in returns `mfa-pending` when the account has active factors.
156
+
157
+ The user can complete the challenge using:
158
+
159
+ - TOTP.
160
+ - A registered passkey.
161
+ - A recovery code.
162
+
163
+ Discoverable passkey sign-in is also supported without requiring an email and password first.
164
+
165
+ A successful MFA challenge creates the authenticated session. The challenge is single-use and expires after its configured lifetime.
166
+
167
+ ### Recovery codes
168
+
169
+ Recovery codes are generated during successful factor enrollment.
170
+
171
+ Store only their hashes. A recovery code:
172
+
173
+ - Can be used once.
174
+ - Is consumed atomically with successful authentication.
175
+ - Must never be written to audit metadata or application logs.
176
+
177
+ Users can regenerate their recovery codes after confirming their current password. Regeneration invalidates the previous unused codes.
178
+
179
+ ### Step-up authentication
180
+
181
+ The step-up manager requires a recent MFA verification for sensitive operations.
182
+
183
+ Supported methods:
184
+
185
+ - TOTP.
186
+ - Passkey.
187
+ - Recovery code.
188
+
189
+ A step-up challenge is bound to the current authenticated session. Successful verification rotates that session and records the authentication method and verification timestamp in session metadata.
190
+
191
+ Use `hasRecentStepUp()` or `requireRecentStepUp()` in server-side actions that require recent MFA verification.
192
+
193
+ ```ts
194
+ const session = await requireRecentStepUp(request, sessionOptions, 300);
195
+ ```
196
+
197
+ The default maximum age is five minutes. The accepted maximum is one hour.
198
+
199
+ ### MFA configuration
200
+
201
+ `AuthMfaManagerOptions` supports:
202
+
203
+ | Option | Purpose |
204
+ | -------------------------- | ------------------------------------------------ |
205
+ | `rateLimitKeySecret` | HMAC secret for rate-limit identifiers |
206
+ | `factorsCollection` | MFA factor records |
207
+ | `challengesCollection` | MFA challenge records |
208
+ | `recoveryCodesCollection` | Hashed recovery codes |
209
+ | `challengeLifetimeSeconds` | Challenge lifetime |
210
+ | `recoveryCodeCount` | Number of recovery codes generated |
211
+ | `issuer` | TOTP issuer displayed in authenticator apps |
212
+ | `totp` | TOTP digits, period and verification window |
213
+ | `webAuthn` | RP ID, RP name and expected origin |
214
+ | `requireVerifiedEmail` | Require verified email before MFA authentication |
215
+ | `clock` | Injectable clock for deterministic tests |
216
+
217
+ WebAuthn must use an exact expected origin and RP ID. Production deployments must use HTTPS.
218
+
219
+ TOTP secrets must be protected using a configured `AuthSecretProtector`. Do not store plaintext TOTP secrets.
220
+
221
+ ### Fullstack integration
222
+
223
+ `@puzzle-ui/fullstack` provides framework-independent server handlers and a browser client.
224
+
225
+ Use the explicit package entry points:
226
+
227
+ - `@puzzle-ui/fullstack/auth/server`
228
+ - `@puzzle-ui/fullstack/auth/client`
229
+ - `@puzzle-ui/fullstack/auth/shared`
230
+ - `@puzzle-ui/fullstack/auth/next`
231
+ - `@puzzle-ui/fullstack/auth/tanstack`
232
+
233
+ The server owns cookies, challenge tokens and session tokens. The browser client receives public state and factor data only.
234
+
235
+ The client authentication statuses include:
236
+
237
+ - `unauthenticated`
238
+ - `mfa-pending`
239
+ - `mfa-enrollment-pending`
240
+ - `mfa-enrollment-required`
241
+ - `step-up-pending`
242
+ - `authenticated`
243
+
244
+ Applications must treat only `authenticated` as normal authenticated access.
245
+
246
+ ### MFA security requirements
247
+
248
+ Production deployments must:
249
+
250
+ 1. Use HTTPS.
251
+ 2. Keep session and challenge tokens in secure, HttpOnly cookies.
252
+ 3. Keep TOTP secrets encrypted at rest.
253
+ 4. Keep recovery codes hashed at rest.
254
+ 5. Configure exact WebAuthn origins and RP IDs.
255
+ 6. Use shared, persistent rate limiting.
256
+ 7. Preserve conditional writes and atomic storage operations.
257
+ 8. Reject expired, consumed and over-attempted challenges.
258
+ 9. Never expose tokens, TOTP secrets, recovery codes or provider errors in logs.
259
+ 10. Restrict application access when a required MFA factor is missing.
260
+
261
+ ### MFA testing
262
+
263
+ Run the authentication package tests:
264
+
265
+ ```sh
266
+ bun run --filter @puzzle-ui/auth test
267
+ ```
268
+
269
+ Run the fullstack tests:
270
+
271
+ ```sh
272
+ bun run --filter @puzzle-ui/fullstack test
273
+ ```
274
+
275
+ Run typechecks:
276
+
277
+ ```sh
278
+ bun run --filter @puzzle-ui/auth typecheck
279
+ bun run --filter @puzzle-ui/fullstack typecheck
280
+ ```
281
+
282
+ Build both packages:
283
+
284
+ ```sh
285
+ bun run --filter @puzzle-ui/auth build
286
+ bun run --filter @puzzle-ui/fullstack build
287
+ ```
288
+
289
+ ## Scope and roadmap
290
+
291
+ `@puzzle-ui/auth` provides the framework-independent authentication core, account lifecycle workflows, MFA, passkeys, recovery codes and step-up authentication.
292
+
293
+ `@puzzle-ui/fullstack` provides the browser client, server handlers and Next.js and TanStack Start integration entry points.
294
+
295
+ The following features remain outside the current authentication scope:
296
+
297
+ - OAuth and external identity providers.
298
+ - Ready-made authentication pages and UI components.
299
+ - Application-specific authorization and role management.
300
+ - Application-specific email templates and delivery workflows.
301
+
302
+ These capabilities can be built on top of the authentication primitives without coupling the core package to a specific application or framework.
package/dist/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sjoerd van Den Berg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.