oauth4webapi 2.1.0 → 2.2.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 +1 -1
- package/build/index.d.ts +58 -0
- package/build/index.js +37 -20
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ import * as oauth2 from 'oauth4webapi'
|
|
|
39
39
|
**`example`** Deno import
|
|
40
40
|
|
|
41
41
|
```js
|
|
42
|
-
import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.
|
|
42
|
+
import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.2.0/mod.ts'
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
- Authorization Code Flow - OpenID Connect [source](examples/code.ts), or plain OAuth 2 [source](examples/oauth.ts)
|
package/build/index.d.ts
CHANGED
|
@@ -121,6 +121,10 @@ export type ClientAuthenticationMethod = 'client_secret_basic' | 'client_secret_
|
|
|
121
121
|
* ```
|
|
122
122
|
*/
|
|
123
123
|
export type JWSAlgorithm = 'PS256' | 'ES256' | 'RS256' | 'EdDSA' | 'ES384' | 'PS384' | 'RS384' | 'ES512' | 'PS512' | 'RS512';
|
|
124
|
+
/** @ignore during Documentation generation but part of the public API */
|
|
125
|
+
export declare const clockSkew: unique symbol;
|
|
126
|
+
/** @ignore during Documentation generation but part of the public API */
|
|
127
|
+
export declare const clockTolerance: unique symbol;
|
|
124
128
|
/**
|
|
125
129
|
* Authorization Server Metadata
|
|
126
130
|
*
|
|
@@ -442,6 +446,50 @@ export interface Client {
|
|
|
442
446
|
introspection_signed_response_alg?: string;
|
|
443
447
|
/** Default Maximum Authentication Age. */
|
|
444
448
|
default_max_age?: number;
|
|
449
|
+
/**
|
|
450
|
+
* Use to adjust the client's assumed current time. Positive and negative finite values
|
|
451
|
+
* representing seconds are allowed. Default is `0` (Date.now() + 0 seconds is used).
|
|
452
|
+
*
|
|
453
|
+
* @ignore during Documentation generation but part of the public API
|
|
454
|
+
*
|
|
455
|
+
* @example Client's local clock is mistakenly 1 hour in the past
|
|
456
|
+
*
|
|
457
|
+
* ```ts
|
|
458
|
+
* const client: oauth.Client = {
|
|
459
|
+
* client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428',
|
|
460
|
+
* // ... other metadata
|
|
461
|
+
* [oauth.clockSkew]: +(60 * 60),
|
|
462
|
+
* }
|
|
463
|
+
* ```
|
|
464
|
+
*
|
|
465
|
+
* @example Client's local clock is mistakenly 1 hour in the future
|
|
466
|
+
*
|
|
467
|
+
* ```ts
|
|
468
|
+
* const client: oauth.Client = {
|
|
469
|
+
* client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428',
|
|
470
|
+
* // ... other metadata
|
|
471
|
+
* [oauth.clockSkew]: -(60 * 60),
|
|
472
|
+
* }
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
[clockSkew]?: number;
|
|
476
|
+
/**
|
|
477
|
+
* Use to set allowed client's clock tolerance when checking DateTime JWT Claims. Only positive
|
|
478
|
+
* finite values representing seconds are allowed. Default is `30` (30 seconds).
|
|
479
|
+
*
|
|
480
|
+
* @ignore during Documentation generation but part of the public API
|
|
481
|
+
*
|
|
482
|
+
* @example Tolerate 30 seconds clock skew when validating JWT claims like `exp` or `nbf`.
|
|
483
|
+
*
|
|
484
|
+
* ```ts
|
|
485
|
+
* const client: oauth.Client = {
|
|
486
|
+
* client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428',
|
|
487
|
+
* // ... other metadata
|
|
488
|
+
* [oauth.clockTolerance]: 30,
|
|
489
|
+
* }
|
|
490
|
+
* ```
|
|
491
|
+
*/
|
|
492
|
+
[clockTolerance]?: number;
|
|
445
493
|
[metadata: string]: JsonValue | undefined;
|
|
446
494
|
}
|
|
447
495
|
export declare class UnsupportedOperationError extends Error {
|
|
@@ -629,6 +677,16 @@ export declare function parseWwwAuthenticateChallenges(response: Response): WWWA
|
|
|
629
677
|
*/
|
|
630
678
|
export declare function processPushedAuthorizationResponse(as: AuthorizationServer, client: Client, response: Response): Promise<PushedAuthorizationResponse | OAuth2Error>;
|
|
631
679
|
export interface ProtectedResourceRequestOptions extends Omit<HttpRequestOptions, 'headers'>, DPoPRequestOptions {
|
|
680
|
+
/**
|
|
681
|
+
* Use to adjust the client's assumed current time. Positive and negative finite values
|
|
682
|
+
* representing seconds are allowed. Default is `0` (Date.now() + 0 seconds is used).
|
|
683
|
+
*
|
|
684
|
+
* This option only affects the request if the {@link ProtectedResourceRequestOptions.DPoP DPoP}
|
|
685
|
+
* option is also used.
|
|
686
|
+
*
|
|
687
|
+
* @ignore during Documentation generation but part of the public API
|
|
688
|
+
*/
|
|
689
|
+
clockSkew?: number;
|
|
632
690
|
}
|
|
633
691
|
/**
|
|
634
692
|
* Performs a protected resource request at an arbitrary URL.
|
package/build/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
let USER_AGENT;
|
|
2
2
|
if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
|
|
3
3
|
const NAME = 'oauth4webapi';
|
|
4
|
-
const VERSION = 'v2.
|
|
4
|
+
const VERSION = 'v2.2.0';
|
|
5
5
|
USER_AGENT = `${NAME}/${VERSION}`;
|
|
6
6
|
}
|
|
7
|
+
export const clockSkew = Symbol();
|
|
8
|
+
export const clockTolerance = Symbol();
|
|
7
9
|
const encoder = new TextEncoder();
|
|
8
10
|
const decoder = new TextDecoder();
|
|
9
11
|
function buf(input) {
|
|
@@ -321,11 +323,24 @@ function keyToJws(key) {
|
|
|
321
323
|
throw new UnsupportedOperationError('unsupported CryptoKey algorithm name');
|
|
322
324
|
}
|
|
323
325
|
}
|
|
326
|
+
function getClockSkew(client) {
|
|
327
|
+
if (Number.isFinite(client[clockSkew])) {
|
|
328
|
+
return client[clockSkew];
|
|
329
|
+
}
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
function getClockTolerance(client) {
|
|
333
|
+
const tolerance = client[clockTolerance];
|
|
334
|
+
if (Number.isFinite(tolerance) && Math.sign(tolerance) !== -1) {
|
|
335
|
+
return tolerance;
|
|
336
|
+
}
|
|
337
|
+
return 30;
|
|
338
|
+
}
|
|
324
339
|
function epochTime() {
|
|
325
340
|
return Math.floor(Date.now() / 1000);
|
|
326
341
|
}
|
|
327
342
|
function clientAssertion(as, client) {
|
|
328
|
-
const now = epochTime();
|
|
343
|
+
const now = epochTime() + getClockSkew(client);
|
|
329
344
|
return {
|
|
330
345
|
jti: randomBytes(),
|
|
331
346
|
aud: [as.issuer, as.token_endpoint],
|
|
@@ -437,7 +452,7 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
|
|
|
437
452
|
throw new TypeError('"privateKey.key" must be a private CryptoKey');
|
|
438
453
|
}
|
|
439
454
|
parameters.set('client_id', client.client_id);
|
|
440
|
-
const now = epochTime();
|
|
455
|
+
const now = epochTime() + getClockSkew(client);
|
|
441
456
|
const claims = {
|
|
442
457
|
...Object.fromEntries(parameters.entries()),
|
|
443
458
|
jti: randomBytes(),
|
|
@@ -474,7 +489,7 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
|
|
|
474
489
|
kid,
|
|
475
490
|
}, claims, key);
|
|
476
491
|
}
|
|
477
|
-
async function dpopProofJwt(headers, options, url, htm, accessToken) {
|
|
492
|
+
async function dpopProofJwt(headers, options, url, htm, clockSkew, accessToken) {
|
|
478
493
|
const { privateKey, publicKey, nonce = dpopNonces.get(url.origin) } = options;
|
|
479
494
|
if (!isPrivateKey(privateKey)) {
|
|
480
495
|
throw new TypeError('"DPoP.privateKey" must be a private CryptoKey');
|
|
@@ -488,7 +503,7 @@ async function dpopProofJwt(headers, options, url, htm, accessToken) {
|
|
|
488
503
|
if (!publicKey.extractable) {
|
|
489
504
|
throw new TypeError('"DPoP.publicKey.extractable" must be true');
|
|
490
505
|
}
|
|
491
|
-
const now = epochTime();
|
|
506
|
+
const now = epochTime() + clockSkew;
|
|
492
507
|
const proof = await jwt({
|
|
493
508
|
alg: keyToJws(privateKey),
|
|
494
509
|
typ: 'dpop+jwt',
|
|
@@ -531,7 +546,7 @@ export async function pushedAuthorizationRequest(as, client, parameters, options
|
|
|
531
546
|
const headers = prepareHeaders(options?.headers);
|
|
532
547
|
headers.set('accept', 'application/json');
|
|
533
548
|
if (options?.DPoP !== undefined) {
|
|
534
|
-
await dpopProofJwt(headers, options.DPoP, url, 'POST');
|
|
549
|
+
await dpopProofJwt(headers, options.DPoP, url, 'POST', getClockSkew(client));
|
|
535
550
|
}
|
|
536
551
|
return authenticatedRequest(as, client, 'POST', url, body, headers, options);
|
|
537
552
|
}
|
|
@@ -644,7 +659,7 @@ export async function protectedResourceRequest(accessToken, method, url, headers
|
|
|
644
659
|
headers.set('authorization', `Bearer ${accessToken}`);
|
|
645
660
|
}
|
|
646
661
|
else {
|
|
647
|
-
await dpopProofJwt(headers, options.DPoP, url, 'GET', accessToken);
|
|
662
|
+
await dpopProofJwt(headers, options.DPoP, url, 'GET', getClockSkew({ [clockSkew]: options?.clockSkew }), accessToken);
|
|
648
663
|
headers.set('authorization', `DPoP ${accessToken}`);
|
|
649
664
|
}
|
|
650
665
|
return fetch(url.href, {
|
|
@@ -670,7 +685,10 @@ export async function userInfoRequest(as, client, accessToken, options) {
|
|
|
670
685
|
headers.set('accept', 'application/json');
|
|
671
686
|
headers.append('accept', 'application/jwt');
|
|
672
687
|
}
|
|
673
|
-
return protectedResourceRequest(accessToken, 'GET', url, headers, null,
|
|
688
|
+
return protectedResourceRequest(accessToken, 'GET', url, headers, null, {
|
|
689
|
+
...options,
|
|
690
|
+
clockSkew: getClockSkew(client),
|
|
691
|
+
});
|
|
674
692
|
}
|
|
675
693
|
let jwksCache;
|
|
676
694
|
async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
@@ -771,7 +789,7 @@ export async function processUserInfoResponse(as, client, expectedSubject, respo
|
|
|
771
789
|
let json;
|
|
772
790
|
if (getContentType(response) === 'application/jwt') {
|
|
773
791
|
assertReadableResponse(response);
|
|
774
|
-
const { claims } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), noSignatureCheck)
|
|
792
|
+
const { claims } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client))
|
|
775
793
|
.then(validateOptionalAudience.bind(undefined, client.client_id))
|
|
776
794
|
.then(validateOptionalIssuer.bind(undefined, as.issuer));
|
|
777
795
|
json = claims;
|
|
@@ -827,7 +845,7 @@ async function tokenEndpointRequest(as, client, grantType, parameters, options)
|
|
|
827
845
|
const headers = prepareHeaders(options?.headers);
|
|
828
846
|
headers.set('accept', 'application/json');
|
|
829
847
|
if (options?.DPoP !== undefined) {
|
|
830
|
-
await dpopProofJwt(headers, options.DPoP, url, 'POST');
|
|
848
|
+
await dpopProofJwt(headers, options.DPoP, url, 'POST', getClockSkew(client));
|
|
831
849
|
}
|
|
832
850
|
return authenticatedRequest(as, client, 'POST', url, parameters, headers, options);
|
|
833
851
|
}
|
|
@@ -899,7 +917,7 @@ async function processGenericAccessTokenResponse(as, client, response, ignoreIdT
|
|
|
899
917
|
throw new OPE('"response" body "id_token" property must be a non-empty string');
|
|
900
918
|
}
|
|
901
919
|
if (json.id_token) {
|
|
902
|
-
const { claims } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), noSignatureCheck)
|
|
920
|
+
const { claims } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client))
|
|
903
921
|
.then(validatePresence.bind(undefined, ['aud', 'exp', 'iat', 'iss', 'sub']))
|
|
904
922
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
905
923
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
@@ -1003,8 +1021,8 @@ export async function processAuthorizationCodeOpenIDResponse(as, client, respons
|
|
|
1003
1021
|
if (typeof maxAge !== 'number' || maxAge < 0) {
|
|
1004
1022
|
throw new TypeError('"options.max_age" must be a non-negative number');
|
|
1005
1023
|
}
|
|
1006
|
-
const now = epochTime();
|
|
1007
|
-
const tolerance =
|
|
1024
|
+
const now = epochTime() + getClockSkew(client);
|
|
1025
|
+
const tolerance = getClockTolerance(client);
|
|
1008
1026
|
if (claims.auth_time + maxAge < now - tolerance) {
|
|
1009
1027
|
throw new OPE('too much time has elapsed since the last End-User authentication');
|
|
1010
1028
|
}
|
|
@@ -1131,7 +1149,7 @@ export async function processIntrospectionResponse(as, client, response) {
|
|
|
1131
1149
|
let json;
|
|
1132
1150
|
if (getContentType(response) === 'application/token-introspection+jwt') {
|
|
1133
1151
|
assertReadableResponse(response);
|
|
1134
|
-
const { claims } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), noSignatureCheck)
|
|
1152
|
+
const { claims } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client))
|
|
1135
1153
|
.then(checkJwtType.bind(undefined, 'token-introspection+jwt'))
|
|
1136
1154
|
.then(validatePresence.bind(undefined, ['aud', 'iat', 'iss']))
|
|
1137
1155
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
@@ -1279,7 +1297,7 @@ function keyToSubtle(key) {
|
|
|
1279
1297
|
throw new UnsupportedOperationError();
|
|
1280
1298
|
}
|
|
1281
1299
|
const noSignatureCheck = Symbol();
|
|
1282
|
-
async function validateJwt(jws, checkAlg, getKey) {
|
|
1300
|
+
async function validateJwt(jws, checkAlg, getKey, clockSkew, clockTolerance) {
|
|
1283
1301
|
const { 0: protectedHeader, 1: payload, 2: encodedSignature, length } = jws.split('.');
|
|
1284
1302
|
if (length === 5) {
|
|
1285
1303
|
throw new UnsupportedOperationError('JWE structure JWTs are not supported');
|
|
@@ -1320,13 +1338,12 @@ async function validateJwt(jws, checkAlg, getKey) {
|
|
|
1320
1338
|
if (!isJsonObject(claims)) {
|
|
1321
1339
|
throw new OPE('JWT Payload must be a top level object');
|
|
1322
1340
|
}
|
|
1323
|
-
const now = epochTime();
|
|
1324
|
-
const tolerance = 30;
|
|
1341
|
+
const now = epochTime() + clockSkew;
|
|
1325
1342
|
if (claims.exp !== undefined) {
|
|
1326
1343
|
if (typeof claims.exp !== 'number') {
|
|
1327
1344
|
throw new OPE('unexpected JWT "exp" (expiration time) claim type');
|
|
1328
1345
|
}
|
|
1329
|
-
if (claims.exp <= now -
|
|
1346
|
+
if (claims.exp <= now - clockTolerance) {
|
|
1330
1347
|
throw new OPE('unexpected JWT "exp" (expiration time) claim value, timestamp is <= now()');
|
|
1331
1348
|
}
|
|
1332
1349
|
}
|
|
@@ -1344,7 +1361,7 @@ async function validateJwt(jws, checkAlg, getKey) {
|
|
|
1344
1361
|
if (typeof claims.nbf !== 'number') {
|
|
1345
1362
|
throw new OPE('unexpected JWT "nbf" (not before) claim type');
|
|
1346
1363
|
}
|
|
1347
|
-
if (claims.nbf > now +
|
|
1364
|
+
if (claims.nbf > now + clockTolerance) {
|
|
1348
1365
|
throw new OPE('unexpected JWT "nbf" (not before) claim value, timestamp is > now()');
|
|
1349
1366
|
}
|
|
1350
1367
|
}
|
|
@@ -1371,7 +1388,7 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
|
|
|
1371
1388
|
if (typeof as.jwks_uri !== 'string') {
|
|
1372
1389
|
throw new TypeError('"as.jwks_uri" must be a string');
|
|
1373
1390
|
}
|
|
1374
|
-
const { claims } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
|
|
1391
|
+
const { claims } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(client), getClockTolerance(client))
|
|
1375
1392
|
.then(validatePresence.bind(undefined, ['aud', 'exp', 'iss']))
|
|
1376
1393
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
1377
1394
|
.then(validateAudience.bind(undefined, client.client_id));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oauth4webapi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "OAuth 2 / OpenID Connect for Web Platform API JavaScript runtimes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -62,21 +62,21 @@
|
|
|
62
62
|
"test": "bash -c 'source .node_flags.sh && ava'"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@esbuild-kit/esm-loader": "^2.5.
|
|
66
|
-
"@types/node": "^18.
|
|
67
|
-
"@types/qunit": "^2.19.
|
|
68
|
-
"ava": "^5.
|
|
69
|
-
"edge-runtime": "^2.
|
|
70
|
-
"esbuild": "^0.17.
|
|
71
|
-
"jose": "^4.
|
|
72
|
-
"patch-package": "^6.5.
|
|
73
|
-
"prettier": "^2.8.
|
|
65
|
+
"@esbuild-kit/esm-loader": "^2.5.5",
|
|
66
|
+
"@types/node": "^18.15.0",
|
|
67
|
+
"@types/qunit": "^2.19.4",
|
|
68
|
+
"ava": "^5.2.0",
|
|
69
|
+
"edge-runtime": "^2.1.2",
|
|
70
|
+
"esbuild": "^0.17.11",
|
|
71
|
+
"jose": "^4.13.1",
|
|
72
|
+
"patch-package": "^6.5.1",
|
|
73
|
+
"prettier": "^2.8.4",
|
|
74
74
|
"prettier-plugin-jsdoc": "^0.4.2",
|
|
75
|
-
"qunit": "^2.19.
|
|
75
|
+
"qunit": "^2.19.4",
|
|
76
76
|
"timekeeper": "^2.2.0",
|
|
77
|
-
"typedoc": "^0.23.
|
|
78
|
-
"typedoc-plugin-markdown": "^3.
|
|
79
|
-
"typescript": "^4.9.
|
|
80
|
-
"undici": "^5.
|
|
77
|
+
"typedoc": "^0.23.26",
|
|
78
|
+
"typedoc-plugin-markdown": "^3.14.0",
|
|
79
|
+
"typescript": "^4.9.5",
|
|
80
|
+
"undici": "^5.20.0"
|
|
81
81
|
}
|
|
82
82
|
}
|