samlesa 2.16.6 → 2.17.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.
Files changed (82) hide show
  1. package/README.md +30 -50
  2. package/build/index.js +2 -1
  3. package/build/src/binding-artifact.js +330 -146
  4. package/build/src/binding-post.js +45 -31
  5. package/build/src/binding-redirect.js +0 -10
  6. package/build/src/binding-simplesign.js +0 -1
  7. package/build/src/entity-idp.js +1 -5
  8. package/build/src/entity-sp.js +21 -96
  9. package/build/src/extractor.js +48 -4
  10. package/build/src/flow.js +24 -166
  11. package/build/src/libsaml.js +468 -264
  12. package/build/src/libsamlSoap.js +115 -0
  13. package/build/src/schema/xml.xsd +88 -88
  14. package/build/src/schemaValidator.js +5 -13
  15. package/build/src/soap.js +123 -3
  16. package/build/src/utility.js +12 -7
  17. package/package.json +77 -81
  18. package/types/api.d.ts +15 -0
  19. package/types/api.d.ts.map +1 -0
  20. package/types/binding-post.d.ts +48 -0
  21. package/types/binding-post.d.ts.map +1 -0
  22. package/types/binding-redirect.d.ts +54 -0
  23. package/types/binding-redirect.d.ts.map +1 -0
  24. package/types/binding-simplesign.d.ts +41 -0
  25. package/types/binding-simplesign.d.ts.map +1 -0
  26. package/types/entity-idp.d.ts +38 -0
  27. package/types/entity-idp.d.ts.map +1 -0
  28. package/types/entity-sp.d.ts +38 -0
  29. package/types/entity-sp.d.ts.map +1 -0
  30. package/types/entity.d.ts +100 -0
  31. package/types/entity.d.ts.map +1 -0
  32. package/types/extractor.d.ts +26 -0
  33. package/types/extractor.d.ts.map +1 -0
  34. package/types/flow.d.ts +7 -0
  35. package/types/flow.d.ts.map +1 -0
  36. package/types/index.d.ts +2 -1
  37. package/types/index.d.ts.map +1 -1
  38. package/types/libsaml.d.ts +208 -0
  39. package/types/libsaml.d.ts.map +1 -0
  40. package/types/metadata-idp.d.ts +25 -0
  41. package/types/metadata-idp.d.ts.map +1 -0
  42. package/types/metadata-sp.d.ts +37 -0
  43. package/types/metadata-sp.d.ts.map +1 -0
  44. package/types/metadata.d.ts +58 -0
  45. package/types/metadata.d.ts.map +1 -0
  46. package/types/src/api.d.ts +3 -3
  47. package/types/src/api.d.ts.map +1 -1
  48. package/types/src/binding-artifact.d.ts +24 -29
  49. package/types/src/binding-artifact.d.ts.map +1 -1
  50. package/types/src/binding-post.d.ts +22 -22
  51. package/types/src/binding-post.d.ts.map +1 -1
  52. package/types/src/binding-redirect.d.ts.map +1 -1
  53. package/types/src/binding-simplesign.d.ts.map +1 -1
  54. package/types/src/entity-idp.d.ts +3 -4
  55. package/types/src/entity-idp.d.ts.map +1 -1
  56. package/types/src/entity-sp.d.ts +13 -24
  57. package/types/src/entity-sp.d.ts.map +1 -1
  58. package/types/src/entity.d.ts.map +1 -1
  59. package/types/src/extractor.d.ts +22 -0
  60. package/types/src/extractor.d.ts.map +1 -1
  61. package/types/src/flow.d.ts +1 -0
  62. package/types/src/flow.d.ts.map +1 -1
  63. package/types/src/libsaml.d.ts +16 -7
  64. package/types/src/libsaml.d.ts.map +1 -1
  65. package/types/src/libsamlSoap.d.ts +7 -0
  66. package/types/src/libsamlSoap.d.ts.map +1 -0
  67. package/types/src/schemaValidator.d.ts +1 -1
  68. package/types/src/schemaValidator.d.ts.map +1 -1
  69. package/types/src/soap.d.ts +33 -0
  70. package/types/src/soap.d.ts.map +1 -1
  71. package/types/src/utility.d.ts.map +1 -1
  72. package/types/src/validator.d.ts.map +1 -1
  73. package/types/types.d.ts +128 -0
  74. package/types/types.d.ts.map +1 -0
  75. package/types/urn.d.ts +195 -0
  76. package/types/urn.d.ts.map +1 -0
  77. package/types/utility.d.ts +133 -0
  78. package/types/utility.d.ts.map +1 -0
  79. package/types/validator.d.ts +4 -0
  80. package/types/validator.d.ts.map +1 -0
  81. package/build/src/schema/XMLSchema.dtd +0 -402
  82. package/build/src/schema/datatypes.dtd +0 -203
