oauth4webapi 1.2.1 → 1.3.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 CHANGED
@@ -52,26 +52,18 @@ import * as oauth2 from 'https://deno.land/x/oauth4webapi/src/index.ts'
52
52
  - FAPI 2.0 (Private Key JWT, PAR, DPoP) - [source](examples/fapi2.ts)
53
53
  - FAPI 2.0 Message Signing (Private Key JWT, PAR, DPoP, JAR, JARM) - [source](examples/fapi2-message-signing.ts) | [diff](examples/fapi2-message-signing.diff)
54
54
 
55
- ## Runtime requirements
56
-
57
- The supported JavaScript runtimes include ones that
58
-
59
- - are reasonably up to date ECMAScript (targets ES2020, but may be further transpiled for compatibility)
60
- - support required Web API globals and standard built-in objects
61
- - [Fetch API][] and its related globals [fetch][], [Response][], [Headers][]
62
- - [Web Crypto API][] and its related globals [crypto][], [CryptoKey][]
63
- - [Encoding API][] and its related globals [TextEncoder][], [TextDecoder][]
64
- - [URL API][] and its related globals [URL][], [URLSearchParams][]
65
- - [atob][] and [btoa][]
66
- - [Uint8Array][]
67
- - These are (not an exhaustive list):
68
- - Browsers
69
- - Cloudflare Workers
70
- - Deno (^1.21.0)
71
- - Electron
72
- - Next.js Middlewares
73
- - Node.js ([runtime flags may be needed](https://github.com/panva/oauth4webapi/issues/8))
74
- - Vercel Edge Functions
55
+ ## Supported Runtimes
56
+
57
+ The supported JavaScript runtimes include ones that support the utilized Web API globals and standard built-in objects
58
+
59
+ These are _(this is not an exhaustive list)_:
60
+ - Browsers
61
+ - Cloudflare Workers
62
+ - Deno
63
+ - Electron
64
+ - Netlify Edge
65
+ - Node.js ([runtime flags may be needed](https://github.com/panva/oauth4webapi/issues/8))
66
+ - Vercel's Edge Runtime
75
67
 
76
68
  ## Out of scope
77
69
 
@@ -81,20 +73,3 @@ The supported JavaScript runtimes include ones that
81
73
  - JSON Web Encryption (JWE)
82
74
  - JSON Web Signature (JWS) rarely used algorithms and HMAC
83
75
  - Automatic polyfills of any kind
84
-
85
- [web crypto api]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API
86
- [fetch api]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
87
- [fetch]: https://developer.mozilla.org/en-US/docs/Web/API/fetch
88
- [textdecoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
89
- [textencoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
90
- [btoa]: https://developer.mozilla.org/en-US/docs/Web/API/btoa
91
- [atob]: https://developer.mozilla.org/en-US/docs/Web/API/atob
92
- [uint8array]: https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array
93
- [response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
94
- [headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers
95
- [crypto]: https://developer.mozilla.org/en-US/docs/Web/API/crypto
96
- [cryptokey]: https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey
97
- [urlsearchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
98
- [encoding api]: https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API
99
- [url api]: https://developer.mozilla.org/en-US/docs/Web/API/URL_API
100
- [url]: https://developer.mozilla.org/en-US/docs/Web/API/URL
package/build/index.d.ts CHANGED
@@ -537,7 +537,7 @@ export interface DPoPOptions extends CryptoKeyPair {
537
537
  * Its algorithm must be compatible with a supported {@link JWSAlgorithm JWS `alg` Algorithm}.
538
538
  */
539
539
  privateKey: CryptoKey;
540
- /** The public key corresponding to {@link DPoPOptions.privateKey} */
540
+ /** The public key corresponding to {@link DPoPOptions.privateKey}. */
541
541
  publicKey: CryptoKey;
542
542
  /**
543
543
  * Server-Provided Nonce to use in the request. This option serves as an override in case the
@@ -703,6 +703,31 @@ export interface UserInfoResponse {
703
703
  * @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse)
704
704
  */
705
705
  export declare const skipSubjectCheck: unique symbol;
706
+ export interface SkipJWTSignatureCheckOptions {
707
+ /**
708
+ * DANGER ZONE
709
+ *
710
+ * When JWT assertions are received via direct communication between the Client and the
711
+ * Token/UserInfo/Introspection endpoint (which they are in this library's supported profiles and
712
+ * exposed functions) the TLS server validation MAY be used to validate the issuer in place of
713
+ * checking the assertion's signature.
714
+ *
715
+ * Set this to `true` to omit verifying the JWT assertion's signature (e.g. ID Token, JWT Signed
716
+ * Introspection, or JWT Signed UserInfo Response).
717
+ *
718
+ * Setting this to `true` also means that:
719
+ *
720
+ * - The Authorization Server's JSON Web Key Set will not be requested. That is useful for
721
+ * javascript runtimes that execute on the edge and cannot reliably share an in-memory cache of
722
+ * the JSON Web Key Set in between invocations.
723
+ * - Any JWS Algorithm may be used, not just the {@link JWSAlgorithm supported ones}.
724
+ *
725
+ * Default is `false`.
726
+ */
727
+ skipJwtSignatureCheck?: boolean;
728
+ }
729
+ export interface ProcessUserInfoResponseOptions extends HttpRequestOptions, SkipJWTSignatureCheckOptions {
730
+ }
706
731
  /**
707
732
  * Validates Response instance to be one coming from the
708
733
  * {@link AuthorizationServer.userinfo_endpoint `as.userinfo_endpoint`}.
@@ -719,7 +744,7 @@ export declare const skipSubjectCheck: unique symbol;
719
744
  * OAuth 2.0 error was returned.
720
745
  * @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo)
721
746
  */
722
- export declare function processUserInfoResponse(as: AuthorizationServer, client: Client, expectedSubject: string | typeof skipSubjectCheck, response: Response, options?: HttpRequestOptions): Promise<UserInfoResponse>;
747
+ export declare function processUserInfoResponse(as: AuthorizationServer, client: Client, expectedSubject: string | typeof skipSubjectCheck, response: Response, options?: ProcessUserInfoResponseOptions): Promise<UserInfoResponse>;
723
748
  export interface TokenEndpointRequestOptions extends HttpRequestOptions, AuthenticatedRequestOptions, DPoPRequestOptions {
724
749
  /** Any additional parameters to send. This cannot override existing parameter values. */
725
750
  additionalParameters?: URLSearchParams;
@@ -755,6 +780,8 @@ export declare function getValidatedIdTokenClaims(ref: OpenIDTokenEndpointRespon
755
780
  * @returns JWT Claims Set from an ID Token, or undefined if there is no ID Token in `ref`.
756
781
  */
757
782
  export declare function getValidatedIdTokenClaims(ref: TokenEndpointResponse): IDToken | undefined;
783
+ export interface ProcessRefreshTokenResponseOptions extends HttpRequestOptions, SkipJWTSignatureCheckOptions {
784
+ }
758
785
  /**
759
786
  * Validates Refresh Token Grant Response instance to be one coming from the
760
787
  * {@link AuthorizationServer.token_endpoint `as.token_endpoint`}.
@@ -769,7 +796,7 @@ export declare function getValidatedIdTokenClaims(ref: TokenEndpointResponse): I
769
796
  * @see [RFC 6749 - The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749.html#section-6)
770
797
  * @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens)
771
798
  */
772
- export declare function processRefreshTokenResponse(as: AuthorizationServer, client: Client, response: Response, options?: HttpRequestOptions): Promise<TokenEndpointResponse | OAuth2Error>;
799
+ export declare function processRefreshTokenResponse(as: AuthorizationServer, client: Client, response: Response, options?: ProcessRefreshTokenResponseOptions): Promise<TokenEndpointResponse | OAuth2Error>;
773
800
  /**
774
801
  * Performs an Authorization Code grant request at the
775
802
  * {@link AuthorizationServer.token_endpoint `as.token_endpoint`}.
@@ -856,6 +883,8 @@ export declare const expectNoNonce: unique symbol;
856
883
  * indicate no `auth_time` ID Token claim value check should be performed.
857
884
  */
858
885
  export declare const skipAuthTimeCheck: unique symbol;
886
+ export interface ProcessAuthorizationCodeOpenIDResponseOptions extends HttpRequestOptions, SkipJWTSignatureCheckOptions {
887
+ }
859
888
  /**
860
889
  * (OpenID Connect only) Validates Authorization Code Grant Response instance to be one coming from
861
890
  * the {@link AuthorizationServer.token_endpoint `as.token_endpoint`}.
@@ -876,7 +905,7 @@ export declare const skipAuthTimeCheck: unique symbol;
876
905
  * @see [RFC 6749 - The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1)
877
906
  * @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)
878
907
  */
879
- export declare function processAuthorizationCodeOpenIDResponse(as: AuthorizationServer, client: Client, response: Response, expectedNonce?: string | typeof expectNoNonce, maxAge?: number | typeof skipAuthTimeCheck, options?: HttpRequestOptions): Promise<OpenIDTokenEndpointResponse | OAuth2Error>;
908
+ export declare function processAuthorizationCodeOpenIDResponse(as: AuthorizationServer, client: Client, response: Response, expectedNonce?: string | typeof expectNoNonce, maxAge?: number | typeof skipAuthTimeCheck, options?: ProcessAuthorizationCodeOpenIDResponseOptions): Promise<OpenIDTokenEndpointResponse | OAuth2Error>;
880
909
  /**
881
910
  * (OAuth 2.0 without OpenID Connect only) Validates Authorization Code Grant Response instance to
882
911
  * be one coming from the {@link AuthorizationServer.token_endpoint `as.token_endpoint`}.
@@ -991,6 +1020,8 @@ export interface IntrospectionResponse {
991
1020
  };
992
1021
  readonly [claim: string]: JsonValue | undefined;
993
1022
  }
1023
+ export interface ProcessIntrospectionResponseOptions extends HttpRequestOptions, SkipJWTSignatureCheckOptions {
1024
+ }
994
1025
  /**
995
1026
  * Validates Response instance to be one coming from the
996
1027
  * {@link AuthorizationServer.introspection_endpoint `as.introspection_endpoint`}.
@@ -1005,7 +1036,7 @@ export interface IntrospectionResponse {
1005
1036
  * @see [RFC 7662 - OAuth 2.0 Token Introspection](https://www.rfc-editor.org/rfc/rfc7662.html#section-2)
1006
1037
  * @see [draft-ietf-oauth-jwt-introspection-response-12 - JWT Response for OAuth Token Introspection](https://www.ietf.org/archive/id/draft-ietf-oauth-jwt-introspection-response-12.html#section-5)
1007
1038
  */
1008
- export declare function processIntrospectionResponse(as: AuthorizationServer, client: Client, response: Response, options?: HttpRequestOptions): Promise<IntrospectionResponse | OAuth2Error>;
1039
+ export declare function processIntrospectionResponse(as: AuthorizationServer, client: Client, response: Response, options?: ProcessIntrospectionResponseOptions): Promise<IntrospectionResponse | OAuth2Error>;
1009
1040
  /** @ignore */
1010
1041
  export interface JwksRequestOptions extends HttpRequestOptions {
1011
1042
  }
@@ -1140,6 +1171,8 @@ export declare function processDeviceAuthorizationResponse(as: AuthorizationServ
1140
1171
  * @see [draft-ietf-oauth-dpop-10 - OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)](https://www.ietf.org/archive/id/draft-ietf-oauth-dpop-10.html#name-dpop-access-token-request)
1141
1172
  */
1142
1173
  export declare function deviceCodeGrantRequest(as: AuthorizationServer, client: Client, deviceCode: string, options?: TokenEndpointRequestOptions): Promise<Response>;
1174
+ export interface ProcessDeviceCodeResponseOptions extends HttpRequestOptions, SkipJWTSignatureCheckOptions {
1175
+ }
1143
1176
  /**
1144
1177
  * Validates Device Authorization Grant Response instance to be one coming from the
1145
1178
  * {@link AuthorizationServer.token_endpoint `as.token_endpoint`}.
@@ -1153,7 +1186,7 @@ export declare function deviceCodeGrantRequest(as: AuthorizationServer, client:
1153
1186
  * OAuth 2.0 error was returned.
1154
1187
  * @see [RFC 8628 - OAuth 2.0 Device Authorization Grant](https://www.rfc-editor.org/rfc/rfc8628.html#section-3.4)
1155
1188
  */
1156
- export declare function processDeviceCodeResponse(as: AuthorizationServer, client: Client, response: Response, options?: HttpRequestOptions): Promise<TokenEndpointResponse | OAuth2Error>;
1189
+ export declare function processDeviceCodeResponse(as: AuthorizationServer, client: Client, response: Response, options?: ProcessDeviceCodeResponseOptions): Promise<TokenEndpointResponse | OAuth2Error>;
1157
1190
  export interface GenerateKeyPairOptions {
1158
1191
  /** Indicates whether or not the private key may be exported. Default is `false`. */
1159
1192
  extractable?: boolean;
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 = 'v1.2.1';
4
+ const VERSION = 'v1.3.0';
5
5
  USER_AGENT = `${NAME}/${VERSION}`;
6
6
  }
7
7
  const encoder = new TextEncoder();
@@ -151,11 +151,17 @@ function prepareHeaders(input) {
151
151
  return headers;
152
152
  }
153
153
  function signal(value) {
154
- return typeof value === 'function' ? value() : value;
154
+ if (typeof value === 'function') {
155
+ value = value();
156
+ }
157
+ if (!(value instanceof AbortSignal)) {
158
+ throw new TypeError('"options.signal" must return or be an instance of AbortSignal');
159
+ }
160
+ return value;
155
161
  }
156
162
  export async function discoveryRequest(issuerIdentifier, options) {
157
163
  if (!(issuerIdentifier instanceof URL)) {
158
- throw new TypeError('"issuer" must be an instance of URL');
164
+ throw new TypeError('"issuerIdentifier" must be an instance of URL');
159
165
  }
160
166
  if (issuerIdentifier.protocol !== 'https:' && issuerIdentifier.protocol !== 'http:') {
161
167
  throw new TypeError('"issuer.protocol" must be "https:" or "http:"');
@@ -315,20 +321,20 @@ async function privateKeyJwt(as, client, key, kid) {
315
321
  kid,
316
322
  }, clientAssertion(as, client), key);
317
323
  }
318
- function assertIssuer(metadata) {
319
- if (typeof metadata !== 'object' || metadata === null) {
320
- throw new TypeError('"issuer" must be an object');
324
+ function assertAs(as) {
325
+ if (typeof as !== 'object' || as === null) {
326
+ throw new TypeError('"as" must be an object');
321
327
  }
322
- if (!validateString(metadata.issuer)) {
323
- throw new TypeError('"issuer.issuer" property must be a non-empty string');
328
+ if (!validateString(as.issuer)) {
329
+ throw new TypeError('"as.issuer" property must be a non-empty string');
324
330
  }
325
331
  return true;
326
332
  }
327
- function assertClient(metadata) {
328
- if (typeof metadata !== 'object' || metadata === null) {
333
+ function assertClient(client) {
334
+ if (typeof client !== 'object' || client === null) {
329
335
  throw new TypeError('"client" must be an object');
330
336
  }
331
- if (!validateString(metadata.client_id)) {
337
+ if (!validateString(client.client_id)) {
332
338
  throw new TypeError('"client.client_id" property must be a non-empty string');
333
339
  }
334
340
  return true;
@@ -399,7 +405,7 @@ async function jwt(header, claimsSet, key) {
399
405
  return `${input}.${signature}`;
400
406
  }
401
407
  export async function issueRequestObject(as, client, parameters, privateKey) {
402
- assertIssuer(as);
408
+ assertAs(as);
403
409
  assertClient(client);
404
410
  if (!(parameters instanceof URLSearchParams)) {
405
411
  throw new TypeError('"parameters" must be an instance of URLSearchParams');
@@ -468,13 +474,13 @@ async function publicJwk(key) {
468
474
  return { kty, crv, e, n, x, y };
469
475
  }
470
476
  export async function pushedAuthorizationRequest(as, client, parameters, options) {
471
- assertIssuer(as);
477
+ assertAs(as);
472
478
  assertClient(client);
473
479
  if (!(parameters instanceof URLSearchParams)) {
474
480
  throw new TypeError('"parameters" must be an instance of URLSearchParams');
475
481
  }
476
482
  if (typeof as.pushed_authorization_request_endpoint !== 'string') {
477
- throw new TypeError('"issuer.pushed_authorization_request_endpoint" must be a string');
483
+ throw new TypeError('"as.pushed_authorization_request_endpoint" must be a string');
478
484
  }
479
485
  const url = new URL(as.pushed_authorization_request_endpoint);
480
486
  const body = new URLSearchParams(parameters);
@@ -555,7 +561,7 @@ export function parseWwwAuthenticateChallenges(response) {
555
561
  return challenges;
556
562
  }
557
563
  export async function processPushedAuthorizationResponse(as, client, response) {
558
- assertIssuer(as);
564
+ assertAs(as);
559
565
  assertClient(client);
560
566
  if (!(response instanceof Response)) {
561
567
  throw new TypeError('"response" must be an instance of Response');
@@ -609,10 +615,10 @@ export async function protectedResourceRequest(accessToken, method, url, headers
609
615
  }).then(processDpopNonce);
610
616
  }
611
617
  export async function userInfoRequest(as, client, accessToken, options) {
612
- assertIssuer(as);
618
+ assertAs(as);
613
619
  assertClient(client);
614
620
  if (typeof as.userinfo_endpoint !== 'string') {
615
- throw new TypeError('"issuer.userinfo_endpoint" must be a string');
621
+ throw new TypeError('"as.userinfo_endpoint" must be a string');
616
622
  }
617
623
  const url = new URL(as.userinfo_endpoint);
618
624
  const headers = prepareHeaders(options?.headers);
@@ -716,7 +722,7 @@ function getContentType(response) {
716
722
  return response.headers.get('content-type')?.split(';')[0];
717
723
  }
718
724
  export async function processUserInfoResponse(as, client, expectedSubject, response, options) {
719
- assertIssuer(as);
725
+ assertAs(as);
720
726
  assertClient(client);
721
727
  if (!(response instanceof Response)) {
722
728
  throw new TypeError('"response" must be an instance of Response');
@@ -726,10 +732,9 @@ export async function processUserInfoResponse(as, client, expectedSubject, respo
726
732
  }
727
733
  let json;
728
734
  if (getContentType(response) === 'application/jwt') {
729
- if (typeof as.jwks_uri !== 'string') {
730
- throw new TypeError('"issuer.jwks_uri" must be a string');
731
- }
732
- const { claims } = await validateJwt(await preserveBodyStream(response).text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
735
+ const { claims } = await validateJwt(await preserveBodyStream(response).text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported), options?.skipJwtSignatureCheck !== true
736
+ ? getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options)
737
+ : noSignatureCheck)
733
738
  .then(validateOptionalAudience.bind(undefined, client.client_id))
734
739
  .then(validateOptionalIssuer.bind(undefined, as.issuer));
735
740
  json = claims;
@@ -764,43 +769,6 @@ export async function processUserInfoResponse(as, client, expectedSubject, respo
764
769
  }
765
770
  return json;
766
771
  }
767
- function padded(buf, length) {
768
- const out = new Uint8Array(length);
769
- out.set(buf);
770
- return out;
771
- }
772
- function timingSafeEqual(a, b) {
773
- const len = Math.max(a.byteLength, b.byteLength);
774
- a = padded(a, len);
775
- b = padded(b, len);
776
- let out = 0;
777
- let i = -1;
778
- while (++i < len) {
779
- out |= a[i] ^ b[i];
780
- }
781
- return out === 0;
782
- }
783
- async function idTokenHash(alg, data) {
784
- let algorithm;
785
- switch (alg) {
786
- case 'RS256':
787
- case 'PS256':
788
- case 'ES256':
789
- algorithm = { name: 'SHA-256' };
790
- break;
791
- case 'EdDSA':
792
- algorithm = { name: 'SHA-512' };
793
- break;
794
- default:
795
- throw new UnsupportedOperationError();
796
- }
797
- const digest = await crypto.subtle.digest(algorithm, buf(data));
798
- return b64u(digest.slice(0, digest.byteLength / 2));
799
- }
800
- async function idTokenHashMatches(alg, data, actual) {
801
- const expected = await idTokenHash(alg, data);
802
- return timingSafeEqual(buf(actual), buf(expected));
803
- }
804
772
  async function authenticatedRequest(as, client, method, url, body, headers, options) {
805
773
  await clientAuthentication(as, client, body, headers, options?.clientPrivateKey);
806
774
  return fetch(url.href, {
@@ -813,7 +781,7 @@ async function authenticatedRequest(as, client, method, url, body, headers, opti
813
781
  }
814
782
  async function tokenEndpointRequest(as, client, grantType, parameters, options) {
815
783
  if (typeof as.token_endpoint !== 'string') {
816
- throw new TypeError('"issuer.token_endpoint" must be a string');
784
+ throw new TypeError('"as.token_endpoint" must be a string');
817
785
  }
818
786
  const url = new URL(as.token_endpoint);
819
787
  parameters.set('grant_type', grantType);
@@ -825,7 +793,7 @@ async function tokenEndpointRequest(as, client, grantType, parameters, options)
825
793
  return authenticatedRequest(as, client, 'POST', url, parameters, headers, options);
826
794
  }
827
795
  export async function refreshTokenGrantRequest(as, client, refreshToken, options) {
828
- assertIssuer(as);
796
+ assertAs(as);
829
797
  assertClient(client);
830
798
  if (!validateString(refreshToken)) {
831
799
  throw new TypeError('"refreshToken" must be a non-empty string');
@@ -841,8 +809,8 @@ export function getValidatedIdTokenClaims(ref) {
841
809
  }
842
810
  return idTokenClaims.get(ref);
843
811
  }
844
- async function processGenericAccessTokenResponse(as, client, response, options, ignoreIdToken = false, ignoreRefreshToken = false) {
845
- assertIssuer(as);
812
+ async function processGenericAccessTokenResponse(as, client, response, options, ignoreIdToken = false, ignoreRefreshToken = false, skipSignatureCheck = false) {
813
+ assertAs(as);
846
814
  assertClient(client);
847
815
  if (!(response instanceof Response)) {
848
816
  throw new TypeError('"response" must be an instance of Response');
@@ -891,10 +859,9 @@ async function processGenericAccessTokenResponse(as, client, response, options,
891
859
  throw new OPE('"response" body "id_token" property must be a non-empty string');
892
860
  }
893
861
  if (json.id_token) {
894
- if (typeof as.jwks_uri !== 'string') {
895
- throw new TypeError('"issuer.jwks_uri" must be a string');
896
- }
897
- const { header, claims } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
862
+ const { claims } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), skipSignatureCheck !== true
863
+ ? getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options)
864
+ : noSignatureCheck)
898
865
  .then(validatePresence.bind(undefined, ['aud', 'exp', 'iat', 'iss', 'sub']))
899
866
  .then(validateIssuer.bind(undefined, as.issuer))
900
867
  .then(validateAudience.bind(undefined, client.client_id));
@@ -904,19 +871,13 @@ async function processGenericAccessTokenResponse(as, client, response, options,
904
871
  if (client.require_auth_time && typeof claims.auth_time !== 'number') {
905
872
  throw new OPE('unexpected ID Token "auth_time" (authentication time) claim value');
906
873
  }
907
- if (claims.at_hash !== undefined) {
908
- if (typeof claims.at_hash !== 'string' ||
909
- !(await idTokenHashMatches(header.alg, json.access_token, claims.at_hash))) {
910
- throw new OPE('unexpected ID Token "at_hash" (access token hash) claim value');
911
- }
912
- }
913
874
  idTokenClaims.set(json, claims);
914
875
  }
915
876
  }
916
877
  return json;
917
878
  }
918
879
  export async function processRefreshTokenResponse(as, client, response, options) {
919
- return processGenericAccessTokenResponse(as, client, response, options);
880
+ return processGenericAccessTokenResponse(as, client, response, options, undefined, undefined, options?.skipJwtSignatureCheck === true);
920
881
  }
921
882
  function validateOptionalAudience(expected, result) {
922
883
  if (result.claims.aud !== undefined) {
@@ -948,7 +909,7 @@ function validateIssuer(expected, result) {
948
909
  return result;
949
910
  }
950
911
  export async function authorizationCodeGrantRequest(as, client, callbackParameters, redirectUri, codeVerifier, options) {
951
- assertIssuer(as);
912
+ assertAs(as);
952
913
  assertClient(client);
953
914
  if (!(callbackParameters instanceof CallbackParameters)) {
954
915
  throw new TypeError('"callbackParameters" must be an instance of CallbackParameters obtained from "validateAuthResponse()", or "validateJwtAuthResponse()');
@@ -987,7 +948,7 @@ function validatePresence(required, result) {
987
948
  export const expectNoNonce = Symbol();
988
949
  export const skipAuthTimeCheck = Symbol();
989
950
  export async function processAuthorizationCodeOpenIDResponse(as, client, response, expectedNonce, maxAge, options) {
990
- const result = await processGenericAccessTokenResponse(as, client, response, options);
951
+ const result = await processGenericAccessTokenResponse(as, client, response, options, undefined, undefined, options?.skipJwtSignatureCheck === true);
991
952
  if (isOAuth2Error(result)) {
992
953
  return result;
993
954
  }
@@ -1050,7 +1011,7 @@ function checkJwtType(expected, result) {
1050
1011
  return result;
1051
1012
  }
1052
1013
  export async function clientCredentialsGrantRequest(as, client, parameters, options) {
1053
- assertIssuer(as);
1014
+ assertAs(as);
1054
1015
  assertClient(client);
1055
1016
  return tokenEndpointRequest(as, client, 'client_credentials', new URLSearchParams(parameters), options);
1056
1017
  }
@@ -1062,13 +1023,13 @@ export async function processClientCredentialsResponse(as, client, response) {
1062
1023
  return result;
1063
1024
  }
1064
1025
  export async function revocationRequest(as, client, token, options) {
1065
- assertIssuer(as);
1026
+ assertAs(as);
1066
1027
  assertClient(client);
1067
1028
  if (!validateString(token)) {
1068
1029
  throw new TypeError('"token" must be a non-empty string');
1069
1030
  }
1070
1031
  if (typeof as.revocation_endpoint !== 'string') {
1071
- throw new TypeError('"issuer.revocation_endpoint" must be a string');
1032
+ throw new TypeError('"as.revocation_endpoint" must be a string');
1072
1033
  }
1073
1034
  const url = new URL(as.revocation_endpoint);
1074
1035
  const body = new URLSearchParams(options?.additionalParameters);
@@ -1096,13 +1057,13 @@ function assertReadableResponse(response) {
1096
1057
  }
1097
1058
  }
1098
1059
  export async function introspectionRequest(as, client, token, options) {
1099
- assertIssuer(as);
1060
+ assertAs(as);
1100
1061
  assertClient(client);
1101
1062
  if (!validateString(token)) {
1102
1063
  throw new TypeError('"token" must be a non-empty string');
1103
1064
  }
1104
1065
  if (typeof as.introspection_endpoint !== 'string') {
1105
- throw new TypeError('"issuer.introspection_endpoint" must be a string');
1066
+ throw new TypeError('"as.introspection_endpoint" must be a string');
1106
1067
  }
1107
1068
  const url = new URL(as.introspection_endpoint);
1108
1069
  const body = new URLSearchParams(options?.additionalParameters);
@@ -1117,7 +1078,7 @@ export async function introspectionRequest(as, client, token, options) {
1117
1078
  return authenticatedRequest(as, client, 'POST', url, body, headers, options);
1118
1079
  }
1119
1080
  export async function processIntrospectionResponse(as, client, response, options) {
1120
- assertIssuer(as);
1081
+ assertAs(as);
1121
1082
  assertClient(client);
1122
1083
  if (!(response instanceof Response)) {
1123
1084
  throw new TypeError('"response" must be an instance of Response');
@@ -1131,10 +1092,9 @@ export async function processIntrospectionResponse(as, client, response, options
1131
1092
  }
1132
1093
  let json;
1133
1094
  if (getContentType(response) === 'application/token-introspection+jwt') {
1134
- if (typeof as.jwks_uri !== 'string') {
1135
- throw new TypeError('"issuer.jwks_uri" must be a string');
1136
- }
1137
- const { claims } = await validateJwt(await preserveBodyStream(response).text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
1095
+ const { claims } = await validateJwt(await preserveBodyStream(response).text(), checkSigningAlgorithm.bind(undefined, client.introspection_signed_response_alg, as.introspection_signing_alg_values_supported), options?.skipJwtSignatureCheck !== true
1096
+ ? getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options)
1097
+ : noSignatureCheck)
1138
1098
  .then(checkJwtType.bind(undefined, 'token-introspection+jwt'))
1139
1099
  .then(validatePresence.bind(undefined, ['aud', 'iat', 'iss']))
1140
1100
  .then(validateIssuer.bind(undefined, as.issuer))
@@ -1161,9 +1121,9 @@ export async function processIntrospectionResponse(as, client, response, options
1161
1121
  return json;
1162
1122
  }
1163
1123
  export async function jwksRequest(as, options) {
1164
- assertIssuer(as);
1124
+ assertAs(as);
1165
1125
  if (typeof as.jwks_uri !== 'string') {
1166
- throw new TypeError('"issuer.jwks_uri" must be a string');
1126
+ throw new TypeError('"as.jwks_uri" must be a string');
1167
1127
  }
1168
1128
  const url = new URL(as.jwks_uri);
1169
1129
  const headers = prepareHeaders(options?.headers);
@@ -1254,8 +1214,9 @@ function subtleAlgorithm(key) {
1254
1214
  }
1255
1215
  throw new UnsupportedOperationError();
1256
1216
  }
1217
+ const noSignatureCheck = Symbol();
1257
1218
  async function validateJwt(jws, checkAlg, getKey) {
1258
- const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split('.');
1219
+ const { 0: protectedHeader, 1: payload, 2: encodedSignature, length } = jws.split('.');
1259
1220
  if (length === 5) {
1260
1221
  throw new UnsupportedOperationError('JWE structure JWTs are not supported');
1261
1222
  }
@@ -1276,11 +1237,14 @@ async function validateJwt(jws, checkAlg, getKey) {
1276
1237
  if (header.crit !== undefined) {
1277
1238
  throw new OPE('unexpected JWT "crit" header parameter');
1278
1239
  }
1279
- const key = await getKey(header);
1280
- const input = `${protectedHeader}.${payload}`;
1281
- const verified = await crypto.subtle.verify(subtleAlgorithm(key), key, b64u(signature), buf(input));
1282
- if (!verified) {
1283
- throw new OPE('JWT signature verification failed');
1240
+ const signature = b64u(encodedSignature);
1241
+ if (getKey !== noSignatureCheck) {
1242
+ const key = await getKey(header);
1243
+ const input = `${protectedHeader}.${payload}`;
1244
+ const verified = await crypto.subtle.verify(subtleAlgorithm(key), key, signature, buf(input));
1245
+ if (!verified) {
1246
+ throw new OPE('JWT signature verification failed');
1247
+ }
1284
1248
  }
1285
1249
  let claims;
1286
1250
  try {
@@ -1325,10 +1289,10 @@ async function validateJwt(jws, checkAlg, getKey) {
1325
1289
  throw new OPE('unexpected JWT "aud" (audience) claim type');
1326
1290
  }
1327
1291
  }
1328
- return { header, claims };
1292
+ return { header, claims, signature };
1329
1293
  }
1330
1294
  export async function validateJwtAuthResponse(as, client, parameters, expectedState, options) {
1331
- assertIssuer(as);
1295
+ assertAs(as);
1332
1296
  assertClient(client);
1333
1297
  if (parameters instanceof URL) {
1334
1298
  parameters = parameters.searchParams;
@@ -1341,7 +1305,7 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
1341
1305
  throw new OPE('"parameters" does not contain a JARM response');
1342
1306
  }
1343
1307
  if (typeof as.jwks_uri !== 'string') {
1344
- throw new TypeError('"issuer.jwks_uri" must be a string');
1308
+ throw new TypeError('"as.jwks_uri" must be a string');
1345
1309
  }
1346
1310
  const { claims } = await validateJwt(response, checkSigningAlgorithm.bind(undefined, client.authorization_signed_response_alg, as.authorization_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options))
1347
1311
  .then(validatePresence.bind(undefined, ['aud', 'exp', 'iss']))
@@ -1384,7 +1348,7 @@ export const expectNoState = Symbol();
1384
1348
  class CallbackParameters extends URLSearchParams {
1385
1349
  }
1386
1350
  export function validateAuthResponse(as, client, parameters, expectedState) {
1387
- assertIssuer(as);
1351
+ assertAs(as);
1388
1352
  assertClient(client);
1389
1353
  if (parameters instanceof URL) {
1390
1354
  parameters = parameters.searchParams;
@@ -1460,13 +1424,13 @@ async function importJwk(jwk) {
1460
1424
  return crypto.subtle.importKey('jwk', key, algorithm, true, ['verify']);
1461
1425
  }
1462
1426
  export async function deviceAuthorizationRequest(as, client, parameters, options) {
1463
- assertIssuer(as);
1427
+ assertAs(as);
1464
1428
  assertClient(client);
1465
1429
  if (!(parameters instanceof URLSearchParams)) {
1466
1430
  throw new TypeError('"parameters" must be an instance of URLSearchParams');
1467
1431
  }
1468
1432
  if (typeof as.device_authorization_endpoint !== 'string') {
1469
- throw new TypeError('"issuer.device_authorization_endpoint" must be a string');
1433
+ throw new TypeError('"as.device_authorization_endpoint" must be a string');
1470
1434
  }
1471
1435
  const url = new URL(as.device_authorization_endpoint);
1472
1436
  const body = new URLSearchParams(parameters);
@@ -1476,7 +1440,7 @@ export async function deviceAuthorizationRequest(as, client, parameters, options
1476
1440
  return authenticatedRequest(as, client, 'POST', url, body, headers, options);
1477
1441
  }
1478
1442
  export async function processDeviceAuthorizationResponse(as, client, response) {
1479
- assertIssuer(as);
1443
+ assertAs(as);
1480
1444
  assertClient(client);
1481
1445
  if (!(response instanceof Response)) {
1482
1446
  throw new TypeError('"response" must be an instance of Response');
@@ -1520,7 +1484,7 @@ export async function processDeviceAuthorizationResponse(as, client, response) {
1520
1484
  return json;
1521
1485
  }
1522
1486
  export async function deviceCodeGrantRequest(as, client, deviceCode, options) {
1523
- assertIssuer(as);
1487
+ assertAs(as);
1524
1488
  assertClient(client);
1525
1489
  if (!validateString(deviceCode)) {
1526
1490
  throw new TypeError('"deviceCode" must be a non-empty string');
@@ -1530,7 +1494,7 @@ export async function deviceCodeGrantRequest(as, client, deviceCode, options) {
1530
1494
  return tokenEndpointRequest(as, client, 'urn:ietf:params:oauth:grant-type:device_code', parameters, options);
1531
1495
  }
1532
1496
  export async function processDeviceCodeResponse(as, client, response, options) {
1533
- return processGenericAccessTokenResponse(as, client, response, options);
1497
+ return processGenericAccessTokenResponse(as, client, response, options, undefined, undefined, options?.skipJwtSignatureCheck === true);
1534
1498
  }
1535
1499
  export async function generateKeyPair(alg, options) {
1536
1500
  let algorithm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oauth4webapi",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "OAuth 2 / OpenID Connect for Web Platform API JavaScript runtimes",
5
5
  "keywords": [
6
6
  "auth",
@@ -10,11 +10,14 @@
10
10
  "browser",
11
11
  "certified",
12
12
  "client",
13
- "cloudflare-workers",
13
+ "cloudflare",
14
14
  "deno",
15
+ "edge",
15
16
  "electron",
16
17
  "fapi",
17
18
  "javascript",
19
+ "netlify",
20
+ "next",
18
21
  "nextjs",
19
22
  "node",
20
23
  "nodejs",
@@ -23,7 +26,8 @@
23
26
  "oidc",
24
27
  "openid-connect",
25
28
  "openid",
26
- "vercel-edge"
29
+ "vercel",
30
+ "workers"
27
31
  ],
28
32
  "homepage": "https://github.com/panva/oauth4webapi",
29
33
  "repository": "panva/oauth4webapi",
@@ -43,38 +47,36 @@
43
47
  "scripts": {
44
48
  "_format": "find src test tap examples conformance -type f -name '*.ts' -o -name '*.mjs' | xargs prettier",
45
49
  "build": "rm -rf build && tsc && tsc --declaration true --emitDeclarationOnly true --removeComments false && tsc -p test && tsc -p examples && tsc -p conformance && tsc -p tap",
46
- "conformance": "patch-package && NODE_OPTIONS='--no-warnings --loader=@esbuild-kit/esm-loader' ava --config conformance/ava.config.ts",
47
- "coverage": "patch-package && c8 ava",
50
+ "conformance": "bash -c 'patch-package && source .node_flags.sh && ava --config conformance/ava.config.ts'",
48
51
  "docs": "patch-package && typedoc",
49
52
  "format": "npm run _format -- --write",
50
53
  "format-check": "npm run _format -- --check",
51
54
  "prepack": "npm run format && npm run docs && ./examples/.update-diffs.sh && git diff --quiet && npm run test && npm run build",
52
55
  "tap:deno": "./tap/.deno.sh",
53
56
  "tap:edge-runtime": "./tap/.edge-runtime.sh",
54
- "tap:node": "./tap/.node.sh",
57
+ "tap:node": "bash -c './tap/.node.sh'",
55
58
  "tap:workers": "./tap/.workers.sh",
56
59
  "tap:browsers": "./tap/.browsers.sh",
57
- "test": "ava"
60
+ "test": "bash -c 'source .node_flags.sh && ava'"
58
61
  },
59
62
  "devDependencies": {
60
63
  "@esbuild-kit/esm-loader": "^2.5.0",
61
- "@types/node": "^18.8.3",
64
+ "@types/node": "^18.11.7",
62
65
  "@types/qunit": "^2.19.3",
63
- "ava": "^4.3.3",
64
- "c8": "^7.12.0",
65
- "edge-runtime": "^1.1.0-beta.31",
66
- "jose": "^4.10.0",
67
- "patch-package": "^6.4.7",
66
+ "ava": "^5.0.1",
67
+ "edge-runtime": "^2.0.0",
68
+ "jose": "^4.10.4",
69
+ "patch-package": "^6.5.0",
68
70
  "prettier": "^2.7.1",
69
71
  "prettier-plugin-jsdoc": "^0.4.2",
70
- "qunit": "^2.19.1",
72
+ "qunit": "^2.19.3",
71
73
  "testcafe": "^2.0.1",
72
74
  "testcafe-browser-provider-browserstack": "^1.14.0",
73
75
  "timekeeper": "^2.2.0",
74
- "typedoc": "^0.23.15",
76
+ "typedoc": "^0.23.19",
75
77
  "typedoc-plugin-markdown": "^3.13.6",
76
78
  "typescript": "^4.8.4",
77
- "undici": "^5.11.0",
79
+ "undici": "^5.12.0",
78
80
  "workerd": "^1.20220926.3"
79
81
  }
80
82
  }