@udibo/oauth2 0.4.1 → 0.5.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.
- package/README.md +3 -3
- package/SECURITY.md +0 -4
- package/esm/adapters/hono/_common.d.ts +10 -13
- package/esm/adapters/hono/_common.js +11 -20
- package/esm/adapters/hono/authorization-server.d.ts +16 -10
- package/esm/adapters/hono/authorization-server.js +13 -8
- package/esm/adapters/hono/bff/auth-request-store.d.ts +4 -0
- package/esm/adapters/hono/bff/auth-request-store.js +4 -0
- package/esm/adapters/hono/bff/bff.d.ts +29 -18
- package/esm/adapters/hono/bff/bff.js +15 -11
- package/esm/adapters/hono/bff/proxy.js +7 -55
- package/esm/adapters/hono/bff/session-store.d.ts +9 -9
- package/esm/adapters/hono/bff/session-store.js +3 -2
- package/esm/adapters/hono/bff/testing.d.ts +14 -3
- package/esm/adapters/hono/bff/testing.js +14 -3
- package/esm/adapters/hono/identity.d.ts +5 -3
- package/esm/adapters/hono/identity.js +1 -17
- package/esm/adapters/hono/resource-server.d.ts +3 -2
- package/esm/adapters/hono/resource-server.js +3 -2
- package/esm/client/_http.d.ts +4 -7
- package/esm/client/_http.js +4 -37
- package/esm/client/base.d.ts +3 -6
- package/esm/client/bff-client.d.ts +7 -6
- package/esm/client/bff-client.js +7 -6
- package/esm/client/browser-storage.d.ts +33 -34
- package/esm/client/browser-storage.js +47 -32
- package/esm/client/check.d.ts +10 -3
- package/esm/client/check.js +28 -8
- package/esm/client/direct-client.d.ts +49 -29
- package/esm/client/direct-client.js +59 -59
- package/esm/client/discovery-cache.d.ts +12 -5
- package/esm/client/discovery-cache.js +11 -4
- package/esm/client/events.d.ts +11 -3
- package/esm/client/events.js +1 -1
- package/esm/client/mod.d.ts +6 -4
- package/esm/client/mod.js +5 -3
- package/esm/client/storage.d.ts +53 -23
- package/esm/client/storage.js +43 -15
- package/esm/errors.d.ts +15 -12
- package/esm/errors.js +12 -8
- package/esm/identity/delivery.d.ts +7 -7
- package/esm/identity/delivery.js +1 -2
- package/esm/identity/errors.d.ts +10 -4
- package/esm/identity/errors.js +4 -1
- package/esm/identity/events.d.ts +21 -5
- package/esm/identity/events.js +4 -2
- package/esm/identity/external/_shared.d.ts +5 -7
- package/esm/identity/external/_shared.js +5 -7
- package/esm/identity/external/_token-exchange.d.ts +3 -6
- package/esm/identity/external/_token-exchange.js +2 -5
- package/esm/identity/external/apple-client-secret.d.ts +13 -8
- package/esm/identity/external/apple-client-secret.js +9 -6
- package/esm/identity/external/apple.d.ts +17 -10
- package/esm/identity/external/apple.js +12 -9
- package/esm/identity/external/discord.d.ts +5 -1
- package/esm/identity/external/errors.d.ts +6 -2
- package/esm/identity/external/errors.js +6 -2
- package/esm/identity/external/flow.d.ts +9 -6
- package/esm/identity/external/flow.js +6 -5
- package/esm/identity/external/github.d.ts +7 -3
- package/esm/identity/external/github.js +2 -2
- package/esm/identity/external/google.d.ts +5 -1
- package/esm/identity/external/oauth2.d.ts +13 -2
- package/esm/identity/external/oauth2.js +3 -3
- package/esm/identity/external/oidc.d.ts +21 -10
- package/esm/identity/external/oidc.js +11 -6
- package/esm/identity/external/provider.d.ts +4 -2
- package/esm/identity/identifier.d.ts +6 -5
- package/esm/identity/identifier.js +6 -5
- package/esm/identity/lockout.d.ts +4 -3
- package/esm/identity/lockout.js +4 -3
- package/esm/identity/mfa/mod.d.ts +2 -1
- package/esm/identity/mfa/mod.js +2 -1
- package/esm/identity/mfa/recovery-codes.d.ts +2 -1
- package/esm/identity/mfa/service.d.ts +29 -11
- package/esm/identity/mfa/service.js +35 -11
- package/esm/identity/mfa/totp.d.ts +4 -2
- package/esm/identity/mfa/totp.js +4 -2
- package/esm/identity/migration.d.ts +8 -7
- package/esm/identity/migration.js +9 -12
- package/esm/identity/mod.d.ts +2 -2
- package/esm/identity/mod.js +2 -2
- package/esm/identity/otp.d.ts +23 -16
- package/esm/identity/otp.js +13 -12
- package/esm/identity/password-policy.d.ts +3 -3
- package/esm/identity/password-policy.js +3 -3
- package/esm/identity/password.d.ts +23 -20
- package/esm/identity/password.js +15 -16
- package/esm/identity/rate-limit.d.ts +1 -7
- package/esm/identity/rate-limit.js +1 -2
- package/esm/identity/service.d.ts +76 -44
- package/esm/identity/service.js +86 -60
- package/esm/identity/session.d.ts +5 -4
- package/esm/identity/session.js +5 -4
- package/esm/identity/token-flow.d.ts +8 -8
- package/esm/identity/token-flow.js +5 -6
- package/esm/models/client.d.ts +7 -4
- package/esm/models/token.d.ts +2 -3
- package/esm/react/callback.d.ts +3 -3
- package/esm/react/callback.js +3 -3
- package/esm/react/components/class-names.d.ts +3 -6
- package/esm/react/components/class-names.js +3 -6
- package/esm/react/components/internal.d.ts +7 -17
- package/esm/react/components/internal.js +7 -17
- package/esm/react/components/mod.d.ts +7 -5
- package/esm/react/components/mod.js +7 -5
- package/esm/react/components/types.d.ts +8 -6
- package/esm/react/context.d.ts +3 -3
- package/esm/react/provider.d.ts +3 -2
- package/esm/react/provider.js +0 -13
- package/esm/react/require-auth.d.ts +0 -3
- package/esm/react/require-auth.js +0 -3
- package/esm/react/testing.d.ts +3 -2
- package/esm/react/testing.js +3 -3
- package/esm/react/use-oauth2.d.ts +1 -1
- package/esm/server/authorization-server.d.ts +81 -54
- package/esm/server/authorization-server.js +76 -100
- package/esm/server/grants/authorization-code.d.ts +7 -3
- package/esm/server/grants/authorization-code.js +6 -2
- package/esm/server/grants/device-authorization.d.ts +1 -1
- package/esm/server/grants/grant.d.ts +15 -8
- package/esm/server/grants/grant.js +26 -18
- package/esm/server/grants/password.d.ts +1 -1
- package/esm/server/grants/refresh-token.d.ts +6 -3
- package/esm/server/grants/refresh-token.js +4 -3
- package/esm/server/introspection-token-reader.d.ts +16 -7
- package/esm/server/introspection-token-reader.js +9 -3
- package/esm/server/jwks-token-reader.d.ts +3 -3
- package/esm/server/jwks-token-reader.js +3 -3
- package/esm/server/redirect-uri.d.ts +6 -9
- package/esm/server/redirect-uri.js +7 -34
- package/esm/server/resource-server.d.ts +5 -3
- package/esm/server/resource-server.js +5 -3
- package/esm/server/services/client.d.ts +5 -1
- package/esm/server/services/device-authorization.d.ts +6 -6
- package/esm/server/services/token.d.ts +15 -8
- package/esm/server/services/token.js +1 -1
- package/esm/server/signing-keys.d.ts +24 -9
- package/esm/server/signing-keys.js +20 -9
- package/esm/testing/contract/mod.d.ts +3 -3
- package/esm/testing/contract/mod.js +3 -3
- package/esm/testing/contract/tenant.d.ts +1 -1
- package/esm/testing/contract/tenant.js +2 -2
- package/esm/testing/mod.d.ts +6 -4
- package/esm/testing/mod.js +6 -4
- package/esm/testing/services.d.ts +1 -1
- package/esm/testing/services.js +1 -1
- package/esm/testing/tenant.d.ts +14 -5
- package/esm/utils/_buffer.d.ts +3 -2
- package/esm/utils/_buffer.js +3 -2
- package/esm/utils/_timing-safe.d.ts +8 -6
- package/esm/utils/_timing-safe.js +8 -6
- package/esm/utils/basic-auth.d.ts +5 -12
- package/esm/utils/basic-auth.js +5 -12
- package/esm/utils/crypto.d.ts +2 -1
- package/esm/utils/crypto.js +2 -1
- package/esm/utils/text.d.ts +2 -9
- package/esm/utils/text.js +2 -9
- package/esm/utils/url.d.ts +2 -2
- package/esm/utils/url.js +0 -2
- package/package.json +1 -1
|
@@ -110,7 +110,9 @@ export interface MfaServiceOptions {
|
|
|
110
110
|
* Throttles {@link verify} per user id (a 6-digit code is brute-forceable
|
|
111
111
|
* without one). When the limit is hit the service emits
|
|
112
112
|
* `mfa.verify.rate_limited` and — under `protectionMode: "enforce"` —
|
|
113
|
-
* throws an `IdentityError` with code `rate_limited
|
|
113
|
+
* throws an `IdentityError` with code `rate_limited`; a limiter whose
|
|
114
|
+
* `check` throws is treated as limited under `"enforce"` and allowed under
|
|
115
|
+
* `"log-only"`. A successful
|
|
114
116
|
* verification resets the user's window, so legitimate sign-ins never
|
|
115
117
|
* accumulate toward a lockout.
|
|
116
118
|
*/
|
|
@@ -236,7 +238,8 @@ export declare class MfaService {
|
|
|
236
238
|
* Builds the service around an app-owned {@link MfaStore}.
|
|
237
239
|
*
|
|
238
240
|
* @throws {TypeError} on unusable configuration — non-positive-integer
|
|
239
|
-
* `digits`/`periodSeconds`/`recoveryCodeCount` or negative
|
|
241
|
+
* `digits`/`periodSeconds`/`recoveryCodeCount` or a negative or non-integer
|
|
242
|
+
* `windows` — so a
|
|
240
243
|
* miswired deploy fails at construction instead of silently rejecting every
|
|
241
244
|
* code an enrolled user submits.
|
|
242
245
|
*/
|
|
@@ -257,32 +260,47 @@ export declare class MfaService {
|
|
|
257
260
|
* and stored, and the plaintext codes are returned for one-time display.
|
|
258
261
|
* Returns `{ confirmed: false }` when the code is wrong, no enrollment is
|
|
259
262
|
* pending, or the pending secret changed under a concurrent enrollment. If
|
|
260
|
-
* storing the recovery codes fails after activation, the
|
|
261
|
-
*
|
|
262
|
-
* user *not* enrolled
|
|
263
|
-
*
|
|
263
|
+
* storing the recovery codes fails after activation, the service calls
|
|
264
|
+
* `clearTotp` to roll the credential back and rethrows the original error,
|
|
265
|
+
* leaving the user *not* enrolled and free to retry. If `clearTotp` also
|
|
266
|
+
* fails, its error is thrown instead and the credential stays active
|
|
267
|
+
* without recovery codes.
|
|
268
|
+
*
|
|
269
|
+
* Guesses against the pending secret count toward the same per-user window
|
|
270
|
+
* {@link verify} uses (`mfa:verify:<userId>`), and a confirmed enrollment
|
|
271
|
+
* resets it — an unthrottled confirm would otherwise be a brute-force path
|
|
272
|
+
* to a secret the user is about to rely on.
|
|
264
273
|
*
|
|
265
274
|
* @throws {IdentityError} `mfa_already_enrolled` when an active credential
|
|
266
275
|
* exists (see {@link startEnrollment}).
|
|
276
|
+
* @throws {IdentityError} `rate_limited` when a `rateLimiter` is configured,
|
|
277
|
+
* `protectionMode` is `"enforce"`, and the per-user limit is hit or the
|
|
278
|
+
* limiter's `check` throws (it fails closed).
|
|
267
279
|
*/
|
|
268
280
|
confirmEnrollment(userId: string, code: string, options?: Pick<MfaVerifyOptions, "timestamp">): Promise<MfaEnrollmentConfirmation>;
|
|
269
281
|
/**
|
|
270
282
|
* Verify a second-factor code: first as TOTP against the *active* secret
|
|
271
283
|
* (pending secrets are ignored — they need {@link confirmEnrollment}), then
|
|
272
284
|
* as a recovery code (restrict with {@link MfaVerifyOptions.method}). An
|
|
273
|
-
* accepted TOTP code
|
|
274
|
-
* code is rejected on replay
|
|
275
|
-
* recovery code is
|
|
285
|
+
* accepted TOTP code advances the stored last step through
|
|
286
|
+
* {@link MfaStore.advanceLastStep}, so the same code is rejected on replay;
|
|
287
|
+
* an accepted recovery code is burned through
|
|
288
|
+
* {@link MfaStore.consumeRecoveryHash}, so it is single-use. Both hold under
|
|
289
|
+
* concurrent submission only if the store implements those methods
|
|
290
|
+
* atomically. Without an
|
|
276
291
|
* active credential every code is rejected — recovery codes are a fallback
|
|
277
292
|
* *for* the enrolled factor, so stale hashes can never authenticate a user
|
|
278
293
|
* whose MFA was disabled. A successful verification resets the per-user
|
|
279
|
-
* rate-limit window
|
|
294
|
+
* rate-limit window; a `reset` that throws is logged and does not fail the
|
|
295
|
+
* verification, since the code is already spent by then — the window just
|
|
296
|
+
* stays filled until it lapses. On failure the result carries a
|
|
280
297
|
* {@link MfaVerifyFailureReason} — `"replayed"` for a valid-but-spent TOTP
|
|
281
298
|
* code, `"invalid"` otherwise — so a caller can distinguish a reused code
|
|
282
299
|
* from a wrong one without subscribing to `onEvent`.
|
|
283
300
|
*
|
|
284
301
|
* @throws {IdentityError} `rate_limited` when a `rateLimiter` is configured,
|
|
285
|
-
* the per-user limit is hit
|
|
302
|
+
* `protectionMode` is `"enforce"`, and the per-user limit is hit or the
|
|
303
|
+
* limiter's `check` throws (it fails closed).
|
|
286
304
|
*/
|
|
287
305
|
verify(userId: string, code: string, options?: MfaVerifyOptions): Promise<MfaVerification>;
|
|
288
306
|
/** Whether the user has an *active* (confirmed) TOTP credential. */
|
|
@@ -50,7 +50,8 @@ export class MfaService {
|
|
|
50
50
|
* Builds the service around an app-owned {@link MfaStore}.
|
|
51
51
|
*
|
|
52
52
|
* @throws {TypeError} on unusable configuration — non-positive-integer
|
|
53
|
-
* `digits`/`periodSeconds`/`recoveryCodeCount` or negative
|
|
53
|
+
* `digits`/`periodSeconds`/`recoveryCodeCount` or a negative or non-integer
|
|
54
|
+
* `windows` — so a
|
|
54
55
|
* miswired deploy fails at construction instead of silently rejecting every
|
|
55
56
|
* code an enrolled user submits.
|
|
56
57
|
*/
|
|
@@ -98,15 +99,25 @@ export class MfaService {
|
|
|
98
99
|
* and stored, and the plaintext codes are returned for one-time display.
|
|
99
100
|
* Returns `{ confirmed: false }` when the code is wrong, no enrollment is
|
|
100
101
|
* pending, or the pending secret changed under a concurrent enrollment. If
|
|
101
|
-
* storing the recovery codes fails after activation, the
|
|
102
|
-
*
|
|
103
|
-
* user *not* enrolled
|
|
104
|
-
*
|
|
102
|
+
* storing the recovery codes fails after activation, the service calls
|
|
103
|
+
* `clearTotp` to roll the credential back and rethrows the original error,
|
|
104
|
+
* leaving the user *not* enrolled and free to retry. If `clearTotp` also
|
|
105
|
+
* fails, its error is thrown instead and the credential stays active
|
|
106
|
+
* without recovery codes.
|
|
107
|
+
*
|
|
108
|
+
* Guesses against the pending secret count toward the same per-user window
|
|
109
|
+
* {@link verify} uses (`mfa:verify:<userId>`), and a confirmed enrollment
|
|
110
|
+
* resets it — an unthrottled confirm would otherwise be a brute-force path
|
|
111
|
+
* to a secret the user is about to rely on.
|
|
105
112
|
*
|
|
106
113
|
* @throws {IdentityError} `mfa_already_enrolled` when an active credential
|
|
107
114
|
* exists (see {@link startEnrollment}).
|
|
115
|
+
* @throws {IdentityError} `rate_limited` when a `rateLimiter` is configured,
|
|
116
|
+
* `protectionMode` is `"enforce"`, and the per-user limit is hit or the
|
|
117
|
+
* limiter's `check` throws (it fails closed).
|
|
108
118
|
*/
|
|
109
119
|
async confirmEnrollment(userId, code, options) {
|
|
120
|
+
await this.#throttle(userId);
|
|
110
121
|
const record = await this.#store.getTotp(userId);
|
|
111
122
|
this.#assertNotEnrolled(record);
|
|
112
123
|
const pending = record?.pendingSecretBase32;
|
|
@@ -131,6 +142,7 @@ export class MfaService {
|
|
|
131
142
|
await this.#store.clearTotp(userId);
|
|
132
143
|
throw error;
|
|
133
144
|
}
|
|
145
|
+
await this.#resetThrottle(userId);
|
|
134
146
|
await this.#emit({ type: "mfa.enrollment.confirmed", userId });
|
|
135
147
|
return { confirmed: true, recoveryCodes };
|
|
136
148
|
}
|
|
@@ -138,19 +150,25 @@ export class MfaService {
|
|
|
138
150
|
* Verify a second-factor code: first as TOTP against the *active* secret
|
|
139
151
|
* (pending secrets are ignored — they need {@link confirmEnrollment}), then
|
|
140
152
|
* as a recovery code (restrict with {@link MfaVerifyOptions.method}). An
|
|
141
|
-
* accepted TOTP code
|
|
142
|
-
* code is rejected on replay
|
|
143
|
-
* recovery code is
|
|
153
|
+
* accepted TOTP code advances the stored last step through
|
|
154
|
+
* {@link MfaStore.advanceLastStep}, so the same code is rejected on replay;
|
|
155
|
+
* an accepted recovery code is burned through
|
|
156
|
+
* {@link MfaStore.consumeRecoveryHash}, so it is single-use. Both hold under
|
|
157
|
+
* concurrent submission only if the store implements those methods
|
|
158
|
+
* atomically. Without an
|
|
144
159
|
* active credential every code is rejected — recovery codes are a fallback
|
|
145
160
|
* *for* the enrolled factor, so stale hashes can never authenticate a user
|
|
146
161
|
* whose MFA was disabled. A successful verification resets the per-user
|
|
147
|
-
* rate-limit window
|
|
162
|
+
* rate-limit window; a `reset` that throws is logged and does not fail the
|
|
163
|
+
* verification, since the code is already spent by then — the window just
|
|
164
|
+
* stays filled until it lapses. On failure the result carries a
|
|
148
165
|
* {@link MfaVerifyFailureReason} — `"replayed"` for a valid-but-spent TOTP
|
|
149
166
|
* code, `"invalid"` otherwise — so a caller can distinguish a reused code
|
|
150
167
|
* from a wrong one without subscribing to `onEvent`.
|
|
151
168
|
*
|
|
152
169
|
* @throws {IdentityError} `rate_limited` when a `rateLimiter` is configured,
|
|
153
|
-
* the per-user limit is hit
|
|
170
|
+
* `protectionMode` is `"enforce"`, and the per-user limit is hit or the
|
|
171
|
+
* limiter's `check` throws (it fails closed).
|
|
154
172
|
*/
|
|
155
173
|
async verify(userId, code, options) {
|
|
156
174
|
await this.#throttle(userId);
|
|
@@ -276,7 +294,13 @@ export class MfaService {
|
|
|
276
294
|
});
|
|
277
295
|
}
|
|
278
296
|
async #resetThrottle(userId) {
|
|
279
|
-
|
|
297
|
+
try {
|
|
298
|
+
await this.#rateLimiter?.reset(this.#rateKey(userId));
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
console.error("[@udibo/oauth2] MFA rate-limit reset failed; the window stays " +
|
|
302
|
+
"filled until it lapses:", error instanceof Error ? error.message : error);
|
|
303
|
+
}
|
|
280
304
|
}
|
|
281
305
|
async #verifyFailed(userId, reason) {
|
|
282
306
|
await this.#emit({ type: "mfa.verify.failed", userId, reason });
|
|
@@ -87,8 +87,10 @@ export interface OtpauthUriOptions {
|
|
|
87
87
|
* `issuer` query parameter, so apps that read either convention group the
|
|
88
88
|
* entry correctly.
|
|
89
89
|
*
|
|
90
|
-
* @throws {TypeError} if the secret
|
|
91
|
-
*
|
|
90
|
+
* @throws {TypeError} if the secret contains characters outside the base32
|
|
91
|
+
* alphabet (after stripping whitespace and `=`) or `digits`/`periodSeconds`
|
|
92
|
+
* are not positive integers, so a corrupted value cannot inject URI
|
|
93
|
+
* parameters. The secret's length is not checked.
|
|
92
94
|
*/
|
|
93
95
|
export declare function buildOtpauthUri(options: OtpauthUriOptions): string;
|
|
94
96
|
/** Options for {@link generateTotpCode}. */
|
package/esm/identity/mfa/totp.js
CHANGED
|
@@ -80,8 +80,10 @@ export function generateTotpSecret() {
|
|
|
80
80
|
* `issuer` query parameter, so apps that read either convention group the
|
|
81
81
|
* entry correctly.
|
|
82
82
|
*
|
|
83
|
-
* @throws {TypeError} if the secret
|
|
84
|
-
*
|
|
83
|
+
* @throws {TypeError} if the secret contains characters outside the base32
|
|
84
|
+
* alphabet (after stripping whitespace and `=`) or `digits`/`periodSeconds`
|
|
85
|
+
* are not positive integers, so a corrupted value cannot inject URI
|
|
86
|
+
* parameters. The secret's length is not checked.
|
|
85
87
|
*/
|
|
86
88
|
export function buildOtpauthUri(options) {
|
|
87
89
|
validateTotpParams(options);
|
|
@@ -77,9 +77,9 @@ export declare function findLegacyVerifier(verifiers: readonly LegacyPasswordVer
|
|
|
77
77
|
/**
|
|
78
78
|
* Verify `password` against a stored foreign hash `phc` using the first matching
|
|
79
79
|
* verifier in `verifiers`. Resolves `false` when no verifier recognizes `phc` or
|
|
80
|
-
* the matched verifier rejects the password. Useful for a
|
|
81
|
-
* dry-run/validation pass; {@link IdentityService.signIn} uses the
|
|
82
|
-
* selection
|
|
80
|
+
* the matched verifier rejects the password or throws. Useful for a
|
|
81
|
+
* bulk-import dry-run/validation pass; {@link IdentityService.signIn} uses the
|
|
82
|
+
* same selection for upgrade-on-login.
|
|
83
83
|
*/
|
|
84
84
|
export declare function verifyLegacyPassword(verifiers: readonly LegacyPasswordVerifier[], password: string, phc: string): Promise<boolean>;
|
|
85
85
|
/**
|
|
@@ -100,8 +100,8 @@ export interface PhcHash {
|
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* Parse a PHC / modular-crypt string (`$id[$v=n][$k=v,...]$salt$hash`) into its
|
|
103
|
-
* parts. Returns `null` for anything that isn't a `$`-led PHC string
|
|
104
|
-
* hash
|
|
103
|
+
* parts. Returns `null` for anything that isn't a `$`-led PHC string, or whose
|
|
104
|
+
* salt or hash segment is not valid base64 (padding optional). This is the parsing helper for
|
|
105
105
|
* writing a bring-your-own argon2/scrypt {@link LegacyPasswordVerifier}: parse
|
|
106
106
|
* the stored string, feed `params`/`salt`/`hash` to your KDF dependency, and
|
|
107
107
|
* constant-time compare.
|
|
@@ -128,8 +128,9 @@ export interface Pbkdf2VerifierOptions {
|
|
|
128
128
|
* - **Django** — `pbkdf2_<digest>$<iterations>$<salt>$<base64 hash>`, where the
|
|
129
129
|
* salt is used as raw UTF-8 bytes and the hash is standard base64. This is
|
|
130
130
|
* Django's `PBKDF2PasswordHasher` output.
|
|
131
|
-
* - **PHC** — `$pbkdf2-<digest>$i=<iterations>$<base64 salt>$<base64 hash
|
|
132
|
-
* with base64 (padding optional)
|
|
131
|
+
* - **PHC** — `$pbkdf2-<digest>$i=<iterations>$<base64 salt>$<base64 hash>`
|
|
132
|
+
* (`rounds=` is accepted in place of `i=`), with base64 (padding optional)
|
|
133
|
+
* salt and hash.
|
|
133
134
|
*
|
|
134
135
|
* `<digest>` is one of `sha1`, `sha256`, `sha512`. The derived-key length is
|
|
135
136
|
* taken from the stored hash. Comparison is constant-time. On a match,
|
|
@@ -45,9 +45,9 @@ export function findLegacyVerifier(verifiers, phc) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Verify `password` against a stored foreign hash `phc` using the first matching
|
|
47
47
|
* verifier in `verifiers`. Resolves `false` when no verifier recognizes `phc` or
|
|
48
|
-
* the matched verifier rejects the password. Useful for a
|
|
49
|
-
* dry-run/validation pass; {@link IdentityService.signIn} uses the
|
|
50
|
-
* selection
|
|
48
|
+
* the matched verifier rejects the password or throws. Useful for a
|
|
49
|
+
* bulk-import dry-run/validation pass; {@link IdentityService.signIn} uses the
|
|
50
|
+
* same selection for upgrade-on-login.
|
|
51
51
|
*/
|
|
52
52
|
export async function verifyLegacyPassword(verifiers, password, phc) {
|
|
53
53
|
const verifier = findLegacyVerifier(verifiers, phc);
|
|
@@ -66,8 +66,8 @@ function decodeB64(segment) {
|
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
68
|
* Parse a PHC / modular-crypt string (`$id[$v=n][$k=v,...]$salt$hash`) into its
|
|
69
|
-
* parts. Returns `null` for anything that isn't a `$`-led PHC string
|
|
70
|
-
* hash
|
|
69
|
+
* parts. Returns `null` for anything that isn't a `$`-led PHC string, or whose
|
|
70
|
+
* salt or hash segment is not valid base64 (padding optional). This is the parsing helper for
|
|
71
71
|
* writing a bring-your-own argon2/scrypt {@link LegacyPasswordVerifier}: parse
|
|
72
72
|
* the stored string, feed `params`/`salt`/`hash` to your KDF dependency, and
|
|
73
73
|
* constant-time compare.
|
|
@@ -126,11 +126,7 @@ const PBKDF2_DIGEST = {
|
|
|
126
126
|
sha512: "SHA-512",
|
|
127
127
|
};
|
|
128
128
|
const encoder = new TextEncoder();
|
|
129
|
-
|
|
130
|
-
* Reject an imported hash whose derived-key segment is implausibly short: a
|
|
131
|
-
* 0-byte checksum would make `deriveBits(0)` compare empty-to-empty and turn
|
|
132
|
-
* any password into a match on a runtime where that returns an empty buffer.
|
|
133
|
-
*/
|
|
129
|
+
// A 0-byte checksum would compare empty-to-empty and match any password.
|
|
134
130
|
const MIN_HASH_BYTES = 16;
|
|
135
131
|
const MAX_PBKDF2_ITERATIONS = 1_000_000;
|
|
136
132
|
function parseDjango(phc) {
|
|
@@ -192,8 +188,9 @@ function parsePbkdf2(phc) {
|
|
|
192
188
|
* - **Django** — `pbkdf2_<digest>$<iterations>$<salt>$<base64 hash>`, where the
|
|
193
189
|
* salt is used as raw UTF-8 bytes and the hash is standard base64. This is
|
|
194
190
|
* Django's `PBKDF2PasswordHasher` output.
|
|
195
|
-
* - **PHC** — `$pbkdf2-<digest>$i=<iterations>$<base64 salt>$<base64 hash
|
|
196
|
-
* with base64 (padding optional)
|
|
191
|
+
* - **PHC** — `$pbkdf2-<digest>$i=<iterations>$<base64 salt>$<base64 hash>`
|
|
192
|
+
* (`rounds=` is accepted in place of `i=`), with base64 (padding optional)
|
|
193
|
+
* salt and hash.
|
|
197
194
|
*
|
|
198
195
|
* `<digest>` is one of `sha1`, `sha256`, `sha512`. The derived-key length is
|
|
199
196
|
* taken from the stored hash. Comparison is constant-time. On a match,
|
package/esm/identity/mod.d.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - {@link IdentityService} — orchestrates the primitives below into sign-up,
|
|
10
10
|
* sign-in, password-reset, and email-verification flows you call from your
|
|
11
11
|
* own routes (storage stays an app-owned {@link IdentityUserStore}).
|
|
12
|
-
* - {@link PasswordIdentityService} — PBKDF2 password hashing
|
|
13
|
-
*
|
|
12
|
+
* - {@link PasswordIdentityService} — PBKDF2 password hashing with a
|
|
13
|
+
* constant-time digest comparison; implement {@link PasswordHasherLike}
|
|
14
14
|
* instead to plug in argon2 or scrypt.
|
|
15
15
|
* - {@link TokenFlowService} — single-use, hashed-at-rest verify/reset/sign-in
|
|
16
16
|
* tokens over an app-owned {@link TokenFlowStore} (magic links reuse it via
|
package/esm/identity/mod.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - {@link IdentityService} — orchestrates the primitives below into sign-up,
|
|
10
10
|
* sign-in, password-reset, and email-verification flows you call from your
|
|
11
11
|
* own routes (storage stays an app-owned {@link IdentityUserStore}).
|
|
12
|
-
* - {@link PasswordIdentityService} — PBKDF2 password hashing
|
|
13
|
-
*
|
|
12
|
+
* - {@link PasswordIdentityService} — PBKDF2 password hashing with a
|
|
13
|
+
* constant-time digest comparison; implement {@link PasswordHasherLike}
|
|
14
14
|
* instead to plug in argon2 or scrypt.
|
|
15
15
|
* - {@link TokenFlowService} — single-use, hashed-at-rest verify/reset/sign-in
|
|
16
16
|
* tokens over an app-owned {@link TokenFlowStore} (magic links reuse it via
|
package/esm/identity/otp.d.ts
CHANGED
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A short numeric code has a different threat model than a URL token: it is
|
|
6
6
|
* **guessable online** (a 6-digit code is one in a million), so unlike
|
|
7
|
-
* {@link TokenFlowService} tokens every code carries an attempt budget —
|
|
8
|
-
*
|
|
9
|
-
* spent. Codes are stored **hashed** (SHA-256, domain-bound to
|
|
10
|
-
* purpose) and
|
|
11
|
-
* in the delivery callback.
|
|
7
|
+
* {@link TokenFlowService} tokens every code carries an attempt budget — each
|
|
8
|
+
* verification attempt is counted and the code is invalidated (`locked`) when
|
|
9
|
+
* the budget is spent. Codes are stored **hashed** (SHA-256, domain-bound to
|
|
10
|
+
* the email + purpose) and the hashes are compared in constant time; the raw
|
|
11
|
+
* code exists only in the delivery callback.
|
|
12
12
|
*
|
|
13
13
|
* Storage is an app-owned {@link OtpStore} (a {@link MemoryOtpStore} ships for
|
|
14
14
|
* dev/tests) and delivery is a caller-supplied callback — the package owns the
|
|
15
15
|
* lifecycle, never your tables or your mailer. `purpose` is a free string, so
|
|
16
|
-
*
|
|
17
|
-
* later.
|
|
16
|
+
* one service can issue sign-in, step-up, and verification codes.
|
|
18
17
|
*
|
|
19
18
|
* @module
|
|
20
19
|
*/
|
|
@@ -46,9 +45,15 @@ export interface OtpRecord {
|
|
|
46
45
|
codeHash: string;
|
|
47
46
|
/** Expiry, epoch ms. */
|
|
48
47
|
expiresAt: number;
|
|
49
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Verification attempts recorded so far — every checked attempt, counted
|
|
50
|
+
* before the code is compared, not only the wrong ones.
|
|
51
|
+
*/
|
|
50
52
|
attempts: number;
|
|
51
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Attempt budget; a wrong code on the attempt that reaches it invalidates
|
|
55
|
+
* the code.
|
|
56
|
+
*/
|
|
52
57
|
maxAttempts: number;
|
|
53
58
|
/** Created, epoch ms. */
|
|
54
59
|
createdAt: number;
|
|
@@ -158,8 +163,8 @@ export interface VerifyOtpOptions {
|
|
|
158
163
|
/**
|
|
159
164
|
* The outcome of {@link EmailOtpService.verify}. `invalid` covers both a wrong
|
|
160
165
|
* code and no active code — indistinguishable on purpose. `locked` is reported
|
|
161
|
-
*
|
|
162
|
-
* further tries report `invalid`.
|
|
166
|
+
* on the attempt that spends the budget (and on any concurrent attempt past
|
|
167
|
+
* it); afterwards the code is gone and further tries report `invalid`.
|
|
163
168
|
*/
|
|
164
169
|
export type VerifyOtpResult = {
|
|
165
170
|
status: "success";
|
|
@@ -172,8 +177,8 @@ export type VerifyOtpResult = {
|
|
|
172
177
|
};
|
|
173
178
|
/**
|
|
174
179
|
* Issues and verifies short-lived numeric one-time codes over an app-owned
|
|
175
|
-
* {@link OtpStore}. Purpose-generic:
|
|
176
|
-
*
|
|
180
|
+
* {@link OtpStore}. Purpose-generic: one instance can serve sign-in, step-up,
|
|
181
|
+
* and verification codes.
|
|
177
182
|
*
|
|
178
183
|
* @example
|
|
179
184
|
* ```ts
|
|
@@ -224,9 +229,11 @@ export declare class EmailOtpService {
|
|
|
224
229
|
invalidateCode(id: string): Promise<void>;
|
|
225
230
|
/**
|
|
226
231
|
* Check a submitted code against the active record for `(email, purpose)`.
|
|
227
|
-
* The
|
|
228
|
-
*
|
|
229
|
-
* attempt is
|
|
232
|
+
* The submitted code is hashed and a constant-time compare runs even when no
|
|
233
|
+
* active record exists. Timing is not fully uniform, though: an expired
|
|
234
|
+
* record returns before any attempt is recorded, and a live record adds the
|
|
235
|
+
* `recordAttempt` store write the no-record path skips. The attempt is
|
|
236
|
+
* reserved atomically — via the store's `recordAttempt` return
|
|
230
237
|
* value — **before** the compare, so concurrent verifies of one live code
|
|
231
238
|
* can't each test a guess against a stale pre-increment count: at most
|
|
232
239
|
* `maxAttempts` guesses ever reach the comparison. Spending the budget
|
package/esm/identity/otp.js
CHANGED
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A short numeric code has a different threat model than a URL token: it is
|
|
6
6
|
* **guessable online** (a 6-digit code is one in a million), so unlike
|
|
7
|
-
* {@link TokenFlowService} tokens every code carries an attempt budget —
|
|
8
|
-
*
|
|
9
|
-
* spent. Codes are stored **hashed** (SHA-256, domain-bound to
|
|
10
|
-
* purpose) and
|
|
11
|
-
* in the delivery callback.
|
|
7
|
+
* {@link TokenFlowService} tokens every code carries an attempt budget — each
|
|
8
|
+
* verification attempt is counted and the code is invalidated (`locked`) when
|
|
9
|
+
* the budget is spent. Codes are stored **hashed** (SHA-256, domain-bound to
|
|
10
|
+
* the email + purpose) and the hashes are compared in constant time; the raw
|
|
11
|
+
* code exists only in the delivery callback.
|
|
12
12
|
*
|
|
13
13
|
* Storage is an app-owned {@link OtpStore} (a {@link MemoryOtpStore} ships for
|
|
14
14
|
* dev/tests) and delivery is a caller-supplied callback — the package owns the
|
|
15
15
|
* lifecycle, never your tables or your mailer. `purpose` is a free string, so
|
|
16
|
-
*
|
|
17
|
-
* later.
|
|
16
|
+
* one service can issue sign-in, step-up, and verification codes.
|
|
18
17
|
*
|
|
19
18
|
* @module
|
|
20
19
|
*/
|
|
@@ -59,8 +58,8 @@ const DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
|
59
58
|
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
60
59
|
/**
|
|
61
60
|
* Issues and verifies short-lived numeric one-time codes over an app-owned
|
|
62
|
-
* {@link OtpStore}. Purpose-generic:
|
|
63
|
-
*
|
|
61
|
+
* {@link OtpStore}. Purpose-generic: one instance can serve sign-in, step-up,
|
|
62
|
+
* and verification codes.
|
|
64
63
|
*
|
|
65
64
|
* @example
|
|
66
65
|
* ```ts
|
|
@@ -153,9 +152,11 @@ export class EmailOtpService {
|
|
|
153
152
|
}
|
|
154
153
|
/**
|
|
155
154
|
* Check a submitted code against the active record for `(email, purpose)`.
|
|
156
|
-
* The
|
|
157
|
-
*
|
|
158
|
-
* attempt is
|
|
155
|
+
* The submitted code is hashed and a constant-time compare runs even when no
|
|
156
|
+
* active record exists. Timing is not fully uniform, though: an expired
|
|
157
|
+
* record returns before any attempt is recorded, and a live record adds the
|
|
158
|
+
* `recordAttempt` store write the no-record path skips. The attempt is
|
|
159
|
+
* reserved atomically — via the store's `recordAttempt` return
|
|
159
160
|
* value — **before** the compare, so concurrent verifies of one live code
|
|
160
161
|
* can't each test a guess against a stale pre-increment count: at most
|
|
161
162
|
* `maxAttempts` guesses ever reach the comparison. Spending the budget
|
|
@@ -28,9 +28,9 @@ export interface PasswordPolicyResult {
|
|
|
28
28
|
/**
|
|
29
29
|
* Check a password against a policy, collecting all issues.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* A non-string `password` (an untyped value off a JSON body) fails with a
|
|
32
|
+
* single issue. A validator that throws rejects this call; use
|
|
33
|
+
* {@link assertPasswordPolicy} to have that surface as `weak_password`.
|
|
34
34
|
*/
|
|
35
35
|
export declare function checkPasswordPolicy(password: string, policy?: PasswordPolicy): Promise<PasswordPolicyResult>;
|
|
36
36
|
/**
|
|
@@ -9,9 +9,9 @@ import { IdentityError } from "./errors.js";
|
|
|
9
9
|
/**
|
|
10
10
|
* Check a password against a policy, collecting all issues.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* A non-string `password` (an untyped value off a JSON body) fails with a
|
|
13
|
+
* single issue. A validator that throws rejects this call; use
|
|
14
|
+
* {@link assertPasswordPolicy} to have that surface as `weak_password`.
|
|
15
15
|
*/
|
|
16
16
|
export async function checkPasswordPolicy(password, policy = {}) {
|
|
17
17
|
if (typeof password !== "string") {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Password hashing primitive for the identity self-service layer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* {@link PasswordHasherLike} with your own dependency and pass it wherever a
|
|
10
|
-
* hasher is accepted — nothing in the package requires the concrete class.
|
|
4
|
+
* The built-in algorithm is PBKDF2-SHA-256 at {@link DEFAULT_PBKDF2_ITERATIONS}
|
|
5
|
+
* iterations, with a constant-time comparison of the derived digest. Want
|
|
6
|
+
* argon2 or scrypt instead? Implement {@link PasswordHasherLike} with your own
|
|
7
|
+
* dependency and pass it wherever a hasher is accepted — nothing in the package
|
|
8
|
+
* requires the concrete class.
|
|
11
9
|
*
|
|
12
10
|
* **Storage is yours.** Persist the whole {@link PasswordCredential} — including
|
|
13
11
|
* its `params` — on your user record and pass it back to
|
|
@@ -32,10 +30,9 @@ export declare function generateSalt(length?: number): string;
|
|
|
32
30
|
export declare const DEFAULT_PBKDF2_ITERATIONS = 600000;
|
|
33
31
|
/**
|
|
34
32
|
* The iteration count a {@link PasswordCredential} with no `params` is assumed
|
|
35
|
-
* to have been hashed at
|
|
36
|
-
* before `params` existed keep verifying against this, and
|
|
33
|
+
* to have been hashed at. Such credentials keep verifying against this, and
|
|
37
34
|
* {@link PasswordIdentityService.needsRehash} reports them as due for an
|
|
38
|
-
* upgrade.
|
|
35
|
+
* upgrade whenever the service mints at a higher count.
|
|
39
36
|
*/
|
|
40
37
|
export declare const LEGACY_PBKDF2_ITERATIONS = 100000;
|
|
41
38
|
/** The algorithm identifier {@link PasswordIdentityService} records and accepts. */
|
|
@@ -51,8 +48,8 @@ export interface PasswordHashParams {
|
|
|
51
48
|
iterations: number;
|
|
52
49
|
}
|
|
53
50
|
/**
|
|
54
|
-
* A stored password credential: the hash, the salt it used, and
|
|
55
|
-
*
|
|
51
|
+
* A stored password credential: the hash, the salt it used, and the work
|
|
52
|
+
* factor behind it.
|
|
56
53
|
*
|
|
57
54
|
* Persist all three fields. A credential with no `params` is interpreted as
|
|
58
55
|
* PBKDF2-SHA-256 at {@link LEGACY_PBKDF2_ITERATIONS}.
|
|
@@ -73,12 +70,16 @@ export interface PasswordCredential {
|
|
|
73
70
|
*
|
|
74
71
|
* Implement the optional `needsRehash` to get rehash-on-successful-sign-in: the
|
|
75
72
|
* identity flows call it after a password verifies and, when it returns `true`,
|
|
76
|
-
* re-hash the just-verified password and persist the result
|
|
73
|
+
* re-hash the just-verified password and persist the result — provided the
|
|
74
|
+
* user store implements `replaceCredential`.
|
|
77
75
|
*/
|
|
78
76
|
export interface PasswordHasherLike {
|
|
79
77
|
/** Hashes a new password into a credential to store. */
|
|
80
78
|
hash(password: string): Promise<PasswordCredential>;
|
|
81
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Whether `password` matches the stored credential. Compare in constant time;
|
|
81
|
+
* resolve `false` rather than throw for a credential you cannot verify.
|
|
82
|
+
*/
|
|
82
83
|
verify(password: string, credential: PasswordCredential): Promise<boolean>;
|
|
83
84
|
/**
|
|
84
85
|
* Whether a stored credential is weaker than what this hasher mints today.
|
|
@@ -101,7 +102,8 @@ export interface PasswordHasherLike {
|
|
|
101
102
|
*/
|
|
102
103
|
export declare function hashPassword(password: string, salt: string, iterations?: number): Promise<string>;
|
|
103
104
|
/**
|
|
104
|
-
* Verifies a password against a stored hash in constant
|
|
105
|
+
* Verifies a password against a stored hash, comparing the digests in constant
|
|
106
|
+
* time.
|
|
105
107
|
*
|
|
106
108
|
* @param password The password to verify
|
|
107
109
|
* @param salt The salt used for the hash
|
|
@@ -120,8 +122,8 @@ export interface PasswordIdentityServiceOptions {
|
|
|
120
122
|
iterations?: number;
|
|
121
123
|
}
|
|
122
124
|
/**
|
|
123
|
-
* Hashes and verifies passwords (PBKDF2-SHA-256, constant-time
|
|
124
|
-
* Construct once and reuse.
|
|
125
|
+
* Hashes and verifies passwords (PBKDF2-SHA-256, constant-time digest
|
|
126
|
+
* comparison). Construct once and reuse.
|
|
125
127
|
*
|
|
126
128
|
* Verification reads the work factor off the credential, so credentials minted
|
|
127
129
|
* under an older default keep verifying; {@link needsRehash} tells you when one
|
|
@@ -157,9 +159,10 @@ export declare class PasswordIdentityService implements PasswordHasherLike {
|
|
|
157
159
|
/** Hashes a new password, generating a fresh random salt. */
|
|
158
160
|
hash(password: string): Promise<PasswordCredential>;
|
|
159
161
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* recorded under an algorithm this service does not
|
|
162
|
+
* Verifies a password against a stored credential at the credential's own
|
|
163
|
+
* recorded work factor, comparing digests in constant time. Resolves `false`
|
|
164
|
+
* for a credential recorded under an algorithm this service does not
|
|
165
|
+
* implement.
|
|
163
166
|
*/
|
|
164
167
|
verify(password: string, credential: PasswordCredential): Promise<boolean>;
|
|
165
168
|
/**
|
package/esm/identity/password.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Password hashing primitive for the identity self-service layer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* {@link PasswordHasherLike} with your own dependency and pass it wherever a
|
|
10
|
-
* hasher is accepted — nothing in the package requires the concrete class.
|
|
4
|
+
* The built-in algorithm is PBKDF2-SHA-256 at {@link DEFAULT_PBKDF2_ITERATIONS}
|
|
5
|
+
* iterations, with a constant-time comparison of the derived digest. Want
|
|
6
|
+
* argon2 or scrypt instead? Implement {@link PasswordHasherLike} with your own
|
|
7
|
+
* dependency and pass it wherever a hasher is accepted — nothing in the package
|
|
8
|
+
* requires the concrete class.
|
|
11
9
|
*
|
|
12
10
|
* **Storage is yours.** Persist the whole {@link PasswordCredential} — including
|
|
13
11
|
* its `params` — on your user record and pass it back to
|
|
@@ -39,10 +37,9 @@ const encoder = new TextEncoder();
|
|
|
39
37
|
export const DEFAULT_PBKDF2_ITERATIONS = 600_000;
|
|
40
38
|
/**
|
|
41
39
|
* The iteration count a {@link PasswordCredential} with no `params` is assumed
|
|
42
|
-
* to have been hashed at
|
|
43
|
-
* before `params` existed keep verifying against this, and
|
|
40
|
+
* to have been hashed at. Such credentials keep verifying against this, and
|
|
44
41
|
* {@link PasswordIdentityService.needsRehash} reports them as due for an
|
|
45
|
-
* upgrade.
|
|
42
|
+
* upgrade whenever the service mints at a higher count.
|
|
46
43
|
*/
|
|
47
44
|
export const LEGACY_PBKDF2_ITERATIONS = 100_000;
|
|
48
45
|
/** The algorithm identifier {@link PasswordIdentityService} records and accepts. */
|
|
@@ -70,7 +67,8 @@ export async function hashPassword(password, salt, iterations = DEFAULT_PBKDF2_I
|
|
|
70
67
|
return encodeHex(new Uint8Array(derivedBits, 0, 32));
|
|
71
68
|
}
|
|
72
69
|
/**
|
|
73
|
-
* Verifies a password against a stored hash in constant
|
|
70
|
+
* Verifies a password against a stored hash, comparing the digests in constant
|
|
71
|
+
* time.
|
|
74
72
|
*
|
|
75
73
|
* @param password The password to verify
|
|
76
74
|
* @param salt The salt used for the hash
|
|
@@ -86,8 +84,8 @@ export async function verifyPassword(password, salt, hash, iterations = DEFAULT_
|
|
|
86
84
|
return timingSafeEqual(encoder.encode(computedHash), encoder.encode(hash));
|
|
87
85
|
}
|
|
88
86
|
/**
|
|
89
|
-
* Hashes and verifies passwords (PBKDF2-SHA-256, constant-time
|
|
90
|
-
* Construct once and reuse.
|
|
87
|
+
* Hashes and verifies passwords (PBKDF2-SHA-256, constant-time digest
|
|
88
|
+
* comparison). Construct once and reuse.
|
|
91
89
|
*
|
|
92
90
|
* Verification reads the work factor off the credential, so credentials minted
|
|
93
91
|
* under an older default keep verifying; {@link needsRehash} tells you when one
|
|
@@ -134,9 +132,10 @@ export class PasswordIdentityService {
|
|
|
134
132
|
};
|
|
135
133
|
}
|
|
136
134
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* recorded under an algorithm this service does not
|
|
135
|
+
* Verifies a password against a stored credential at the credential's own
|
|
136
|
+
* recorded work factor, comparing digests in constant time. Resolves `false`
|
|
137
|
+
* for a credential recorded under an algorithm this service does not
|
|
138
|
+
* implement.
|
|
140
139
|
*/
|
|
141
140
|
verify(password, credential) {
|
|
142
141
|
const params = credential.params;
|