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.
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/flow.ts ADDED
@@ -0,0 +1,467 @@
1
+ import { inflateString, base64Decode } from './utility.js';
2
+ import { verifyTime } from './validator.js';
3
+ import libsaml from './libsaml.js';
4
+ import {
5
+ extract,
6
+ loginRequestFields,
7
+ loginResponseFields,
8
+ logoutRequestFields,
9
+ logoutResponseFields,
10
+ ExtractorFields,
11
+ logoutResponseStatusFields,
12
+ loginResponseStatusFields
13
+ } from './extractor.js';
14
+
15
+ import {
16
+ BindingNamespace,
17
+ ParserType,
18
+ wording,
19
+ MessageSignatureOrder,
20
+ StatusCode
21
+ } from './urn.js';
22
+
23
+ const bindDict = wording.binding;
24
+ const urlParams = wording.urlParams;
25
+
26
+ export interface FlowResult {
27
+ samlContent: string;
28
+ extract: any;
29
+ sigAlg?: string|null ;
30
+ }
31
+
32
+ // get the default extractor fields based on the parserType
33
+ function getDefaultExtractorFields(parserType: ParserType, assertion?: any): ExtractorFields {
34
+ switch (parserType) {
35
+ case ParserType.SAMLRequest:
36
+ return loginRequestFields;
37
+ case ParserType.SAMLResponse:
38
+ if (!assertion) {
39
+ // unexpected hit
40
+ throw new Error('ERR_EMPTY_ASSERTION');
41
+ }
42
+ return loginResponseFields(assertion);
43
+ case ParserType.LogoutRequest:
44
+ return logoutRequestFields;
45
+ case ParserType.LogoutResponse:
46
+ return logoutResponseFields;
47
+ default:
48
+ throw new Error('ERR_UNDEFINED_PARSERTYPE');
49
+ }
50
+ }
51
+
52
+ // proceed the redirect binding flow
53
+ async function redirectFlow(options): Promise<FlowResult> {
54
+
55
+ const { request, parserType, self, checkSignature = true, from } = options;
56
+ const { query, octetString } = request;
57
+ const { SigAlg: sigAlg, Signature: signature } = query;
58
+
59
+ const targetEntityMetadata = from.entityMeta;
60
+
61
+ // ?SAMLRequest= or ?SAMLResponse=
62
+ const direction = libsaml.getQueryParamByType(parserType);
63
+ const content = query[direction];
64
+
65
+ // query must contain the saml content
66
+ if (content === undefined) {
67
+ return Promise.reject('ERR_REDIRECT_FLOW_BAD_ARGS');
68
+ }
69
+
70
+ const xmlString = inflateString(decodeURIComponent(content));
71
+
72
+ // validate the xml
73
+ try {
74
+ await libsaml.isValidXml(xmlString);
75
+ } catch (e) {
76
+ return Promise.reject('ERR_INVALID_XML');
77
+ }
78
+
79
+ // check status based on different scenarios
80
+ await checkStatus(xmlString, parserType);
81
+
82
+ let assertion: string = '';
83
+
84
+ if (parserType === urlParams.samlResponse){
85
+ // Extract assertion shortcut
86
+ const verifiedDoc = extract(xmlString, [{
87
+ key: 'assertion',
88
+ localPath: ['~Response', 'Assertion'],
89
+ attributes: [],
90
+ context: true
91
+ }]);
92
+ if (verifiedDoc && verifiedDoc.assertion){
93
+ assertion = verifiedDoc.assertion as string;
94
+ }
95
+ }
96
+
97
+ const extractorFields = getDefaultExtractorFields(parserType, assertion.length > 0 ? assertion : null);
98
+
99
+ const parseResult: { samlContent: string, extract: any, sigAlg: (string | null) } = {
100
+ samlContent: xmlString,
101
+ sigAlg: null,
102
+ extract: extract(xmlString, extractorFields),
103
+ };
104
+
105
+ // see if signature check is required
106
+ // only verify message signature is enough
107
+ if (checkSignature) {
108
+ if (!signature || !sigAlg) {
109
+ return Promise.reject('ERR_MISSING_SIG_ALG');
110
+ }
111
+
112
+ // put the below two assignments into verifyMessageSignature function
113
+ const base64Signature = Buffer.from(decodeURIComponent(signature), 'base64');
114
+ const decodeSigAlg = decodeURIComponent(sigAlg);
115
+
116
+ const verified = libsaml.verifyMessageSignature(targetEntityMetadata, octetString, base64Signature, sigAlg);
117
+
118
+ if (!verified) {
119
+ // Fail to verify message signature
120
+ return Promise.reject('ERR_FAILED_MESSAGE_SIGNATURE_VERIFICATION');
121
+ }
122
+
123
+ parseResult.sigAlg = decodeSigAlg;
124
+ }
125
+
126
+ /**
127
+ * Validation part: validate the context of response after signature is verified and decrypted (optional)
128
+ */
129
+ const issuer = targetEntityMetadata.getEntityID();
130
+ const extractedProperties = parseResult.extract;
131
+
132
+ // unmatched issuer
133
+ if (
134
+ (parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
135
+ && extractedProperties
136
+ && extractedProperties.issuer !== issuer
137
+ ) {
138
+ return Promise.reject('ERR_UNMATCH_ISSUER');
139
+ }
140
+
141
+ // invalid session time
142
+ // only run the verifyTime when `SessionNotOnOrAfter` exists
143
+ if (
144
+ parserType === 'SAMLResponse'
145
+ && extractedProperties.sessionIndex.sessionNotOnOrAfter
146
+ && !verifyTime(
147
+ undefined,
148
+ extractedProperties.sessionIndex.sessionNotOnOrAfter,
149
+ self.entitySetting.clockDrifts
150
+ )
151
+ ) {
152
+ return Promise.reject('ERR_EXPIRED_SESSION');
153
+ }
154
+
155
+ // invalid time
156
+ // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
157
+ if (
158
+ parserType === 'SAMLResponse'
159
+ && extractedProperties.conditions
160
+ && !verifyTime(
161
+ extractedProperties.conditions.notBefore,
162
+ extractedProperties.conditions.notOnOrAfter,
163
+ self.entitySetting.clockDrifts
164
+ )
165
+ ) {
166
+ return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
167
+ }
168
+
169
+ return Promise.resolve(parseResult);
170
+ }
171
+
172
+ // proceed the post flow
173
+ async function postFlow(options): Promise<FlowResult> {
174
+
175
+ const {
176
+ request,
177
+ from,
178
+ self,
179
+ parserType,
180
+ checkSignature = true
181
+ } = options;
182
+
183
+ const { body } = request;
184
+
185
+ const direction = libsaml.getQueryParamByType(parserType);
186
+ const encodedRequest = body[direction];
187
+
188
+ let samlContent = String(base64Decode(encodedRequest));
189
+
190
+ const verificationOptions = {
191
+ metadata: from.entityMeta,
192
+ signatureAlgorithm: from.entitySetting.requestSignatureAlgorithm,
193
+ };
194
+
195
+ const decryptRequired = from.entitySetting.isAssertionEncrypted;
196
+ let extractorFields: ExtractorFields = [];
197
+
198
+ // validate the xml first
199
+ await libsaml.isValidXml(samlContent);
200
+
201
+ if (parserType !== urlParams.samlResponse) {
202
+ extractorFields = getDefaultExtractorFields(parserType, null);
203
+ }
204
+
205
+ // check status based on different scenarios
206
+ await checkStatus(samlContent, parserType);
207
+
208
+ // verify the signatures (the response is encrypted then signed, then verify first then decrypt)
209
+ if (
210
+ checkSignature &&
211
+ from.entitySetting.messageSigningOrder === MessageSignatureOrder.ETS
212
+ ) {
213
+ const [verified, verifiedAssertionNode] = libsaml.verifySignature(samlContent, verificationOptions);
214
+ if (!verified) {
215
+ return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
216
+ }
217
+ if (!decryptRequired) {
218
+ extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
219
+ }
220
+ }
221
+
222
+ if (parserType === 'SAMLResponse' && decryptRequired) {
223
+ const result = await libsaml.decryptAssertion(self, samlContent);
224
+ samlContent = result[0];
225
+ extractorFields = getDefaultExtractorFields(parserType, result[1]);
226
+ }
227
+
228
+ // verify the signatures (the response is signed then encrypted, then decrypt first then verify)
229
+ if (
230
+ checkSignature &&
231
+ from.entitySetting.messageSigningOrder === MessageSignatureOrder.STE
232
+ ) {
233
+ const [verified, verifiedAssertionNode] = libsaml.verifySignature(samlContent, verificationOptions);
234
+ if (verified) {
235
+ extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
236
+ } else {
237
+ return Promise.reject('ERR_FAIL_TO_VERIFY_STE_SIGNATURE');
238
+ }
239
+ }
240
+
241
+ const parseResult = {
242
+ samlContent: samlContent,
243
+ extract: extract(samlContent, extractorFields),
244
+ };
245
+
246
+ /**
247
+ * Validation part: validate the context of response after signature is verified and decrypted (optional)
248
+ */
249
+ const targetEntityMetadata = from.entityMeta;
250
+ const issuer = targetEntityMetadata.getEntityID();
251
+ const extractedProperties = parseResult.extract;
252
+
253
+ // unmatched issuer
254
+ if (
255
+ (parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
256
+ && extractedProperties
257
+ && extractedProperties.issuer !== issuer
258
+ ) {
259
+ return Promise.reject('ERR_UNMATCH_ISSUER');
260
+ }
261
+
262
+ // invalid session time
263
+ // only run the verifyTime when `SessionNotOnOrAfter` exists
264
+ if (
265
+ parserType === 'SAMLResponse'
266
+ && extractedProperties.sessionIndex.sessionNotOnOrAfter
267
+ && !verifyTime(
268
+ undefined,
269
+ extractedProperties.sessionIndex.sessionNotOnOrAfter,
270
+ self.entitySetting.clockDrifts
271
+ )
272
+ ) {
273
+ return Promise.reject('ERR_EXPIRED_SESSION');
274
+ }
275
+
276
+ // invalid time
277
+ // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
278
+ if (
279
+ parserType === 'SAMLResponse'
280
+ && extractedProperties.conditions
281
+ && !verifyTime(
282
+ extractedProperties.conditions.notBefore,
283
+ extractedProperties.conditions.notOnOrAfter,
284
+ self.entitySetting.clockDrifts
285
+ )
286
+ ) {
287
+ return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
288
+ }
289
+
290
+ return Promise.resolve(parseResult);
291
+ }
292
+
293
+
294
+ // proceed the post simple sign binding flow
295
+ async function postSimpleSignFlow(options): Promise<FlowResult> {
296
+
297
+ const { request, parserType, self, checkSignature = true, from } = options;
298
+
299
+ const { body, octetString } = request;
300
+
301
+ const targetEntityMetadata = from.entityMeta;
302
+
303
+ // ?SAMLRequest= or ?SAMLResponse=
304
+ const direction = libsaml.getQueryParamByType(parserType);
305
+ const encodedRequest: string = body[direction];
306
+ const sigAlg: string = body['SigAlg'];
307
+ const signature: string = body['Signature'];
308
+
309
+ // query must contain the saml content
310
+ if (encodedRequest === undefined) {
311
+ return Promise.reject('ERR_SIMPLESIGN_FLOW_BAD_ARGS');
312
+ }
313
+
314
+ const xmlString = String(base64Decode(encodedRequest));
315
+
316
+ // validate the xml
317
+ try {
318
+ await libsaml.isValidXml(xmlString);
319
+ } catch (e) {
320
+ return Promise.reject('ERR_INVALID_XML');
321
+ }
322
+
323
+ // check status based on different scenarios
324
+ await checkStatus(xmlString, parserType);
325
+
326
+ let assertion: string = '';
327
+
328
+ if (parserType === urlParams.samlResponse){
329
+ // Extract assertion shortcut
330
+ const verifiedDoc = extract(xmlString, [{
331
+ key: 'assertion',
332
+ localPath: ['~Response', 'Assertion'],
333
+ attributes: [],
334
+ context: true
335
+ }]);
336
+ if (verifiedDoc && verifiedDoc.assertion){
337
+ assertion = verifiedDoc.assertion as string;
338
+ }
339
+ }
340
+
341
+ const extractorFields = getDefaultExtractorFields(parserType, assertion.length > 0 ? assertion : null);
342
+
343
+ const parseResult: { samlContent: string, extract: any, sigAlg: (string | null) } = {
344
+ samlContent: xmlString,
345
+ sigAlg: null,
346
+ extract: extract(xmlString, extractorFields),
347
+ };
348
+
349
+ // see if signature check is required
350
+ // only verify message signature is enough
351
+ if (checkSignature) {
352
+ if (!signature || !sigAlg) {
353
+ return Promise.reject('ERR_MISSING_SIG_ALG');
354
+ }
355
+
356
+ // put the below two assignments into verifyMessageSignature function
357
+ const base64Signature = Buffer.from(signature, 'base64');
358
+
359
+ const verified = libsaml.verifyMessageSignature(targetEntityMetadata, octetString, base64Signature, sigAlg);
360
+
361
+ if (!verified) {
362
+ // Fail to verify message signature
363
+ return Promise.reject('ERR_FAILED_MESSAGE_SIGNATURE_VERIFICATION');
364
+ }
365
+
366
+ parseResult.sigAlg = sigAlg;
367
+ }
368
+
369
+ /**
370
+ * Validation part: validate the context of response after signature is verified and decrypted (optional)
371
+ */
372
+ const issuer = targetEntityMetadata.getEntityID();
373
+ const extractedProperties = parseResult.extract;
374
+
375
+ // unmatched issuer
376
+ if (
377
+ (parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
378
+ && extractedProperties
379
+ && extractedProperties.issuer !== issuer
380
+ ) {
381
+ return Promise.reject('ERR_UNMATCH_ISSUER');
382
+ }
383
+
384
+ // invalid session time
385
+ // only run the verifyTime when `SessionNotOnOrAfter` exists
386
+ if (
387
+ parserType === 'SAMLResponse'
388
+ && extractedProperties.sessionIndex.sessionNotOnOrAfter
389
+ && !verifyTime(
390
+ undefined,
391
+ extractedProperties.sessionIndex.sessionNotOnOrAfter,
392
+ self.entitySetting.clockDrifts
393
+ )
394
+ ) {
395
+ return Promise.reject('ERR_EXPIRED_SESSION');
396
+ }
397
+
398
+ // invalid time
399
+ // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
400
+ if (
401
+ parserType === 'SAMLResponse'
402
+ && extractedProperties.conditions
403
+ && !verifyTime(
404
+ extractedProperties.conditions.notBefore,
405
+ extractedProperties.conditions.notOnOrAfter,
406
+ self.entitySetting.clockDrifts
407
+ )
408
+ ) {
409
+ return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
410
+ }
411
+
412
+ return Promise.resolve(parseResult);
413
+ }
414
+
415
+
416
+ function checkStatus(content: string, parserType: string): Promise<string> {
417
+
418
+ // only check response parser
419
+ if (parserType !== urlParams.samlResponse && parserType !== urlParams.logoutResponse) {
420
+ return Promise.resolve('SKIPPED');
421
+ }
422
+
423
+ const fields = parserType === urlParams.samlResponse
424
+ ? loginResponseStatusFields
425
+ : logoutResponseStatusFields;
426
+
427
+ const {top, second} = extract(content, fields);
428
+
429
+ // only resolve when top-tier status code is success
430
+ if (top === StatusCode.Success) {
431
+ return Promise.resolve('OK');
432
+ }
433
+
434
+ if (!top) {
435
+ throw new Error('ERR_UNDEFINED_STATUS');
436
+ }
437
+
438
+ // returns a detailed error for two-tier error code
439
+ throw new Error(`ERR_FAILED_STATUS with top tier code: ${top}, second tier code: ${second}`);
440
+ }
441
+
442
+ export function flow(options): Promise<FlowResult> {
443
+
444
+ const binding = options.binding;
445
+ const parserType = options.parserType;
446
+
447
+ options.supportBindings = [BindingNamespace.Redirect, BindingNamespace.Post, BindingNamespace.SimpleSign];
448
+ // saml response allows POST, REDIRECT
449
+ if (parserType === ParserType.SAMLResponse) {
450
+ options.supportBindings = [BindingNamespace.Post, BindingNamespace.Redirect, BindingNamespace.SimpleSign];
451
+ }
452
+
453
+ if (binding === bindDict.post) {
454
+ return postFlow(options);
455
+ }
456
+
457
+ if (binding === bindDict.redirect) {
458
+ return redirectFlow(options);
459
+ }
460
+
461
+ if (binding === bindDict.simpleSign) {
462
+ return postSimpleSignFlow(options);
463
+ }
464
+
465
+ return Promise.reject('ERR_UNEXPECTED_FLOW');
466
+
467
+ }