package/README.md CHANGED
@@ -1,64 +1,44 @@
1
- # samlify · [![构建状态](https://img.shields.io/circleci/build/github/tngan/samlify?style=for-the-badge&logo=circleci)](https://app.circleci.com/pipelines/github/tngan/samlify) [![npm 版本](https://img.shields.io/npm/v/samlify.svg?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/samlify) [![下载量](https://img.shields.io/npm/dm/samlify.svg?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/samlify) [![覆盖率](https://img.shields.io/coveralls/tngan/samlify/master.svg?style=for-the-badge&logo=coveralls)](https://coveralls.io/github/tngan/samlify?branch=master)
2
-
3
- 高度可配置的 Node.js SAML 2.0 单点登录库
4
- Highly configurable Node.js SAML 2.0 library for Single Sign On
1
+ # samlify · [![构建状态](https://img.shields.io/circleci/build/github/tngan/samlify?style=for-the-badge&logo=circleci)](https://app.circleci.com/pipelines/github/tngan/samlify) [![npm 版本](https://img.shields.io/npm/v/samlify.svg?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/samlify) [![下载量](https://img.shields.io/npm/dm/samlify.svg?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/samlify) [![覆盖率](https://img.shields.io/coveralls/tngan/samlify/master.svg?style=for-the-badge&logo=coveralls)](https://coveralls.io/github/tngan/samlify?branch=master)
5
2
 
6
3
  ---
