@openstax/ts-utils 1.35.5 → 1.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- if (!header.iss) {
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 jwksUrl = new URL('/.well-known/jwks.json', iss);
65
- const launchDomain = jwksUrl.hostname;
57
+ const domain = new URL(iss).host;
66
58
  const trustedDomain = await getTrustedDomain();
67
- if (launchDomain !== trustedDomain && !launchDomain.endsWith(`.${trustedDomain}`)) {
68
- return callback(new Error(`Untrusted launch domain: "${launchDomain}"`));
59
+ if (domain !== trustedDomain && !domain.endsWith(`.${trustedDomain}`)) {
60
+ return callback(new Error(`Untrusted JWKS domain: "${domain}"`));
69
61
  }
70
- callback(null, await getJwksKey(jwksUrl.toString(), kid));
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);