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.
- package/README.md +30 -50
- package/build/index.js +2 -1
- package/build/src/binding-artifact.js +330 -146
- package/build/src/binding-post.js +45 -31
- package/build/src/binding-redirect.js +0 -10
- package/build/src/binding-simplesign.js +0 -1
- package/build/src/entity-idp.js +1 -5
- package/build/src/entity-sp.js +21 -96
- package/build/src/extractor.js +48 -4
- package/build/src/flow.js +24 -166
- package/build/src/libsaml.js +468 -264
- package/build/src/libsamlSoap.js +115 -0
- package/build/src/schema/xml.xsd +88 -88
- package/build/src/schemaValidator.js +5 -13
- package/build/src/soap.js +123 -3
- package/build/src/utility.js +12 -7
- package/package.json +77 -81
- package/types/api.d.ts +15 -0
- package/types/api.d.ts.map +1 -0
- package/types/binding-post.d.ts +48 -0
- package/types/binding-post.d.ts.map +1 -0
- package/types/binding-redirect.d.ts +54 -0
- package/types/binding-redirect.d.ts.map +1 -0
- package/types/binding-simplesign.d.ts +41 -0
- package/types/binding-simplesign.d.ts.map +1 -0
- package/types/entity-idp.d.ts +38 -0
- package/types/entity-idp.d.ts.map +1 -0
- package/types/entity-sp.d.ts +38 -0
- package/types/entity-sp.d.ts.map +1 -0
- package/types/entity.d.ts +100 -0
- package/types/entity.d.ts.map +1 -0
- package/types/extractor.d.ts +26 -0
- package/types/extractor.d.ts.map +1 -0
- package/types/flow.d.ts +7 -0
- package/types/flow.d.ts.map +1 -0
- package/types/index.d.ts +2 -1
- package/types/index.d.ts.map +1 -1
- package/types/libsaml.d.ts +208 -0
- package/types/libsaml.d.ts.map +1 -0
- package/types/metadata-idp.d.ts +25 -0
- package/types/metadata-idp.d.ts.map +1 -0
- package/types/metadata-sp.d.ts +37 -0
- package/types/metadata-sp.d.ts.map +1 -0
- package/types/metadata.d.ts +58 -0
- package/types/metadata.d.ts.map +1 -0
- package/types/src/api.d.ts +3 -3
- package/types/src/api.d.ts.map +1 -1
- package/types/src/binding-artifact.d.ts +24 -29
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/binding-post.d.ts +22 -22
- package/types/src/binding-post.d.ts.map +1 -1
- package/types/src/binding-redirect.d.ts.map +1 -1
- package/types/src/binding-simplesign.d.ts.map +1 -1
- package/types/src/entity-idp.d.ts +3 -4
- package/types/src/entity-idp.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts +13 -24
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/entity.d.ts.map +1 -1
- package/types/src/extractor.d.ts +22 -0
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts +1 -0
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +16 -7
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/libsamlSoap.d.ts +7 -0
- package/types/src/libsamlSoap.d.ts.map +1 -0
- package/types/src/schemaValidator.d.ts +1 -1
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/soap.d.ts +33 -0
- package/types/src/soap.d.ts.map +1 -1
- package/types/src/utility.d.ts.map +1 -1
- package/types/src/validator.d.ts.map +1 -1
- package/types/types.d.ts +128 -0
- package/types/types.d.ts.map +1 -0
- package/types/urn.d.ts +195 -0
- package/types/urn.d.ts.map +1 -0
- package/types/utility.d.ts +133 -0
- package/types/utility.d.ts.map +1 -0
- package/types/validator.d.ts +4 -0
- package/types/validator.d.ts.map +1 -0
- package/build/src/schema/XMLSchema.dtd +0 -402
- package/build/src/schema/datatypes.dtd +0 -203
package/README.md
CHANGED
|
@@ -1,64 +1,44 @@
|
|
|
1
|
-
# samlify
|
|
2
|
-
|
|
3
|
-
高度可配置的 Node.js SAML 2.0 单点登录库
|
|
4
|
-
Highly configurable Node.js SAML 2.0 library for Single Sign On
|
|
1
|
+
# samlify · [](https://app.circleci.com/pipelines/github/tngan/samlify) [](https://www.npmjs.com/package/samlify) [](https://www.npmjs.com/package/samlify) [](https://coveralls.io/github/tngan/samlify?branch=master)
|
|
5
2
|
|
|
6
3
|
---
|
|
7
|
-
|
|
8
|
-
## 🔄
|
|
9
|
-
|
|
10
|
-
###
|
|
11
|
-
|
|
12
|
-
- 📦
|
|
13
|
-
|
|
14
|
-
- ✅
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
44
|
-
您应该在使用的前提下首先设置验证其
|
|
45
|
-
```js
|
|
32
|
+
## How to use?
|
|
46
33
|
|
|
47
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
43
|
+
* @param customTagReplacement
|
|
15
44
|
*/
|
|
16
|
-
function
|
|
17
|
-
const metadata = {
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
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: {
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
313
|
+
idp: idp.entityMeta,
|
|
314
|
+
sp: sp.entityMeta,
|
|
275
315
|
};
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
512
|
+
parseLoginRequestResolve,
|
|
513
|
+
soapLoginRequest,
|
|
514
|
+
parseLoginResponseResolve,
|
|
515
|
+
soapLoginResponse,
|
|
332
516
|
};
|
|
333
517
|
export default artifactSignBinding;
|