7
-
8
- ## 🔄 本仓库为 [samlify](https://github.com/tngan/samlify) 的改进分支版本,原作者[tngan](https://github.com/tngan)
9
-
10
- ### 主要改进 / Key Improvements
11
-
12
- - 📦 CJS模块打包转为 ESModule
13
-
14
- - ✅ 将依赖包 `@authenio/xml-encryption` 替换为 `xml-encryption` 并升级版本对 sha256/512 加密密钥 OAEP 摘要方法的支持
15
-
16
- - 🛠️ 修复加密断言验证签名函数 verifySignature 提取`Assertion` 字段的错误,增加对加密断言 `EncryptedAssertion` 字段提取逻辑
17
-
18
- - 📦 ServiceProvider实例化函数 attributeConsumingService字段参函数, 生成默认的 `AttributeConsumingService` 元素和属性值
19
-
20
- - 🗑️ 移除作为Idp使用 IdentityProvider 函数自定义函数模板loginResponseTemplate字段的支持,并改进了自定义函数替换。
21
- 改进createLoginResponse函数签名改为对象的传参方式
22
-
23
- - 🔒 默认签名算法升级为 SHA-256,Idp默认加密算法为 AES_256_GCM
24
-
25
- - ⬆️ 升级所有能够升级的依赖版本,移除 `node-rsa`/`node-forge` 模块儿,改用原生nodejs `crypto` 模块实现。
26
-
27
- - 🌐 将 `url` 库替换为 `URL` 原生 API
28
- - 改进了如果响应为的绑定`urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect`,某些情况下未能DEFLATE压缩导致不能提取xml的异常情况的处理
29
- - 现在如果遇到加密响应无需显示传递 `isAssertionEncrypted` 字段,也无需传递 `MessageSignatureOrder`
30
- 字段。因为我认为是否加密应该是可以自动判断的,MessageSignatureOrder我修改了判断逻辑并在Keycloak 验证可以通过。使用前你应该自行验证这其中的风险
31
- - 默认 elementsOrder 增加了 AttributeConsumingService 适配
32
- - 我已经使用 Burp SAML Raider测试了 八种XSW都能良好的应对,以及XXE。你应该自行验证
4
+ [English Version](#README.md) | [中文版本](#readmeCN.md)
5
+ ## 🔄 This repository is an improved fork of [samlify](https://github.com/tngan/samlify) by [tngan](https://github.com/tngan)
6
+
7
+ ### Key Improvements
8
+
9
+ - 📦 Converted from CJS to ESModule
10
+ - ✅ Replaced `@authenio/xml-encryption` with `xml-encryption` and added support for sha256/512 encryption key OAEP digest methods
11
+ - ✅ Upgraded `@xmldom/xmldom` to the latest version
12
+ - 🛠️ Fixed encrypted assertion signature verification by adding `EncryptedAssertion` field extraction logic
13
+ - 📦 Added default `AttributeConsumingService` element generation for ServiceProvider
14
+ - 📦 Added partial Artifact binding support
15
+ - 🗑️ Removed custom template support for IdentityProvider and improved parameter passing
16
+ - 🔒 Upgraded default signature algorithm to SHA-256 and default encryption to AES_256_GCM
17
+ - 🧪 Added built-in XML XSD validator
18
+ - 🐛 Improved handling of HTTP-Redirect binding without DEFLATE compression
19
+ - 🔓 Automatic detection of encrypted assertions without explicit flags
20
+ - 📝 Added AttributeConsumingService to default elementsOrder
21
+ - ✅ Tested against Burp SAML Raider (XSW and XXE attacks)
22
+ - Migrated tests to Vitest
33
23
 
34
24
  ---
35
25
 
36
- ## 欢迎 PR / Welcome PRs
26
+ ## Welcome PRs
37
27
 
38
- 欢迎贡献代码或提供与其他框架集成的用例
39
- Welcome contributions or integration examples with frameworks
28
+ Contributions are welcome! Please feel free to submit pull requests or provide integration examples with other frameworks.
40
29
 
41
30
  ---
42
31
 
43
- ## 安装 / Installation
44
- 您应该在使用的前提下首先设置验证其
45
- ```js
32
+ ## How to use?
46
33
 
47
- import * as validator from '@authenio/samlify-xsd-schema-validator';
48
- import * as Saml from "samlesa";
49
- import {Extractor,} from "samlesa";
50
- import validator from '@authenio/samlify-node-xmllint'
51
- // 设置模式验证器 / Set schema validator
52
- Saml.setSchemaValidator(validator);
34
+ Refer to the `type/flows.test.ts` test cases and the original documentation at [https://samlify.js.org](https://samlify.js.org). Note that some parameters have been changed in this fork.
53
35
 
36
+ ---
54
37
 
55
- ```
56
-
57
- ## 生成密钥
58
-
59
- 我们使用 openssl 生成密钥和证书用于测试。私钥可以使用密码保护,这是可选的。以下是生成私钥和自签名证书的命令。
38
+ ## Generating Keys
60
39
 
61
- > openssl genrsa -passout pass:foobar -out encryptKey.pem 4096
62
- > openssl req -new -x509 -key encryptKey.pem -out encryptionCert.cer -days 3650
40
+ Use OpenSSL to generate keys and certificates for testing. Private keys can be password-protected (optional). Here are the commands:
63
41
 
64
- #
42
+ ```bash
43
+ openssl genrsa -passout pass:foobar -out encryptKey.pem 4096
44
+ openssl req -new -x509 -key encryptKey.pem -out encryptionCert.cer -days 3650
package/build/index.js CHANGED
@@ -9,6 +9,7 @@ export { default as SamlLib } from './src/libsaml.js';
9
9
  // new name convention in version >= 3.0
10
10
  import * as Constants from './src/urn.js';
11
11
  import * as Extractor from './src/extractor.js';
12
+ import * as Soap from './src/soap.js';
12
13
  import { validate } from './src/schemaValidator.js';
13
14
  // exposed methods for customizing samlify
14
15
  import { setSchemaValidator, setDOMParserOptions } from './src/api.js';
@@ -16,4 +17,4 @@ export { Constants, Extractor,
16
17
  // temp: resolve the conflict after version >= 3.0
17
18
  IdentityProvider, IdentityProviderInstance, ServiceProvider, ServiceProviderInstance,
18
19
  // set context
19
- setSchemaValidator, setDOMParserOptions, validate };
20
+ setSchemaValidator, setDOMParserOptions, validate, Soap };
@@ -3,20 +3,57 @@
3
3
  * @author tngan
4
4
  * @desc Binding-level API, declare the functions using POST binding
5
5
  */
6
- import { wording, StatusCode } from './urn.js';
6
+ import { checkStatus } from "./flow.js";
7
+ import { ParserType, StatusCode, wording } from './urn.js';
7
8
  import libsaml from './libsaml.js';
9
+ import libsamlSoap from './libsamlSoap.js';
8
10
  import utility, { get } from './utility.js';
11
+ import { fileURLToPath } from "node:url";
12
+ import * as uuid from 'uuid';
13
+ import { artifactResolveFields, extract, loginRequestFields, loginResponseFields, logoutRequestFields, logoutResponseFields } from "./extractor.js";
14
+ import { verifyTime } from "./validator.js";
15
+ import { sendArtifactResolve } from "./soap.js";
16
+ import path from "node:path";
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+ // get the default extractor fields based on the parserType
20
+ function getDefaultExtractorFields(parserType, assertion) {
21
+ switch (parserType) {
22
+ case ParserType.SAMLRequest:
23
+ return loginRequestFields;
24
+ case ParserType.SAMLResponse:
25
+ if (!assertion) {
26
+ // unexpected hit
27
+ throw new Error('ERR_EMPTY_ASSERTION');
28
+ }
29
+ return loginResponseFields(assertion);
30
+ case ParserType.LogoutRequest:
31
+ return logoutRequestFields;
32
+ case ParserType.LogoutResponse:
33
+ return logoutResponseFields;
34
+ default:
35
+ throw new Error('ERR_UNDEFINED_PARSERTYPE');
36
+ }
37
+ }
9
38
  const binding = wording.binding;
10
39
  /**
11
40
  * @desc Generate a base64 encoded login request
12
41
  * @param {string} referenceTagXPath reference uri
13
42
  * @param {object} entity object includes both idp and sp
14
- * @param {function} customTagReplacement used when developers have their own login response template
43
+ * @param customTagReplacement
15
44
  */
16
- function base64LoginRequest(referenceTagXPath, entity, customTagReplacement) {
17
- const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta };
45
+ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
46
+ const metadata = {
47
+ idp: entity.idp.entityMeta,
48
+ sp: entity.sp.entityMeta,
49
+ inResponse: entity?.inResponse,
50
+ relayState: entity?.relayState
51
+ };
18
52
  const spSetting = entity.sp.entitySetting;
19
53
  let id = '';
54
+ let id2 = spSetting.generateID();
55
+ let soapTemplate = '';
56
+ let Response = '';
20
57
  if (metadata && metadata.idp && metadata.sp) {
21
58
  const base = metadata.idp.getSingleSignOnService(binding.post);
22
59
  let rawSamlRequest;
@@ -40,30 +77,59 @@ function base64LoginRequest(referenceTagXPath, entity, customTagReplacement) {
40
77
  NameIDFormat: selectedNameIDFormat
41
78
  });
42
79
  }
80
+ const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
43
81
  if (metadata.idp.isWantAuthnRequestsSigned()) {
44
- const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
45
- return {
46
- id,
47
- context: libsaml.constructSAMLSignature({
48
- referenceTagXPath,
49
- privateKey,
50
- privateKeyPass,
51
- signatureAlgorithm,
52
- transformationAlgorithms,
53
- rawSamlMessage: rawSamlRequest,
54
- signingCert: metadata.sp.getX509Certificate('signing'),
55
- signatureConfig: spSetting.signatureConfig || {
56
- prefix: 'ds',
57
- location: { reference: "/*[local-name(.)='AuthnRequest']/*[local-name(.)='Issuer']", action: 'after' },
58
- }
59
- }),
60
- };
82
+ Response = libsaml.constructSAMLSignature({
83
+ referenceTagXPath,
84
+ privateKey,
85
+ privateKeyPass,
86
+ signatureAlgorithm,
87
+ transformationAlgorithms,
88
+ rawSamlMessage: rawSamlRequest,
89
+ isBase64Output: false,
90
+ signingCert: metadata.sp.getX509Certificate('signing'),
91
+ signatureConfig: spSetting.signatureConfig || {
92
+ prefix: 'ds',
93
+ location: { reference: "/*[local-name(.)='AuthnRequest']/!*[local-name(.)='Issuer']", action: 'after' },
94
+ }
95
+ });
96
+ soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
97
+ ID: id2,
98
+ IssueInstant: new Date().toISOString(),
99
+ InResponseTo: metadata.inResponse ?? "",
100
+ Issuer: metadata.sp.getEntityID(),
101
+ AuthnRequest: Response
102
+ });
103
+ }
104
+ else {
105
+ soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
106
+ ID: id2,
107
+ IssueInstant: new Date().toISOString(),
108
+ InResponseTo: metadata.inResponse ?? "",
109
+ Issuer: metadata.sp.getEntityID(),
110
+ AuthnRequest: rawSamlRequest
111
+ });
61
112
  }
113
+ /** 构建响应签名*/
62
114
  // No need to embeded XML signature
63
- return {
64
- id,
65
- context: utility.base64Encode(rawSamlRequest),
66
- };
115
+ return libsaml.constructSAMLSignature({
116
+ referenceTagXPath: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']",
117
+ privateKey,
118
+ privateKeyPass,
119
+ signatureAlgorithm,
120
+ transformationAlgorithms,
121
+ rawSamlMessage: soapTemplate,
122
+ isBase64Output: false,
123
+ isMessageSigned: false,
124
+ signingCert: metadata.sp.getX509Certificate('signing'),
125
+ signatureConfig: {
126
+ prefix: 'ds',
127
+ location: {
128
+ reference: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']/*[local-name(.)='Issuer']",
129
+ action: 'after'
130
+ }
131
+ },
132
+ });
67
133
  }
68
134
  throw new Error('ERR_GENERATE_POST_LOGIN_REQUEST_MISSING_METADATA');
69
135
  }
@@ -76,7 +142,7 @@ function base64LoginRequest(referenceTagXPath, entity, customTagReplacement) {
76
142
  * @param {boolean} encryptThenSign whether or not to encrypt then sign first (if signing). Defaults to sign-then-encrypt
77
143
  * @param AttributeStatement
78
144
  */
79
- async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTagReplacement, encryptThenSign = false, AttributeStatement = []) {
145
+ async function soapLoginResponse(requestInfo = {}, entity, user = {}, customTagReplacement, encryptThenSign = false, AttributeStatement = []) {
80
146
  const idpSetting = entity.idp.entitySetting;
81
147
  const spSetting = entity.sp.entitySetting;
82
148
  const id = idpSetting.generateID();
@@ -141,7 +207,6 @@ async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTa
141
207
  };
142
208
  // step: sign assertion ? -> encrypted ? -> sign message ?
143
209
  if (metadata.sp.isWantAssertionsSigned()) {
144
- // console.debug('sp wants assertion signed');
145
210
  rawSamlResponse = libsaml.constructSAMLSignature({
146
211
  ...config,
147
212
  rawSamlMessage: rawSamlResponse,
@@ -149,7 +214,10 @@ async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTa
149
214
  referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']",
150
215
  signatureConfig: {
151
216
  prefix: 'ds',
152
- location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']", action: 'after' },
217
+ location: {
218
+ reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']",
219
+ action: 'after'
220
+ },
153
221
  },
154
222
  });
155
223
  }
@@ -200,134 +268,250 @@ async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTa
200
268
  }
201
269
  throw new Error('ERR_GENERATE_POST_LOGIN_RESPONSE_MISSING_METADATA');
202
270
  }
203
- /**
204
- * @desc Generate a base64 encoded logout request
205
- * @param {object} user current logged user (e.g. req.user)
206
- * @param {string} referenceTagXPath reference uri
207
- * @param {object} entity object includes both idp and sp
208
- * @param {function} customTagReplacement used when developers have their own login response template
209
- * @return {string} base64 encoded request
210
- */
211
- function base64LogoutRequest(user, referenceTagXPath, entity, customTagReplacement) {
212
- const metadata = { init: entity.init.entityMeta, target: entity.target.entityMeta };
213
- const initSetting = entity.init.entitySetting;
214
- const nameIDFormat = initSetting.nameIDFormat;
215
- const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
216
- let id = '';
217
- if (metadata && metadata.init && metadata.target) {
218
- let rawSamlRequest;
219
- if (initSetting.logoutRequestTemplate && customTagReplacement) {
220
- const template = customTagReplacement(initSetting.logoutRequestTemplate.context);
221
- id = get(template, 'id', null);
222
- rawSamlRequest = get(template, 'context', null);
223
- }
224
- else {
225
- id = initSetting.generateID();
226
- const tvalue = {
227
- ID: id,
228
- Destination: metadata.target.getSingleLogoutService(binding.post),
229
- Issuer: metadata.init.getEntityID(),
230
- IssueInstant: new Date().toISOString(),
231
- EntityID: metadata.init.getEntityID(),
232
- NameIDFormat: selectedNameIDFormat,
233
- NameID: user.NameID || '',
234
- };
235
- rawSamlRequest = libsaml.replaceTagsByValue(libsaml.defaultLogoutRequestTemplate.context, tvalue);
236
- }
237
- if (entity.target.entitySetting.wantLogoutRequestSigned) {
238
- // Need to embeded XML signature
239
- const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = initSetting;
240
- return {
241
- id,
242
- context: libsaml.constructSAMLSignature({
243
- referenceTagXPath,
244
- privateKey,
245
- privateKeyPass,
246
- signatureAlgorithm,
247
- transformationAlgorithms,
248
- rawSamlMessage: rawSamlRequest,
249
- signingCert: metadata.init.getX509Certificate('signing'),
250
- signatureConfig: initSetting.signatureConfig || {
251
- prefix: 'ds',
252
- location: { reference: "/*[local-name(.)='LogoutRequest']/*[local-name(.)='Issuer']", action: 'after' },
253
- }
254
- }),
255
- };
256
- }
257
- return {
258
- id,
259
- context: utility.base64Encode(rawSamlRequest),
260
- };
271
+ async function parseLoginRequestResolve(params) {
272
+ let { idp, sp, xml, } = params;
273
+ const verificationOptions = {
274
+ metadata: idp.entityMeta,
275
+ signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
276
+ };
277
+ let res = await libsaml.isValidXml(xml, true).catch((error) => {
278
+ return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
279
+ });
280
+ if (res !== true) {
281
+ return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
261
282
  }
262
- throw new Error('ERR_GENERATE_POST_LOGOUT_REQUEST_MISSING_METADATA');
283
+ /** 首先先验证签名*/
284
+ // @ts-ignore
285
+ let [verify, xmlString, isEncrypted, noSignature] = await libsamlSoap.verifyAndDecryptSoapMessage(xml, verificationOptions);
286
+ if (!verify) {
287
+ return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE');
288
+ }
289
+ const parseResult = {
290
+ samlContent: xmlString,
291
+ extract: extract(xmlString, artifactResolveFields),
292
+ };
293
+ /**
294
+ * Validation part: validate the context of response after signature is verified and decrypted (optional)
295
+ */
296
+ const targetEntityMetadata = sp.entityMeta;
297
+ const issuer = targetEntityMetadata.getEntityID();
298
+ const extractedProperties = parseResult.extract;
299
+ // unmatched issuer
300
+ if (extractedProperties.issuer !== issuer) {
301
+ return Promise.reject('ERR_UNMATCH_ISSUER');
302
+ }
303
+ // invalid session time
304
+ // only run the verifyTime when `SessionNotOnOrAfter` exists
305
+ if (!verifyTime(undefined, new Date(new Date(extractedProperties.request.issueInstant).getTime() + 5 * 60 * 1000).toISOString(), sp.entitySetting.clockDrifts)) {
306
+ return Promise.reject('ERR_EXPIRED_SESSION');
307
+ }
308
+ return Promise.resolve(parseResult);
263
309
  }
264
- /**
265
- * @desc Generate a base64 encoded logout response
266
- * @param {object} requestInfo corresponding request, used to obtain the id
267
- * @param {string} referenceTagXPath reference uri
268
- * @param {object} entity object includes both idp and sp
269
- * @param {function} customTagReplacement used when developers have their own login response template
270
- */
271
- function base64LogoutResponse(requestInfo, entity, customTagReplacement) {
310
+ async function parseLoginResponseResolve(params) {
311
+ let { idp, sp, art } = params;
272
312
  const metadata = {
273
- init: entity.init.entityMeta,
274
- target: entity.target.entityMeta,
313
+ idp: idp.entityMeta,
314
+ sp: sp.entityMeta,
275
315
  };
276
- let id = '';
277
- const initSetting = entity.init.entitySetting;
278
- if (metadata && metadata.init && metadata.target) {
279
- let rawSamlResponse;
280
- if (initSetting.logoutResponseTemplate) {
281
- const template = customTagReplacement(initSetting.logoutResponseTemplate.context);
282
- id = template.id;
283
- rawSamlResponse = template.context;
316
+ const verificationOptions = {
317
+ metadata: idp.entityMeta,
318
+ signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
319
+ };
320
+ let parserType = 'SAMLResponse';
321
+ /** 断言是否加密应根据响应里面的字段判断*/
322
+ let decryptRequired = idp.entitySetting.isAssertionEncrypted;
323
+ let extractorFields = [];
324
+ let samlContent = '';
325
+ const spSetting = sp.entitySetting;
326
+ let ID = '_' + uuid.v4();
327
+ let url = metadata.idp.getArtifactResolutionService('soap');
328
+ let samlSoapRaw = libsaml.replaceTagsByValue(libsaml.defaultArtifactResolveTemplate.context, {
329
+ ID: ID,
330
+ Destination: url,
331
+ Issuer: metadata.sp.getEntityID(),
332
+ IssueInstant: new Date().toISOString(),
333
+ Art: art
334
+ });
335
+ if (!metadata.idp.isWantAuthnRequestsSigned()) {
336
+ samlContent = await sendArtifactResolve(url, samlSoapRaw);
337
+ // check status based on different scenarios
338
+ // validate the xml
339
+ try {
340
+ await libsaml.isValidXml(samlContent, true);
284
341
  }
285
- else {
286
- id = initSetting.generateID();
287
- const tvalue = {
288
- ID: id,
289
- Destination: metadata.target.getSingleLogoutService(binding.post),
290
- EntityID: metadata.init.getEntityID(),
291
- Issuer: metadata.init.getEntityID(),
292
- IssueInstant: new Date().toISOString(),
293
- StatusCode: StatusCode.Success,
294
- InResponseTo: get(requestInfo, 'extract.request.id', '')
295
- };
296
- rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLogoutResponseTemplate.context, tvalue);
342
+ catch (e) {
343
+ return Promise.reject('ERR_INVALID_XML');
297
344
  }
298
- if (entity.target.entitySetting.wantLogoutResponseSigned) {
299
- const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = initSetting;
300
- return {
301
- id,
302
- context: libsaml.constructSAMLSignature({
303
- isMessageSigned: true,
304
- transformationAlgorithms: transformationAlgorithms,
305
- privateKey,
306
- privateKeyPass,
307
- signatureAlgorithm,
308
- rawSamlMessage: rawSamlResponse,
309
- signingCert: metadata.init.getX509Certificate('signing'),
310
- signatureConfig: {
311
- prefix: 'ds',
312
- location: {
313
- reference: "/*[local-name(.)='LogoutResponse']/*[local-name(.)='Issuer']",
314
- action: 'after'
315
- }
316
- }
317
- }),
318
- };
345
+ await checkStatus(samlContent, parserType, true);
346
+ }
347
+ if (metadata.idp.isWantAuthnRequestsSigned()) {
348
+ const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
349
+ //@ts-ignore
350
+ let signatureSoap = libsaml.constructSAMLSignature({
351
+ referenceTagXPath: "//*[local-name(.)='ArtifactResolve']",
352
+ isMessageSigned: false,
353
+ isBase64Output: false,
354
+ transformationAlgorithms: transformationAlgorithms,
355
+ //@ts-ignore
356
+ privateKey,
357
+ privateKeyPass,
358
+ //@ts-ignore
359
+ signatureAlgorithm,
360
+ rawSamlMessage: samlSoapRaw,
361
+ signingCert: metadata.sp.getX509Certificate('signing'),
362
+ signatureConfig: {
363
+ prefix: 'ds',
364
+ location: {
365
+ reference: "//*[local-name(.)='Issuer']",
366
+ action: 'after'
367
+ }
368
+ }
369
+ });
370
+ samlContent = await sendArtifactResolve(url, signatureSoap);
371
+ // check status based on different scenarios
372
+ // validate the xml
373
+ try {
374
+ await libsaml.isValidXml(samlContent, true);
319
375
  }
320
- return {
321
- id,
322
- context: utility.base64Encode(rawSamlResponse),
376
+ catch (e) {
377
+ return Promise.reject('ERR_INVALID_XML');
378
+ }
379
+ await checkStatus(samlContent, parserType, true);
380
+ const [verified1, verifiedAssertionNode1, isDecryptRequired1, noSignature1] = await libsamlSoap.verifyAndDecryptSoapMessage(samlContent, verificationOptions);
381
+ /* decryptRequired = isDecryptRequired*/
382
+ if (!verified1) {
383
+ return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
384
+ }
385
+ samlContent = verifiedAssertionNode1;
386
+ const [verified, verifiedAssertionNode, isDecryptRequired, noSignature] = libsaml.verifySignature(samlContent, verificationOptions);
387
+ if (isDecryptRequired && noSignature) {
388
+ const result = await libsaml.decryptAssertion(sp, samlContent);
389
+ samlContent = result[0];
390
+ extractorFields = getDefaultExtractorFields(parserType, result[1]);
391
+ }
392
+ if (!verified && !noSignature && !isDecryptRequired) {
393
+ return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
394
+ }
395
+ if (!isDecryptRequired) {
396
+ extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
397
+ }
398
+ if (parserType === 'SAMLResponse' && isDecryptRequired && !noSignature) {
399
+ const result = await libsaml.decryptAssertion(sp, samlContent);
400
+ samlContent = result[0];
401
+ extractorFields = getDefaultExtractorFields(parserType, result[1]);
402
+ }
403
+ const parseResult = {
404
+ samlContent: samlContent,
405
+ extract: extract(samlContent, extractorFields),
323
406
  };
407
+ /**
408
+ * Validation part: validate the context of response after signature is verified and decrypted (optional)
409
+ */
410
+ const targetEntityMetadata = idp.entityMeta;
411
+ const issuer = targetEntityMetadata.getEntityID();
412
+ const extractedProperties = parseResult.extract;
413
+ // unmatched issuer
414
+ if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
415
+ && extractedProperties
416
+ && extractedProperties.issuer !== issuer) {
417
+ return Promise.reject('ERR_UNMATCH_ISSUER');
418
+ }
419
+ // invalid session time
420
+ // only run the verifyTime when `SessionNotOnOrAfter` exists
421
+ if (parserType === 'SAMLResponse'
422
+ && extractedProperties.sessionIndex.sessionNotOnOrAfter
423
+ && !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, sp.entitySetting.clockDrifts)) {
424
+ return Promise.reject('ERR_EXPIRED_SESSION');
425
+ }
426
+ // invalid time
427
+ // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
428
+ if (parserType === 'SAMLResponse'
429
+ && extractedProperties.conditions
430
+ && !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, sp.entitySetting.clockDrifts)) {
431
+ return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
432
+ }
433
+ //valid destination
434
+ //There is no validation of the response here. The upper-layer application
435
+ // should verify the result by itself to see if the destination is equal to the SP acs and
436
+ // whether the response.id is used to prevent replay attacks.
437
+ /*
438
+ let destination = extractedProperties?.response?.destination
439
+ let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
440
+ return item?.Location === destination
441
+ })
442
+ if (isExit?.length === 0) {
443
+ return Promise.reject('ERR_Destination_URL');
444
+ }
445
+ if (parserType === 'SAMLResponse') {
446
+ let destination = extractedProperties?.response?.destination
447
+ let isExit = self.entitySetting?.assertionConsumerService?.filter((item: { Location: any; }) => {
448
+ return item?.Location === destination
449
+ })
450
+ if (isExit?.length === 0) {
451
+ return Promise.reject('ERR_Destination_URL');
452
+ }
453
+ }
454
+ */
455
+ return Promise.resolve(parseResult);
456
+ }
457
+ const parseResult = {
458
+ samlContent: samlContent,
459
+ extract: extract(samlContent, extractorFields),
460
+ };
461
+ /**
462
+ * Validation part: validate the context of response after signature is verified and decrypted (optional)
463
+ */
464
+ const targetEntityMetadata = idp.entityMeta;
465
+ const issuer = targetEntityMetadata.getEntityID();
466
+ const extractedProperties = parseResult.extract;
467
+ // unmatched issuer
468
+ if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
469
+ && extractedProperties
470
+ && extractedProperties.issuer !== issuer) {
471
+ return Promise.reject('ERR_UNMATCH_ISSUER');
324
472
  }
325
- throw new Error('ERR_GENERATE_POST_LOGOUT_RESPONSE_MISSING_METADATA');
473
+ // invalid session time
474
+ // only run the verifyTime when `SessionNotOnOrAfter` exists
475
+ if (parserType === 'SAMLResponse'
476
+ && extractedProperties.sessionIndex.sessionNotOnOrAfter
477
+ && !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, sp.entitySetting.clockDrifts)) {
478
+ return Promise.reject('ERR_EXPIRED_SESSION');
479
+ }
480
+ // invalid time
481
+ // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
482
+ if (parserType === 'SAMLResponse'
483
+ && extractedProperties.conditions
484
+ && !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, sp.entitySetting.clockDrifts)) {
485
+ return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
486
+ }
487
+ //valid destination
488
+ //There is no validation of the response here. The upper-layer application
489
+ // should verify the result by itself to see if the destination is equal to the SP acs and
490
+ // whether the response.id is used to prevent replay attacks.
491
+ /*
492
+ let destination = extractedProperties?.response?.destination
493
+ let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
494
+ return item?.Location === destination
495
+ })
496
+ if (isExit?.length === 0) {
497
+ return Promise.reject('ERR_Destination_URL');
498
+ }
499
+ if (parserType === 'SAMLResponse') {
500
+ let destination = extractedProperties?.response?.destination
501
+ let isExit = self.entitySetting?.assertionConsumerService?.filter((item: { Location: any; }) => {
502
+ return item?.Location === destination
503
+ })
504
+ if (isExit?.length === 0) {
505
+ return Promise.reject('ERR_Destination_URL');
506
+ }
507
+ }
508
+ */
509
+ return Promise.resolve(parseResult);
326
510
  }
327
511
  const artifactSignBinding = {
328
- base64LoginRequest,
329
- base64LoginResponse,
330
- base64LogoutRequest,
331
- base64LogoutResponse,
512
+ parseLoginRequestResolve,
513
+ soapLoginRequest,
514
+ parseLoginResponseResolve,
515
+ soapLoginResponse,
332
516
  };
333
517
  export default artifactSignBinding;