@udibo/oauth2 0.34.0 → 0.36.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.
@@ -208,11 +208,19 @@ export class MemoryTokenService {
208
208
  #tokensByCodeHash = new Map();
209
209
  #revokedByRefreshHash = new Map();
210
210
  #revokedFamilies = new Set();
211
+ /**
212
+ * Claims a code from the paired `authorizationCodeService` and saves the
213
+ * token issued from it in one step, or does neither, resolving `undefined`
214
+ * when the code was already claimed. Present only when the service was
215
+ * constructed with `authorizationCodeService`, so the grant falls back to
216
+ * two calls for a token service that has no code store to claim from.
217
+ */
218
+ exchangeCode;
211
219
  /**
212
220
  * Creates an empty token store.
213
221
  *
214
- * @param options Backing client/user services and optional default token
215
- * lifetimes.
222
+ * @param options Backing client/user services, optional default token
223
+ * lifetimes, and the code store that enables `exchangeCode`.
216
224
  */
217
225
  constructor(options) {
218
226
  this.#clientService = options.clientService;
@@ -221,6 +229,21 @@ export class MemoryTokenService {
221
229
  this.refreshTokenLifetime =
222
230
  options.refreshTokenLifetime ?? 14 * 24 * 60 * 60;
223
231
  this.refreshTokenMaxLifetime = options.refreshTokenMaxLifetime;
232
+ const codes = options.authorizationCodeService;
233
+ if (codes) {
234
+ this.exchangeCode = (authorizationCode, token) => this.#exchangeCode(codes, authorizationCode, token);
235
+ }
236
+ }
237
+ async #exchangeCode(codes, authorizationCode, token) {
238
+ const codeHash = await sha256Hash(authorizationCode.code);
239
+ const stored = await this.#prepare({
240
+ ...token,
241
+ code: authorizationCode.code,
242
+ });
243
+ if (!claimStoredCode(codes, codeHash))
244
+ return undefined;
245
+ this.#store(stored);
246
+ return token;
224
247
  }
225
248
  /**
226
249
  * Accepts the requested scope verbatim; the stub performs no scope
@@ -344,10 +367,16 @@ export class MemoryTokenService {
344
367
  if (stored.codeHash) {
345
368
  let bucket = this.#tokensByCodeHash.get(stored.codeHash);
346
369
  if (!bucket) {
347
- bucket = new Set();
370
+ bucket = {
371
+ clientId: stored.clientId,
372
+ accessTokenHashes: new Set(),
373
+ familyIds: new Set(),
374
+ };
348
375
  this.#tokensByCodeHash.set(stored.codeHash, bucket);
349
376
  }
350
- bucket.add(stored.accessTokenHash);
377
+ bucket.accessTokenHashes.add(stored.accessTokenHash);
378
+ if (stored.familyId)
379
+ bucket.familyIds.add(stored.familyId);
351
380
  }
352
381
  if (stored.refreshTokenHash) {
353
382
  if (stored.familyId && this.#revokedFamilies.has(stored.familyId)) {
@@ -460,6 +489,9 @@ export class MemoryTokenService {
460
489
  * live token was removed.
461
490
  */
462
491
  revokeFamily(familyId) {
492
+ return Promise.resolve(this.#revokeFamily(familyId));
493
+ }
494
+ #revokeFamily(familyId) {
463
495
  this.#revokedFamilies.add(familyId);
464
496
  let revoked = false;
465
497
  for (const [hash, stored] of this.#tokensByRefreshHash) {
@@ -476,18 +508,27 @@ export class MemoryTokenService {
476
508
  revoked = true;
477
509
  }
478
510
  }
479
- return Promise.resolve(revoked);
511
+ return revoked;
480
512
  }
481
513
  /**
482
514
  * Removes every token issued from the given authorization code, used when a
483
- * code is replayed. Resolves `true` when at least one token was removed.
515
+ * code is replayed, and revokes the rotation families of those tokens as
516
+ * {@linkcode revokeFamily} does, so their rotated successors end with them.
517
+ * Resolves `true` when tokens were issued from the code.
518
+ * Removes nothing and resolves `false` when `client` is given and is not the
519
+ * client the code's tokens were issued to.
484
520
  */
485
- async revokeCode(code) {
521
+ async revokeCode(code, client) {
486
522
  const codeHash = await sha256Hash(code);
487
- const tokenHashes = this.#tokensByCodeHash.get(codeHash);
488
- if (!tokenHashes || tokenHashes.size === 0)
523
+ const bucket = this.#tokensByCodeHash.get(codeHash);
524
+ if (!bucket || bucket.accessTokenHashes.size === 0)
525
+ return false;
526
+ if (client && client.id.toString() !== bucket.clientId.toString()) {
489
527
  return false;
490
- for (const accessTokenHash of tokenHashes) {
528
+ }
529
+ for (const familyId of bucket.familyIds)
530
+ this.#revokeFamily(familyId);
531
+ for (const accessTokenHash of bucket.accessTokenHashes) {
491
532
  const stored = this.#tokensByAccessHash.get(accessTokenHash);
492
533
  if (stored) {
493
534
  this.#tokensByAccessHash.delete(accessTokenHash);
@@ -500,6 +541,7 @@ export class MemoryTokenService {
500
541
  return true;
501
542
  }
502
543
  }
544
+ let claimStoredCode;
503
545
  /** In-memory `AuthorizationCodeServiceInterface` implementation. */
504
546
  export class MemoryAuthorizationCodeService {
505
547
  /** Authorization-code lifetime in seconds applied by {@linkcode expiresAt}. */
@@ -507,6 +549,9 @@ export class MemoryAuthorizationCodeService {
507
549
  #clientService;
508
550
  #userService;
509
551
  #codes = new Map();
552
+ static {
553
+ claimStoredCode = (service, codeHash) => service.#codes.delete(codeHash);
554
+ }
510
555
  /**
511
556
  * Creates an empty authorization-code store.
512
557
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@udibo/oauth2",
3
- "version": "0.34.0",
3
+ "version": "0.36.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",