oauth4webapi 2.15.0 → 2.17.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 +4 -3
- package/build/index.d.ts +196 -64
- package/build/index.js +71 -48
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# OAuth 2 / OpenID Connect for JavaScript Runtimes
|
|
1
|
+
# Low-Level OAuth 2 / OpenID Connect Client API for JavaScript Runtimes
|
|
2
2
|
|
|
3
3
|
This software provides a collection of routines that can be used to build client modules for OAuth 2.1, OAuth 2.0 with the latest Security Best Current Practices (BCP), and FAPI 2.0, as well as OpenID Connect where applicable. The primary goal of this software is to promote secure and up-to-date best practices while using only the capabilities common to both browser and non-browser JavaScript runtimes.
|
|
4
4
|
|
|
@@ -60,8 +60,9 @@ import * as oauth from 'oauth4webapi'
|
|
|
60
60
|
- Device Authorization Grant - [source](examples/device_authorization_grant.ts)
|
|
61
61
|
- Refresh Token Grant - [source](examples/refresh_token.ts) | [diff](examples/refresh_token.diff)
|
|
62
62
|
- FAPI
|
|
63
|
-
- FAPI 1.0 Advanced
|
|
64
|
-
- FAPI 2.0 Security Profile
|
|
63
|
+
- FAPI 1.0 Advanced - [source](examples/fapi1-advanced.ts) | [diff](examples/fapi1-advanced.diff)
|
|
64
|
+
- FAPI 2.0 Security Profile - [source](examples/fapi2.ts) | [diff](examples/fapi2.diff)
|
|
65
|
+
- FAPI 2.0 Message Signing - [source](examples/fapi2-message-signing.ts) | [diff](examples/fapi2-message-signing.diff)
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
## Supported Runtimes
|
package/build/index.d.ts
CHANGED
|
@@ -16,6 +16,17 @@ export type JsonPrimitive = string | number | boolean | null;
|
|
|
16
16
|
* JSON Values
|
|
17
17
|
*/
|
|
18
18
|
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
|
19
|
+
export interface ModifyAssertionFunction {
|
|
20
|
+
(
|
|
21
|
+
/**
|
|
22
|
+
* JWS Header to modify right before it is signed.
|
|
23
|
+
*/
|
|
24
|
+
header: Record<string, JsonValue | undefined>,
|
|
25
|
+
/**
|
|
26
|
+
* JWT Claims Set to modify right before it is signed.
|
|
27
|
+
*/
|
|
28
|
+
payload: Record<string, JsonValue | undefined>): void;
|
|
29
|
+
}
|
|
19
30
|
/**
|
|
20
31
|
* Interface to pass an asymmetric private key and, optionally, its associated JWK Key ID to be
|
|
21
32
|
* added as a `kid` JOSE Header Parameter.
|
|
@@ -32,6 +43,12 @@ export interface PrivateKey {
|
|
|
32
43
|
* ID) will be added to the JOSE Header.
|
|
33
44
|
*/
|
|
34
45
|
kid?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Use to modify the JWT signed by this key right before it is signed.
|
|
48
|
+
*
|
|
49
|
+
* @see {@link modifyAssertion}
|
|
50
|
+
*/
|
|
51
|
+
[modifyAssertion]?: ModifyAssertionFunction;
|
|
35
52
|
}
|
|
36
53
|
/**
|
|
37
54
|
* Supported Client Authentication Methods.
|
|
@@ -278,6 +295,100 @@ export declare const clockTolerance: unique symbol;
|
|
|
278
295
|
* ```
|
|
279
296
|
*/
|
|
280
297
|
export declare const customFetch: unique symbol;
|
|
298
|
+
/**
|
|
299
|
+
* Use to mutate JWT header and payload before they are signed. Its intended use is working around
|
|
300
|
+
* non-conform server behaviours, such as modifying JWT "aud" (audience) claims, or otherwise
|
|
301
|
+
* changing fixed claims used by this library.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
*
|
|
305
|
+
* Changing Private Key JWT client assertion audience issued from an array to a string
|
|
306
|
+
*
|
|
307
|
+
* ```ts
|
|
308
|
+
* import * as oauth from 'oauth4webapi'
|
|
309
|
+
*
|
|
310
|
+
* // Prerequisites
|
|
311
|
+
* let as!: oauth.AuthorizationServer
|
|
312
|
+
* let client!: oauth.Client
|
|
313
|
+
* let parameters!: URLSearchParams
|
|
314
|
+
* let clientPrivateKey!: CryptoKey
|
|
315
|
+
*
|
|
316
|
+
* const response = await oauth.pushedAuthorizationRequest(as, client, parameters, {
|
|
317
|
+
* clientPrivateKey: {
|
|
318
|
+
* key: clientPrivateKey,
|
|
319
|
+
* [oauth.modifyAssertion](header, payload) {
|
|
320
|
+
* payload.aud = as.issuer
|
|
321
|
+
* },
|
|
322
|
+
* },
|
|
323
|
+
* })
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
*
|
|
328
|
+
* Changing Request Object issued by {@link issueRequestObject} to have an expiration of 5 minutes
|
|
329
|
+
*
|
|
330
|
+
* ```ts
|
|
331
|
+
* import * as oauth from 'oauth4webapi'
|
|
332
|
+
*
|
|
333
|
+
* // Prerequisites
|
|
334
|
+
* let as!: oauth.AuthorizationServer
|
|
335
|
+
* let client!: oauth.Client
|
|
336
|
+
* let parameters!: URLSearchParams
|
|
337
|
+
* let jarPrivateKey!: CryptoKey
|
|
338
|
+
*
|
|
339
|
+
* const request = await oauth.issueRequestObject(as, client, parameters, {
|
|
340
|
+
* key: jarPrivateKey,
|
|
341
|
+
* [oauth.modifyAssertion](header, payload) {
|
|
342
|
+
* payload.exp = <number>payload.iat + 300
|
|
343
|
+
* },
|
|
344
|
+
* })
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
export declare const modifyAssertion: unique symbol;
|
|
348
|
+
/**
|
|
349
|
+
* Use to add support for decrypting JWEs the client encounters, namely
|
|
350
|
+
*
|
|
351
|
+
* - Encrypted ID Tokens returned by the Token Endpoint
|
|
352
|
+
* - Encrypted ID Tokens returned as part of FAPI 1.0 Advanced Detached Signature authorization
|
|
353
|
+
* responses
|
|
354
|
+
* - Encrypted JWT UserInfo responses
|
|
355
|
+
* - Encrypted JWT Introspection responses
|
|
356
|
+
* - Encrypted JARM Responses
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
*
|
|
360
|
+
* Decrypting JARM responses
|
|
361
|
+
*
|
|
362
|
+
* ```ts
|
|
363
|
+
* import * as oauth from 'oauth4webapi'
|
|
364
|
+
* import * as jose from 'jose'
|
|
365
|
+
*
|
|
366
|
+
* // Prerequisites
|
|
367
|
+
* let as!: oauth.AuthorizationServer
|
|
368
|
+
* let key!: CryptoKey
|
|
369
|
+
* let alg!: string
|
|
370
|
+
* let enc!: string
|
|
371
|
+
*
|
|
372
|
+
* const decoder = new TextDecoder()
|
|
373
|
+
*
|
|
374
|
+
* const client: oauth.Client = {
|
|
375
|
+
* client_id: 'urn:example:client_id',
|
|
376
|
+
* async [oauth.jweDecrypt](jwe) {
|
|
377
|
+
* const { plaintext } = await compactDecrypt(jwe, key, {
|
|
378
|
+
* keyManagementAlgorithms: [alg],
|
|
379
|
+
* contentEncryptionAlgorithms: [enc],
|
|
380
|
+
* }).catch((cause) => {
|
|
381
|
+
* throw new oauth.OperationProcessingError('decryption failed', { cause })
|
|
382
|
+
* })
|
|
383
|
+
*
|
|
384
|
+
* return decoder.decode(plaintext)
|
|
385
|
+
* },
|
|
386
|
+
* }
|
|
387
|
+
*
|
|
388
|
+
* const params = await oauth.validateJwtAuthResponse(as, client, currentUrl)
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
export declare const jweDecrypt: unique symbol;
|
|
281
392
|
/**
|
|
282
393
|
* DANGER ZONE - This option has security implications that must be understood, assessed for
|
|
283
394
|
* applicability, and accepted before use. It is critical that the JSON Web Key Set cache only be
|
|
@@ -333,66 +444,9 @@ export declare const customFetch: unique symbol;
|
|
|
333
444
|
*/
|
|
334
445
|
export declare const jwksCache: unique symbol;
|
|
335
446
|
/**
|
|
336
|
-
*
|
|
337
|
-
* certificates) this can be used to target FAPI 2.0 profiles that utilize Mutual-TLS for either
|
|
338
|
-
* client authentication or sender constraining. FAPI 1.0 Advanced profiles that use PAR and JARM
|
|
339
|
-
* can also be targetted.
|
|
340
|
-
*
|
|
341
|
-
* When configured on an interface that extends {@link UseMTLSAliasOptions} this makes the client
|
|
342
|
-
* prioritize an endpoint URL present in
|
|
343
|
-
* {@link AuthorizationServer.mtls_endpoint_aliases `as.mtls_endpoint_aliases`}.
|
|
344
|
-
*
|
|
345
|
-
* @example
|
|
346
|
-
*
|
|
347
|
-
* (Node.js) Using [nodejs/undici](https://github.com/nodejs/undici) for Mutual-TLS Client
|
|
348
|
-
* Authentication and Certificate-Bound Access Tokens support.
|
|
349
|
-
*
|
|
350
|
-
* ```js
|
|
351
|
-
* import * as undici from 'undici'
|
|
352
|
-
* import * as oauth from 'oauth4webapi'
|
|
353
|
-
*
|
|
354
|
-
* const response = await oauth.pushedAuthorizationRequest(as, client, params, {
|
|
355
|
-
* [oauth.useMtlsAlias]: true,
|
|
356
|
-
* [oauth.customFetch]: (...args) => {
|
|
357
|
-
* return undici.fetch(args[0], {
|
|
358
|
-
* ...args[1],
|
|
359
|
-
* dispatcher: new undici.Agent({
|
|
360
|
-
* connect: {
|
|
361
|
-
* key: clientKey,
|
|
362
|
-
* cert: clientCertificate,
|
|
363
|
-
* },
|
|
364
|
-
* }),
|
|
365
|
-
* })
|
|
366
|
-
* },
|
|
367
|
-
* })
|
|
368
|
-
* ```
|
|
369
|
-
*
|
|
370
|
-
* @example
|
|
371
|
-
*
|
|
372
|
-
* (Deno) Using Deno.createHttpClient API for Mutual-TLS Client Authentication and Certificate-Bound
|
|
373
|
-
* Access Tokens support. This is currently (Jan 2023) locked behind the --unstable command line
|
|
374
|
-
* flag.
|
|
375
|
-
*
|
|
376
|
-
* ```js
|
|
377
|
-
* import * as oauth from 'oauth4webapi'
|
|
378
|
-
*
|
|
379
|
-
* const agent = Deno.createHttpClient({
|
|
380
|
-
* certChain: clientCertificate,
|
|
381
|
-
* privateKey: clientKey,
|
|
382
|
-
* })
|
|
383
|
-
*
|
|
384
|
-
* const response = await oauth.pushedAuthorizationRequest(as, client, params, {
|
|
385
|
-
* [oauth.useMtlsAlias]: true,
|
|
386
|
-
* [oauth.customFetch]: (...args) => {
|
|
387
|
-
* return fetch(args[0], {
|
|
388
|
-
* ...args[1],
|
|
389
|
-
* client: agent,
|
|
390
|
-
* })
|
|
391
|
-
* },
|
|
392
|
-
* })
|
|
393
|
-
* ```
|
|
447
|
+
* @ignore
|
|
394
448
|
*
|
|
395
|
-
* @
|
|
449
|
+
* @deprecated Use {@link Client.use_mtls_endpoint_aliases `client.use_mtls_endpoint_aliases`}.
|
|
396
450
|
*/
|
|
397
451
|
export declare const useMtlsAlias: unique symbol;
|
|
398
452
|
/**
|
|
@@ -774,6 +828,64 @@ export interface Client {
|
|
|
774
828
|
* Default Maximum Authentication Age.
|
|
775
829
|
*/
|
|
776
830
|
default_max_age?: number;
|
|
831
|
+
/**
|
|
832
|
+
* Indicates the requirement for a client to use mutual TLS endpoint aliases defined by the AS
|
|
833
|
+
* where present. Default is `false`.
|
|
834
|
+
*
|
|
835
|
+
* When combined with {@link customFetch} (to use a Fetch API implementation that supports client
|
|
836
|
+
* certificates) this can be used to target FAPI 2.0 profiles that utilize Mutual-TLS for either
|
|
837
|
+
* client authentication or sender constraining. FAPI 1.0 Advanced profiles that use PAR and JARM
|
|
838
|
+
* can also be targetted.
|
|
839
|
+
*
|
|
840
|
+
* @example
|
|
841
|
+
*
|
|
842
|
+
* (Node.js) Using [nodejs/undici](https://github.com/nodejs/undici) for Mutual-TLS Client
|
|
843
|
+
* Authentication and Certificate-Bound Access Tokens support.
|
|
844
|
+
*
|
|
845
|
+
* ```ts
|
|
846
|
+
* import * as undici from 'undici'
|
|
847
|
+
* import * as oauth from 'oauth4webapi'
|
|
848
|
+
*
|
|
849
|
+
* // Prerequisites
|
|
850
|
+
* let as!: oauth.AuthorizationServer
|
|
851
|
+
* let client!: oauth.Client & { use_mtls_endpoint_aliases: true }
|
|
852
|
+
* let params!: URLSearchParams
|
|
853
|
+
* let key!: string // PEM-encoded key
|
|
854
|
+
* let cert!: string // PEM-encoded certificate
|
|
855
|
+
*
|
|
856
|
+
* const agent = new undici.Agent({ connect: { key, cert } })
|
|
857
|
+
*
|
|
858
|
+
* const response = await oauth.pushedAuthorizationRequest(as, client, params, {
|
|
859
|
+
* [oauth.customFetch]: (...args) =>
|
|
860
|
+
* undici.fetch(args[0], { ...args[1], dispatcher: agent }),
|
|
861
|
+
* })
|
|
862
|
+
* ```
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
*
|
|
866
|
+
* (Deno) Using Deno.createHttpClient API for Mutual-TLS Client Authentication and
|
|
867
|
+
* Certificate-Bound Access Tokens support.
|
|
868
|
+
*
|
|
869
|
+
* ```ts
|
|
870
|
+
* import * as oauth from 'oauth4webapi'
|
|
871
|
+
*
|
|
872
|
+
* // Prerequisites
|
|
873
|
+
* let as!: oauth.AuthorizationServer
|
|
874
|
+
* let client!: oauth.Client & { use_mtls_endpoint_aliases: true }
|
|
875
|
+
* let params!: URLSearchParams
|
|
876
|
+
* let key!: string // PEM-encoded key
|
|
877
|
+
* let cert!: string // PEM-encoded certificate
|
|
878
|
+
*
|
|
879
|
+
* const agent = Deno.createHttpClient({ key, cert })
|
|
880
|
+
*
|
|
881
|
+
* const response = await oauth.pushedAuthorizationRequest(as, client, params, {
|
|
882
|
+
* [oauth.customFetch]: (...args) => fetch(args[0], { ...args[1], client: agent }),
|
|
883
|
+
* })
|
|
884
|
+
* ```
|
|
885
|
+
*
|
|
886
|
+
* @see [RFC 8705 - OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://www.rfc-editor.org/rfc/rfc8705.html)
|
|
887
|
+
*/
|
|
888
|
+
use_mtls_endpoint_aliases?: boolean;
|
|
777
889
|
/**
|
|
778
890
|
* See {@link clockSkew}.
|
|
779
891
|
*/
|
|
@@ -782,6 +894,10 @@ export interface Client {
|
|
|
782
894
|
* See {@link clockTolerance}.
|
|
783
895
|
*/
|
|
784
896
|
[clockTolerance]?: number;
|
|
897
|
+
/**
|
|
898
|
+
* See {@link jweDecrypt}.
|
|
899
|
+
*/
|
|
900
|
+
[jweDecrypt]?: JweDecryptFunction;
|
|
785
901
|
[metadata: string]: JsonValue | undefined;
|
|
786
902
|
}
|
|
787
903
|
/**
|
|
@@ -894,7 +1010,7 @@ export declare function generateRandomState(): string;
|
|
|
894
1010
|
*/
|
|
895
1011
|
export declare function generateRandomNonce(): string;
|
|
896
1012
|
/**
|
|
897
|
-
* Calculates the PKCE `
|
|
1013
|
+
* Calculates the PKCE `code_challenge` value to send with an authorization request using the S256
|
|
898
1014
|
* PKCE Code Challenge Method transformation.
|
|
899
1015
|
*
|
|
900
1016
|
* @param codeVerifier `code_verifier` value generated e.g. from {@link generateRandomCodeVerifier}.
|
|
@@ -923,6 +1039,12 @@ export interface DPoPOptions extends CryptoKeyPair {
|
|
|
923
1039
|
* will be used automatically.
|
|
924
1040
|
*/
|
|
925
1041
|
nonce?: string;
|
|
1042
|
+
/**
|
|
1043
|
+
* Use to modify the DPoP Proof JWT right before it is signed.
|
|
1044
|
+
*
|
|
1045
|
+
* @see {@link modifyAssertion}
|
|
1046
|
+
*/
|
|
1047
|
+
[modifyAssertion]?: ModifyAssertionFunction;
|
|
926
1048
|
}
|
|
927
1049
|
export interface DPoPRequestOptions {
|
|
928
1050
|
/**
|
|
@@ -930,9 +1052,16 @@ export interface DPoPRequestOptions {
|
|
|
930
1052
|
*/
|
|
931
1053
|
DPoP?: DPoPOptions;
|
|
932
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* @ignore
|
|
1057
|
+
*
|
|
1058
|
+
* @deprecated Use {@link Client.use_mtls_endpoint_aliases `client.use_mtls_endpoint_aliases`}.
|
|
1059
|
+
*/
|
|
933
1060
|
export interface UseMTLSAliasOptions {
|
|
934
1061
|
/**
|
|
935
|
-
*
|
|
1062
|
+
* @ignore
|
|
1063
|
+
*
|
|
1064
|
+
* @deprecated Use {@link Client.use_mtls_endpoint_aliases `client.use_mtls_endpoint_aliases`}.
|
|
936
1065
|
*/
|
|
937
1066
|
[useMtlsAlias]?: boolean;
|
|
938
1067
|
}
|
|
@@ -1611,6 +1740,9 @@ export declare function processIntrospectionResponse(as: AuthorizationServer, cl
|
|
|
1611
1740
|
export interface JWKS {
|
|
1612
1741
|
readonly keys: JWK[];
|
|
1613
1742
|
}
|
|
1743
|
+
export interface JweDecryptFunction {
|
|
1744
|
+
(jwe: string): Promise<string>;
|
|
1745
|
+
}
|
|
1614
1746
|
/**
|
|
1615
1747
|
* Same as {@link validateAuthResponse} but for signed JARM responses.
|
|
1616
1748
|
*
|
|
@@ -1855,19 +1987,19 @@ export declare const experimental_customFetch: symbol;
|
|
|
1855
1987
|
/**
|
|
1856
1988
|
* @ignore
|
|
1857
1989
|
*
|
|
1858
|
-
* @deprecated Use {@link
|
|
1990
|
+
* @deprecated Use {@link Client.use_mtls_endpoint_aliases `client.use_mtls_endpoint_aliases`}.
|
|
1859
1991
|
*/
|
|
1860
1992
|
export declare const experimentalUseMtlsAlias: symbol;
|
|
1861
1993
|
/**
|
|
1862
1994
|
* @ignore
|
|
1863
1995
|
*
|
|
1864
|
-
* @deprecated Use {@link
|
|
1996
|
+
* @deprecated Use {@link Client.use_mtls_endpoint_aliases `client.use_mtls_endpoint_aliases`}.
|
|
1865
1997
|
*/
|
|
1866
1998
|
export declare const experimental_useMtlsAlias: symbol;
|
|
1867
1999
|
/**
|
|
1868
2000
|
* @ignore
|
|
1869
2001
|
*
|
|
1870
|
-
* @deprecated Use {@link
|
|
2002
|
+
* @deprecated Use {@link Client.use_mtls_endpoint_aliases `client.use_mtls_endpoint_aliases`}.
|
|
1871
2003
|
*/
|
|
1872
2004
|
export type ExperimentalUseMTLSAliasOptions = UseMTLSAliasOptions;
|
|
1873
2005
|
/**
|
package/build/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.17.0';
|
|
5
5
|
USER_AGENT = `${NAME}/${VERSION}`;
|
|
6
6
|
}
|
|
7
7
|
function looseInstanceOf(input, expected) {
|
|
@@ -19,6 +19,8 @@ function looseInstanceOf(input, expected) {
|
|
|
19
19
|
export const clockSkew = Symbol();
|
|
20
20
|
export const clockTolerance = Symbol();
|
|
21
21
|
export const customFetch = Symbol();
|
|
22
|
+
export const modifyAssertion = Symbol();
|
|
23
|
+
export const jweDecrypt = Symbol();
|
|
22
24
|
export const jwksCache = Symbol();
|
|
23
25
|
export const useMtlsAlias = Symbol();
|
|
24
26
|
const encoder = new TextEncoder();
|
|
@@ -279,7 +281,11 @@ function getKeyAndKid(input) {
|
|
|
279
281
|
if (input.kid !== undefined && !validateString(input.kid)) {
|
|
280
282
|
throw new TypeError('"kid" must be a non-empty string');
|
|
281
283
|
}
|
|
282
|
-
return {
|
|
284
|
+
return {
|
|
285
|
+
key: input.key,
|
|
286
|
+
kid: input.kid,
|
|
287
|
+
modifyAssertion: input[modifyAssertion],
|
|
288
|
+
};
|
|
283
289
|
}
|
|
284
290
|
function formUrlEncode(token) {
|
|
285
291
|
return encodeURIComponent(token).replace(/%20/g, '+');
|
|
@@ -366,11 +372,11 @@ function clientAssertion(as, client) {
|
|
|
366
372
|
sub: client.client_id,
|
|
367
373
|
};
|
|
368
374
|
}
|
|
369
|
-
async function privateKeyJwt(as, client, key, kid) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
375
|
+
async function privateKeyJwt(as, client, key, kid, modifyAssertion) {
|
|
376
|
+
const header = { alg: keyToJws(key), kid };
|
|
377
|
+
const payload = clientAssertion(as, client);
|
|
378
|
+
modifyAssertion?.(header, payload);
|
|
379
|
+
return jwt(header, payload, key);
|
|
374
380
|
}
|
|
375
381
|
function assertAs(as) {
|
|
376
382
|
if (typeof as !== 'object' || as === null) {
|
|
@@ -428,13 +434,13 @@ async function clientAuthentication(as, client, body, headers, clientPrivateKey)
|
|
|
428
434
|
if (clientPrivateKey === undefined) {
|
|
429
435
|
throw new TypeError('"options.clientPrivateKey" must be provided when "client.token_endpoint_auth_method" is "private_key_jwt"');
|
|
430
436
|
}
|
|
431
|
-
const { key, kid } = getKeyAndKid(clientPrivateKey);
|
|
437
|
+
const { key, kid, modifyAssertion } = getKeyAndKid(clientPrivateKey);
|
|
432
438
|
if (!isPrivateKey(key)) {
|
|
433
439
|
throw new TypeError('"options.clientPrivateKey.key" must be a private CryptoKey');
|
|
434
440
|
}
|
|
435
441
|
body.set('client_id', client.client_id);
|
|
436
442
|
body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
|
|
437
|
-
body.set('client_assertion', await privateKeyJwt(as, client, key, kid));
|
|
443
|
+
body.set('client_assertion', await privateKeyJwt(as, client, key, kid, modifyAssertion));
|
|
438
444
|
break;
|
|
439
445
|
}
|
|
440
446
|
case 'tls_client_auth':
|
|
@@ -449,11 +455,11 @@ async function clientAuthentication(as, client, body, headers, clientPrivateKey)
|
|
|
449
455
|
throw new UnsupportedOperationError('unsupported client token_endpoint_auth_method');
|
|
450
456
|
}
|
|
451
457
|
}
|
|
452
|
-
async function jwt(header,
|
|
458
|
+
async function jwt(header, payload, key) {
|
|
453
459
|
if (!key.usages.includes('sign')) {
|
|
454
460
|
throw new TypeError('CryptoKey instances used for signing assertions must include "sign" in their "usages"');
|
|
455
461
|
}
|
|
456
|
-
const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(
|
|
462
|
+
const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(payload)))}`;
|
|
457
463
|
const signature = b64u(await crypto.subtle.sign(keyToSubtle(key), key, buf(input)));
|
|
458
464
|
return `${input}.${signature}`;
|
|
459
465
|
}
|
|
@@ -461,7 +467,7 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
|
|
|
461
467
|
assertAs(as);
|
|
462
468
|
assertClient(client);
|
|
463
469
|
parameters = new URLSearchParams(parameters);
|
|
464
|
-
const { key, kid } = getKeyAndKid(privateKey);
|
|
470
|
+
const { key, kid, modifyAssertion } = getKeyAndKid(privateKey);
|
|
465
471
|
if (!isPrivateKey(key)) {
|
|
466
472
|
throw new TypeError('"privateKey.key" must be a private CryptoKey');
|
|
467
473
|
}
|
|
@@ -519,11 +525,13 @@ export async function issueRequestObject(as, client, parameters, privateKey) {
|
|
|
519
525
|
}
|
|
520
526
|
}
|
|
521
527
|
}
|
|
522
|
-
|
|
528
|
+
const header = {
|
|
523
529
|
alg: keyToJws(key),
|
|
524
530
|
typ: 'oauth-authz-req+jwt',
|
|
525
531
|
kid,
|
|
526
|
-
}
|
|
532
|
+
};
|
|
533
|
+
modifyAssertion?.(header, claims);
|
|
534
|
+
return jwt(header, claims, key);
|
|
527
535
|
}
|
|
528
536
|
async function dpopProofJwt(headers, options, url, htm, clockSkew, accessToken) {
|
|
529
537
|
const { privateKey, publicKey, nonce = dpopNonces.get(url.origin) } = options;
|
|
@@ -540,19 +548,21 @@ async function dpopProofJwt(headers, options, url, htm, clockSkew, accessToken)
|
|
|
540
548
|
throw new TypeError('"DPoP.publicKey.extractable" must be true');
|
|
541
549
|
}
|
|
542
550
|
const now = epochTime() + clockSkew;
|
|
543
|
-
const
|
|
551
|
+
const header = {
|
|
544
552
|
alg: keyToJws(privateKey),
|
|
545
553
|
typ: 'dpop+jwt',
|
|
546
554
|
jwk: await publicJwk(publicKey),
|
|
547
|
-
}
|
|
555
|
+
};
|
|
556
|
+
const payload = {
|
|
548
557
|
iat: now,
|
|
549
558
|
jti: randomBytes(),
|
|
550
559
|
htm,
|
|
551
560
|
nonce,
|
|
552
561
|
htu: `${url.origin}${url.pathname}`,
|
|
553
562
|
ath: accessToken ? b64u(await crypto.subtle.digest('SHA-256', buf(accessToken))) : undefined,
|
|
554
|
-
}
|
|
555
|
-
|
|
563
|
+
};
|
|
564
|
+
options[modifyAssertion]?.(header, payload);
|
|
565
|
+
headers.set('dpop', await jwt(header, payload, privateKey));
|
|
556
566
|
}
|
|
557
567
|
let jwkCache;
|
|
558
568
|
async function getSetPublicJwkCache(key) {
|
|
@@ -565,25 +575,31 @@ async function publicJwk(key) {
|
|
|
565
575
|
jwkCache || (jwkCache = new WeakMap());
|
|
566
576
|
return jwkCache.get(key) || getSetPublicJwkCache(key);
|
|
567
577
|
}
|
|
568
|
-
function validateEndpoint(value, endpoint,
|
|
578
|
+
function validateEndpoint(value, endpoint, useMtlsAlias) {
|
|
569
579
|
if (typeof value !== 'string') {
|
|
570
|
-
if (
|
|
580
|
+
if (useMtlsAlias) {
|
|
571
581
|
throw new TypeError(`"as.mtls_endpoint_aliases.${endpoint}" must be a string`);
|
|
572
582
|
}
|
|
573
583
|
throw new TypeError(`"as.${endpoint}" must be a string`);
|
|
574
584
|
}
|
|
575
585
|
return new URL(value);
|
|
576
586
|
}
|
|
577
|
-
function resolveEndpoint(as, endpoint,
|
|
578
|
-
if (
|
|
579
|
-
return validateEndpoint(as.mtls_endpoint_aliases[endpoint], endpoint,
|
|
587
|
+
function resolveEndpoint(as, endpoint, useMtlsAlias = false) {
|
|
588
|
+
if (useMtlsAlias && as.mtls_endpoint_aliases && endpoint in as.mtls_endpoint_aliases) {
|
|
589
|
+
return validateEndpoint(as.mtls_endpoint_aliases[endpoint], endpoint, useMtlsAlias);
|
|
580
590
|
}
|
|
581
|
-
return validateEndpoint(as[endpoint], endpoint);
|
|
591
|
+
return validateEndpoint(as[endpoint], endpoint, useMtlsAlias);
|
|
592
|
+
}
|
|
593
|
+
function alias(client, options) {
|
|
594
|
+
if (client.use_mtls_endpoint_aliases || options?.[useMtlsAlias]) {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
return false;
|
|
582
598
|
}
|
|
583
599
|
export async function pushedAuthorizationRequest(as, client, parameters, options) {
|
|
584
600
|
assertAs(as);
|
|
585
601
|
assertClient(client);
|
|
586
|
-
const url = resolveEndpoint(as, 'pushed_authorization_request_endpoint', options);
|
|
602
|
+
const url = resolveEndpoint(as, 'pushed_authorization_request_endpoint', alias(client, options));
|
|
587
603
|
const body = new URLSearchParams(parameters);
|
|
588
604
|
body.set('client_id', client.client_id);
|
|
589
605
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -716,7 +732,7 @@ export async function protectedResourceRequest(accessToken, method, url, headers
|
|
|
716
732
|
export async function userInfoRequest(as, client, accessToken, options) {
|
|
717
733
|
assertAs(as);
|
|
718
734
|
assertClient(client);
|
|
719
|
-
const url = resolveEndpoint(as, 'userinfo_endpoint', options);
|
|
735
|
+
const url = resolveEndpoint(as, 'userinfo_endpoint', alias(client, options));
|
|
720
736
|
const headers = prepareHeaders(options?.headers);
|
|
721
737
|
if (client.userinfo_signed_response_alg) {
|
|
722
738
|
headers.set('accept', 'application/jwt');
|
|
@@ -858,8 +874,7 @@ export async function processUserInfoResponse(as, client, expectedSubject, respo
|
|
|
858
874
|
let json;
|
|
859
875
|
if (getContentType(response) === 'application/jwt') {
|
|
860
876
|
assertReadableResponse(response);
|
|
861
|
-
const jwt = await response.text()
|
|
862
|
-
const { claims } = await validateJwt(jwt, checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client))
|
|
877
|
+
const { claims, jwt } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client), client[jweDecrypt])
|
|
863
878
|
.then(validateOptionalAudience.bind(undefined, client.client_id))
|
|
864
879
|
.then(validateOptionalIssuer.bind(undefined, as.issuer));
|
|
865
880
|
jwtResponseBodies.set(response, jwt);
|
|
@@ -908,7 +923,7 @@ async function authenticatedRequest(as, client, method, url, body, headers, opti
|
|
|
908
923
|
}).then(processDpopNonce);
|
|
909
924
|
}
|
|
910
925
|
async function tokenEndpointRequest(as, client, grantType, parameters, options) {
|
|
911
|
-
const url = resolveEndpoint(as, 'token_endpoint', options);
|
|
926
|
+
const url = resolveEndpoint(as, 'token_endpoint', alias(client, options));
|
|
912
927
|
parameters.set('grant_type', grantType);
|
|
913
928
|
const headers = prepareHeaders(options?.headers);
|
|
914
929
|
headers.set('accept', 'application/json');
|
|
@@ -937,14 +952,14 @@ export function getValidatedIdTokenClaims(ref) {
|
|
|
937
952
|
if (!claims) {
|
|
938
953
|
throw new TypeError('"ref" was already garbage collected or did not resolve from the proper sources');
|
|
939
954
|
}
|
|
940
|
-
return claims;
|
|
955
|
+
return claims[0];
|
|
941
956
|
}
|
|
942
957
|
export async function validateIdTokenSignature(as, ref, options) {
|
|
943
958
|
assertAs(as);
|
|
944
|
-
if (!
|
|
959
|
+
if (!idTokenClaims.has(ref)) {
|
|
945
960
|
throw new OPE('"ref" does not contain an ID Token to verify the signature of');
|
|
946
961
|
}
|
|
947
|
-
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = ref.
|
|
962
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature, } = idTokenClaims.get(ref)[1].split('.');
|
|
948
963
|
const header = JSON.parse(buf(b64u(protectedHeader)));
|
|
949
964
|
if (header.alg.startsWith('HS')) {
|
|
950
965
|
throw new UnsupportedOperationError();
|
|
@@ -1024,7 +1039,7 @@ async function processGenericAccessTokenResponse(as, client, response, ignoreIdT
|
|
|
1024
1039
|
throw new OPE('"response" body "id_token" property must be a non-empty string');
|
|
1025
1040
|
}
|
|
1026
1041
|
if (json.id_token) {
|
|
1027
|
-
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))
|
|
1042
|
+
const { claims, jwt } = 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), client[jweDecrypt])
|
|
1028
1043
|
.then(validatePresence.bind(undefined, ['aud', 'exp', 'iat', 'iss', 'sub']))
|
|
1029
1044
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
1030
1045
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
@@ -1040,7 +1055,7 @@ async function processGenericAccessTokenResponse(as, client, response, ignoreIdT
|
|
|
1040
1055
|
(!Number.isFinite(claims.auth_time) || Math.sign(claims.auth_time) !== 1)) {
|
|
1041
1056
|
throw new OPE('ID Token "auth_time" (authentication time) must be a positive number');
|
|
1042
1057
|
}
|
|
1043
|
-
idTokenClaims.set(json, claims);
|
|
1058
|
+
idTokenClaims.set(json, [claims, jwt]);
|
|
1044
1059
|
}
|
|
1045
1060
|
}
|
|
1046
1061
|
return json;
|
|
@@ -1219,7 +1234,7 @@ export async function revocationRequest(as, client, token, options) {
|
|
|
1219
1234
|
if (!validateString(token)) {
|
|
1220
1235
|
throw new TypeError('"token" must be a non-empty string');
|
|
1221
1236
|
}
|
|
1222
|
-
const url = resolveEndpoint(as, 'revocation_endpoint', options);
|
|
1237
|
+
const url = resolveEndpoint(as, 'revocation_endpoint', alias(client, options));
|
|
1223
1238
|
const body = new URLSearchParams(options?.additionalParameters);
|
|
1224
1239
|
body.set('token', token);
|
|
1225
1240
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -1250,7 +1265,7 @@ export async function introspectionRequest(as, client, token, options) {
|
|
|
1250
1265
|
if (!validateString(token)) {
|
|
1251
1266
|
throw new TypeError('"token" must be a non-empty string');
|
|
1252
1267
|
}
|
|
1253
|
-
const url = resolveEndpoint(as, 'introspection_endpoint', options);
|
|
1268
|
+
const url = resolveEndpoint(as, 'introspection_endpoint', alias(client, options));
|
|
1254
1269
|
const body = new URLSearchParams(options?.additionalParameters);
|
|
1255
1270
|
body.set('token', token);
|
|
1256
1271
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -1278,8 +1293,7 @@ export async function processIntrospectionResponse(as, client, response) {
|
|
|
1278
1293
|
let json;
|
|
1279
1294
|
if (getContentType(response) === 'application/token-introspection+jwt') {
|
|
1280
1295
|
assertReadableResponse(response);
|
|
1281
|
-
const jwt = await response.text()
|
|
1282
|
-
const { claims } = await validateJwt(jwt, checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client))
|
|
1296
|
+
const { claims, jwt } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), noSignatureCheck, getClockSkew(client), getClockTolerance(client), client[jweDecrypt])
|
|
1283
1297
|
.then(checkJwtType.bind(undefined, 'token-introspection+jwt'))
|
|
1284
1298
|
.then(validatePresence.bind(undefined, ['aud', 'iat', 'iss']))
|
|
1285
1299
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
@@ -1432,10 +1446,16 @@ async function validateJwsSignature(protectedHeader, payload, key, signature) {
|
|
|
1432
1446
|
throw new OPE('JWT signature verification failed');
|
|
1433
1447
|
}
|
|
1434
1448
|
}
|
|
1435
|
-
async function validateJwt(jws, checkAlg, getKey, clockSkew, clockTolerance) {
|
|
1436
|
-
|
|
1449
|
+
async function validateJwt(jws, checkAlg, getKey, clockSkew, clockTolerance, decryptJwt) {
|
|
1450
|
+
let { 0: protectedHeader, 1: payload, 2: encodedSignature, length } = jws.split('.');
|
|
1437
1451
|
if (length === 5) {
|
|
1438
|
-
|
|
1452
|
+
if (decryptJwt !== undefined) {
|
|
1453
|
+
jws = await decryptJwt(jws);
|
|
1454
|
+
({ 0: protectedHeader, 1: payload, 2: encodedSignature, length } = jws.split('.'));
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
throw new UnsupportedOperationError('JWE structure JWTs are not supported');
|
|
1458
|
+
}
|
|
1439
1459
|
}
|
|
1440
1460
|
if (length !== 3) {
|
|
1441
1461
|
throw new OPE('Invalid JWT');
|
|
@@ -1502,7 +1522,7 @@ async function validateJwt(jws, checkAlg, getKey, clockSkew, clockTolerance) {
|
|
|
1502
1522
|
throw new OPE('unexpected JWT "aud" (audience) claim type');
|
|
1503
1523
|
}
|
|
1504
1524
|
}
|
|
1505
|
-
return { header, claims, signature, key };
|
|
1525
|
+
return { header, claims, signature, key, jwt: jws };
|
|
1506
1526
|
}
|
|
1507
1527
|
export async function validateJwtAuthResponse(as, client, parameters, expectedState, options) {
|
|
1508
1528
|
assertAs(as);
|
|
@@ -1517,7 +1537,7 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
|
|
|
1517
1537
|
if (!response) {
|
|
1518
1538
|
throw new OPE('"parameters" does not contain a JARM response');
|
|
1519
1539
|
}
|
|
1520
|
-
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))
|
|
1540
|
+
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), client[jweDecrypt])
|
|
1521
1541
|
.then(validatePresence.bind(undefined, ['aud', 'exp', 'iss']))
|
|
1522
1542
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
1523
1543
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
@@ -1613,7 +1633,7 @@ export async function validateDetachedSignatureResponse(as, client, parameters,
|
|
|
1613
1633
|
if (typeof expectedState === 'string') {
|
|
1614
1634
|
requiredClaims.push('s_hash');
|
|
1615
1635
|
}
|
|
1616
|
-
const { claims, header, key } = await validateJwt(id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(client), getClockTolerance(client))
|
|
1636
|
+
const { claims, header, key } = await validateJwt(id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(client), getClockTolerance(client), client[jweDecrypt])
|
|
1617
1637
|
.then(validatePresence.bind(undefined, requiredClaims))
|
|
1618
1638
|
.then(validateIssuer.bind(undefined, as.issuer))
|
|
1619
1639
|
.then(validateAudience.bind(undefined, client.client_id));
|
|
@@ -1785,7 +1805,7 @@ async function importJwk(alg, jwk) {
|
|
|
1785
1805
|
export async function deviceAuthorizationRequest(as, client, parameters, options) {
|
|
1786
1806
|
assertAs(as);
|
|
1787
1807
|
assertClient(client);
|
|
1788
|
-
const url = resolveEndpoint(as, 'device_authorization_endpoint', options);
|
|
1808
|
+
const url = resolveEndpoint(as, 'device_authorization_endpoint', alias(client, options));
|
|
1789
1809
|
const body = new URLSearchParams(parameters);
|
|
1790
1810
|
body.set('client_id', client.client_id);
|
|
1791
1811
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -1861,7 +1881,10 @@ export async function generateKeyPair(alg, options) {
|
|
|
1861
1881
|
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
|
1862
1882
|
});
|
|
1863
1883
|
}
|
|
1864
|
-
return
|
|
1884
|
+
return crypto.subtle.generateKey(algorithm, options?.extractable ?? false, [
|
|
1885
|
+
'sign',
|
|
1886
|
+
'verify',
|
|
1887
|
+
]);
|
|
1865
1888
|
}
|
|
1866
1889
|
function normalizeHtu(htu) {
|
|
1867
1890
|
const url = new URL(htu);
|
|
@@ -1890,7 +1913,7 @@ async function validateDPoP(as, request, accessToken, accessTokenClaims, options
|
|
|
1890
1913
|
throw new OPE('DPoP Proof jwk header parameter must contain a public key');
|
|
1891
1914
|
}
|
|
1892
1915
|
return key;
|
|
1893
|
-
}, clockSkew, getClockTolerance(options))
|
|
1916
|
+
}, clockSkew, getClockTolerance(options), undefined)
|
|
1894
1917
|
.then(checkJwtType.bind(undefined, 'dpop+jwt'))
|
|
1895
1918
|
.then(validatePresence.bind(undefined, ['iat', 'jti', 'ath', 'htm', 'htu']));
|
|
1896
1919
|
const now = epochTime() + clockSkew;
|
|
@@ -1981,7 +2004,7 @@ export async function validateJwtAccessToken(as, request, expectedAudience, opti
|
|
|
1981
2004
|
if (options?.requireDPoP || scheme === 'dpop' || request.headers.has('dpop')) {
|
|
1982
2005
|
requiredClaims.push('cnf');
|
|
1983
2006
|
}
|
|
1984
|
-
const { claims } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined, undefined, SUPPORTED_JWS_ALGS), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(options), getClockTolerance(options))
|
|
2007
|
+
const { claims } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined, undefined, SUPPORTED_JWS_ALGS), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(options), getClockTolerance(options), undefined)
|
|
1985
2008
|
.then(checkJwtType.bind(undefined, 'at+jwt'))
|
|
1986
2009
|
.then(validatePresence.bind(undefined, requiredClaims))
|
|
1987
2010
|
.then(validateIssuer.bind(undefined, as.issuer))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oauth4webapi",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "OAuth 2 / OpenID Connect for JavaScript Runtimes",
|
|
3
|
+
"version": "2.17.0",
|
|
4
|
+
"description": "Low-Level OAuth 2 / OpenID Connect Client API for JavaScript Runtimes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"access token",
|
|
7
7
|
"auth",
|