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