oauth4webapi 2.1.0 → 2.2.1
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 +43 -22
- package/package.json +16 -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.1
|
|
42
|
+
import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.2.1/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.1
|
|
4
|
+
const VERSION = 'v2.2.1';
|
|
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
|
}
|
|
@@ -843,10 +861,14 @@ export async function refreshTokenGrantRequest(as, client, refreshToken, options
|
|
|
843
861
|
}
|
|
844
862
|
const idTokenClaims = new WeakMap();
|
|
845
863
|
export function getValidatedIdTokenClaims(ref) {
|
|
846
|
-
if (!
|
|
864
|
+
if (!ref.id_token) {
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
const claims = idTokenClaims.get(ref);
|
|
868
|
+
if (!claims) {
|
|
847
869
|
throw new TypeError('"ref" was already garbage collected or did not resolve from the proper sources');
|
|
848
870
|
}
|
|
849
|
-
return
|
|
871
|
+
return claims;
|
|
850
872
|
}
|
|
851
873
|
async function processGenericAccessTokenResponse(as, client, response, ignoreIdToken = false, ignoreRefreshToken = false) {
|
|
852
874
|
assertAs(as);
|
|
@@ -899,7 +921,7 @@ async function processGenericAccessTokenResponse(as, client, response, ignoreIdT
|
|
|
899
921
|
throw new OPE('"response" body "id_token" property must be a non-empty string');
|
|
900
922
|
}
|
|
901
923
|
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)
|
|
924
|
+
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
925
|
.then(validatePresence.bind(undefined, ['aud', 'exp', 'iat', 'iss', 'sub']))
|
|
904
926
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
905
927
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
@@ -1003,8 +1025,8 @@ export async function processAuthorizationCodeOpenIDResponse(as, client, respons
|
|
|
1003
1025
|
if (typeof maxAge !== 'number' || maxAge < 0) {
|
|
1004
1026
|
throw new TypeError('"options.max_age" must be a non-negative number');
|
|
1005
1027
|
}
|
|
1006
|
-
const now = epochTime();
|
|
1007
|
-
const tolerance =
|
|
1028
|
+
const now = epochTime() + getClockSkew(client);
|
|
1029
|
+
const tolerance = getClockTolerance(client);
|
|
1008
1030
|
if (claims.auth_time + maxAge < now - tolerance) {
|
|
1009
1031
|
throw new OPE('too much time has elapsed since the last End-User authentication');
|
|
1010
1032
|
}
|
|
@@ -1131,7 +1153,7 @@ export async function processIntrospectionResponse(as, client, response) {
|
|
|
1131
1153
|
let json;
|
|
1132
1154
|
if (getContentType(response) === 'application/token-introspection+jwt') {
|
|
1133
1155
|
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)
|
|
1156
|
+
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
1157
|
.then(checkJwtType.bind(undefined, 'token-introspection+jwt'))
|
|
1136
1158
|
.then(validatePresence.bind(undefined, ['aud', 'iat', 'iss']))
|
|
1137
1159
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
@@ -1279,7 +1301,7 @@ function keyToSubtle(key) {
|
|
|
1279
1301
|
throw new UnsupportedOperationError();
|
|
1280
1302
|
}
|
|
1281
1303
|
const noSignatureCheck = Symbol();
|
|
1282
|
-
async function validateJwt(jws, checkAlg, getKey) {
|
|
1304
|
+
async function validateJwt(jws, checkAlg, getKey, clockSkew, clockTolerance) {
|
|
1283
1305
|
const { 0: protectedHeader, 1: payload, 2: encodedSignature, length } = jws.split('.');
|
|
1284
1306
|
if (length === 5) {
|
|
1285
1307
|
throw new UnsupportedOperationError('JWE structure JWTs are not supported');
|
|
@@ -1320,13 +1342,12 @@ async function validateJwt(jws, checkAlg, getKey) {
|
|
|
1320
1342
|
if (!isJsonObject(claims)) {
|
|
1321
1343
|
throw new OPE('JWT Payload must be a top level object');
|
|
1322
1344
|
}
|
|
1323
|
-
const now = epochTime();
|
|
1324
|
-
const tolerance = 30;
|
|
1345
|
+
const now = epochTime() + clockSkew;
|
|
1325
1346
|
if (claims.exp !== undefined) {
|
|
1326
1347
|
if (typeof claims.exp !== 'number') {
|
|
1327
1348
|
throw new OPE('unexpected JWT "exp" (expiration time) claim type');
|
|
1328
1349
|
}
|
|
1329
|
-
if (claims.exp <= now -
|
|
1350
|
+
if (claims.exp <= now - clockTolerance) {
|
|
1330
1351
|
throw new OPE('unexpected JWT "exp" (expiration time) claim value, timestamp is <= now()');
|
|
1331
1352
|
}
|
|
1332
1353
|
}
|
|
@@ -1344,7 +1365,7 @@ async function validateJwt(jws, checkAlg, getKey) {
|
|
|
1344
1365
|
if (typeof claims.nbf !== 'number') {
|
|
1345
1366
|
throw new OPE('unexpected JWT "nbf" (not before) claim type');
|
|
1346
1367
|
}
|
|
1347
|
-
if (claims.nbf > now +
|
|
1368
|
+
if (claims.nbf > now + clockTolerance) {
|
|
1348
1369
|
throw new OPE('unexpected JWT "nbf" (not before) claim value, timestamp is > now()');
|
|
1349
1370
|
}
|
|
1350
1371
|
}
|
|
@@ -1371,7 +1392,7 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
|
|
|
1371
1392
|
if (typeof as.jwks_uri !== 'string') {
|
|
1372
1393
|
throw new TypeError('"as.jwks_uri" must be a string');
|
|
1373
1394
|
}
|
|
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))
|
|
1395
|
+
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
1396
|
.then(validatePresence.bind(undefined, ['aud', 'exp', 'iss']))
|
|
1376
1397
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
1377
1398
|
.then(validateAudience.bind(undefined, client.client_id));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oauth4webapi",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "OAuth 2 / OpenID Connect for Web Platform API JavaScript runtimes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -62,21 +62,22 @@
|
|
|
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.11
|
|
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.11",
|
|
67
|
+
"@types/qunit": "^2.19.4",
|
|
68
|
+
"ava": "^5.2.0",
|
|
69
|
+
"edge-runtime": "^2.1.4",
|
|
70
|
+
"esbuild": "^0.17.16",
|
|
71
|
+
"jose": "^4.13.2",
|
|
72
|
+
"patch-package": "^6.5.1",
|
|
73
|
+
"prettier": "^2.8.7",
|
|
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.
|
|
78
|
-
"typedoc-plugin-markdown": "^3.
|
|
79
|
-
"
|
|
80
|
-
"
|
|
77
|
+
"typedoc": "^0.24.1",
|
|
78
|
+
"typedoc-plugin-markdown": "^3.15.1",
|
|
79
|
+
"typedoc-plugin-mdn-links": "^3.0.3",
|
|
80
|
+
"typescript": "^5.0.4",
|
|
81
|
+
"undici": "^5.21.2"
|
|
81
82
|
}
|
|
82
83
|
}
|