@openstax/ts-utils 1.35.5 → 1.36.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/dist/cjs/misc/jwks.d.ts +7 -0
- package/dist/cjs/misc/jwks.js +13 -0
- package/dist/cjs/services/httpMessageVerifier/index.d.ts +24 -0
- package/dist/cjs/services/httpMessageVerifier/index.js +94 -0
- package/dist/cjs/services/launchParams/verifier.js +8 -15
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/misc/jwks.d.ts +7 -0
- package/dist/esm/misc/jwks.js +10 -0
- package/dist/esm/services/httpMessageVerifier/index.d.ts +24 -0
- package/dist/esm/services/httpMessageVerifier/index.js +90 -0
- package/dist/esm/services/launchParams/verifier.js +8 -15
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/script/bin/.init-params-script.bash.swp +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { JwksClient } from 'jwks-rsa';
|
|
2
|
+
import type { JWK } from 'node-jose';
|
|
3
|
+
export type JwksFetcher = (uri: string) => Promise<{
|
|
4
|
+
keys: JWK.RawKey[];
|
|
5
|
+
}>;
|
|
6
|
+
export declare const getJwksClient: (iss: string, fetcher?: JwksFetcher) => JwksClient;
|
|
7
|
+
export declare const getJwksKey: (iss: string, kid?: string, fetcher?: JwksFetcher) => Promise<import("jwks-rsa").SigningKey>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getJwksKey = exports.getJwksClient = void 0;
|
|
4
|
+
const jwks_rsa_1 = require("jwks-rsa");
|
|
5
|
+
const helpers_1 = require("./helpers");
|
|
6
|
+
exports.getJwksClient = (0, helpers_1.memoize)((iss, fetcher) => {
|
|
7
|
+
const jwksUri = new URL('/.well-known/jwks.json', iss).toString();
|
|
8
|
+
return new jwks_rsa_1.JwksClient({ jwksUri, fetcher });
|
|
9
|
+
});
|
|
10
|
+
exports.getJwksKey = (0, helpers_1.memoize)(async (iss, kid, fetcher) => {
|
|
11
|
+
const client = (0, exports.getJwksClient)(iss, fetcher);
|
|
12
|
+
return client.getSigningKey(kid);
|
|
13
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { APIGatewayProxyEventV2 } from 'aws-lambda';
|
|
2
|
+
import { VerifyConfig } from 'http-message-signatures';
|
|
3
|
+
import { JWK } from 'node-jose';
|
|
4
|
+
import { ConfigProviderForConfig } from '../../config';
|
|
5
|
+
type Config = {
|
|
6
|
+
bypassSignatureVerification: string;
|
|
7
|
+
};
|
|
8
|
+
interface Initializer<C> {
|
|
9
|
+
configSpace?: C;
|
|
10
|
+
fetcher?: (uri: string) => Promise<{
|
|
11
|
+
keys: JWK.RawKey[];
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export type SignatureAgentVerifier = (signatureAgent: string) => boolean | Promise<boolean>;
|
|
15
|
+
export declare const createHttpMessageVerifier: <C extends string = "verifier">({ configSpace, fetcher }: Initializer<C>) => (configProvider: { [_key in C]: ConfigProviderForConfig<Config>; }) => ({ request }: {
|
|
16
|
+
request: APIGatewayProxyEventV2;
|
|
17
|
+
}) => {
|
|
18
|
+
verify: ({ configOverride, signatureAgentVerifier }: {
|
|
19
|
+
configOverride?: Partial<VerifyConfig>;
|
|
20
|
+
signatureAgentVerifier: SignatureAgentVerifier;
|
|
21
|
+
}) => Promise<boolean>;
|
|
22
|
+
};
|
|
23
|
+
export type HttpMessageVerifier = ReturnType<ReturnType<ReturnType<typeof createHttpMessageVerifier>>>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHttpMessageVerifier = void 0;
|
|
4
|
+
// cspell:ignore algs, httpbis, keyid
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const http_message_signatures_1 = require("http-message-signatures");
|
|
7
|
+
const jwks_rsa_1 = require("jwks-rsa");
|
|
8
|
+
const assertions_1 = require("../../assertions");
|
|
9
|
+
const config_1 = require("../../config");
|
|
10
|
+
const errors_1 = require("../../errors");
|
|
11
|
+
const helpers_1 = require("../../misc/helpers");
|
|
12
|
+
const jwks_1 = require("../../misc/jwks");
|
|
13
|
+
const createHttpMessageVerifier = ({ configSpace, fetcher }) => (configProvider) => {
|
|
14
|
+
const config = configProvider[configSpace !== null && configSpace !== void 0 ? configSpace : 'verifier'];
|
|
15
|
+
const getBypassSignatureVerification = (0, helpers_1.once)(async () => (await (0, config_1.resolveConfigValue)(config.bypassSignatureVerification)) === 'true');
|
|
16
|
+
return ({ request }) => ({
|
|
17
|
+
verify: async ({ configOverride, signatureAgentVerifier }) => {
|
|
18
|
+
var _a;
|
|
19
|
+
if (await getBypassSignatureVerification()) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
const headers = {};
|
|
23
|
+
Object.entries(request.headers).forEach(([key, value]) => {
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
headers[key.toLowerCase()] = value;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// https://datatracker.ietf.org/doc/html/draft-meunier-http-message-signatures-directory-04#name-http-method-context-signatu
|
|
29
|
+
// We accept Signature-Agent as either a simple URL string (as in an earlier RFC draft) or a dictionary of URLs,
|
|
30
|
+
// but we do not bother matching the Signature-Agent names with the Signature names
|
|
31
|
+
// as that would be awkward with the packages we use
|
|
32
|
+
const signatureAgentString = (_a = headers['signature-agent']) !== null && _a !== void 0 ? _a : '';
|
|
33
|
+
const signatureAgentMatches = [...signatureAgentString.matchAll(/([^=]+)="([^"]+)"/g)];
|
|
34
|
+
const signatureAgents = signatureAgentMatches.length == 0 ?
|
|
35
|
+
[signatureAgentString] : [...new Set(signatureAgentMatches.map((match) => match[2]))];
|
|
36
|
+
const keys = (await Promise.all(signatureAgents.map(async (signatureAgent) => {
|
|
37
|
+
if (!await signatureAgentVerifier(signatureAgent)) {
|
|
38
|
+
throw new errors_1.InvalidRequestError('Signature-Agent verification failed');
|
|
39
|
+
}
|
|
40
|
+
return (0, jwks_1.getJwksClient)(signatureAgent, fetcher).getSigningKeys();
|
|
41
|
+
}))).flat();
|
|
42
|
+
const { body, requestContext } = request;
|
|
43
|
+
const verificationRequest = {
|
|
44
|
+
body,
|
|
45
|
+
headers,
|
|
46
|
+
method: requestContext.http.method,
|
|
47
|
+
// Node's request.url is really just the path and querystring
|
|
48
|
+
url: requestContext.http.path,
|
|
49
|
+
};
|
|
50
|
+
if (!await http_message_signatures_1.httpbis.verifyMessage({
|
|
51
|
+
all: true,
|
|
52
|
+
keyLookup: async (parameters) => {
|
|
53
|
+
var _a;
|
|
54
|
+
const { keyid } = parameters;
|
|
55
|
+
// This is basically JwksClient.getSigningKey() but using the keys above from the Signature-Agent
|
|
56
|
+
const kidDefined = keyid !== undefined && keyid !== null;
|
|
57
|
+
if (!kidDefined && keys.length > 1) {
|
|
58
|
+
throw new jwks_rsa_1.SigningKeyNotFoundError('No keyid specified and JWKS endpoint returned more than 1 key');
|
|
59
|
+
}
|
|
60
|
+
const key = keys.find(k => !kidDefined || k.kid === keyid);
|
|
61
|
+
if (!key)
|
|
62
|
+
throw new jwks_rsa_1.SigningKeyNotFoundError(`Unable to find a signing key that matches "${keyid}"`);
|
|
63
|
+
const alg = (_a = parameters.alg) !== null && _a !== void 0 ? _a : key.alg;
|
|
64
|
+
if (!alg) {
|
|
65
|
+
throw new errors_1.InvalidRequestError('Signature algorithm must be specified either as a signature param or in the JWKS key');
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
id: key.kid,
|
|
69
|
+
algs: [alg],
|
|
70
|
+
verify: (0, http_message_signatures_1.createVerifier)(key.getPublicKey(), alg)
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
requiredFields: ['@method', '@target-uri', 'content-digest', 'signature-agent'],
|
|
74
|
+
tolerance: 300,
|
|
75
|
+
...configOverride,
|
|
76
|
+
}, verificationRequest))
|
|
77
|
+
throw new errors_1.InvalidRequestError('Signature verification failed');
|
|
78
|
+
// For example, if a GET request uses configOverride to make content-digest not required
|
|
79
|
+
if (!headers['content-digest'])
|
|
80
|
+
return true;
|
|
81
|
+
const match = headers['content-digest'].match(/^(sha-256|sha-512)=:([^:]+):/);
|
|
82
|
+
if (!match)
|
|
83
|
+
throw new errors_1.InvalidRequestError('Unsupported Content-Digest header format');
|
|
84
|
+
const contentDigestAlg = match[1];
|
|
85
|
+
const contentDigestHash = match[2];
|
|
86
|
+
const calculatedContentDigestHash = (0, crypto_1.createHash)(contentDigestAlg).update((0, assertions_1.assertString)(body)).digest('base64');
|
|
87
|
+
if (calculatedContentDigestHash !== contentDigestHash.replace(/:/g, '')) {
|
|
88
|
+
throw new errors_1.InvalidRequestError('Calculated Content-Digest value did not match header');
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
exports.createHttpMessageVerifier = createHttpMessageVerifier;
|
|
@@ -35,12 +35,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.createLaunchVerifier = void 0;
|
|
37
37
|
const jsonwebtoken_1 = __importStar(require("jsonwebtoken"));
|
|
38
|
-
const jwks_rsa_1 = require("jwks-rsa");
|
|
39
|
-
const __1 = require("../..");
|
|
40
38
|
const config_1 = require("../../config");
|
|
41
39
|
const errors_1 = require("../../errors");
|
|
42
40
|
const guards_1 = require("../../guards");
|
|
43
41
|
const helpers_1 = require("../../misc/helpers");
|
|
42
|
+
const jwks_1 = require("../../misc/jwks");
|
|
44
43
|
/**
|
|
45
44
|
* Creates a class that can verify launch params
|
|
46
45
|
*/
|
|
@@ -48,26 +47,20 @@ const createLaunchVerifier = ({ configSpace, fetcher }) => (configProvider) => {
|
|
|
48
47
|
const config = configProvider[(0, guards_1.ifDefined)(configSpace, 'launch')];
|
|
49
48
|
const getTrustedDomain = (0, helpers_1.once)(() => (0, config_1.resolveConfigValue)(config.trustedDomain));
|
|
50
49
|
const getBypassSignatureVerification = (0, helpers_1.once)(async () => (await (0, config_1.resolveConfigValue)(config.bypassSignatureVerification)) === 'true');
|
|
51
|
-
const getJwksClient = (0, __1.memoize)((jwksUri) => new jwks_rsa_1.JwksClient({ fetcher, jwksUri }));
|
|
52
|
-
const getJwksKey = (0, __1.memoize)(async (jwksUri, kid) => {
|
|
53
|
-
const client = getJwksClient(jwksUri);
|
|
54
|
-
const key = await client.getSigningKey(kid);
|
|
55
|
-
return key.getPublicKey();
|
|
56
|
-
});
|
|
57
50
|
const getKey = async (header, callback) => {
|
|
58
51
|
// The JWT spec allows iss in the header as a copy of the iss claim, but we require it
|
|
59
|
-
|
|
52
|
+
const { iss, kid } = header;
|
|
53
|
+
if (!iss) {
|
|
60
54
|
return callback(new Error('JWT header missing iss claim'));
|
|
61
55
|
}
|
|
62
|
-
const { iss, kid } = header;
|
|
63
56
|
try {
|
|
64
|
-
const
|
|
65
|
-
const launchDomain = jwksUrl.hostname;
|
|
57
|
+
const domain = new URL(iss).host;
|
|
66
58
|
const trustedDomain = await getTrustedDomain();
|
|
67
|
-
if (
|
|
68
|
-
return callback(new Error(`Untrusted
|
|
59
|
+
if (domain !== trustedDomain && !domain.endsWith(`.${trustedDomain}`)) {
|
|
60
|
+
return callback(new Error(`Untrusted JWKS domain: "${domain}"`));
|
|
69
61
|
}
|
|
70
|
-
|
|
62
|
+
const key = await (0, jwks_1.getJwksKey)(iss, kid, fetcher);
|
|
63
|
+
callback(null, key.getPublicKey());
|
|
71
64
|
}
|
|
72
65
|
catch (error) {
|
|
73
66
|
callback(error);
|