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 +12 -37
- package/build/index.d.ts +39 -6
- package/build/index.js +67 -103
- package/package.json +18 -16
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
|
-
##
|
|
56
|
-
|
|
57
|
-
The supported JavaScript runtimes include ones that
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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.
|
|
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
|
-
|
|
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('"
|
|
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
|
|
319
|
-
if (typeof
|
|
320
|
-
throw new TypeError('"
|
|
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(
|
|
323
|
-
throw new TypeError('"
|
|
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(
|
|
328
|
-
if (typeof
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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('"
|
|
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
|
-
|
|
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
|
-
|
|
618
|
+
assertAs(as);
|
|
613
619
|
assertClient(client);
|
|
614
620
|
if (typeof as.userinfo_endpoint !== 'string') {
|
|
615
|
-
throw new TypeError('"
|
|
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
|
-
|
|
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
|
-
|
|
730
|
-
|
|
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('"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
895
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('"
|
|
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
|
-
|
|
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('"
|
|
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
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
1124
|
+
assertAs(as);
|
|
1165
1125
|
if (typeof as.jwks_uri !== 'string') {
|
|
1166
|
-
throw new TypeError('"
|
|
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:
|
|
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
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
-
|
|
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('"
|
|
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
|
-
|
|
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
|
-
|
|
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('"
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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 &&
|
|
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.
|
|
64
|
+
"@types/node": "^18.11.7",
|
|
62
65
|
"@types/qunit": "^2.19.3",
|
|
63
|
-
"ava": "^
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
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.
|
|
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.
|
|
76
|
+
"typedoc": "^0.23.19",
|
|
75
77
|
"typedoc-plugin-markdown": "^3.13.6",
|
|
76
78
|
"typescript": "^4.8.4",
|
|
77
|
-
"undici": "^5.
|
|
79
|
+
"undici": "^5.12.0",
|
|
78
80
|
"workerd": "^1.20220926.3"
|
|
79
81
|
}
|
|
80
82
|
}
|