@north-light/crouter-identity 0.3.360 → 0.3.362
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/index.d.ts +40 -0
- package/dist/index.js +52 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -20,9 +20,11 @@ export declare class JwksCache {
|
|
|
20
20
|
private readonly fetcher;
|
|
21
21
|
private keys;
|
|
22
22
|
private lastUnknownRefresh;
|
|
23
|
+
private loading;
|
|
23
24
|
private discovery;
|
|
24
25
|
constructor(issuer: string, fetcher?: typeof fetch, jwksUri?: string);
|
|
25
26
|
load(): Promise<JSONWebKeySet>;
|
|
27
|
+
private fetchKeys;
|
|
26
28
|
forKid(kid: string | undefined): Promise<JSONWebKeySet>;
|
|
27
29
|
}
|
|
28
30
|
export declare function verifyAccessToken(options: {
|
|
@@ -31,6 +33,44 @@ export declare function verifyAccessToken(options: {
|
|
|
31
33
|
issuer: string;
|
|
32
34
|
audience: string;
|
|
33
35
|
}): Promise<AccessTokenClaims>;
|
|
36
|
+
/** One level of the outbound token's RFC 8693 `act` nesting: the actor, and the actor it acted for. */
|
|
37
|
+
export interface OutboundActor {
|
|
38
|
+
sub: string;
|
|
39
|
+
act?: OutboundActor;
|
|
40
|
+
}
|
|
41
|
+
/** The claims of the outbound token the directory mints for one provider call (contract 3). */
|
|
42
|
+
export interface OutboundTokenClaims extends JWTPayload {
|
|
43
|
+
iss: string;
|
|
44
|
+
aud: string;
|
|
45
|
+
/** The user the call is made for, `user:<id>`. */
|
|
46
|
+
sub: string;
|
|
47
|
+
/** The principal the grant was issued to, `app:<id>` or the user. */
|
|
48
|
+
grantee: string;
|
|
49
|
+
/** The runtime's registered OAuth client id. */
|
|
50
|
+
client_id: string;
|
|
51
|
+
/** The provider's `<provider>:<group>` strings the caller holds, space-separated; may be empty. */
|
|
52
|
+
scope: string;
|
|
53
|
+
initiating_app: string;
|
|
54
|
+
/** Audit only; nothing authorizes on it. */
|
|
55
|
+
act: OutboundActor;
|
|
56
|
+
iat: number;
|
|
57
|
+
exp: number;
|
|
58
|
+
jti: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Verifies the outbound token a provider receives (contract 3 D3-TOK-12): ES256 or RS256 with
|
|
62
|
+
* a `kid`, typ `at+jwt`, signature against `jwks` (a `JwksCache` refetches once on an unknown
|
|
63
|
+
* `kid`), `iss` = `issuer`, `aud` exactly `audience` (a trailing-slash difference is refused),
|
|
64
|
+
* and `exp`. `sub` must be a `user:` principal and `grantee` and `initiating_app`
|
|
65
|
+
* an `app:` or `user:` principal; the remaining claims are checked for presence and type only; which of them gates a
|
|
66
|
+
* call is the caller's rule (D3-TOK-13 gates on `scope` alone, and nothing gates on `act`).
|
|
67
|
+
*/
|
|
68
|
+
export declare function verifyOutboundToken(options: {
|
|
69
|
+
token: string;
|
|
70
|
+
jwks: JSONWebKeySet | JwksCache;
|
|
71
|
+
issuer: string;
|
|
72
|
+
audience: string;
|
|
73
|
+
}): Promise<OutboundTokenClaims>;
|
|
34
74
|
export interface SyncNoticeClaims extends JWTPayload {
|
|
35
75
|
purpose: 'grant_sync';
|
|
36
76
|
iat: number;
|
package/dist/index.js
CHANGED
|
@@ -73,6 +73,7 @@ export class JwksCache {
|
|
|
73
73
|
fetcher;
|
|
74
74
|
keys;
|
|
75
75
|
lastUnknownRefresh = -Infinity;
|
|
76
|
+
loading;
|
|
76
77
|
discovery;
|
|
77
78
|
constructor(issuer, fetcher = fetch, jwksUri) {
|
|
78
79
|
this.issuer = issuer;
|
|
@@ -81,6 +82,18 @@ export class JwksCache {
|
|
|
81
82
|
this.discovery = { jwks_uri: jwksUri };
|
|
82
83
|
}
|
|
83
84
|
async load() {
|
|
85
|
+
if (this.loading)
|
|
86
|
+
return this.loading;
|
|
87
|
+
const loading = this.fetchKeys();
|
|
88
|
+
this.loading = loading;
|
|
89
|
+
try {
|
|
90
|
+
return await loading;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
this.loading = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async fetchKeys() {
|
|
84
97
|
if (!this.discovery) {
|
|
85
98
|
const response = await this.fetcher(`${this.issuer.replace(/\/$/, '')}/.well-known/openid-configuration`);
|
|
86
99
|
if (!response.ok)
|
|
@@ -104,6 +117,10 @@ export class JwksCache {
|
|
|
104
117
|
return this.load();
|
|
105
118
|
if (this.keys.keys.some((key) => key.kid === kid))
|
|
106
119
|
return this.keys;
|
|
120
|
+
// Concurrent requests for a newly published kid must await the same refetch, not use
|
|
121
|
+
// the stale set while the 30-second unknown-kid throttle is running.
|
|
122
|
+
if (this.loading)
|
|
123
|
+
return this.loading;
|
|
107
124
|
if (Date.now() - this.lastUnknownRefresh >= 30_000) {
|
|
108
125
|
this.lastUnknownRefresh = Date.now();
|
|
109
126
|
await this.load();
|
|
@@ -132,6 +149,41 @@ export async function verifyAccessToken(options) {
|
|
|
132
149
|
}
|
|
133
150
|
return payload;
|
|
134
151
|
}
|
|
152
|
+
function nonEmptyString(value) {
|
|
153
|
+
return typeof value === 'string' && value !== '';
|
|
154
|
+
}
|
|
155
|
+
function appOrUser(value) {
|
|
156
|
+
return validatePrincipal(value, 'app') || validatePrincipal(value, 'user');
|
|
157
|
+
}
|
|
158
|
+
function isActor(value) {
|
|
159
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && nonEmptyString(value.sub);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Verifies the outbound token a provider receives (contract 3 D3-TOK-12): ES256 or RS256 with
|
|
163
|
+
* a `kid`, typ `at+jwt`, signature against `jwks` (a `JwksCache` refetches once on an unknown
|
|
164
|
+
* `kid`), `iss` = `issuer`, `aud` exactly `audience` (a trailing-slash difference is refused),
|
|
165
|
+
* and `exp`. `sub` must be a `user:` principal and `grantee` and `initiating_app`
|
|
166
|
+
* an `app:` or `user:` principal; the remaining claims are checked for presence and type only; which of them gates a
|
|
167
|
+
* call is the caller's rule (D3-TOK-13 gates on `scope` alone, and nothing gates on `act`).
|
|
168
|
+
*/
|
|
169
|
+
export async function verifyOutboundToken(options) {
|
|
170
|
+
const header = decodeProtectedHeader(options.token);
|
|
171
|
+
if (header.typ !== 'at+jwt' || (header.alg !== 'ES256' && header.alg !== 'RS256'))
|
|
172
|
+
throw new Error('Invalid outbound token header');
|
|
173
|
+
if (!nonEmptyString(header.kid))
|
|
174
|
+
throw new Error('Outbound token has no key id');
|
|
175
|
+
const keys = await keySet(options.jwks, header.kid);
|
|
176
|
+
const { payload } = await jwtVerify(options.token, createLocalJWKSet(keys), {
|
|
177
|
+
issuer: options.issuer, audience: options.audience, algorithms: ['ES256', 'RS256'], typ: 'at+jwt', requiredClaims: ['exp'],
|
|
178
|
+
});
|
|
179
|
+
if (payload.aud !== options.audience || typeof payload.exp !== 'number' || typeof payload.iat !== 'number' ||
|
|
180
|
+
!validatePrincipal(payload.sub, 'user') || !appOrUser(payload.grantee) || !nonEmptyString(payload.client_id) ||
|
|
181
|
+
typeof payload.scope !== 'string' || !appOrUser(payload.initiating_app) || !isActor(payload.act) ||
|
|
182
|
+
!nonEmptyString(payload.jti)) {
|
|
183
|
+
throw new Error('Invalid outbound token claims');
|
|
184
|
+
}
|
|
185
|
+
return payload;
|
|
186
|
+
}
|
|
135
187
|
/** The directory's grant-sync notice: ES256 with the directory signing key named
|
|
136
188
|
* by `kid`, no `typ`, `aud` = the runtime's registered URL, a lifetime of at most
|
|
137
189
|
* five minutes, and a `jti` the runtime uses to answer replays without a sync. */
|