@axa-fr/oidc-client-service-worker 7.29.8 → 7.30.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/README.md +64 -0
- package/dist/OidcServiceWorker.d.ts +2 -1
- package/dist/OidcServiceWorker.d.ts.map +1 -0
- package/dist/OidcServiceWorker.js +995 -765
- package/dist/OidcServiceWorker.js.map +1 -1
- package/dist/constants.d.ts.map +1 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/dpop.d.ts.map +1 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/oidcConfig.d.ts.map +1 -0
- package/dist/protocol.d.ts +140 -2
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +50 -2
- package/dist/protocol.js.map +1 -1
- package/dist/resource.d.ts +22 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/tokenContexts.d.ts +5 -0
- package/dist/tokenContexts.d.ts.map +1 -0
- package/dist/{src/types.d.ts → types.d.ts} +26 -1
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/codeVerifier.d.ts.map +1 -0
- package/dist/{src/utils → utils}/domains.d.ts +2 -2
- package/dist/utils/domains.d.ts.map +1 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/normalizeUrl.d.ts.map +1 -0
- package/dist/utils/serializeHeaders.d.ts.map +1 -0
- package/dist/utils/shouldBypassDestination.d.ts.map +1 -0
- package/dist/utils/sleep.d.ts.map +1 -0
- package/dist/utils/strings.d.ts.map +1 -0
- package/dist/{src/utils → utils}/tokens.d.ts +12 -3
- package/dist/utils/tokens.d.ts.map +1 -0
- package/dist/utils/waitForValidTokens.d.ts.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/OidcServiceWorker.ts +290 -34
- package/src/__tests__/jwt.spec.ts +17 -0
- package/src/__tests__/resource.spec.ts +98 -0
- package/src/__tests__/workerResource.spec.ts +919 -0
- package/src/jwt.ts +4 -1
- package/src/oidcConfig.ts +2 -1
- package/src/protocol.ts +16 -1
- package/src/resource.ts +104 -0
- package/src/tokenContexts.ts +39 -0
- package/src/types.ts +27 -1
- package/src/utils/domains.ts +20 -2
- package/src/utils/tokens.ts +155 -8
- package/src/version.ts +1 -1
- package/dist/src/OidcServiceWorker.d.ts +0 -2
- package/dist/src/OidcServiceWorker.d.ts.map +0 -1
- package/dist/src/__tests__/jwt.spec.d.ts +0 -2
- package/dist/src/__tests__/jwt.spec.d.ts.map +0 -1
- package/dist/src/__tests__/oidcConfig.spec.d.ts +0 -2
- package/dist/src/__tests__/oidcConfig.spec.d.ts.map +0 -1
- package/dist/src/__tests__/protocol.spec.d.ts +0 -2
- package/dist/src/__tests__/protocol.spec.d.ts.map +0 -1
- package/dist/src/constants.d.ts.map +0 -1
- package/dist/src/crypto.d.ts.map +0 -1
- package/dist/src/dpop.d.ts.map +0 -1
- package/dist/src/jwt.d.ts.map +0 -1
- package/dist/src/oidcConfig.d.ts.map +0 -1
- package/dist/src/protocol.d.ts +0 -134
- package/dist/src/protocol.d.ts.map +0 -1
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/utils/__tests__/codeVerifier.spec.d.ts +0 -2
- package/dist/src/utils/__tests__/codeVerifier.spec.d.ts.map +0 -1
- package/dist/src/utils/__tests__/domains.spec.d.ts +0 -2
- package/dist/src/utils/__tests__/domains.spec.d.ts.map +0 -1
- package/dist/src/utils/__tests__/normalizeUrl.spec.d.ts +0 -2
- package/dist/src/utils/__tests__/normalizeUrl.spec.d.ts.map +0 -1
- package/dist/src/utils/__tests__/serializeHeaders.spec.d.ts +0 -2
- package/dist/src/utils/__tests__/serializeHeaders.spec.d.ts.map +0 -1
- package/dist/src/utils/__tests__/shouldBypassDestination.spec.d.ts +0 -2
- package/dist/src/utils/__tests__/shouldBypassDestination.spec.d.ts.map +0 -1
- package/dist/src/utils/__tests__/strings.spec.d.ts +0 -2
- package/dist/src/utils/__tests__/strings.spec.d.ts.map +0 -1
- package/dist/src/utils/__tests__/testHelper.d.ts +0 -58
- package/dist/src/utils/__tests__/testHelper.d.ts.map +0 -1
- package/dist/src/utils/__tests__/tokens.spec.d.ts +0 -2
- package/dist/src/utils/__tests__/tokens.spec.d.ts.map +0 -1
- package/dist/src/utils/__tests__/waitForValidTokens.spec.d.ts +0 -2
- package/dist/src/utils/__tests__/waitForValidTokens.spec.d.ts.map +0 -1
- package/dist/src/utils/codeVerifier.d.ts.map +0 -1
- package/dist/src/utils/domains.d.ts.map +0 -1
- package/dist/src/utils/index.d.ts.map +0 -1
- package/dist/src/utils/normalizeUrl.d.ts.map +0 -1
- package/dist/src/utils/serializeHeaders.d.ts.map +0 -1
- package/dist/src/utils/shouldBypassDestination.d.ts.map +0 -1
- package/dist/src/utils/sleep.d.ts.map +0 -1
- package/dist/src/utils/strings.d.ts.map +0 -1
- package/dist/src/utils/tokens.d.ts.map +0 -1
- package/dist/src/utils/waitForValidTokens.d.ts.map +0 -1
- package/dist/src/version.d.ts +0 -3
- package/dist/src/version.d.ts.map +0 -1
- /package/dist/{src/constants.d.ts → constants.d.ts} +0 -0
- /package/dist/{src/crypto.d.ts → crypto.d.ts} +0 -0
- /package/dist/{src/dpop.d.ts → dpop.d.ts} +0 -0
- /package/dist/{src/jwt.d.ts → jwt.d.ts} +0 -0
- /package/dist/{src/oidcConfig.d.ts → oidcConfig.d.ts} +0 -0
- /package/dist/{src/utils → utils}/codeVerifier.d.ts +0 -0
- /package/dist/{src/utils → utils}/index.d.ts +0 -0
- /package/dist/{src/utils → utils}/normalizeUrl.d.ts +0 -0
- /package/dist/{src/utils → utils}/serializeHeaders.d.ts +0 -0
- /package/dist/{src/utils → utils}/shouldBypassDestination.d.ts +0 -0
- /package/dist/{src/utils → utils}/sleep.d.ts +0 -0
- /package/dist/{src/utils → utils}/strings.d.ts +0 -0
- /package/dist/{src/utils → utils}/waitForValidTokens.d.ts +0 -0
package/src/OidcServiceWorker.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { base64urlOfHashOfASCIIEncodingAsync } from './crypto';
|
|
|
3
3
|
import { getDpopConfiguration, getDpopOnlyWhenDpopHeaderPresent } from './dpop';
|
|
4
4
|
import { generateJwkAsync, generateJwtDemonstratingProofOfPossessionAsync } from './jwt';
|
|
5
5
|
import { getCurrentDatabasesTokenEndpoint, shouldBypassNonOidcRequest } from './oidcConfig';
|
|
6
|
+
import {
|
|
7
|
+
getContextResource,
|
|
8
|
+
getResourceConfiguration,
|
|
9
|
+
RESOURCE_TOKEN_REQUEST_HEADER,
|
|
10
|
+
} from './resource';
|
|
11
|
+
import { getOrCreateResourceContext, getTokenContexts } from './tokenContexts';
|
|
6
12
|
import { Database, MessageEventData, OidcConfig, TrustedDomains } from './types';
|
|
7
13
|
import {
|
|
8
14
|
checkDomain,
|
|
@@ -88,17 +94,26 @@ async function generateDpopAsync(
|
|
|
88
94
|
const dpopConfiguration = currentDatabase.demonstratingProofOfPossessionConfiguration;
|
|
89
95
|
const jwk = currentDatabase.demonstratingProofOfPossessionJwkJson;
|
|
90
96
|
const method = originalRequest.method;
|
|
97
|
+
const resourceNonce = currentDatabase.resource
|
|
98
|
+
? url === normalizeUrl(currentDatabase.oidcServerConfiguration!.tokenEndpoint)
|
|
99
|
+
? currentDatabase.demonstratingProofOfPossessionNonce
|
|
100
|
+
: currentDatabase.demonstratingProofOfPossessionApiNonce
|
|
101
|
+
: undefined;
|
|
91
102
|
const dpop = await generateJwtDemonstratingProofOfPossessionAsync(self)(dpopConfiguration)(
|
|
92
103
|
jwk,
|
|
93
104
|
method,
|
|
94
105
|
url,
|
|
95
|
-
|
|
106
|
+
{
|
|
107
|
+
...extrasClaims,
|
|
108
|
+
...(resourceNonce ? { nonce: resourceNonce } : {}),
|
|
109
|
+
},
|
|
96
110
|
);
|
|
97
111
|
|
|
98
112
|
headersExtras.dpop = dpop;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
const nonceHeader =
|
|
114
|
+
resourceNonce ??
|
|
115
|
+
(!currentDatabase.resource ? currentDatabase.demonstratingProofOfPossessionNonce : undefined);
|
|
116
|
+
if (nonceHeader) headersExtras.nonce = nonceHeader;
|
|
102
117
|
}
|
|
103
118
|
return headersExtras;
|
|
104
119
|
}
|
|
@@ -153,8 +168,19 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
const currentDatabaseForRequestAccessToken = currentDatabasesForRequestAccessToken?.find(
|
|
156
|
-
c =>
|
|
171
|
+
c =>
|
|
172
|
+
key.includes('#resource=')
|
|
173
|
+
? c.configurationName === key
|
|
174
|
+
: !c.resource && c.configurationName.endsWith(key),
|
|
157
175
|
);
|
|
176
|
+
if (key.includes('#resource=') && !currentDatabaseForRequestAccessToken) {
|
|
177
|
+
console.error(
|
|
178
|
+
'[OidcServiceWorker] Resource token destination or context is not permitted.',
|
|
179
|
+
);
|
|
180
|
+
return new Response('Resource token destination or context is not permitted.', {
|
|
181
|
+
status: 403,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
158
184
|
|
|
159
185
|
// Inject the access token into the request if one is available
|
|
160
186
|
if (currentDatabaseForRequestAccessToken?.tokens?.access_token) {
|
|
@@ -163,6 +189,10 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
163
189
|
if (tokenError) {
|
|
164
190
|
return tokenError;
|
|
165
191
|
}
|
|
192
|
+
const accessToken = currentDatabaseForRequestAccessToken.tokens!.access_token;
|
|
193
|
+
const generation = currentDatabaseForRequestAccessToken.generation;
|
|
194
|
+
const parentGeneration =
|
|
195
|
+
currentDatabaseForRequestAccessToken.parentConfiguration?.generation;
|
|
166
196
|
|
|
167
197
|
// Adjust request mode for CORS if configured
|
|
168
198
|
let requestMode = originalRequest.mode;
|
|
@@ -192,9 +222,7 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
192
222
|
) {
|
|
193
223
|
// DPoP mode
|
|
194
224
|
const claimsExtras = {
|
|
195
|
-
ath: await base64urlOfHashOfASCIIEncodingAsync(
|
|
196
|
-
currentDatabaseForRequestAccessToken.tokens.access_token,
|
|
197
|
-
),
|
|
225
|
+
ath: await base64urlOfHashOfASCIIEncodingAsync(accessToken),
|
|
198
226
|
};
|
|
199
227
|
const dpopHeaders = await generateDpopAsync(
|
|
200
228
|
originalRequest,
|
|
@@ -204,13 +232,13 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
204
232
|
);
|
|
205
233
|
headers = {
|
|
206
234
|
...dpopHeaders,
|
|
207
|
-
authorization: `DPoP ${
|
|
235
|
+
authorization: `DPoP ${accessToken}`,
|
|
208
236
|
};
|
|
209
237
|
} else {
|
|
210
238
|
// Bearer mode
|
|
211
239
|
headers = {
|
|
212
240
|
...serializeHeaders(originalRequest.headers),
|
|
213
|
-
authorization: `${authenticationMode} ${
|
|
241
|
+
authorization: `${authenticationMode} ${accessToken}`,
|
|
214
242
|
};
|
|
215
243
|
}
|
|
216
244
|
}
|
|
@@ -228,7 +256,31 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
228
256
|
}
|
|
229
257
|
|
|
230
258
|
const newRequest = new Request(originalRequest, init);
|
|
231
|
-
|
|
259
|
+
if (
|
|
260
|
+
currentDatabaseForRequestAccessToken.resource &&
|
|
261
|
+
(currentDatabaseForRequestAccessToken.generation !== generation ||
|
|
262
|
+
currentDatabaseForRequestAccessToken.parentConfiguration?.generation !==
|
|
263
|
+
parentGeneration ||
|
|
264
|
+
currentDatabaseForRequestAccessToken.tokens?.access_token !== accessToken)
|
|
265
|
+
) {
|
|
266
|
+
console.error('[OidcServiceWorker] The resource token changed before dispatch.');
|
|
267
|
+
return new Response('The resource token changed before dispatch.', { status: 403 });
|
|
268
|
+
}
|
|
269
|
+
const response = await fetch(
|
|
270
|
+
newRequest,
|
|
271
|
+
currentDatabaseForRequestAccessToken.resource ? { redirect: 'error' } : undefined,
|
|
272
|
+
);
|
|
273
|
+
if (
|
|
274
|
+
currentDatabaseForRequestAccessToken.resource &&
|
|
275
|
+
currentDatabaseForRequestAccessToken.generation === generation &&
|
|
276
|
+
currentDatabaseForRequestAccessToken.parentConfiguration?.generation ===
|
|
277
|
+
parentGeneration &&
|
|
278
|
+
response.headers.has('DPoP-Nonce')
|
|
279
|
+
) {
|
|
280
|
+
currentDatabaseForRequestAccessToken.demonstratingProofOfPossessionApiNonce =
|
|
281
|
+
response.headers.get('DPoP-Nonce');
|
|
282
|
+
}
|
|
283
|
+
return response;
|
|
232
284
|
}
|
|
233
285
|
|
|
234
286
|
// Pass through non-POST requests without modification
|
|
@@ -264,29 +316,112 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
264
316
|
currentDb.tokens.access_token,
|
|
265
317
|
),
|
|
266
318
|
};
|
|
267
|
-
headers = await generateDpopAsync(
|
|
268
|
-
originalRequest,
|
|
269
|
-
currentDb,
|
|
270
|
-
url,
|
|
271
|
-
claimsExtras,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
319
|
const keyRefreshToken = encodeURIComponent(
|
|
275
320
|
`${TOKEN.REFRESH_TOKEN}_${currentDb.configurationName}`,
|
|
276
321
|
);
|
|
277
|
-
if (
|
|
322
|
+
if (
|
|
323
|
+
[
|
|
324
|
+
new URLSearchParams(actualBody).get('refresh_token'),
|
|
325
|
+
new URLSearchParams(actualBody).get('token'),
|
|
326
|
+
].includes(decodeURIComponent(keyRefreshToken))
|
|
327
|
+
) {
|
|
328
|
+
const form = new URLSearchParams(actualBody);
|
|
329
|
+
const selected = form.getAll('resource');
|
|
330
|
+
const resourceHeader = originalRequest.headers.get(
|
|
331
|
+
RESOURCE_TOKEN_REQUEST_HEADER,
|
|
332
|
+
);
|
|
333
|
+
const isRevocation =
|
|
334
|
+
currentDb.oidcServerConfiguration?.revocationEndpoint &&
|
|
335
|
+
url ===
|
|
336
|
+
normalizeUrl(currentDb.oidcServerConfiguration.revocationEndpoint) &&
|
|
337
|
+
form.has('token');
|
|
338
|
+
if (isRevocation) {
|
|
339
|
+
currentDatabase = currentDb;
|
|
340
|
+
} else if (resourceHeader !== null) {
|
|
341
|
+
const resource = decodeURIComponent(resourceHeader);
|
|
342
|
+
if (selected.length !== 1 || selected[0] !== resource)
|
|
343
|
+
throw new Error('The resource routing hint must match the request.');
|
|
344
|
+
if (!currentDb.oidcConfiguration)
|
|
345
|
+
throw new Error('The resource grant configuration is unavailable.');
|
|
346
|
+
const resourceConfiguration = getResourceConfiguration(
|
|
347
|
+
currentDb.oidcConfiguration.resources,
|
|
348
|
+
resource,
|
|
349
|
+
);
|
|
350
|
+
const grantedScope =
|
|
351
|
+
currentDb.tokens.scope ??
|
|
352
|
+
currentDb.authorizationScope ??
|
|
353
|
+
currentDb.oidcConfiguration.scope;
|
|
354
|
+
const requestedScopes = [
|
|
355
|
+
form.get('scope'),
|
|
356
|
+
resourceConfiguration.scope,
|
|
357
|
+
].filter((scope): scope is string => typeof scope === 'string');
|
|
358
|
+
if (
|
|
359
|
+
requestedScopes.some(
|
|
360
|
+
scope =>
|
|
361
|
+
!scope
|
|
362
|
+
.split(/\s+/)
|
|
363
|
+
.filter(Boolean)
|
|
364
|
+
.every(value => grantedScope?.split(/\s+/).includes(value)),
|
|
365
|
+
)
|
|
366
|
+
) {
|
|
367
|
+
throw new Error('The resource scope is outside this refresh grant.');
|
|
368
|
+
}
|
|
369
|
+
if (
|
|
370
|
+
currentDb.resource !== undefined
|
|
371
|
+
? currentDb.resource !== resource
|
|
372
|
+
: !(
|
|
373
|
+
typeof currentDb.oidcConfiguration.resource === 'string'
|
|
374
|
+
? [currentDb.oidcConfiguration.resource]
|
|
375
|
+
: (currentDb.oidcConfiguration.resource ?? [])
|
|
376
|
+
).includes(resource)
|
|
377
|
+
)
|
|
378
|
+
throw new Error('The resource is outside this refresh grant.');
|
|
379
|
+
currentDatabase = currentDb.resource
|
|
380
|
+
? currentDb
|
|
381
|
+
: getOrCreateResourceContext(currentDb, resource);
|
|
382
|
+
currentDatabase.refreshGrant = currentDb;
|
|
383
|
+
currentDatabase.requestedScope = form.get('scope') ?? grantedScope;
|
|
384
|
+
currentDatabase.nonce = currentDb.nonce;
|
|
385
|
+
currentDatabase.demonstratingProofOfPossessionJwkJson =
|
|
386
|
+
currentDb.demonstratingProofOfPossessionJwkJson;
|
|
387
|
+
currentDatabase.demonstratingProofOfPossessionConfiguration =
|
|
388
|
+
currentDb.demonstratingProofOfPossessionConfiguration;
|
|
389
|
+
currentDatabase.demonstratingProofOfPossessionNonce =
|
|
390
|
+
currentDb.demonstratingProofOfPossessionNonce;
|
|
391
|
+
} else if (currentDb.resource) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
'A resource refresh must select exactly its own resource.',
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
currentDatabase = currentDb;
|
|
397
|
+
currentDatabase.requestedScope =
|
|
398
|
+
form.get('scope') ?? currentDb.tokens.scope;
|
|
399
|
+
}
|
|
400
|
+
headers = await generateDpopAsync(
|
|
401
|
+
originalRequest,
|
|
402
|
+
currentDatabase,
|
|
403
|
+
url,
|
|
404
|
+
claimsExtras,
|
|
405
|
+
);
|
|
278
406
|
newBody = newBody.replace(
|
|
279
407
|
keyRefreshToken,
|
|
280
408
|
encodeURIComponent(currentDb.tokens.refresh_token as string),
|
|
281
409
|
);
|
|
282
|
-
currentDatabase = currentDb;
|
|
283
410
|
break;
|
|
284
411
|
}
|
|
285
|
-
|
|
286
412
|
const keyAccessToken = encodeURIComponent(
|
|
287
413
|
`${TOKEN.ACCESS_TOKEN}_${currentDb.configurationName}`,
|
|
288
414
|
);
|
|
289
|
-
if (
|
|
415
|
+
if (
|
|
416
|
+
new URLSearchParams(actualBody).get('token') ===
|
|
417
|
+
decodeURIComponent(keyAccessToken)
|
|
418
|
+
) {
|
|
419
|
+
headers = await generateDpopAsync(
|
|
420
|
+
originalRequest,
|
|
421
|
+
currentDb,
|
|
422
|
+
url,
|
|
423
|
+
claimsExtras,
|
|
424
|
+
);
|
|
290
425
|
newBody = newBody.replace(
|
|
291
426
|
keyAccessToken,
|
|
292
427
|
encodeURIComponent(currentDb.tokens.access_token),
|
|
@@ -296,6 +431,25 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
296
431
|
}
|
|
297
432
|
}
|
|
298
433
|
}
|
|
434
|
+
if (!currentDatabase)
|
|
435
|
+
throw new Error(
|
|
436
|
+
'The token placeholder does not identify an available grant.',
|
|
437
|
+
);
|
|
438
|
+
delete headers[RESOURCE_TOKEN_REQUEST_HEADER];
|
|
439
|
+
const generation = currentDatabase.generation;
|
|
440
|
+
const grant = currentDatabase.refreshGrant ?? currentDatabase;
|
|
441
|
+
const grantGeneration = grant.generation;
|
|
442
|
+
if (
|
|
443
|
+
currentDatabase.resource &&
|
|
444
|
+
url !==
|
|
445
|
+
normalizeUrl(currentDatabase.oidcServerConfiguration!.tokenEndpoint) &&
|
|
446
|
+
url !==
|
|
447
|
+
normalizeUrl(currentDatabase.oidcServerConfiguration!.revocationEndpoint)
|
|
448
|
+
) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
'Resource tokens may only be sent to their exact token endpoint.',
|
|
451
|
+
);
|
|
452
|
+
}
|
|
299
453
|
|
|
300
454
|
const fetchPromise = fetch(originalRequest, {
|
|
301
455
|
body: newBody,
|
|
@@ -324,7 +478,13 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
324
478
|
|
|
325
479
|
// Hide real token values in the response
|
|
326
480
|
const hidden = await fetchPromise.then(
|
|
327
|
-
hideTokens(
|
|
481
|
+
hideTokens(
|
|
482
|
+
currentDatabase,
|
|
483
|
+
() =>
|
|
484
|
+
currentDatabase!.generation === generation &&
|
|
485
|
+
grant.generation === grantGeneration,
|
|
486
|
+
() => grant.generation === grantGeneration,
|
|
487
|
+
),
|
|
328
488
|
);
|
|
329
489
|
resolve(hidden);
|
|
330
490
|
return;
|
|
@@ -341,7 +501,34 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
341
501
|
) {
|
|
342
502
|
throw new Error('No configuration name found in code_verifier');
|
|
343
503
|
}
|
|
344
|
-
currentDatabase =
|
|
504
|
+
currentDatabase =
|
|
505
|
+
getTokenContexts(database).find(
|
|
506
|
+
context =>
|
|
507
|
+
context.configurationName === currentLoginCallbackConfigurationName,
|
|
508
|
+
) ?? null;
|
|
509
|
+
if (!currentDatabase)
|
|
510
|
+
throw new Error(
|
|
511
|
+
'No available authorization transaction matches the code verifier.',
|
|
512
|
+
);
|
|
513
|
+
if (currentDatabase.resource) {
|
|
514
|
+
const selected = new URLSearchParams(actualBody).getAll('resource');
|
|
515
|
+
if (selected.length !== 1 || selected[0] !== currentDatabase.resource)
|
|
516
|
+
throw new Error('Authorization resource mismatch.');
|
|
517
|
+
if (
|
|
518
|
+
url !== normalizeUrl(currentDatabase.oidcServerConfiguration!.tokenEndpoint)
|
|
519
|
+
)
|
|
520
|
+
throw new Error('Invalid resource token endpoint.');
|
|
521
|
+
currentDatabase.refreshGrant = currentDatabase;
|
|
522
|
+
}
|
|
523
|
+
currentDatabase.requestedScope =
|
|
524
|
+
currentDatabase.authorizationScope ??
|
|
525
|
+
(currentDatabase.resource
|
|
526
|
+
? getResourceConfiguration(
|
|
527
|
+
currentDatabase.oidcConfiguration?.resources,
|
|
528
|
+
currentDatabase.resource,
|
|
529
|
+
).scope
|
|
530
|
+
: currentDatabase.oidcConfiguration?.scope);
|
|
531
|
+
const generation = currentDatabase.generation;
|
|
345
532
|
let newBody = actualBody;
|
|
346
533
|
const codeVerifier = currentDatabase.codeVerifier;
|
|
347
534
|
if (codeVerifier != null) {
|
|
@@ -364,7 +551,12 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
364
551
|
credentials: clonedRequest.credentials,
|
|
365
552
|
integrity: clonedRequest.integrity,
|
|
366
553
|
});
|
|
367
|
-
const hidden = await hideTokens(
|
|
554
|
+
const hidden = await hideTokens(
|
|
555
|
+
currentDatabase,
|
|
556
|
+
() => currentDatabase!.generation === generation,
|
|
557
|
+
undefined,
|
|
558
|
+
true,
|
|
559
|
+
)(resp);
|
|
368
560
|
resolve(hidden);
|
|
369
561
|
return;
|
|
370
562
|
}
|
|
@@ -389,7 +581,7 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
389
581
|
.catch(reject);
|
|
390
582
|
});
|
|
391
583
|
|
|
392
|
-
return responsePromise;
|
|
584
|
+
return await responsePromise;
|
|
393
585
|
}
|
|
394
586
|
|
|
395
587
|
// Default: pass through the request unchanged
|
|
@@ -403,7 +595,7 @@ const handleFetch = (event: FetchEvent): void => {
|
|
|
403
595
|
);
|
|
404
596
|
};
|
|
405
597
|
|
|
406
|
-
const
|
|
598
|
+
const handleMessageAsync = async (event: ExtendableMessageEvent): Promise<void> => {
|
|
407
599
|
const port = event.ports[0];
|
|
408
600
|
const data = event.data as MessageEventData;
|
|
409
601
|
|
|
@@ -417,6 +609,7 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
|
|
|
417
609
|
}
|
|
418
610
|
|
|
419
611
|
const configurationName = data.configurationName.split('#')[0];
|
|
612
|
+
const resource = getContextResource(data.configurationName);
|
|
420
613
|
|
|
421
614
|
if (trustedDomains == null) {
|
|
422
615
|
trustedDomains = {};
|
|
@@ -468,9 +661,23 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
|
|
|
468
661
|
trustedDomains[configurationName] = [];
|
|
469
662
|
}
|
|
470
663
|
}
|
|
664
|
+
if (resource !== undefined) {
|
|
665
|
+
try {
|
|
666
|
+
currentDatabase = getOrCreateResourceContext(currentDatabase, resource);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error('[OidcServiceWorker] Invalid resource context:', error);
|
|
669
|
+
port.postMessage({ configurationName, error: 'The resource is not registered.' });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
471
673
|
|
|
472
674
|
switch (data.type) {
|
|
473
675
|
case 'clear':
|
|
676
|
+
currentDatabase.generation = (currentDatabase.generation ?? 0) + 1;
|
|
677
|
+
for (const entry of Object.values(currentDatabase.resourceEntries ?? {})) {
|
|
678
|
+
entry.generation = (entry.generation ?? 0) + 1;
|
|
679
|
+
}
|
|
680
|
+
currentDatabase.resourceEntries = undefined;
|
|
474
681
|
currentDatabase.tokens = null;
|
|
475
682
|
currentDatabase.state = null;
|
|
476
683
|
currentDatabase.codeVerifier = null;
|
|
@@ -479,6 +686,9 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
|
|
|
479
686
|
currentDatabase.demonstratingProofOfPossessionJwkJson = null;
|
|
480
687
|
currentDatabase.demonstratingProofOfPossessionConfiguration = null;
|
|
481
688
|
currentDatabase.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent = false;
|
|
689
|
+
currentDatabase.authorizationScope = undefined;
|
|
690
|
+
currentDatabase.requestedScope = undefined;
|
|
691
|
+
currentDatabase.demonstratingProofOfPossessionApiNonce = null;
|
|
482
692
|
currentDatabase.status = data.data.status;
|
|
483
693
|
port.postMessage({ configurationName });
|
|
484
694
|
return;
|
|
@@ -500,10 +710,24 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
|
|
|
500
710
|
|
|
501
711
|
currentDatabase.oidcServerConfiguration = oidcServerConfiguration;
|
|
502
712
|
currentDatabase.oidcConfiguration = data.data.oidcConfiguration;
|
|
713
|
+
if (data.data.where === 'loginAsync')
|
|
714
|
+
currentDatabase.authorizationScope = currentDatabase.oidcConfiguration.scope;
|
|
715
|
+
if (resource !== undefined)
|
|
716
|
+
getResourceConfiguration(currentDatabase.oidcConfiguration.resources, resource);
|
|
503
717
|
|
|
504
718
|
if (currentDatabase.demonstratingProofOfPossessionConfiguration == null) {
|
|
719
|
+
const parent = currentDatabase.parentConfiguration;
|
|
720
|
+
if (parent?.demonstratingProofOfPossessionJwkJson) {
|
|
721
|
+
currentDatabase.demonstratingProofOfPossessionJwkJson =
|
|
722
|
+
parent.demonstratingProofOfPossessionJwkJson;
|
|
723
|
+
currentDatabase.demonstratingProofOfPossessionConfiguration =
|
|
724
|
+
parent.demonstratingProofOfPossessionConfiguration;
|
|
725
|
+
}
|
|
505
726
|
const demonstratingProofOfPossessionConfiguration = getDpopConfiguration(trustedDomain);
|
|
506
|
-
if (
|
|
727
|
+
if (
|
|
728
|
+
demonstratingProofOfPossessionConfiguration != null &&
|
|
729
|
+
currentDatabase.demonstratingProofOfPossessionConfiguration == null
|
|
730
|
+
) {
|
|
507
731
|
if (currentDatabase.oidcConfiguration.demonstrating_proof_of_possession) {
|
|
508
732
|
console.warn(
|
|
509
733
|
'In service worker, demonstrating_proof_of_possession must be configured from trustedDomains file',
|
|
@@ -525,23 +749,32 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
|
|
|
525
749
|
status: currentDatabase.status,
|
|
526
750
|
configurationName,
|
|
527
751
|
version,
|
|
752
|
+
resourceTokenManagement: true,
|
|
753
|
+
resourceContexts: Object.keys(currentDatabase.resourceEntries ?? {}),
|
|
528
754
|
});
|
|
529
755
|
} else {
|
|
530
|
-
const tokens = {
|
|
756
|
+
const tokens = {
|
|
757
|
+
...currentDatabase.tokens,
|
|
758
|
+
idTokenPayload: currentDatabase.tokens.idTokenPayload
|
|
759
|
+
? { ...currentDatabase.tokens.idTokenPayload }
|
|
760
|
+
: undefined,
|
|
761
|
+
};
|
|
531
762
|
if (currentDatabase.hideAccessToken) {
|
|
532
|
-
tokens.access_token = `${TOKEN.ACCESS_TOKEN}_${configurationName}
|
|
763
|
+
tokens.access_token = `${TOKEN.ACCESS_TOKEN}_${currentDatabase.configurationName}`;
|
|
533
764
|
}
|
|
534
765
|
if (tokens.refresh_token) {
|
|
535
|
-
tokens.refresh_token = `${TOKEN.REFRESH_TOKEN}_${configurationName}
|
|
766
|
+
tokens.refresh_token = `${TOKEN.REFRESH_TOKEN}_${(currentDatabase.refreshGrant ?? currentDatabase).configurationName}`;
|
|
536
767
|
}
|
|
537
768
|
if (tokens?.idTokenPayload?.nonce && currentDatabase.nonce != null) {
|
|
538
|
-
tokens.idTokenPayload.nonce = `${TOKEN.NONCE_TOKEN}_${configurationName}
|
|
769
|
+
tokens.idTokenPayload.nonce = `${TOKEN.NONCE_TOKEN}_${currentDatabase.configurationName}`;
|
|
539
770
|
}
|
|
540
771
|
port.postMessage({
|
|
541
772
|
tokens,
|
|
542
773
|
status: currentDatabase.status,
|
|
543
774
|
configurationName,
|
|
544
775
|
version,
|
|
776
|
+
resourceTokenManagement: true,
|
|
777
|
+
resourceContexts: Object.keys(currentDatabase.resourceEntries ?? {}),
|
|
545
778
|
});
|
|
546
779
|
}
|
|
547
780
|
return;
|
|
@@ -585,7 +818,7 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
|
|
|
585
818
|
case 'getCodeVerifier': {
|
|
586
819
|
const codeVerifier =
|
|
587
820
|
currentDatabase.codeVerifier != null
|
|
588
|
-
? `${TOKEN.CODE_VERIFIER}_${configurationName}
|
|
821
|
+
? `${TOKEN.CODE_VERIFIER}_${currentDatabase.configurationName}`
|
|
589
822
|
: null;
|
|
590
823
|
port.postMessage({
|
|
591
824
|
configurationName,
|
|
@@ -609,6 +842,17 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
|
|
|
609
842
|
case 'setNonce': {
|
|
610
843
|
const nonce = data.data.nonce;
|
|
611
844
|
if (nonce) {
|
|
845
|
+
if (
|
|
846
|
+
!currentDatabase.resource &&
|
|
847
|
+
currentDatabase.nonce &&
|
|
848
|
+
nonce.nonce !== currentDatabase.nonce.nonce
|
|
849
|
+
) {
|
|
850
|
+
currentDatabase.generation = (currentDatabase.generation ?? 0) + 1;
|
|
851
|
+
for (const entry of Object.values(currentDatabase.resourceEntries ?? {})) {
|
|
852
|
+
entry.generation = (entry.generation ?? 0) + 1;
|
|
853
|
+
}
|
|
854
|
+
currentDatabase.resourceEntries = undefined;
|
|
855
|
+
}
|
|
612
856
|
currentDatabase.nonce = nonce;
|
|
613
857
|
}
|
|
614
858
|
port.postMessage({ configurationName });
|
|
@@ -616,7 +860,7 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
|
|
|
616
860
|
}
|
|
617
861
|
|
|
618
862
|
case 'getNonce': {
|
|
619
|
-
const keyNonce = `${TOKEN.NONCE_TOKEN}_${configurationName}
|
|
863
|
+
const keyNonce = `${TOKEN.NONCE_TOKEN}_${currentDatabase.configurationName}`;
|
|
620
864
|
const nonce = currentDatabase.nonce ? keyNonce : null;
|
|
621
865
|
port.postMessage({ configurationName, nonce });
|
|
622
866
|
return;
|
|
@@ -627,6 +871,18 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
|
|
|
627
871
|
}
|
|
628
872
|
};
|
|
629
873
|
|
|
874
|
+
const handleMessage = async (event: ExtendableMessageEvent): Promise<void> => {
|
|
875
|
+
try {
|
|
876
|
+
await handleMessageAsync(event);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
console.error('[OidcServiceWorker] Message failed:', error);
|
|
879
|
+
event.ports?.[0]?.postMessage({
|
|
880
|
+
configurationName: event.data?.configurationName,
|
|
881
|
+
error: error instanceof Error ? error.message : 'The worker could not process this message.',
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
630
886
|
// Event listeners
|
|
631
887
|
_self.addEventListener('fetch', handleFetch);
|
|
632
888
|
_self.addEventListener('message', handleMessage);
|
|
@@ -12,6 +12,23 @@ const decodePayload = (jwt: string) => {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
describe('generateJwtDemonstratingProofOfPossessionAsync', () => {
|
|
15
|
+
it.each([
|
|
16
|
+
['https://api.example.com/resource?limit=2#section', 'https://api.example.com/resource'],
|
|
17
|
+
['/resource?limit=2#section', 'https://api.example.com/resource'],
|
|
18
|
+
])('excludes query and fragment from the worker DPoP target %s', async (url, expected) => {
|
|
19
|
+
const environment = {
|
|
20
|
+
crypto: globalThis.crypto,
|
|
21
|
+
location: { href: 'https://api.example.com/' },
|
|
22
|
+
};
|
|
23
|
+
const jwk = await generateJwkAsync(environment)(
|
|
24
|
+
defaultDemonstratingProofOfPossessionConfiguration.generateKeyAlgorithm,
|
|
25
|
+
);
|
|
26
|
+
const jwt = await generateJwtDemonstratingProofOfPossessionAsync(environment)(
|
|
27
|
+
defaultDemonstratingProofOfPossessionConfiguration,
|
|
28
|
+
)(jwk, 'GET', url);
|
|
29
|
+
expect(decodePayload(jwt).htu).toBe(expected);
|
|
30
|
+
});
|
|
31
|
+
|
|
15
32
|
it('uses a cryptographically secure UUID for the DPoP jti in the service worker', async () => {
|
|
16
33
|
const uuid = '123e4567-e89b-42d3-a456-426614174000';
|
|
17
34
|
const randomUUID = vi.fn(() => uuid);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getContextResource,
|
|
5
|
+
getResourceConfiguration,
|
|
6
|
+
getResourceContextName,
|
|
7
|
+
isResourceOriginManaged,
|
|
8
|
+
isResourceUrlAllowed,
|
|
9
|
+
validateResourceIdentifier,
|
|
10
|
+
} from '../resource';
|
|
11
|
+
|
|
12
|
+
describe('RFC 8707 resource configuration', () => {
|
|
13
|
+
it('recognizes managed origins outside mapped paths without permitting those paths', () => {
|
|
14
|
+
const configuration = {
|
|
15
|
+
resource: 'urn:orders',
|
|
16
|
+
allowedUrlPrefixes: ['https://api.example.com/orders'],
|
|
17
|
+
};
|
|
18
|
+
expect(isResourceOriginManaged([configuration], 'https://api.example.com/v2/items')).toBe(true);
|
|
19
|
+
expect(isResourceUrlAllowed(configuration, 'https://api.example.com/v2/items')).toBe(false);
|
|
20
|
+
expect(
|
|
21
|
+
isResourceOriginManaged([configuration], 'https://api.example.com.evil.test/orders'),
|
|
22
|
+
).toBe(false);
|
|
23
|
+
expect(() =>
|
|
24
|
+
isResourceOriginManaged(
|
|
25
|
+
[{ ...configuration, allowedUrlPrefixes: ['https://api.example.com/orders?query'] }],
|
|
26
|
+
'https://api.example.com/orders',
|
|
27
|
+
),
|
|
28
|
+
).toThrow('query');
|
|
29
|
+
});
|
|
30
|
+
const configuration = {
|
|
31
|
+
resource: 'urn:example:api',
|
|
32
|
+
allowedUrlPrefixes: ['https://api.example.com/orders'],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
it.each(['urn:example:api', 'https://api.example.com/?tenant=one'])(
|
|
36
|
+
'accepts the absolute identifier %s',
|
|
37
|
+
resource => {
|
|
38
|
+
expect(() => validateResourceIdentifier(resource)).not.toThrow();
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
it.each([
|
|
43
|
+
'',
|
|
44
|
+
'/api',
|
|
45
|
+
'https://api.example.com/#fragment',
|
|
46
|
+
'urn:example:api#',
|
|
47
|
+
' https://api.example.com',
|
|
48
|
+
])('rejects the invalid identifier %s', resource => {
|
|
49
|
+
expect(() => validateResourceIdentifier(resource)).toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('requires an explicit, unique registration and API mappings', () => {
|
|
53
|
+
expect(getResourceConfiguration([configuration], configuration.resource)).toBe(configuration);
|
|
54
|
+
expect(() => getResourceConfiguration([], configuration.resource)).toThrow();
|
|
55
|
+
expect(() =>
|
|
56
|
+
getResourceConfiguration([configuration, configuration], configuration.resource),
|
|
57
|
+
).toThrow();
|
|
58
|
+
expect(() =>
|
|
59
|
+
getResourceConfiguration(
|
|
60
|
+
[{ ...configuration, allowedUrlPrefixes: [] }],
|
|
61
|
+
configuration.resource,
|
|
62
|
+
),
|
|
63
|
+
).toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it.each(['https://api.example.com/orders', 'https://api.example.com/orders/42?view=full'])(
|
|
67
|
+
'permits the mapped destination %s',
|
|
68
|
+
url => {
|
|
69
|
+
expect(isResourceUrlAllowed(configuration, url)).toBe(true);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
it.each([
|
|
74
|
+
'https://api.example.com.evil.test/orders',
|
|
75
|
+
'https://api.example.com/orders-other',
|
|
76
|
+
'https://other.example.com/orders',
|
|
77
|
+
'https://api.example.com:444/orders',
|
|
78
|
+
])('rejects the unmapped destination %s', url => {
|
|
79
|
+
expect(isResourceUrlAllowed(configuration, url)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it.each([
|
|
83
|
+
'https://user:password@api.example.com/orders',
|
|
84
|
+
'https://api.example.com/orders%2fother',
|
|
85
|
+
'file:///orders',
|
|
86
|
+
'https://api.example.com/orders#fragment',
|
|
87
|
+
])('rejects the unsafe destination %s', url => {
|
|
88
|
+
expect(() => isResourceUrlAllowed(configuration, url)).toThrow();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('round-trips resource context markers without collisions', () => {
|
|
92
|
+
const resource = 'urn:example:a&b=%25';
|
|
93
|
+
const name = getResourceContextName('default#tabId=one', resource);
|
|
94
|
+
expect(getContextResource(name)).toBe(resource);
|
|
95
|
+
expect(getContextResource('default')).toBeUndefined();
|
|
96
|
+
expect(name).not.toBe(getResourceContextName('default#tabId=one', 'urn:example:a'));
|
|
97
|
+
});
|
|
98
|
+
});
|