@c9up/visa 0.1.3 → 0.1.4

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 (47) hide show
  1. package/README.md +82 -11
  2. package/dist/VisaManager.d.ts +47 -0
  3. package/dist/VisaManager.d.ts.map +1 -1
  4. package/dist/VisaManager.js +169 -7
  5. package/dist/VisaManager.js.map +1 -1
  6. package/dist/VisaProvider.d.ts +4 -0
  7. package/dist/VisaProvider.d.ts.map +1 -1
  8. package/dist/VisaProvider.js +87 -5
  9. package/dist/VisaProvider.js.map +1 -1
  10. package/dist/configure.d.ts.map +1 -1
  11. package/dist/configure.js +16 -1
  12. package/dist/configure.js.map +1 -1
  13. package/dist/guard.d.ts +2 -1
  14. package/dist/guard.d.ts.map +1 -1
  15. package/dist/guard.js +12 -13
  16. package/dist/guard.js.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/register.d.ts.map +1 -1
  21. package/dist/register.js +9 -4
  22. package/dist/register.js.map +1 -1
  23. package/dist/store.d.ts +10 -0
  24. package/dist/store.d.ts.map +1 -1
  25. package/dist/stores/atlas.d.ts +1 -1
  26. package/dist/stores/atlas.d.ts.map +1 -1
  27. package/dist/stores/atlas.js +6 -1
  28. package/dist/stores/atlas.js.map +1 -1
  29. package/dist/stores/memory.d.ts +1 -2
  30. package/dist/stores/memory.d.ts.map +1 -1
  31. package/dist/stores/memory.js +1 -3
  32. package/dist/stores/memory.js.map +1 -1
  33. package/dist/token.js +17 -5
  34. package/dist/token.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/VisaManager.ts +212 -11
  37. package/src/VisaProvider.ts +108 -6
  38. package/src/configure.ts +17 -1
  39. package/src/guard.ts +12 -13
  40. package/src/index.ts +1 -0
  41. package/src/register.ts +11 -4
  42. package/src/store.ts +11 -0
  43. package/src/stores/atlas.ts +6 -1
  44. package/src/stores/memory.ts +1 -3
  45. package/src/token.ts +30 -11
  46. package/stubs/database/migrations/9000_create_visa_tables.stub +2 -0
  47. package/stubs/database/migrations/9001_add_visa_client_secret_expiry.stub +29 -0
package/src/token.ts CHANGED
@@ -224,6 +224,13 @@ async function exchangeRefresh(
224
224
  const requested = asString(request.scope);
225
225
  const scopes =
226
226
  requested === undefined ? record.scopes : narrow(requested, record.scopes);
227
+ const resources = narrowResources(
228
+ validateResources(
229
+ readResourceParameter(request.resource),
230
+ options.resourcesSupported,
231
+ ),
232
+ record.resources ?? [],
233
+ );
227
234
 
228
235
  if (!(await store.consumeRefreshToken(record.tokenHash, now))) throw invalid;
229
236
 
@@ -233,13 +240,18 @@ async function exchangeRefresh(
233
240
  ...(record.userId === undefined ? {} : { userId: record.userId }),
234
241
  scopes,
235
242
  familyId: record.familyId,
236
- resources: narrowResources(
237
- validateResources(
238
- readResourceParameter(request.resource),
239
- options.resourcesSupported,
240
- ),
241
- record.resources ?? [],
242
- ),
243
+ resources,
244
+ // Only the ACCESS token is narrowed. RFC 6749 §6: a rotated refresh
245
+ // token's scope "MUST be identical to that of the refresh token
246
+ // included by the client in the request" — otherwise asking for a
247
+ // down-scoped token for one call would cost the rest of the user's
248
+ // consent for the whole session. Resources follow the same rule:
249
+ // RFC 8707 has a client refresh once per resource it calls, which
250
+ // only works if a token for one leaves the others reachable.
251
+ refresh: {
252
+ scopes: record.scopes,
253
+ resources: record.resources ?? [],
254
+ },
243
255
  },
244
256
  store,
245
257
  options,
@@ -306,6 +318,13 @@ interface Issued {
306
318
  familyId?: string;
307
319
  /** RFC 8707 — what the token is for. Empty means unbound. */
308
320
  resources?: string[];
321
+ /**
322
+ * What the refresh token carries, when it differs from the access token.
323
+ *
324
+ * Absent, the refresh token carries the access token's scopes and
325
+ * resources — the case of a code exchange, where both halves are the grant.
326
+ */
327
+ refresh?: { scopes: string[]; resources: string[] };
309
328
  }
310
329
 
311
330
  /** Mint the pair and store both, hashed. */
@@ -347,18 +366,18 @@ async function issue(
347
366
  const refreshTtl =
348
367
  options.refreshTokenTtlSeconds ?? DEFAULTS.refreshTokenTtlSeconds;
349
368
  const refreshToken = randomToken();
369
+ const refreshScopes = grant.refresh?.scopes ?? grant.scopes;
370
+ const refreshResources = grant.refresh?.resources ?? grant.resources ?? [];
350
371
  await store.saveRefreshToken({
351
372
  tokenHash: hashSecret(refreshToken),
352
373
  familyId: grant.familyId,
353
374
  clientId: grant.client.id,
354
375
  ...(grant.userId === undefined ? {} : { userId: grant.userId }),
355
- scopes: grant.scopes,
376
+ scopes: refreshScopes,
356
377
  expiresAt: new Date(now.getTime() + refreshTtl * 1000),
357
378
  // Carried so a rotation can narrow from the same list the code bound —
358
379
  // without it, the second token of a session would be unbound.
359
- ...(grant.resources === undefined || grant.resources.length === 0
360
- ? {}
361
- : { resources: grant.resources }),
380
+ ...(refreshResources.length === 0 ? {} : { resources: refreshResources }),
362
381
  });
363
382
  response.refresh_token = refreshToken;
364
383
  return response;
@@ -26,6 +26,8 @@ export default class CreateVisaTables extends Migration {
26
26
  t.string("name", 255).notNullable();
27
27
  // Null for a public client — a SPA or a native app has no secret to keep.
28
28
  t.string("secret_hash", 255).nullable();
29
+ // `secret_expires_at` is added by 9001, so a table created before it
30
+ // existed gets it the same way a new one does.
29
31
  t.json("redirect_uris").notNullable();
30
32
  t.json("grant_types").notNullable();
31
33
  t.json("scopes").notNullable();
@@ -0,0 +1,29 @@
1
+ ---
2
+ to: database/migrations/9001_add_visa_client_secret_expiry.ts
3
+ ---
4
+ import { Migration } from "@c9up/atlas";
5
+
6
+ /**
7
+ * When a client's secret stops working (RFC 7591 §3.2.1).
8
+ *
9
+ * Its own migration rather than a line in 9000, so an application whose
10
+ * tables already exist gets the column by running `ream configure @c9up/visa`
11
+ * again: 9000 is already there and is left alone, this one lands and adds it.
12
+ *
13
+ * Null means the secret does not expire. The store reads it at every client
14
+ * authentication: an expiry announced at registration and not stored here
15
+ * would never be enforced.
16
+ */
17
+ export default class AddVisaClientSecretExpiry extends Migration {
18
+ async up() {
19
+ this.schema.alterTable("visa_clients", (t) => {
20
+ t.timestamp("secret_expires_at").nullable();
21
+ });
22
+ }
23
+
24
+ async down() {
25
+ this.schema.alterTable("visa_clients", (t) => {
26
+ t.dropColumn("secret_expires_at");
27
+ });
28
+ }
29
+ }