samlesa 2.12.3

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.

Potentially problematic release.


This version of samlesa might be problematic. Click here for more details.

Files changed (99) hide show
  1. package/.editorconfig +19 -0
  2. package/.github/FUNDING.yml +1 -0
  3. package/.idea/compiler.xml +6 -0
  4. package/.idea/deployment.xml +14 -0
  5. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  6. package/.idea/jsLibraryMappings.xml +6 -0
  7. package/.idea/modules.xml +8 -0
  8. package/.idea/samlify.iml +12 -0
  9. package/.idea/vcs.xml +6 -0
  10. package/.pre-commit.sh +15 -0
  11. package/.snyk +8 -0
  12. package/.travis.yml +29 -0
  13. package/LICENSE +22 -0
  14. package/Makefile +25 -0
  15. package/README.md +84 -0
  16. package/build/.idea/workspace.xml +58 -0
  17. package/build/index.js +65 -0
  18. package/build/index.js.map +1 -0
  19. package/build/src/api.js +24 -0
  20. package/build/src/api.js.map +1 -0
  21. package/build/src/binding-post.js +369 -0
  22. package/build/src/binding-post.js.map +1 -0
  23. package/build/src/binding-redirect.js +333 -0
  24. package/build/src/binding-redirect.js.map +1 -0
  25. package/build/src/binding-simplesign.js +233 -0
  26. package/build/src/binding-simplesign.js.map +1 -0
  27. package/build/src/entity-idp.js +131 -0
  28. package/build/src/entity-idp.js.map +1 -0
  29. package/build/src/entity-sp.js +97 -0
  30. package/build/src/entity-sp.js.map +1 -0
  31. package/build/src/entity.js +236 -0
  32. package/build/src/entity.js.map +1 -0
  33. package/build/src/extractor.js +370 -0
  34. package/build/src/extractor.js.map +1 -0
  35. package/build/src/flow.js +320 -0
  36. package/build/src/flow.js.map +1 -0
  37. package/build/src/libsaml.js +642 -0
  38. package/build/src/libsaml.js.map +1 -0
  39. package/build/src/metadata-idp.js +128 -0
  40. package/build/src/metadata-idp.js.map +1 -0
  41. package/build/src/metadata-sp.js +232 -0
  42. package/build/src/metadata-sp.js.map +1 -0
  43. package/build/src/metadata.js +177 -0
  44. package/build/src/metadata.js.map +1 -0
  45. package/build/src/types.js +12 -0
  46. package/build/src/types.js.map +1 -0
  47. package/build/src/urn.js +213 -0
  48. package/build/src/urn.js.map +1 -0
  49. package/build/src/utility.js +249 -0
  50. package/build/src/utility.js.map +1 -0
  51. package/build/src/validator.js +27 -0
  52. package/build/src/validator.js.map +1 -0
  53. package/index.d.ts +10 -0
  54. package/index.js +19 -0
  55. package/index.js.map +1 -0
  56. package/index.ts +28 -0
  57. package/package.json +74 -0
  58. package/qodana.yaml +29 -0
  59. package/src/.idea/modules.xml +8 -0
  60. package/src/.idea/src.iml +12 -0
  61. package/src/.idea/vcs.xml +6 -0
  62. package/src/api.ts +36 -0
  63. package/src/binding-post.ts +338 -0
  64. package/src/binding-redirect.ts +331 -0
  65. package/src/binding-simplesign.ts +231 -0
  66. package/src/entity-idp.ts +145 -0
  67. package/src/entity-sp.ts +114 -0
  68. package/src/entity.ts +243 -0
  69. package/src/extractor.ts +392 -0
  70. package/src/flow.ts +467 -0
  71. package/src/libsaml.ts +786 -0
  72. package/src/metadata-idp.ts +146 -0
  73. package/src/metadata-sp.ts +268 -0
  74. package/src/metadata.ts +166 -0
  75. package/src/types.ts +153 -0
  76. package/src/urn.ts +211 -0
  77. package/src/utility.ts +248 -0
  78. package/src/validator.ts +44 -0
  79. package/tsconfig.json +38 -0
  80. package/tslint.json +35 -0
  81. package/types/index.d.ts +10 -0
  82. package/types/src/api.d.ts +13 -0
  83. package/types/src/binding-post.d.ts +46 -0
  84. package/types/src/binding-redirect.d.ts +52 -0
  85. package/types/src/binding-simplesign.d.ts +39 -0
  86. package/types/src/entity-idp.d.ts +42 -0
  87. package/types/src/entity-sp.d.ts +36 -0
  88. package/types/src/entity.d.ts +99 -0
  89. package/types/src/extractor.d.ts +25 -0
  90. package/types/src/flow.d.ts +6 -0
  91. package/types/src/libsaml.d.ts +210 -0
  92. package/types/src/metadata-idp.d.ts +24 -0
  93. package/types/src/metadata-sp.d.ts +36 -0
  94. package/types/src/metadata.d.ts +57 -0
  95. package/types/src/types.d.ts +127 -0
  96. package/types/src/urn.d.ts +194 -0
  97. package/types/src/utility.d.ts +134 -0
  98. package/types/src/validator.d.ts +3 -0
  99. package/types.d.ts +2 -0
