-
Notifications
You must be signed in to change notification settings - Fork 40
/
saml-client.ts
172 lines (161 loc) · 5.62 KB
/
saml-client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import https from "https";
import { constants } from "crypto";
import axios from "axios";
import * as AWS from "aws-sdk";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import { SamlCertsAndKeys } from "./security/types";
import { Config } from "../../../../util/config";
import { out } from "../../../../util/log";
import { MetriportError } from "../../../../util/error/metriport-error";
import { createMtomContentTypeAndPayload } from "../outbound/xca/mtom/builder";
import { executeWithNetworkRetries } from "@metriport/shared";
import {
parseMtomResponse,
getBoundaryFromMtomResponse,
MtomAttachments,
convertSoapResponseToMtomResponse,
} from "../outbound/xca/mtom/parser";
dayjs.extend(duration);
const { log } = out("Saml Client");
const httpTimeout = dayjs.duration({ seconds: 120 }).asMilliseconds();
const initialDelay = dayjs.duration({ seconds: 3 }).asMilliseconds();
const maxPayloadSize = Infinity;
let rejectUnauthorized = true;
let trustedStore: string | undefined = undefined;
async function getTrustedKeyStore(): Promise<string> {
if (!trustedStore) trustedStore = await loadTrustedKeyStore();
return trustedStore;
}
/*
* ONLY use this function for testing purposes. It will turn off SSL Verification of the server if set to false.
* See saml-server.ts for usage.
*/
export function setRejectUnauthorized(value: boolean): void {
rejectUnauthorized = value;
}
export function getRejectUnauthorized(): boolean {
return rejectUnauthorized;
}
export type SamlClientResponse = {
response: string;
success: boolean;
};
async function loadTrustedKeyStore(): Promise<string> {
try {
const s3 = new AWS.S3({ region: Config.getAWSRegion() });
const trustBundleBucketName = Config.getCqTrustBundleBucketName();
const envType = Config.isDev() || Config.isStaging() ? Config.STAGING_ENV : Config.PROD_ENV;
const key = `trust_store_${envType}_aws.pem`;
const response = await s3.getObject({ Bucket: trustBundleBucketName, Key: key }).promise();
if (!response.Body) {
log("Trust bundle not found.");
throw new Error("Trust bundle not found.");
}
const trustBundle = response.Body.toString();
return trustBundle;
} catch (error) {
const msg = `Error getting trust bundle`;
log(`${msg}. Error: ${error}`);
throw new MetriportError(msg, error);
}
}
export async function sendSignedXml({
signedXml,
url,
samlCertsAndKeys,
}: {
signedXml: string;
url: string;
samlCertsAndKeys: SamlCertsAndKeys;
}): Promise<{ response: string; contentType: string }> {
const trustedKeyStore = await getTrustedKeyStore();
const agent = new https.Agent({
rejectUnauthorized: getRejectUnauthorized(),
requestCert: true,
cert: samlCertsAndKeys.certChain,
key: samlCertsAndKeys.privateKey,
passphrase: samlCertsAndKeys.privateKeyPassword,
ca: trustedKeyStore,
ciphers: "DEFAULT:!DH",
secureOptions: constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION,
});
const response = await executeWithNetworkRetries(
async () => {
return axios.post(url, signedXml, {
timeout: httpTimeout,
headers: {
"Content-Type": "application/soap+xml;charset=UTF-8",
Accept: "application/soap+xml",
"Cache-Control": "no-cache",
},
httpsAgent: agent,
});
},
{
initialDelay: initialDelay,
maxAttempts: 3,
//TODO: This introduces retry on timeout without needing to specify the http Code: https://github.com/metriport/metriport/pull/2285. Remove once PR is merged
httpCodesToRetry: ["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"],
}
);
return { response: response.data, contentType: response.headers["content-type"] };
}
export async function sendSignedXmlMtom({
signedXml,
url,
samlCertsAndKeys,
}: {
signedXml: string;
url: string;
samlCertsAndKeys: SamlCertsAndKeys;
}): Promise<{ mtomParts: MtomAttachments; rawResponse: Buffer }> {
const trustedKeyStore = await getTrustedKeyStore();
const agent = new https.Agent({
rejectUnauthorized: getRejectUnauthorized(),
requestCert: true,
cert: samlCertsAndKeys.certChain,
key: samlCertsAndKeys.privateKey,
passphrase: samlCertsAndKeys.privateKeyPassword,
ca: trustedKeyStore,
ciphers: "DEFAULT:!DH",
secureOptions: constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION,
});
const { contentType, payload } = createMtomContentTypeAndPayload(signedXml);
const response = await executeWithNetworkRetries(
async () => {
return axios.post(url, payload, {
timeout: httpTimeout,
headers: {
"Accept-Encoding": "gzip, deflate",
"Content-Type": contentType,
"Cache-Control": "no-cache",
},
httpsAgent: agent,
responseType: "arraybuffer",
maxBodyLength: maxPayloadSize,
maxContentLength: maxPayloadSize,
});
},
{
initialDelay: initialDelay,
maxAttempts: 4,
//TODO: This introduces retry on timeout without needing to specify the http Code: https://github.com/metriport/metriport/pull/2285. Remove once PR is merged
httpCodesToRetry: [
"ECONNREFUSED",
"ECONNRESET",
"ETIMEDOUT",
"ECONNABORTED",
"ERR_BAD_RESPONSE",
],
}
);
const binaryData: Buffer = Buffer.isBuffer(response.data)
? response.data
: Buffer.from(response.data, "binary");
const boundary = getBoundaryFromMtomResponse(response.headers["content-type"]);
if (boundary) {
return { mtomParts: await parseMtomResponse(binaryData, boundary), rawResponse: binaryData };
}
return { mtomParts: convertSoapResponseToMtomResponse(binaryData), rawResponse: binaryData };
}