@udibo/oauth2 0.43.0 → 0.44.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.
- package/dist/identity/service.d.ts +22 -5
- package/dist/identity/service.js +27 -8
- package/package.json +1 -1
|
@@ -45,7 +45,14 @@ export interface IdentityUserStore<User extends IdentityUser> {
|
|
|
45
45
|
create(profile: Record<string, unknown>, credential: PasswordCredential): Promise<User>;
|
|
46
46
|
/** Find a user by a sign-in identifier (email/username/phone). */
|
|
47
47
|
findByIdentifier(identifier: string): Promise<User | undefined>;
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Find a user by email (password-reset / sign-in-link requests).
|
|
50
|
+
*
|
|
51
|
+
* Also called when a mailed reset, sign-in or account-unlock link is
|
|
52
|
+
* redeemed: the link works only while this still resolves the address it
|
|
53
|
+
* was sent to to the same user. Normalize as at write time; a lookup that
|
|
54
|
+
* case-folds lets a link requested with different casing still redeem.
|
|
55
|
+
*/
|
|
49
56
|
findByEmail(email: string): Promise<User | undefined>;
|
|
50
57
|
/**
|
|
51
58
|
* The user's stored password credential, or `undefined`.
|
|
@@ -482,7 +489,10 @@ export declare class IdentityService<User extends IdentityUser> {
|
|
|
482
489
|
* was configured) revoke **all** of the user's sessions via
|
|
483
490
|
* `revokeAllByUser`. Clears any failed-attempt lockout — the emailed token
|
|
484
491
|
* proves the same account ownership the unlock flow does. Returns
|
|
485
|
-
* `{ userId }`, or `null` for an invalid/expired/used token
|
|
492
|
+
* `{ userId }`, or `null` for an invalid/expired/used token — including one
|
|
493
|
+
* whose address no longer finds this user through
|
|
494
|
+
* {@link IdentityUserStore.findByEmail}, because the account's email moved
|
|
495
|
+
* after the link was mailed. That refusal leaves the token unspent.
|
|
486
496
|
*
|
|
487
497
|
* A reset is the user's "lock everyone else out" action, so the outstanding
|
|
488
498
|
* passwordless credentials for the same account are voided too: sign-in links
|
|
@@ -558,7 +568,10 @@ export declare class IdentityService<User extends IdentityUser> {
|
|
|
558
568
|
* Mint + deliver a single-use account-unlock link after a lockout. Call it
|
|
559
569
|
* with the locked account's email (typically from your `lockout` event
|
|
560
570
|
* handler — the service only knows opaque user ids). Requires `tokens` and
|
|
561
|
-
* a `delivery.sendAccountUnlock` hook to actually send.
|
|
571
|
+
* a `delivery.sendAccountUnlock` hook to actually send. `email` must be an
|
|
572
|
+
* address {@link IdentityUserStore.findByEmail} resolves to `userId`:
|
|
573
|
+
* {@link IdentityService.unlockAccount} refuses the link once it does not.
|
|
574
|
+
* With a `rateLimiter`
|
|
562
575
|
* (or a tighter `rateLimiters.accountUnlock`), requests throttle per user and
|
|
563
576
|
* throw {@link IdentityError} `rate_limited` when exceeded.
|
|
564
577
|
*/
|
|
@@ -568,7 +581,9 @@ export declare class IdentityService<User extends IdentityUser> {
|
|
|
568
581
|
}): Promise<void>;
|
|
569
582
|
/**
|
|
570
583
|
* Consume an unlock token and clear the account's lockout state (failures
|
|
571
|
-
* and lock). The self-service unlock path: the emailed link lands here.
|
|
584
|
+
* and lock). The self-service unlock path: the emailed link lands here. A
|
|
585
|
+
* link whose address no longer finds this user through
|
|
586
|
+
* {@link IdentityUserStore.findByEmail} reports `invalid` and stays unspent.
|
|
572
587
|
*/
|
|
573
588
|
unlockAccount(token: string): Promise<UnlockAccountResult>;
|
|
574
589
|
/**
|
|
@@ -598,7 +613,9 @@ export declare class IdentityService<User extends IdentityUser> {
|
|
|
598
613
|
* stays your app's job. So does re-resolving the user: a user deleted or
|
|
599
614
|
* disabled since the link was minted must be treated as `invalid` by your
|
|
600
615
|
* user store, not signed in. `expired` and `invalid` are distinguished so
|
|
601
|
-
* the UI can offer a fresh link.
|
|
616
|
+
* the UI can offer a fresh link. A link whose address no longer finds this
|
|
617
|
+
* user through {@link IdentityUserStore.findByEmail} — the account's email
|
|
618
|
+
* moved after it was mailed — reports `invalid` and stays unspent.
|
|
602
619
|
*/
|
|
603
620
|
consumeSignInLink(token: string): Promise<ConsumeSignInLinkResult>;
|
|
604
621
|
/**
|
package/dist/identity/service.js
CHANGED
|
@@ -236,7 +236,6 @@ export class IdentityService {
|
|
|
236
236
|
hook: "sendPasswordReset",
|
|
237
237
|
to: normalized,
|
|
238
238
|
subject: user.id,
|
|
239
|
-
data: { email: normalized },
|
|
240
239
|
ttlMs: this.#ttl.passwordReset,
|
|
241
240
|
requested: {
|
|
242
241
|
type: "password_reset.requested",
|
|
@@ -250,7 +249,10 @@ export class IdentityService {
|
|
|
250
249
|
* was configured) revoke **all** of the user's sessions via
|
|
251
250
|
* `revokeAllByUser`. Clears any failed-attempt lockout — the emailed token
|
|
252
251
|
* proves the same account ownership the unlock flow does. Returns
|
|
253
|
-
* `{ userId }`, or `null` for an invalid/expired/used token
|
|
252
|
+
* `{ userId }`, or `null` for an invalid/expired/used token — including one
|
|
253
|
+
* whose address no longer finds this user through
|
|
254
|
+
* {@link IdentityUserStore.findByEmail}, because the account's email moved
|
|
255
|
+
* after the link was mailed. That refusal leaves the token unspent.
|
|
254
256
|
*
|
|
255
257
|
* A reset is the user's "lock everyone else out" action, so the outstanding
|
|
256
258
|
* passwordless credentials for the same account are voided too: sign-in links
|
|
@@ -298,6 +300,7 @@ export class IdentityService {
|
|
|
298
300
|
type: "password_reset.failed",
|
|
299
301
|
reason: "invalid_token",
|
|
300
302
|
}),
|
|
303
|
+
addressBound: true,
|
|
301
304
|
});
|
|
302
305
|
if (consumed.status !== "success")
|
|
303
306
|
return null;
|
|
@@ -371,7 +374,6 @@ export class IdentityService {
|
|
|
371
374
|
to: input.email,
|
|
372
375
|
subject: input.userId,
|
|
373
376
|
ttlMs: this.#ttl.emailVerification,
|
|
374
|
-
data: { email: input.email },
|
|
375
377
|
requested: {
|
|
376
378
|
type: "email_verification.requested",
|
|
377
379
|
userId: input.userId,
|
|
@@ -421,7 +423,10 @@ export class IdentityService {
|
|
|
421
423
|
* Mint + deliver a single-use account-unlock link after a lockout. Call it
|
|
422
424
|
* with the locked account's email (typically from your `lockout` event
|
|
423
425
|
* handler — the service only knows opaque user ids). Requires `tokens` and
|
|
424
|
-
* a `delivery.sendAccountUnlock` hook to actually send.
|
|
426
|
+
* a `delivery.sendAccountUnlock` hook to actually send. `email` must be an
|
|
427
|
+
* address {@link IdentityUserStore.findByEmail} resolves to `userId`:
|
|
428
|
+
* {@link IdentityService.unlockAccount} refuses the link once it does not.
|
|
429
|
+
* With a `rateLimiter`
|
|
425
430
|
* (or a tighter `rateLimiters.accountUnlock`), requests throttle per user and
|
|
426
431
|
* throw {@link IdentityError} `rate_limited` when exceeded.
|
|
427
432
|
*/
|
|
@@ -446,7 +451,9 @@ export class IdentityService {
|
|
|
446
451
|
}
|
|
447
452
|
/**
|
|
448
453
|
* Consume an unlock token and clear the account's lockout state (failures
|
|
449
|
-
* and lock). The self-service unlock path: the emailed link lands here.
|
|
454
|
+
* and lock). The self-service unlock path: the emailed link lands here. A
|
|
455
|
+
* link whose address no longer finds this user through
|
|
456
|
+
* {@link IdentityUserStore.findByEmail} reports `invalid` and stays unspent.
|
|
450
457
|
*/
|
|
451
458
|
async unlockAccount(token) {
|
|
452
459
|
const tokens = this.#requireTokens("unlockAccount");
|
|
@@ -455,6 +462,7 @@ export class IdentityService {
|
|
|
455
462
|
purpose: TokenPurpose.AccountUnlock,
|
|
456
463
|
token,
|
|
457
464
|
failed: (reason) => ({ type: "account_unlock.failed", reason }),
|
|
465
|
+
addressBound: true,
|
|
458
466
|
});
|
|
459
467
|
if (result.status !== "success")
|
|
460
468
|
return result;
|
|
@@ -527,7 +535,9 @@ export class IdentityService {
|
|
|
527
535
|
* stays your app's job. So does re-resolving the user: a user deleted or
|
|
528
536
|
* disabled since the link was minted must be treated as `invalid` by your
|
|
529
537
|
* user store, not signed in. `expired` and `invalid` are distinguished so
|
|
530
|
-
* the UI can offer a fresh link.
|
|
538
|
+
* the UI can offer a fresh link. A link whose address no longer finds this
|
|
539
|
+
* user through {@link IdentityUserStore.findByEmail} — the account's email
|
|
540
|
+
* moved after it was mailed — reports `invalid` and stays unspent.
|
|
531
541
|
*/
|
|
532
542
|
async consumeSignInLink(token) {
|
|
533
543
|
const tokens = this.#requireTokens("consumeSignInLink");
|
|
@@ -536,6 +546,7 @@ export class IdentityService {
|
|
|
536
546
|
purpose: TokenPurpose.SignIn,
|
|
537
547
|
token,
|
|
538
548
|
failed: (reason) => ({ type: "signin_link.failed", reason }),
|
|
549
|
+
addressBound: true,
|
|
539
550
|
});
|
|
540
551
|
if (result.status !== "success")
|
|
541
552
|
return result;
|
|
@@ -783,7 +794,7 @@ export class IdentityService {
|
|
|
783
794
|
const { token, expiresAt } = await tokens.create({
|
|
784
795
|
purpose: DELIVERY_PURPOSES[request.hook],
|
|
785
796
|
subject: request.subject,
|
|
786
|
-
data: request.
|
|
797
|
+
data: { email: request.to },
|
|
787
798
|
ttlMs: request.ttlMs,
|
|
788
799
|
invalidateExisting: true,
|
|
789
800
|
});
|
|
@@ -797,7 +808,9 @@ export class IdentityService {
|
|
|
797
808
|
await this.#emit(input.failed("expired"));
|
|
798
809
|
return { status: "expired" };
|
|
799
810
|
}
|
|
800
|
-
if (status.status !== "valid"
|
|
811
|
+
if (status.status !== "valid" ||
|
|
812
|
+
(input.addressBound &&
|
|
813
|
+
!(await this.#stillAddressedTo(status.subject, status.data)))) {
|
|
801
814
|
await this.#emit(input.failed("invalid"));
|
|
802
815
|
return { status: "invalid" };
|
|
803
816
|
}
|
|
@@ -809,6 +822,12 @@ export class IdentityService {
|
|
|
809
822
|
}
|
|
810
823
|
return { status: "success", resolved };
|
|
811
824
|
}
|
|
825
|
+
async #stillAddressedTo(subject, data) {
|
|
826
|
+
const email = emailFromData(data);
|
|
827
|
+
if (email === undefined)
|
|
828
|
+
return true;
|
|
829
|
+
return (await this.#users.findByEmail(email))?.id === subject;
|
|
830
|
+
}
|
|
812
831
|
#emit(event) {
|
|
813
832
|
return dispatchIdentityEvent(this.#onEvent, "identity", event);
|
|
814
833
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@udibo/oauth2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "A flexible OAuth2 toolkit: framework-agnostic authorization & resource servers, a typed client, Hono adapters (including a backend-for-frontend), and React bindings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authorization-server",
|