package/src/libsaml.ts ADDED
@@ -0,0 +1,786 @@
1
+ /**
2
+ * @file SamlLib.js
3
+ * @author tngan
4
+ * @desc A simple library including some common functions
5
+ */
6
+
7
+ import utility, { flattenDeep, isString } from './utility.js';
8
+ import { algorithms, wording, namespace } from './urn.js';
9
+ import { select } from 'xpath';
10
+ import { MetadataInterface } from './metadata.js';
11
+ import nrsa, { SigningSchemeHash } from 'node-rsa';
12
+ import { SignedXml } from 'xml-crypto';
13
+ import * as xmlenc from 'xml-encryption';
14
+ import { extract } from './extractor.js';
15
+ import camelCase from 'camelcase';
16
+ import { getContext } from './api.js';
17
+ import xmlEscape from 'xml-escape';
18
+ import * as fs from 'fs';
19
+ import {DOMParser} from '@xmldom/xmldom';
20
+
21
+ const signatureAlgorithms = algorithms.signature;
22
+ const digestAlgorithms = algorithms.digest;
23
+ const certUse = wording.certUse;
24
+ const urlParams = wording.urlParams;
25
+
26
+ export interface SignatureConstructor {
27
+ rawSamlMessage: string;
28
+ referenceTagXPath?: string;
29
+ privateKey: string;
30
+ privateKeyPass?: string;
31
+ signatureAlgorithm: string;
32
+ signingCert: string | Buffer;
33
+ isBase64Output?: boolean;
34
+ signatureConfig?: any;
35
+ isMessageSigned?: boolean;
36
+ transformationAlgorithms?: string[];
37
+ }
38
+
39
+ export interface SignatureVerifierOptions {
40
+ metadata?: MetadataInterface;
41
+ keyFile?: string;
42
+ signatureAlgorithm?: string;
43
+ }
44
+
45
+ export interface ExtractorResult {
46
+ [key: string]: any;
47
+ signature?: string | string[];
48
+ issuer?: string | string[];
49
+ nameID?: string;
50
+ notexist?: boolean;
51
+ }
52
+
53
+ export interface LoginResponseAttribute {
54
+ name: string;
55
+ nameFormat: string; //
56
+ valueXsiType: string; //
57
+ valueTag: string;
58
+ valueXmlnsXs?: string;
59
+ valueXmlnsXsi?: string;
60
+ type?: string | string[];
61
+ }
62
+
63
+ export interface LoginResponseAdditionalTemplates {
64
+ attributeStatementTemplate?: AttributeStatementTemplate;
65
+ attributeTemplate?: AttributeTemplate;
66
+ }
67
+
68
+ export interface BaseSamlTemplate {
69
+ context: string;
70
+ }
71
+
72
+ export interface LoginResponseTemplate extends BaseSamlTemplate {
73
+ attributes?: LoginResponseAttribute[];
74
+ additionalTemplates?: LoginResponseAdditionalTemplates;
75
+ }
76
+ export interface AttributeStatementTemplate extends BaseSamlTemplate { }
77
+
78
+ export interface AttributeTemplate extends BaseSamlTemplate { }
79
+
80
+ export interface LoginRequestTemplate extends BaseSamlTemplate { }
81
+
82
+ export interface LogoutRequestTemplate extends BaseSamlTemplate { }
83
+
84
+ export interface LogoutResponseTemplate extends BaseSamlTemplate { }
85
+
86
+ export type KeyUse = 'signing' | 'encryption';
87
+
88
+ export interface KeyComponent {
89
+ [key: string]: any;
90
+ }
91
+
92
+ export interface LibSamlInterface {
93
+ getQueryParamByType: (type: string) => string;
94
+ createXPath: (local, isExtractAll?: boolean) => string;
95
+ replaceTagsByValue: (rawXML: string, tagValues: any) => string;
96
+ attributeStatementBuilder: (attributes: LoginResponseAttribute[], attributeTemplate: AttributeTemplate, attributeStatementTemplate: AttributeStatementTemplate) => string;
97
+ constructSAMLSignature: (opts: SignatureConstructor) => string;
98
+ verifySignature: (xml: string, opts: SignatureVerifierOptions) => [boolean, any];
99
+ createKeySection: (use: KeyUse, cert: string | Buffer) => {};
100
+ constructMessageSignature: (octetString: string, key: string, passphrase?: string, isBase64?: boolean, signingAlgorithm?: string) => string;
101
+
102
+ verifyMessageSignature: (metadata, octetString: string, signature: string | Buffer, verifyAlgorithm?: string) => boolean;
103
+ getKeyInfo: (x509Certificate: string, signatureConfig?: any) => void;
104
+ encryptAssertion: (sourceEntity, targetEntity, entireXML: string) => Promise<string>;
105
+ decryptAssertion: (here, entireXML: string) => Promise<[string, any]>;
106
+
107
+ getSigningScheme: (sigAlg: string) => string | null;
108
+ getDigestMethod: (sigAlg: string) => string | null;
109
+
110
+ nrsaAliasMapping: any;
111
+ defaultLoginRequestTemplate: LoginRequestTemplate;
112
+ defaultLoginResponseTemplate: LoginResponseTemplate;
113
+ defaultAttributeStatementTemplate: AttributeStatementTemplate;
114
+ defaultAttributeTemplate: AttributeTemplate;
115
+ defaultLogoutRequestTemplate: LogoutRequestTemplate;
116
+ defaultLogoutResponseTemplate: LogoutResponseTemplate;
117
+ }
118
+
119
+ const libSaml = () => {
120
+
121
+ /**
122
+ * @desc helper function to get back the query param for redirect binding for SLO/SSO
123
+ * @type {string}
124
+ */
125
+ function getQueryParamByType(type: string) {
126
+ if ([urlParams.logoutRequest, urlParams.samlRequest].indexOf(type) !== -1) {
127
+ return 'SAMLRequest';
128
+ }
129
+ if ([urlParams.logoutResponse, urlParams.samlResponse].indexOf(type) !== -1) {
130
+ return 'SAMLResponse';
131
+ }
132
+ throw new Error('ERR_UNDEFINED_QUERY_PARAMS');
133
+ }
134
+ /**
135
+ *
136
+ */
137
+ const nrsaAliasMapping = {
138
+ 'http://www.w3.org/2000/09/xmldsig#rsa-sha1': 'pkcs1-sha1',
139
+ 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256': 'pkcs1-sha256',
140
+ 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512': 'pkcs1-sha512',
141
+ };
142
+ /**
143
+ * @desc Default login request template
144
+ * @type {LoginRequestTemplate}
145
+ */
146
+ const defaultLoginRequestTemplate = {
147
+ context: '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>',
148
+ };
149
+ /**
150
+ * @desc Default logout request template
151
+ * @type {LogoutRequestTemplate}
152
+ */
153
+ const defaultLogoutRequestTemplate = {
154
+ context: '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}"><saml:Issuer>{Issuer}</saml:Issuer><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID></samlp:LogoutRequest>',
155
+ };
156
+
157
+ /**
158
+ * @desc Default AttributeStatement template
159
+ * @type {AttributeStatementTemplate}
160
+ */
161
+ const defaultAttributeStatementTemplate = {
162
+ context: '<saml:AttributeStatement>{Attributes}</saml:AttributeStatement>',
163
+ };
164
+
165
+ /**
166
+ * @desc Default Attribute template
167
+ * @type {AttributeTemplate}
168
+ */
169
+ const defaultAttributeTemplate = {
170
+ context: '<saml:Attribute Name="{Name}" NameFormat="{NameFormat}"><saml:AttributeValue xmlns:xs="{ValueXmlnsXs}" xmlns:xsi="{ValueXmlnsXsi}" xsi:type="{ValueXsiType}">{Value}</saml:AttributeValue></saml:Attribute>',
171
+ };
172
+
173
+ /**
174
+ * @desc Default login response template
175
+ * @type {LoginResponseTemplate}
176
+ */
177
+ const defaultLoginResponseTemplate = {
178
+ context: '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AuthnStatement}{AttributeStatement}</saml:Assertion></samlp:Response>',
179
+ attributes: [],
180
+ additionalTemplates: {
181
+ 'attributeStatementTemplate': defaultAttributeStatementTemplate,
182
+ 'attributeTemplate': defaultAttributeTemplate
183
+ }
184
+ };
185
+ /**
186
+ * @desc Default logout response template
187
+ * @type {LogoutResponseTemplate}
188
+ */
189
+ const defaultLogoutResponseTemplate = {
190
+ context: '<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status></samlp:LogoutResponse>',
191
+ };
192
+ /**
193
+ * @private
194
+ * @desc Get the signing scheme alias by signature algorithms, used by the node-rsa module
195
+ * @param {string} sigAlg signature algorithm
196
+ * @return {string/null} signing algorithm short-hand for the module node-rsa
197
+ */
198
+ function getSigningScheme(sigAlg?: string): SigningSchemeHash {
199
+ if (sigAlg) {
200
+ const algAlias = nrsaAliasMapping[sigAlg];
201
+ if (!(algAlias === undefined)) {
202
+ return algAlias;
203
+ }
204
+ }
205
+ return nrsaAliasMapping[signatureAlgorithms.RSA_SHA1];
206
+ }
207
+ /**
208
+ * @private
209
+ * @desc Get the digest algorithms by signature algorithms
210
+ * @param {string} sigAlg signature algorithm
211
+ * @return {string/undefined} digest algorithm
212
+ */
213
+ function getDigestMethod(sigAlg: string): string | undefined {
214
+ return digestAlgorithms[sigAlg];
215
+ }
216
+ /**
217
+ * @public
218
+ * @desc Create XPath
219
+ * @param {string/object} local parameters to create XPath
220
+ * @param {boolean} isExtractAll define whether returns whole content according to the XPath
221
+ * @return {string} xpath
222
+ */
223
+ function createXPath(local, isExtractAll?: boolean): string {
224
+ if (isString(local)) {
225
+ return isExtractAll === true ? "//*[local-name(.)='" + local + "']/text()" : "//*[local-name(.)='" + local + "']";
226
+ }
227
+ return "//*[local-name(.)='" + local.name + "']/@" + local.attr;
228
+ }
229
+
230
+ /**
231
+ * @private
232
+ * @desc Tag normalization
233
+ * @param {string} prefix prefix of the tag
234
+ * @param {content} content normalize it to capitalized camel case
235
+ * @return {string}
236
+ */
237
+ function tagging(prefix: string, content: string): string {
238
+ const camelContent = camelCase(content, {locale: 'en-us'});
239
+ return prefix + camelContent.charAt(0).toUpperCase() + camelContent.slice(1);
240
+ }
241
+
242
+ function escapeTag(replacement: unknown): (...args: string[]) => string {
243
+ return (_match: string, quote?: string) => {
244
+ const text: string = (replacement === null || replacement === undefined) ? '' : String(replacement);
245
+
246
+ // not having a quote means this interpolation isn't for an attribute, and so does not need escaping
247
+ return quote ? `${quote}${xmlEscape(text)}` : text;
248
+ }
249
+ }
250
+
251
+ return {
252
+
253
+ createXPath,
254
+ getQueryParamByType,
255
+ defaultLoginRequestTemplate,
256
+ defaultLoginResponseTemplate,
257
+ defaultAttributeStatementTemplate,
258
+ defaultAttributeTemplate,
259
+ defaultLogoutRequestTemplate,
260
+ defaultLogoutResponseTemplate,
261
+
262
+ /**
263
+ * @desc Replace the tag (e.g. {tag}) inside the raw XML
264
+ * @param {string} rawXML raw XML string used to do keyword replacement
265
+ * @param {array} tagValues tag values
266
+ * @return {string}
267
+ */
268
+ replaceTagsByValue(rawXML: string, tagValues: Record<string, unknown>): string {
269
+ Object.keys(tagValues).forEach(t => {
270
+ rawXML = rawXML.replace(
271
+ new RegExp(`("?)\\{${t}\\}`, 'g'),
272
+ escapeTag(tagValues[t])
273
+ );
274
+ });
275
+ return rawXML;
276
+ },
277
+ /**
278
+ * @desc Helper function to build the AttributeStatement tag
279
+ * @param {LoginResponseAttribute} attributes an array of attribute configuration
280
+ * @param {AttributeTemplate} attributeTemplate the attribute tag template to be used
281
+ * @param {AttributeStatementTemplate} attributeStatementTemplate the attributeStatement tag template to be used
282
+ * @return {string}
283
+ */
284
+ attributeStatementBuilder(
285
+ attributes: LoginResponseAttribute[],
286
+ attributeTemplate: AttributeTemplate = defaultAttributeTemplate,
287
+ attributeStatementTemplate: AttributeStatementTemplate = defaultAttributeStatementTemplate
288
+ ): string {
289
+ const attr = attributes.map(({name, nameFormat, valueTag, valueXsiType,type, valueXmlnsXs, valueXmlnsXsi }) => {
290
+ const defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema';
291
+ const defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance';
292
+ let attributeLine = attributeTemplate.context;
293
+ if (attributeLine && typeof attributeLine === 'function') {
294
+ // 安全调用
295
+ // @ts-ignore
296
+ return attributeLine({ name, nameFormat, valueTag, valueXsiType,type, valueXmlnsXs: valueXmlnsXs ?? defaultValueXmlnsXs, valueXmlnsXsi :valueXmlnsXsi ?? defaultValueXmlnsXsi })
297
+ }else{
298
+ attributeLine = attributeLine.replace('{Name}', name);
299
+ attributeLine = attributeLine.replace('{NameFormat}', nameFormat);
300
+ attributeLine = attributeLine.replace('{ValueXmlnsXs}', valueXmlnsXs ? valueXmlnsXs : defaultValueXmlnsXs);
301
+ attributeLine = attributeLine.replace('{ValueXmlnsXsi}', valueXmlnsXsi ? valueXmlnsXsi : defaultValueXmlnsXsi);
302
+ attributeLine = attributeLine.replace('{ValueXsiType}', valueXsiType);
303
+ attributeLine = attributeLine.replace('{Value}', `{${tagging('attr', valueTag)}}`);
304
+ return attributeLine;
305
+ }
306
+
307
+ }).join('');
308
+ return attributeStatementTemplate.context.replace('{Attributes}', attr);
309
+ },
310
+
311
+ /**
312
+ * @desc Construct the XML signature for POST binding
313
+ * @param {string} rawSamlMessage request/response xml string
314
+ * @param {string} referenceTagXPath reference uri
315
+ * @param {string} privateKey declares the private key
316
+ * @param {string} passphrase passphrase of the private key [optional]
317
+ * @param {string|buffer} signingCert signing certificate
318
+ * @param {string} signatureAlgorithm signature algorithm
319
+ * @param {string[]} transformationAlgorithms canonicalization and transformation Algorithms
320
+ * @return {string} base64 encoded string
321
+ */
322
+ constructSAMLSignature(opts: SignatureConstructor) {
323
+ const {
324
+ rawSamlMessage,
325
+ referenceTagXPath,
326
+ privateKey,
327
+ privateKeyPass,
328
+ signatureAlgorithm = signatureAlgorithms.RSA_SHA512,
329
+ transformationAlgorithms = [
330
+ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
331
+ 'http://www.w3.org/2001/10/xml-exc-c14n#',
332
+ ],
333
+ signingCert,
334
+ signatureConfig,
335
+ isBase64Output = true,
336
+ isMessageSigned = false,
337
+ } = opts;
338
+ const sig = new SignedXml();
339
+ // Add assertion sections as reference
340
+ const digestAlgorithm = getDigestMethod(signatureAlgorithm);
341
+ if (referenceTagXPath) {
342
+ sig.addReference({
343
+ xpath: referenceTagXPath,
344
+ transforms: transformationAlgorithms,
345
+ digestAlgorithm: digestAlgorithm
346
+ });
347
+ }
348
+ if (isMessageSigned) {
349
+ sig.addReference({
350
+ // reference to the root node
351
+ xpath: '/*',
352
+ transforms: transformationAlgorithms,
353
+ digestAlgorithm
354
+ });
355
+ }
356
+ sig.signatureAlgorithm = signatureAlgorithm;
357
+ sig.publicCert = this.getKeyInfo(signingCert, signatureConfig).getKey();
358
+ sig.getKeyInfoContent = this.getKeyInfo(signingCert, signatureConfig).getKeyInfo;
359
+ sig.privateKey = utility.readPrivateKey(privateKey, privateKeyPass, true);
360
+ sig.canonicalizationAlgorithm = 'http://www.w3.org/2001/10/xml-exc-c14n#';
361
+
362
+ if (signatureConfig) {
363
+ sig.computeSignature(rawSamlMessage, signatureConfig);
364
+ } else {
365
+ sig.computeSignature(rawSamlMessage);
366
+ }
367
+ return isBase64Output !== false ? utility.base64Encode(sig.getSignedXml()) : sig.getSignedXml();
368
+ },
369
+ /**
370
+ * @desc Verify the XML signature
371
+ * @param {string} xml xml
372
+ * @param {SignatureVerifierOptions} opts cert declares the X509 certificate
373
+ * @return {[boolean, string | null]} - A tuple where:
374
+ * - The first element is `true` if the signature is valid, `false` otherwise.
375
+ * - The second element is the cryptographically authenticated assertion node as a string, or `null` if not found.
376
+ */
377
+ verifySignature(xml: string, opts: SignatureVerifierOptions) {
378
+ const { dom } = getContext();
379
+ const doc = dom.parseFromString(xml);
380
+
381
+ const docParser = new DOMParser();
382
+ // In order to avoid the wrapping attack, we have changed to use absolute xpath instead of naively fetching the signature element
383
+ // message signature (logout response / saml response)
384
+ const messageSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Signature']";
385
+ // assertion signature (logout response / saml response)
386
+ const assertionSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']/*[local-name(.)='Signature']";
387
+ // check if there is a potential malicious wrapping signature
388
+ const wrappingElementsXPath = "/*[contains(local-name(), 'Response')]/*[local-name(.)='Assertion']/*[local-name(.)='Subject']/*[local-name(.)='SubjectConfirmation']/*[local-name(.)='SubjectConfirmationData']//*[local-name(.)='Assertion' or local-name(.)='Signature']";
389
+
390
+ // select the signature node
391
+ let selection: any = [];
392
+ const messageSignatureNode = select(messageSignatureXpath, doc);
393
+ const assertionSignatureNode = select(assertionSignatureXpath, doc);
394
+ const wrappingElementNode = select(wrappingElementsXPath, doc);
395
+
396
+ selection = selection.concat(messageSignatureNode);
397
+ selection = selection.concat(assertionSignatureNode);
398
+
399
+ // try to catch potential wrapping attack
400
+ if (wrappingElementNode.length !== 0) {
401
+ throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
402
+ }
403
+
404
+ // guarantee to have a signature in saml response
405
+ if (selection.length === 0) {
406
+ throw new Error('ERR_ZERO_SIGNATURE');
407
+ }
408
+
409
+
410
+ // need to refactor later on
411
+ for (const signatureNode of selection){
412
+ const sig = new SignedXml();
413
+ let verified = false;
414
+
415
+ sig.signatureAlgorithm = opts.signatureAlgorithm!;
416
+
417
+ if (!opts.keyFile && !opts.metadata) {
418
+ throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
419
+ }
420
+
421
+ if (opts.keyFile) {
422
+ sig.publicCert = fs.readFileSync(opts.keyFile)
423
+ }
424
+
425
+ if (opts.metadata) {
426
+ const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode) as any;
427
+ // certificate in metadata
428
+ let metadataCert: any = opts.metadata.getX509Certificate(certUse.signing);
429
+ // flattens the nested array of Certificates from each KeyDescriptor
430
+ if (Array.isArray(metadataCert)) {
431
+ metadataCert = flattenDeep(metadataCert);
432
+ } else if (typeof metadataCert === 'string') {
433
+ metadataCert = [metadataCert];
434
+ }
435
+ // normalise the certificate string
436
+ metadataCert = metadataCert.map(utility.normalizeCerString);
437
+
438
+ // no certificate in node response nor metadata
439
+ if (certificateNode.length === 0 && metadataCert.length === 0) {
440
+ throw new Error('NO_SELECTED_CERTIFICATE');
441
+ }
442
+
443
+ // certificate node in response
444
+ if (certificateNode.length !== 0) {
445
+ const x509CertificateData = certificateNode[0].firstChild.data;
446
+ const x509Certificate = utility.normalizeCerString(x509CertificateData);
447
+ if (
448
+ metadataCert.length >= 1 &&
449
+ !metadataCert.find(cert => cert.trim() === x509Certificate.trim())
450
+ ) {
451
+ // keep this restriction for rolling certificate usage
452
+ // to make sure the response certificate is one of those specified in metadata
453
+ throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
454
+ }
455
+
456
+ sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
457
+
458
+ } else {
459
+ // Select first one from metadata
460
+ sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
461
+
462
+ }
463
+ }
464
+
465
+ sig.loadSignature(signatureNode);
466
+
467
+ doc.removeChild(signatureNode);
468
+
469
+ verified = sig.checkSignature(doc.toString());
470
+
471
+ // immediately throw error when any one of the signature is failed to get verified
472
+ if (!verified) {
473
+ throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
474
+ }
475
+
476
+ // attempt is made to get the signed Reference as a string();
477
+ // note, we don't have access to the actual signedReferences API unfortunately
478
+ // mainly a sanity check here for SAML. (Although ours would still be secure, if multiple references are used)
479
+ if (!(sig.getReferences().length >= 1)) {
480
+ throw new Error('NO_SIGNATURE_REFERENCES')
481
+ }
482
+ const signedVerifiedXML = sig.getSignedReferences()[0];
483
+ const rootNode = docParser.parseFromString(signedVerifiedXML, 'text/xml').documentElement;
484
+ // process the verified signature:
485
+ // case 1, rootSignedDoc is a response:
486
+ if (rootNode.localName === 'Response') {
487
+
488
+ // try getting the Xml from the first assertion
489
+ const EncryptedAssertions = select(
490
+ "./*[local-name()='EncryptedAssertion']",
491
+ rootNode
492
+ );
493
+ const assertions = select(
494
+ "./*[local-name()='Assertion']",
495
+ rootNode
496
+ );
497
+
498
+ // now we can process the assertion as an assertion
499
+ if (EncryptedAssertions.length === 1) {
500
+
501
+ return [true, EncryptedAssertions[0].toString()];
502
+ }
503
+
504
+ if (assertions.length === 1) {
505
+
506
+ return [true, assertions[0].toString()];
507
+ }
508
+
509
+ } else if (rootNode.localName === 'Assertion') {
510
+ return [true, rootNode.toString()];
511
+ } else {
512
+ return [true, null]; // signature is valid. But there is no assertion node here. It could be metadata node, hence return null
513
+ }
514
+ };
515
+
516
+ // something has gone seriously wrong if we are still here
517
+ throw new Error('ERR_ZERO_SIGNATURE');
518
+
519
+ // response must be signed, either entire document or assertion
520
+ // default we will take the assertion section under root
521
+ /* if (messageSignatureNode.length === 1) {
522
+ const node = select("/!*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/!*[local-name(.)='Assertion']", doc);
523
+ if (node.length === 1) {
524
+ assertionNode = node[0].toString();
525
+ }
526
+ }
527
+
528
+ if (assertionSignatureNode.length === 1) {
529
+ const verifiedAssertionInfo = extract(assertionSignatureNode[0].toString(), [{
530
+ key: 'refURI',
531
+ localPath: ['Signature', 'SignedInfo', 'Reference'],
532
+ attributes: ['URI']
533
+ }]);
534
+ // get the assertion supposed to be the one should be verified
535
+ const desiredAssertionInfo = extract(doc.toString(), [{
536
+ key: 'id',
537
+ localPath: ['~Response', 'Assertion'],
538
+ attributes: ['ID']
539
+ }]);
540
+ // 5.4.2 References
541
+ // SAML assertions and protocol messages MUST supply a value for the ID attribute on the root element of
542
+ // the assertion or protocol message being signed. The assertion’s or protocol message's root element may
543
+ // or may not be the root element of the actual XML document containing the signed assertion or protocol
544
+ // message (e.g., it might be contained within a SOAP envelope).
545
+ // Signatures MUST contain a single <ds:Reference> containing a same-document reference to the ID
546
+ // attribute value of the root element of the assertion or protocol message being signed. For example, if the
547
+ // ID attribute value is "foo", then the URI attribute in the <ds:Reference> element MUST be "#foo".
548
+ if (verifiedAssertionInfo.refURI !== `#${desiredAssertionInfo.id}`) {
549
+ throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
550
+ }
551
+ const verifiedDoc = extract(doc.toString(), [{
552
+ key: 'assertion',
553
+ localPath: ['~Response', 'Assertion'],
554
+ attributes: [],
555
+ context: true
556
+ }]);
557
+ assertionNode = verifiedDoc.assertion.toString();
558
+ }
559
+
560
+ return [verified, assertionNode];*/
561
+ },
562
+ /**
563
+ * @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
564
+ * @param {string} use type of certificate (e.g. signing, encrypt)
565
+ * @param {string} certString declares the certificate String
566
+ * @return {object} object used in xml module
567
+ */
568
+ createKeySection(use: KeyUse, certString: string | Buffer): KeyComponent {
569
+ return {
570
+ ['KeyDescriptor']: [
571
+ {
572
+ _attr: { use },
573
+ },
574
+ {
575
+ ['ds:KeyInfo']: [
576
+ {
577
+ _attr: {
578
+ 'xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
579
+ },
580
+ },
581
+ {
582
+ ['ds:X509Data']: [{
583
+ 'ds:X509Certificate': utility.normalizeCerString(certString),
584
+ }],
585
+ },
586
+ ],
587
+ }],
588
+ };
589
+ },
590
+ /**
591
+ * @desc Constructs SAML message
592
+ * @param {string} octetString see "Bindings for the OASIS Security Assertion Markup Language (SAML V2.0)" P.17/46
593
+ * @param {string} key declares the pem-formatted private key
594
+ * @param {string} passphrase passphrase of private key [optional]
595
+ * @param {string} signingAlgorithm signing algorithm
596
+ * @return {string} message signature
597
+ */
598
+ constructMessageSignature(
599
+ octetString: string,
600
+ key: string,
601
+ passphrase?: string,
602
+ isBase64?: boolean,
603
+ signingAlgorithm?: string
604
+ ) {
605
+ // Default returning base64 encoded signature
606
+ // Embed with node-rsa module
607
+ const decryptedKey = new nrsa(
608
+ utility.readPrivateKey(key, passphrase),
609
+ undefined,
610
+ {
611
+ signingScheme: getSigningScheme(signingAlgorithm),
612
+ }
613
+ );
614
+ const signature = decryptedKey.sign(octetString);
615
+ // Use private key to sign data
616
+ return isBase64 !== false ? signature.toString('base64') : signature;
617
+ },
618
+ /**
619
+ * @desc Verifies message signature
620
+ * @param {Metadata} metadata metadata object of identity provider or service provider
621
+ * @param {string} octetString see "Bindings for the OASIS Security Assertion Markup Language (SAML V2.0)" P.17/46
622
+ * @param {string} signature context of XML signature
623
+ * @param {string} verifyAlgorithm algorithm used to verify
624
+ * @return {boolean} verification result
625
+ */
626
+ verifyMessageSignature(
627
+ metadata,
628
+ octetString: string,
629
+ signature: string | Buffer,
630
+ verifyAlgorithm?: string
631
+ ) {
632
+ const signCert = metadata.getX509Certificate(certUse.signing);
633
+ const signingScheme = getSigningScheme(verifyAlgorithm);
634
+ const key = new nrsa(utility.getPublicKeyPemFromCertificate(signCert), 'public', { signingScheme });
635
+ return key.verify(Buffer.from(octetString), Buffer.from(signature));
636
+ },
637
+ /**
638
+ * @desc Get the public key in string format
639
+ * @param {string} x509Certificate certificate
640
+ * @return {string} public key
641
+ */
642
+ getKeyInfo(x509Certificate: string, signatureConfig: any = {}) {
643
+ const prefix = signatureConfig.prefix ? `${signatureConfig.prefix}:` : '';
644
+ return {
645
+ getKeyInfo: () => {
646
+ return `<${prefix}X509Data><${prefix}X509Certificate>${x509Certificate}</${prefix}X509Certificate></${prefix}X509Data>`;
647
+ },
648
+ getKey: () => {
649
+ return utility.getPublicKeyPemFromCertificate(x509Certificate).toString();
650
+ },
651
+ };
652
+ },
653
+ /**
654
+ * @desc Encrypt the assertion section in Response
655
+ * @param {Entity} sourceEntity source entity
656
+ * @param {Entity} targetEntity target entity
657
+ * @param {string} xml response in xml string format
658
+ * @return {Promise} a promise to resolve the finalized xml
659
+ */
660
+ encryptAssertion(sourceEntity, targetEntity, xml?: string) {
661
+ // Implement encryption after signature if it has
662
+ return new Promise<string>((resolve, reject) => {
663
+
664
+ if (!xml) {
665
+ return reject(new Error('ERR_UNDEFINED_ASSERTION'));
666
+ }
667
+
668
+ const sourceEntitySetting = sourceEntity.entitySetting;
669
+ const targetEntityMetadata = targetEntity.entityMeta;
670
+ const { dom } = getContext();
671
+ const doc = dom.parseFromString(xml);
672
+ const assertions = select("//*[local-name(.)='Assertion']", doc) as Node[];
673
+ if (!Array.isArray(assertions) || assertions.length === 0) {
674
+ throw new Error('ERR_NO_ASSERTION');
675
+ }
676
+ if (assertions.length > 1) {
677
+ throw new Error('ERR_MULTIPLE_ASSERTION');
678
+ }
679
+ const rawAssertionNode = assertions[0];
680
+
681
+ // Perform encryption depends on the setting, default is false
682
+ if (sourceEntitySetting.isAssertionEncrypted) {
683
+
684
+ const publicKeyPem = utility.getPublicKeyPemFromCertificate(targetEntityMetadata.getX509Certificate(certUse.encrypt));
685
+
686
+ xmlenc.encrypt(rawAssertionNode.toString(), {
687
+ // use xml-encryption module
688
+ rsa_pub: Buffer.from(publicKeyPem), // public key from certificate
689
+ pem: Buffer.from(`-----BEGIN CERTIFICATE-----${targetEntityMetadata.getX509Certificate(certUse.encrypt)}-----END CERTIFICATE-----`),
690
+ encryptionAlgorithm: sourceEntitySetting.dataEncryptionAlgorithm,
691
+ keyEncryptionAlgorithm: sourceEntitySetting.keyEncryptionAlgorithm,
692
+ keyEncryptionDigest: 'SHA-512',
693
+ disallowEncryptionWithInsecureAlgorithm: true,
694
+ warnInsecureAlgorithm: true
695
+ }, (err, res) => {
696
+ if (err) {
697
+ console.error(err);
698
+ return reject(new Error('ERR_EXCEPTION_OF_ASSERTION_ENCRYPTION'));
699
+ }
700
+ if (!res) {
701
+ return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
702
+ }
703
+ const { encryptedAssertion: encAssertionPrefix } = sourceEntitySetting.tagPrefix;
704
+ const encryptAssertionDoc = dom.parseFromString(`<${encAssertionPrefix}:EncryptedAssertion xmlns:${encAssertionPrefix}="${namespace.names.assertion}">${res}</${encAssertionPrefix}:EncryptedAssertion>`);
705
+ doc.documentElement.replaceChild(encryptAssertionDoc.documentElement, rawAssertionNode);
706
+ return resolve(utility.base64Encode(doc.toString()));
707
+ });
708
+ } else {
709
+ return resolve(utility.base64Encode(xml)); // No need to do encryption
710
+ }
711
+ });
712
+ },
713
+ /**
714
+ * @desc Decrypt the assertion section in Response
715
+ * @param {string} type only accept SAMLResponse to proceed decryption
716
+ * @param {Entity} here this entity
717
+ * @param {Entity} from from the entity where the message is sent
718
+ * @param {string} entireXML response in xml string format
719
+ * @return {function} a promise to get back the entire xml with decrypted assertion
720
+ */
721
+ decryptAssertion(here, entireXML: string) {
722
+ return new Promise<[string, any]>((resolve, reject) => {
723
+ // Implement decryption first then check the signature
724
+ if (!entireXML) {
725
+ return reject(new Error('ERR_UNDEFINED_ASSERTION'));
726
+ }
727
+ // Perform encryption depends on the setting of where the message is sent, default is false
728
+ const hereSetting = here.entitySetting;
729
+ const { dom } = getContext();
730
+ const doc = dom.parseFromString(entireXML);
731
+ const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc) as Node[];
732
+ if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
733
+ throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
734
+ }
735
+ if (encryptedAssertions.length > 1) {
736
+ throw new Error('ERR_MULTIPLE_ASSERTION');
737
+ }
738
+ const encAssertionNode = encryptedAssertions[0];
739
+ return xmlenc.decrypt(encAssertionNode.toString(), {
740
+ key: utility.readPrivateKey(hereSetting.encPrivateKey, hereSetting.encPrivateKeyPass),
741
+ }, (err, res) => {
742
+ if (err) {
743
+ console.error(err);
744
+ return reject(new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION'));
745
+ }
746
+ if (!res) {
747
+ return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
748
+ }
749
+ const rawAssertionDoc = dom.parseFromString(res);
750
+ doc.documentElement.replaceChild(rawAssertionDoc.documentElement, encAssertionNode);
751
+ return resolve([doc.toString(), res]);
752
+ });
753
+ });
754
+ },
755
+ /**
756
+ * @desc Check if the xml string is valid and bounded
757
+ */
758
+ async isValidXml(input: string) {
759
+
760
+ // check if global api contains the validate function
761
+ const { validate } = getContext();
762
+
763
+ /**
764
+ * user can write a validate function that always returns
765
+ * a resolved promise and skip the validator even in
766
+ * production, user will take the responsibility if
767
+ * they intend to skip the validation
768
+ */
769
+ if (!validate) {
770
+
771
+ // otherwise, an error will be thrown
772
+ return Promise.reject('Your application is potentially vulnerable because no validation function found. Please read the documentation on how to setup the validator. (https://github.com/tngan/samlify#installation)');
773
+
774
+ }
775
+
776
+ try {
777
+ return await validate(input);
778
+ } catch (e) {
779
+ throw e;
780
+ }
781
+
782
+ },
783
+ };
784
+ };
785
+
786
+ export default libSaml();