@c9up/visa 0.1.1 → 0.1.3
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/VisaManager.d.ts +42 -7
- package/dist/VisaManager.d.ts.map +1 -1
- package/dist/VisaManager.js +89 -3
- package/dist/VisaManager.js.map +1 -1
- package/dist/VisaProvider.d.ts.map +1 -1
- package/dist/VisaProvider.js +56 -0
- package/dist/VisaProvider.js.map +1 -1
- package/dist/authorize.d.ts +18 -0
- package/dist/authorize.d.ts.map +1 -1
- package/dist/authorize.js +6 -0
- package/dist/authorize.js.map +1 -1
- package/dist/clients.d.ts +1 -1
- package/dist/clients.d.ts.map +1 -1
- package/dist/clients.js +9 -1
- package/dist/clients.js.map +1 -1
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +9 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/guard.d.ts +21 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +25 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/introspect.d.ts +9 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +18 -1
- package/dist/introspect.js.map +1 -1
- package/dist/protectedResource.d.ts +81 -0
- package/dist/protectedResource.d.ts.map +1 -0
- package/dist/protectedResource.js +124 -0
- package/dist/protectedResource.js.map +1 -0
- package/dist/register.d.ts +107 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +203 -0
- package/dist/register.js.map +1 -0
- package/dist/resource.d.ts +50 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +101 -0
- package/dist/resource.js.map +1 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/stores/atlas.d.ts +7 -0
- package/dist/stores/atlas.d.ts.map +1 -1
- package/dist/stores/atlas.js +70 -16
- package/dist/stores/atlas.js.map +1 -1
- package/dist/stores/memory.d.ts +4 -0
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +25 -0
- package/dist/stores/memory.js.map +1 -1
- package/dist/token.d.ts +2 -0
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +21 -1
- package/dist/token.js.map +1 -1
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/VisaManager.ts +122 -3
- package/src/VisaProvider.ts +64 -0
- package/src/authorize.ts +27 -0
- package/src/clients.ts +10 -0
- package/src/config.ts +35 -0
- package/src/errors.ts +16 -2
- package/src/guard.ts +41 -1
- package/src/index.ts +25 -0
- package/src/introspect.ts +17 -0
- package/src/protectedResource.ts +171 -0
- package/src/register.ts +335 -0
- package/src/resource.ts +123 -0
- package/src/store.ts +29 -0
- package/src/stores/atlas.ts +99 -15
- package/src/stores/memory.ts +33 -0
- package/src/token.ts +45 -1
- package/src/types.ts +52 -0
- package/stubs/database/migrations/9000_create_visa_tables.stub +9 -0
package/src/stores/atlas.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface AtlasQuery {
|
|
|
45
45
|
whereNull(column: string): AtlasQuery;
|
|
46
46
|
orderBy(column: string, direction?: "asc" | "desc"): AtlasQuery;
|
|
47
47
|
first(): Promise<Row | null>;
|
|
48
|
+
/** Run the select and return every row — atlas' `exec()`. */
|
|
49
|
+
exec(): Promise<Row[]>;
|
|
48
50
|
insert(data: Row): PromiseLike<unknown>;
|
|
49
51
|
/** Resolves to the number of rows affected, which is what the atomic paths read. */
|
|
50
52
|
update(data: Row): PromiseLike<number | Row[]>;
|
|
@@ -205,6 +207,7 @@ export class AtlasStore implements VisaStore {
|
|
|
205
207
|
expires_at: code.expiresAt,
|
|
206
208
|
consumed_at: code.consumedAt ?? null,
|
|
207
209
|
nonce: code.nonce ?? null,
|
|
210
|
+
resources: encodeList(code.resources ?? []),
|
|
208
211
|
created_at: new Date(),
|
|
209
212
|
});
|
|
210
213
|
}
|
|
@@ -231,6 +234,7 @@ export class AtlasStore implements VisaStore {
|
|
|
231
234
|
expiresAt: requireDate(row.expires_at, "expires_at"),
|
|
232
235
|
...(consumedAt === undefined ? {} : { consumedAt }),
|
|
233
236
|
...(nonce === undefined ? {} : { nonce }),
|
|
237
|
+
...listOrNothing("resources", row.resources),
|
|
234
238
|
};
|
|
235
239
|
}
|
|
236
240
|
|
|
@@ -261,6 +265,7 @@ export class AtlasStore implements VisaStore {
|
|
|
261
265
|
revoked_at: token.revokedAt ?? null,
|
|
262
266
|
last_used_at: token.lastUsedAt ?? null,
|
|
263
267
|
family_id: token.familyId ?? null,
|
|
268
|
+
audience: encodeList(token.audience ?? []),
|
|
264
269
|
created_at: new Date(),
|
|
265
270
|
});
|
|
266
271
|
}
|
|
@@ -270,21 +275,7 @@ export class AtlasStore implements VisaStore {
|
|
|
270
275
|
.from(this.#tables.accessTokens)
|
|
271
276
|
.where({ token_hash: tokenHash })
|
|
272
277
|
.first();
|
|
273
|
-
|
|
274
|
-
const userId = optionalString(row.user_id);
|
|
275
|
-
const revokedAt = decodeDate(row.revoked_at);
|
|
276
|
-
const lastUsedAt = decodeDate(row.last_used_at);
|
|
277
|
-
const familyId = optionalString(row.family_id);
|
|
278
|
-
return {
|
|
279
|
-
tokenHash: requireString(row.token_hash, "token_hash"),
|
|
280
|
-
clientId: requireString(row.client_id, "client_id"),
|
|
281
|
-
...(userId === undefined ? {} : { userId }),
|
|
282
|
-
scopes: decodeList(row.scopes, "scopes"),
|
|
283
|
-
expiresAt: requireDate(row.expires_at, "expires_at"),
|
|
284
|
-
...(revokedAt === undefined ? {} : { revokedAt }),
|
|
285
|
-
...(lastUsedAt === undefined ? {} : { lastUsedAt }),
|
|
286
|
-
...(familyId === undefined ? {} : { familyId }),
|
|
287
|
-
};
|
|
278
|
+
return row === null ? null : readAccessToken(row);
|
|
288
279
|
}
|
|
289
280
|
|
|
290
281
|
async revokeAccessToken(tokenHash: string, at: Date): Promise<void> {
|
|
@@ -312,6 +303,7 @@ export class AtlasStore implements VisaStore {
|
|
|
312
303
|
expires_at: token.expiresAt,
|
|
313
304
|
consumed_at: token.consumedAt ?? null,
|
|
314
305
|
revoked_at: token.revokedAt ?? null,
|
|
306
|
+
resources: encodeList(token.resources ?? []),
|
|
315
307
|
created_at: new Date(),
|
|
316
308
|
});
|
|
317
309
|
}
|
|
@@ -334,6 +326,7 @@ export class AtlasStore implements VisaStore {
|
|
|
334
326
|
expiresAt: requireDate(row.expires_at, "expires_at"),
|
|
335
327
|
...(consumedAt === undefined ? {} : { consumedAt }),
|
|
336
328
|
...(revokedAt === undefined ? {} : { revokedAt }),
|
|
329
|
+
...listOrNothing("resources", row.resources),
|
|
337
330
|
};
|
|
338
331
|
}
|
|
339
332
|
|
|
@@ -400,6 +393,54 @@ export class AtlasStore implements VisaStore {
|
|
|
400
393
|
});
|
|
401
394
|
}
|
|
402
395
|
|
|
396
|
+
async listConsents(userId: string): Promise<Consent[]> {
|
|
397
|
+
const rows = await this.#db
|
|
398
|
+
.from(this.#tables.consents)
|
|
399
|
+
.where({ user_id: userId })
|
|
400
|
+
.orderBy("granted_at", "desc")
|
|
401
|
+
.exec();
|
|
402
|
+
return rows.map((row) => ({
|
|
403
|
+
userId: requireString(row.user_id, "user_id"),
|
|
404
|
+
clientId: requireString(row.client_id, "client_id"),
|
|
405
|
+
scopes: decodeList(row.scopes, "scopes"),
|
|
406
|
+
grantedAt: requireDate(row.granted_at, "granted_at"),
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Expired ones included — see the contract for why. */
|
|
411
|
+
async listAccessTokens(userId: string): Promise<AccessToken[]> {
|
|
412
|
+
const rows = await this.#db
|
|
413
|
+
.from(this.#tables.accessTokens)
|
|
414
|
+
.where({ user_id: userId })
|
|
415
|
+
.orderBy("expires_at", "desc")
|
|
416
|
+
.exec();
|
|
417
|
+
return rows.map(readAccessToken);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async revokeAccessFor(
|
|
421
|
+
userId: string,
|
|
422
|
+
clientId: string,
|
|
423
|
+
at: Date,
|
|
424
|
+
): Promise<void> {
|
|
425
|
+
for (const table of [
|
|
426
|
+
this.#tables.accessTokens,
|
|
427
|
+
this.#tables.refreshTokens,
|
|
428
|
+
]) {
|
|
429
|
+
await this.#db
|
|
430
|
+
.from(table)
|
|
431
|
+
.where({ user_id: userId, client_id: clientId })
|
|
432
|
+
.whereNull("revoked_at")
|
|
433
|
+
.update({ revoked_at: at });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async deleteConsent(userId: string, clientId: string): Promise<void> {
|
|
438
|
+
await this.#db
|
|
439
|
+
.from(this.#tables.consents)
|
|
440
|
+
.where({ user_id: userId, client_id: clientId })
|
|
441
|
+
.delete();
|
|
442
|
+
}
|
|
443
|
+
|
|
403
444
|
/** Drop what has expired and can no longer be presented. */
|
|
404
445
|
async prune(now: Date): Promise<void> {
|
|
405
446
|
for (const table of [
|
|
@@ -412,6 +453,49 @@ export class AtlasStore implements VisaStore {
|
|
|
412
453
|
}
|
|
413
454
|
}
|
|
414
455
|
|
|
456
|
+
/** One access-token row, as the contract's shape. */
|
|
457
|
+
function readAccessToken(row: Row): AccessToken {
|
|
458
|
+
const userId = optionalString(row.user_id);
|
|
459
|
+
const revokedAt = decodeDate(row.revoked_at);
|
|
460
|
+
const lastUsedAt = decodeDate(row.last_used_at);
|
|
461
|
+
const familyId = optionalString(row.family_id);
|
|
462
|
+
return {
|
|
463
|
+
tokenHash: requireString(row.token_hash, "token_hash"),
|
|
464
|
+
clientId: requireString(row.client_id, "client_id"),
|
|
465
|
+
...(userId === undefined ? {} : { userId }),
|
|
466
|
+
scopes: decodeList(row.scopes, "scopes"),
|
|
467
|
+
expiresAt: requireDate(row.expires_at, "expires_at"),
|
|
468
|
+
...(revokedAt === undefined ? {} : { revokedAt }),
|
|
469
|
+
...(lastUsedAt === undefined ? {} : { lastUsedAt }),
|
|
470
|
+
...(familyId === undefined ? {} : { familyId }),
|
|
471
|
+
...listOrNothing("audience", row.audience),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* A list column, present in the object only when it holds something.
|
|
477
|
+
*
|
|
478
|
+
* Empty and absent mean the same thing for an audience — unbound — and the
|
|
479
|
+
* contract spells that as absent, so an empty column must not become `[]` or
|
|
480
|
+
* a round trip would stop matching what went in.
|
|
481
|
+
*/
|
|
482
|
+
function listOrNothing(
|
|
483
|
+
key: "audience",
|
|
484
|
+
value: unknown,
|
|
485
|
+
): { audience?: string[] };
|
|
486
|
+
function listOrNothing(
|
|
487
|
+
key: "resources",
|
|
488
|
+
value: unknown,
|
|
489
|
+
): { resources?: string[] };
|
|
490
|
+
function listOrNothing(
|
|
491
|
+
key: "audience" | "resources",
|
|
492
|
+
value: unknown,
|
|
493
|
+
): { audience?: string[]; resources?: string[] } {
|
|
494
|
+
const list = decodeList(value, key);
|
|
495
|
+
if (list.length === 0) return {};
|
|
496
|
+
return key === "audience" ? { audience: list } : { resources: list };
|
|
497
|
+
}
|
|
498
|
+
|
|
415
499
|
function isGrantType(value: string): value is GrantType {
|
|
416
500
|
return (
|
|
417
501
|
value === "authorization_code" ||
|
package/src/stores/memory.ts
CHANGED
|
@@ -70,6 +70,39 @@ export class MemoryStore implements VisaStore {
|
|
|
70
70
|
if (token !== undefined) token.revokedAt = at;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
async listConsents(userId: string): Promise<Consent[]> {
|
|
74
|
+
return [...this.#consents.values()].filter(
|
|
75
|
+
(consent) => consent.userId === userId,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async listAccessTokens(userId: string): Promise<AccessToken[]> {
|
|
80
|
+
return [...this.#accessTokens.values()].filter(
|
|
81
|
+
(token) => token.userId === userId,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async revokeAccessFor(
|
|
86
|
+
userId: string,
|
|
87
|
+
clientId: string,
|
|
88
|
+
at: Date,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
for (const token of this.#accessTokens.values()) {
|
|
91
|
+
if (token.userId !== userId || token.clientId !== clientId) continue;
|
|
92
|
+
if (token.revokedAt === undefined) token.revokedAt = at;
|
|
93
|
+
}
|
|
94
|
+
for (const token of this.#refreshTokens.values()) {
|
|
95
|
+
if (token.userId !== userId || token.clientId !== clientId) continue;
|
|
96
|
+
if (token.revokedAt === undefined) token.revokedAt = at;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async deleteConsent(userId: string, clientId: string): Promise<void> {
|
|
101
|
+
// The same NUL-separated key `saveConsent` writes: a colon would make
|
|
102
|
+
// `a:b` + `c` collide with `a` + `b:c`.
|
|
103
|
+
this.#consents.delete(`${userId}\u0000${clientId}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
73
106
|
async touchAccessToken(tokenHash: string, at: Date): Promise<void> {
|
|
74
107
|
const token = this.#accessTokens.get(tokenHash);
|
|
75
108
|
if (token !== undefined) token.lastUsedAt = at;
|
package/src/token.ts
CHANGED
|
@@ -14,11 +14,18 @@ import {
|
|
|
14
14
|
} from "./clients.js";
|
|
15
15
|
import { hashMatches, hashSecret, randomToken, sha256 } from "./crypto.js";
|
|
16
16
|
import { OAuthError } from "./errors.js";
|
|
17
|
+
import {
|
|
18
|
+
narrowResources,
|
|
19
|
+
readResourceParameter,
|
|
20
|
+
validateResources,
|
|
21
|
+
} from "./resource.js";
|
|
17
22
|
import type { VisaStore } from "./store.js";
|
|
18
23
|
import type { Client, GrantType, TokenResponse } from "./types.js";
|
|
19
24
|
|
|
20
25
|
export interface TokenOptions {
|
|
21
26
|
accessTokenTtlSeconds?: number;
|
|
27
|
+
/** The resources this server issues tokens for — see `AuthorizeOptions`. */
|
|
28
|
+
resourcesSupported?: readonly string[];
|
|
22
29
|
refreshTokenTtlSeconds?: number;
|
|
23
30
|
/** Hand out refresh tokens at all. */
|
|
24
31
|
issueRefreshTokens?: boolean;
|
|
@@ -135,12 +142,23 @@ async function exchangeCode(
|
|
|
135
142
|
throw invalid;
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
// RFC 8707: a request may narrow the resources the code was bound to,
|
|
146
|
+
// never widen them — the user saw the list at authorization time.
|
|
147
|
+
const resources = narrowResources(
|
|
148
|
+
validateResources(
|
|
149
|
+
readResourceParameter(request.resource),
|
|
150
|
+
options.resourcesSupported,
|
|
151
|
+
),
|
|
152
|
+
record.resources ?? [],
|
|
153
|
+
);
|
|
154
|
+
|
|
138
155
|
return issue(
|
|
139
156
|
{
|
|
140
157
|
client,
|
|
141
158
|
userId: record.userId,
|
|
142
159
|
scopes: record.scopes,
|
|
143
160
|
familyId: codeFamily(record.codeHash),
|
|
161
|
+
resources,
|
|
144
162
|
},
|
|
145
163
|
store,
|
|
146
164
|
options,
|
|
@@ -215,6 +233,13 @@ async function exchangeRefresh(
|
|
|
215
233
|
...(record.userId === undefined ? {} : { userId: record.userId }),
|
|
216
234
|
scopes,
|
|
217
235
|
familyId: record.familyId,
|
|
236
|
+
resources: narrowResources(
|
|
237
|
+
validateResources(
|
|
238
|
+
readResourceParameter(request.resource),
|
|
239
|
+
options.resourcesSupported,
|
|
240
|
+
),
|
|
241
|
+
record.resources ?? [],
|
|
242
|
+
),
|
|
218
243
|
},
|
|
219
244
|
store,
|
|
220
245
|
options,
|
|
@@ -258,7 +283,16 @@ async function clientCredentials(
|
|
|
258
283
|
}
|
|
259
284
|
const scopes = resolveScopes(asString(request.scope), client);
|
|
260
285
|
return issue(
|
|
261
|
-
{
|
|
286
|
+
{
|
|
287
|
+
client,
|
|
288
|
+
scopes,
|
|
289
|
+
// No user consented to a list here, so the request IS the list —
|
|
290
|
+
// checked against what this server issues for, and nothing wider.
|
|
291
|
+
resources: validateResources(
|
|
292
|
+
readResourceParameter(request.resource),
|
|
293
|
+
options.resourcesSupported,
|
|
294
|
+
),
|
|
295
|
+
},
|
|
262
296
|
store,
|
|
263
297
|
{ ...options, issueRefreshTokens: false },
|
|
264
298
|
now,
|
|
@@ -270,6 +304,8 @@ interface Issued {
|
|
|
270
304
|
userId?: string;
|
|
271
305
|
scopes: string[];
|
|
272
306
|
familyId?: string;
|
|
307
|
+
/** RFC 8707 — what the token is for. Empty means unbound. */
|
|
308
|
+
resources?: string[];
|
|
273
309
|
}
|
|
274
310
|
|
|
275
311
|
/** Mint the pair and store both, hashed. */
|
|
@@ -290,6 +326,9 @@ async function issue(
|
|
|
290
326
|
scopes: grant.scopes,
|
|
291
327
|
expiresAt: new Date(now.getTime() + accessTtl * 1000),
|
|
292
328
|
...(grant.familyId === undefined ? {} : { familyId: grant.familyId }),
|
|
329
|
+
...(grant.resources === undefined || grant.resources.length === 0
|
|
330
|
+
? {}
|
|
331
|
+
: { audience: grant.resources }),
|
|
293
332
|
});
|
|
294
333
|
|
|
295
334
|
const response: TokenResponse = {
|
|
@@ -315,6 +354,11 @@ async function issue(
|
|
|
315
354
|
...(grant.userId === undefined ? {} : { userId: grant.userId }),
|
|
316
355
|
scopes: grant.scopes,
|
|
317
356
|
expiresAt: new Date(now.getTime() + refreshTtl * 1000),
|
|
357
|
+
// Carried so a rotation can narrow from the same list the code bound —
|
|
358
|
+
// without it, the second token of a session would be unbound.
|
|
359
|
+
...(grant.resources === undefined || grant.resources.length === 0
|
|
360
|
+
? {}
|
|
361
|
+
: { resources: grant.resources }),
|
|
318
362
|
});
|
|
319
363
|
response.refresh_token = refreshToken;
|
|
320
364
|
return response;
|
package/src/types.ts
CHANGED
|
@@ -32,6 +32,15 @@ export interface Client {
|
|
|
32
32
|
name: string;
|
|
33
33
|
/** Hashed. The plaintext is shown once, at registration, and never stored. */
|
|
34
34
|
secretHash?: string;
|
|
35
|
+
/**
|
|
36
|
+
* When the secret stops working (RFC 7591 §3.2.1).
|
|
37
|
+
*
|
|
38
|
+
* Absent means it does not expire, which is the common case. Present, it is
|
|
39
|
+
* enforced at authentication — a `client_secret_expires_at` answered at
|
|
40
|
+
* registration and never checked afterwards is a promise the server makes
|
|
41
|
+
* and does not keep.
|
|
42
|
+
*/
|
|
43
|
+
secretExpiresAt?: Date;
|
|
35
44
|
redirectUris: string[];
|
|
36
45
|
grantTypes: GrantType[];
|
|
37
46
|
/** What this client may ever ask for; a request narrows it, never widens it. */
|
|
@@ -57,6 +66,13 @@ export interface AuthorizationCode {
|
|
|
57
66
|
expiresAt: Date;
|
|
58
67
|
/** Set the moment it is exchanged, so a replay is detectable (§7.5.1). */
|
|
59
68
|
consumedAt?: Date;
|
|
69
|
+
/**
|
|
70
|
+
* The resources this code may buy a token for (RFC 8707).
|
|
71
|
+
*
|
|
72
|
+
* Bound here rather than at the token endpoint: the user saw the request
|
|
73
|
+
* that named them, and the exchange may narrow the list but never widen it.
|
|
74
|
+
*/
|
|
75
|
+
resources?: string[];
|
|
60
76
|
/** Carried through to the id_token when OIDC lands. */
|
|
61
77
|
nonce?: string;
|
|
62
78
|
}
|
|
@@ -76,6 +92,15 @@ export interface AccessToken {
|
|
|
76
92
|
* and what tells a dormant token from a live one.
|
|
77
93
|
*/
|
|
78
94
|
lastUsedAt?: Date;
|
|
95
|
+
/**
|
|
96
|
+
* Which resources this token is for (RFC 8707).
|
|
97
|
+
*
|
|
98
|
+
* Absent means unbound — a token for everything, which is what every token
|
|
99
|
+
* was before a client started asking. A resource server checks it against
|
|
100
|
+
* its own identifier, so a token that travelled to the wrong one is refused
|
|
101
|
+
* there instead of being honoured.
|
|
102
|
+
*/
|
|
103
|
+
audience?: string[];
|
|
79
104
|
/**
|
|
80
105
|
* The refresh family this token was minted in, when there is one.
|
|
81
106
|
*
|
|
@@ -102,6 +127,8 @@ export interface RefreshToken {
|
|
|
102
127
|
expiresAt: Date;
|
|
103
128
|
consumedAt?: Date;
|
|
104
129
|
revokedAt?: Date;
|
|
130
|
+
/** The resources a token minted from this one may be for (RFC 8707). */
|
|
131
|
+
resources?: string[];
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
/** What a user agreed to give a client, so they are asked once. */
|
|
@@ -112,6 +139,29 @@ export interface Consent {
|
|
|
112
139
|
grantedAt: Date;
|
|
113
140
|
}
|
|
114
141
|
|
|
142
|
+
/**
|
|
143
|
+
* One application a user has authorised, as a "connected applications" screen
|
|
144
|
+
* shows it.
|
|
145
|
+
*
|
|
146
|
+
* The unit is the APPLICATION, not the token: a person authorised an
|
|
147
|
+
* application, and the tokens come and go under that one decision. Upstream's
|
|
148
|
+
* equivalent listing is per token because it has no clients; here a user with
|
|
149
|
+
* four live tokens for one application has authorised one thing, and seeing
|
|
150
|
+
* four rows would be a lie about what revoking does.
|
|
151
|
+
*/
|
|
152
|
+
export interface AuthorizedApplication {
|
|
153
|
+
clientId: string;
|
|
154
|
+
/** The client's registered name, or its id when the client is gone. */
|
|
155
|
+
name: string;
|
|
156
|
+
/** What was granted — the consent, not what any one token happens to carry. */
|
|
157
|
+
scopes: string[];
|
|
158
|
+
grantedAt: Date;
|
|
159
|
+
/** The most recent use across this application's tokens, if any was used. */
|
|
160
|
+
lastUsedAt?: Date;
|
|
161
|
+
/** Whether anything issued to it is still usable right now. */
|
|
162
|
+
active: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
115
165
|
/** The token endpoint's success body (§4.1.4). */
|
|
116
166
|
export interface TokenResponse {
|
|
117
167
|
access_token: string;
|
|
@@ -138,4 +188,6 @@ export interface IntrospectionResponse {
|
|
|
138
188
|
exp?: number;
|
|
139
189
|
iat?: number;
|
|
140
190
|
token_type?: "Bearer";
|
|
191
|
+
/** Which resources the token is for — RFC 8707 §3 names introspection as where this travels. */
|
|
192
|
+
aud?: string[];
|
|
141
193
|
}
|
|
@@ -47,6 +47,10 @@ export default class CreateVisaTables extends Migration {
|
|
|
47
47
|
// UPDATE guarded on this column being null.
|
|
48
48
|
t.timestamp("consumed_at").nullable();
|
|
49
49
|
t.string("nonce", 255).nullable();
|
|
50
|
+
// RFC 8707 — which resources a token bought with this code may be
|
|
51
|
+
// for. Empty means unbound, which is every code a client sent no
|
|
52
|
+
// `resource` with.
|
|
53
|
+
t.json("resources").notNullable();
|
|
50
54
|
t.timestamp("created_at").notNullable();
|
|
51
55
|
t.index("user_id");
|
|
52
56
|
});
|
|
@@ -63,6 +67,9 @@ export default class CreateVisaTables extends Migration {
|
|
|
63
67
|
// The refresh family this token was minted in. Indexed because
|
|
64
68
|
// revoking a leaked family reads every token by it.
|
|
65
69
|
t.string("family_id", 64).nullable();
|
|
70
|
+
// RFC 8707 — what this token is for. Empty means unbound, and a
|
|
71
|
+
// resource server accepts it; bound, it is refused anywhere else.
|
|
72
|
+
t.json("audience").notNullable();
|
|
66
73
|
t.timestamp("created_at").notNullable();
|
|
67
74
|
t.index("family_id");
|
|
68
75
|
t.index("user_id");
|
|
@@ -77,6 +84,8 @@ export default class CreateVisaTables extends Migration {
|
|
|
77
84
|
t.timestamp("expires_at").notNullable();
|
|
78
85
|
t.timestamp("consumed_at").nullable();
|
|
79
86
|
t.timestamp("revoked_at").nullable();
|
|
87
|
+
// Carried so a rotation narrows from the same list the code bound.
|
|
88
|
+
t.json("resources").notNullable();
|
|
80
89
|
t.timestamp("created_at").notNullable();
|
|
81
90
|
t.index("family_id");
|
|
82
91
|
t.index("user_id");
|