samlesa 2.16.5 → 2.17.0
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/src/binding-post.js +45 -31
- package/build/src/binding-redirect.js +88 -3
- package/build/src/binding-simplesign.js +0 -1
- package/build/src/entity-idp.js +1 -5
- package/build/src/entity-sp.js +115 -23
- package/build/src/extractor.js +29 -4
- package/build/src/flow.js +36 -103
- package/build/src/libsaml.js +172 -162
- package/build/src/metadata-sp.js +2 -0
- package/build/src/metadata.js +0 -2
- package/build/src/schema/saml-schema-ecp-2.0.xsd +1 -1
- package/build/src/schema/saml-schema-metadata-2.0.xsd +3 -3
- package/build/src/schema/saml-schema-protocol-2.0.xsd +1 -1
- package/build/src/schema/{env.xsd → soap-envelope.xsd} +1 -33
- package/build/src/schema/xml.xsd +88 -0
- package/build/src/schemaValidator.js +29 -12
- package/build/src/utility.js +12 -7
- package/package.json +14 -20
- package/types/src/api.d.ts +3 -3
- package/types/src/api.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 +14 -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 +44 -21
- 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 +5 -0
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +15 -4
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/metadata-sp.d.ts.map +1 -1
- package/types/src/metadata.d.ts.map +1 -1
- package/types/src/schemaValidator.d.ts +1 -1
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/utility.d.ts.map +1 -1
- package/build/index.js.map +0 -1
- package/build/src/api.js.map +0 -1
- package/build/src/binding-post.js.map +0 -1
- package/build/src/binding-redirect.js.map +0 -1
- package/build/src/binding-simplesign.js.map +0 -1
- package/build/src/entity-idp.js.map +0 -1
- package/build/src/entity-sp.js.map +0 -1
- package/build/src/entity.js.map +0 -1
- package/build/src/extractor.js.map +0 -1
- package/build/src/flow.js.map +0 -1
- package/build/src/libsaml.js.map +0 -1
- package/build/src/metadata-idp.js.map +0 -1
- package/build/src/metadata-sp.js.map +0 -1
- package/build/src/metadata.js.map +0 -1
- package/build/src/types.js.map +0 -1
- package/build/src/urn.js.map +0 -1
- package/build/src/utility.js.map +0 -1
- package/build/src/validator.js.map +0 -1
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
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file binding-post.ts
|
|
3
|
-
* @author tngan
|
|
4
|
-
* @desc Binding-level API, declare the functions using POST binding
|
|
5
|
-
*/
|
|
2
|
+
* @file binding-post.ts
|
|
3
|
+
* @author tngan
|
|
4
|
+
* @desc Binding-level API, declare the functions using POST binding
|
|
5
|
+
*/
|
|
6
6
|
import { wording, StatusCode } from './urn.js';
|
|
7
7
|
import libsaml from './libsaml.js';
|
|
8
8
|
import utility, { get } from './utility.js';
|
|
9
9
|
const binding = wording.binding;
|
|
10
10
|
/**
|
|
11
|
-
* @desc Generate a base64 encoded login request
|
|
12
|
-
* @param {string} referenceTagXPath reference uri
|
|
13
|
-
* @param {object} entity object includes both idp and sp
|
|
14
|
-
* @param {function} customTagReplacement used when developers have their own login response template
|
|
15
|
-
*/
|
|
11
|
+
* @desc Generate a base64 encoded login request
|
|
12
|
+
* @param {string} referenceTagXPath reference uri
|
|
13
|
+
* @param {object} entity object includes both idp and sp
|
|
14
|
+
* @param {function} customTagReplacement used when developers have their own login response template
|
|
15
|
+
*/
|
|
16
16
|
function base64LoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
17
17
|
const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta };
|
|
18
18
|
const spSetting = entity.sp.entitySetting;
|
|
@@ -141,7 +141,6 @@ async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTa
|
|
|
141
141
|
};
|
|
142
142
|
// step: sign assertion ? -> encrypted ? -> sign message ?
|
|
143
143
|
if (metadata.sp.isWantAssertionsSigned()) {
|
|
144
|
-
// console.debug('sp wants assertion signed');
|
|
145
144
|
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
146
145
|
...config,
|
|
147
146
|
rawSamlMessage: rawSamlResponse,
|
|
@@ -149,7 +148,10 @@ async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTa
|
|
|
149
148
|
referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']",
|
|
150
149
|
signatureConfig: {
|
|
151
150
|
prefix: 'ds',
|
|
152
|
-
location: {
|
|
151
|
+
location: {
|
|
152
|
+
reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']",
|
|
153
|
+
action: 'after'
|
|
154
|
+
},
|
|
153
155
|
},
|
|
154
156
|
});
|
|
155
157
|
}
|
|
@@ -168,7 +170,19 @@ async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTa
|
|
|
168
170
|
},
|
|
169
171
|
});
|
|
170
172
|
}
|
|
171
|
-
|
|
173
|
+
/* if (spSetting.wantMessageSigned) {
|
|
174
|
+
// console.debug('sign then encrypt and sign entire message');
|
|
175
|
+
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
176
|
+
...config,
|
|
177
|
+
rawSamlMessage: rawSamlResponse,
|
|
178
|
+
isMessageSigned: true,
|
|
179
|
+
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
180
|
+
signatureConfig: spSetting.signatureConfig || {
|
|
181
|
+
prefix: 'ds',
|
|
182
|
+
location: {reference: "/!*[local-name(.)='Response']/!*[local-name(.)='Issuer']", action: 'after'},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}*/
|
|
172
186
|
if (idpSetting.isAssertionEncrypted) {
|
|
173
187
|
// console.debug('idp is configured to do encryption');
|
|
174
188
|
const context = await libsaml.encryptAssertion(entity.idp, entity.sp, rawSamlResponse);
|
|
@@ -181,18 +195,18 @@ async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTa
|
|
|
181
195
|
}
|
|
182
196
|
}
|
|
183
197
|
//sign after encrypting
|
|
184
|
-
if (encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
|
|
185
|
-
|
|
198
|
+
/* if (encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
|
|
199
|
+
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
186
200
|
...config,
|
|
187
201
|
rawSamlMessage: rawSamlResponse,
|
|
188
202
|
isMessageSigned: true,
|
|
189
203
|
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
190
204
|
signatureConfig: spSetting.signatureConfig || {
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
prefix: 'ds',
|
|
206
|
+
location: {reference: "/!*[local-name(.)='Response']/!*[local-name(.)='Issuer']", action: 'after'},
|
|
193
207
|
},
|
|
194
|
-
|
|
195
|
-
|
|
208
|
+
});
|
|
209
|
+
}*/
|
|
196
210
|
return Promise.resolve({
|
|
197
211
|
id,
|
|
198
212
|
context: utility.base64Encode(rawSamlResponse),
|
|
@@ -201,13 +215,13 @@ async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTa
|
|
|
201
215
|
throw new Error('ERR_GENERATE_POST_LOGIN_RESPONSE_MISSING_METADATA');
|
|
202
216
|
}
|
|
203
217
|
/**
|
|
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
|
-
*/
|
|
218
|
+
* @desc Generate a base64 encoded logout request
|
|
219
|
+
* @param {object} user current logged user (e.g. req.user)
|
|
220
|
+
* @param {string} referenceTagXPath reference uri
|
|
221
|
+
* @param {object} entity object includes both idp and sp
|
|
222
|
+
* @param {function} customTagReplacement used when developers have their own login response template
|
|
223
|
+
* @return {string} base64 encoded request
|
|
224
|
+
*/
|
|
211
225
|
function base64LogoutRequest(user, referenceTagXPath, entity, customTagReplacement) {
|
|
212
226
|
const metadata = { init: entity.init.entityMeta, target: entity.target.entityMeta };
|
|
213
227
|
const initSetting = entity.init.entitySetting;
|
|
@@ -262,12 +276,12 @@ function base64LogoutRequest(user, referenceTagXPath, entity, customTagReplaceme
|
|
|
262
276
|
throw new Error('ERR_GENERATE_POST_LOGOUT_REQUEST_MISSING_METADATA');
|
|
263
277
|
}
|
|
264
278
|
/**
|
|
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
|
-
*/
|
|
279
|
+
* @desc Generate a base64 encoded logout response
|
|
280
|
+
* @param {object} requestInfo corresponding request, used to obtain the id
|
|
281
|
+
* @param {string} referenceTagXPath reference uri
|
|
282
|
+
* @param {object} entity object includes both idp and sp
|
|
283
|
+
* @param {function} customTagReplacement used when developers have their own login response template
|
|
284
|
+
*/
|
|
271
285
|
function base64LogoutResponse(requestInfo, entity, customTagReplacement) {
|
|
272
286
|
const metadata = {
|
|
273
287
|
init: entity.init.entityMeta,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import utility, { get } from './utility.js';
|
|
7
7
|
import libsaml from './libsaml.js';
|
|
8
|
-
import {
|
|
8
|
+
import { namespace, wording } from './urn.js';
|
|
9
9
|
const binding = wording.binding;
|
|
10
10
|
const urlParams = wording.urlParams;
|
|
11
11
|
/**
|
|
@@ -59,8 +59,9 @@ function buildRedirectURL(opts) {
|
|
|
59
59
|
* @param {function} customTagReplacement used when developers have their own login response template
|
|
60
60
|
* @return {string} redirect URL
|
|
61
61
|
*/
|
|
62
|
+
// @ts-ignore
|
|
62
63
|
function loginRequestRedirectURL(entity, customTagReplacement) {
|
|
63
|
-
const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta };
|
|
64
|
+
const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta, soap: entity.soap ?? false };
|
|
64
65
|
const spSetting = entity.sp.entitySetting;
|
|
65
66
|
let id = '';
|
|
66
67
|
if (metadata && metadata.idp && metadata.sp) {
|
|
@@ -100,6 +101,90 @@ function loginRequestRedirectURL(entity, customTagReplacement) {
|
|
|
100
101
|
}
|
|
101
102
|
throw new Error('ERR_GENERATE_REDIRECT_LOGIN_REQUEST_MISSING_METADATA');
|
|
102
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* @desc Redirect URL for login request
|
|
106
|
+
* @param {object} entity object includes both idp and sp
|
|
107
|
+
* @param {function} customTagReplacement used when developers have their own login response template
|
|
108
|
+
* @return {string} redirect URL
|
|
109
|
+
*/
|
|
110
|
+
// @ts-ignore
|
|
111
|
+
function loginRequestRedirectURLArt(entity, customTagReplacement) {
|
|
112
|
+
const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta, inResponse: entity.inResponse ?? false };
|
|
113
|
+
const spSetting = entity.sp.entitySetting;
|
|
114
|
+
let id = '';
|
|
115
|
+
if (metadata && metadata.idp && metadata.sp) {
|
|
116
|
+
const base = metadata.idp.getSingleSignOnService(binding.redirect);
|
|
117
|
+
let rawSamlRequest;
|
|
118
|
+
if (spSetting.loginRequestTemplate && customTagReplacement) {
|
|
119
|
+
const info = customTagReplacement(spSetting.loginRequestTemplate);
|
|
120
|
+
id = get(info, 'id', null);
|
|
121
|
+
rawSamlRequest = get(info, 'context', null);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const nameIDFormat = spSetting.nameIDFormat;
|
|
125
|
+
const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
|
|
126
|
+
id = spSetting.generateID();
|
|
127
|
+
rawSamlRequest = libsaml.replaceTagsByValue(libsaml.defaultLoginRequestTemplate.context, {
|
|
128
|
+
ID: id,
|
|
129
|
+
Destination: base,
|
|
130
|
+
Issuer: metadata.sp.getEntityID(),
|
|
131
|
+
IssueInstant: new Date().toISOString(),
|
|
132
|
+
NameIDFormat: selectedNameIDFormat,
|
|
133
|
+
AssertionConsumerServiceURL: metadata.sp.getAssertionConsumerService(binding.post),
|
|
134
|
+
EntityID: metadata.sp.getEntityID(),
|
|
135
|
+
AllowCreate: spSetting.allowCreate,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
|
|
139
|
+
if (metadata.idp.isWantAuthnRequestsSigned()) {
|
|
140
|
+
let signAuthnRequest = libsaml.constructSAMLSignature({
|
|
141
|
+
referenceTagXPath: "/*[local-name(.)='AuthnRequest']",
|
|
142
|
+
privateKey,
|
|
143
|
+
privateKeyPass,
|
|
144
|
+
signatureAlgorithm,
|
|
145
|
+
transformationAlgorithms,
|
|
146
|
+
isBase64Output: false,
|
|
147
|
+
rawSamlMessage: rawSamlRequest,
|
|
148
|
+
signingCert: metadata.sp.getX509Certificate('signing'),
|
|
149
|
+
signatureConfig: spSetting.signatureConfig || {
|
|
150
|
+
prefix: 'ds',
|
|
151
|
+
location: {
|
|
152
|
+
reference: "/*[local-name(.)='AuthnRequest']/*[local-name(.)='Issuer']",
|
|
153
|
+
action: 'after'
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
rawSamlRequest = signAuthnRequest;
|
|
158
|
+
}
|
|
159
|
+
/* console.log(metadata.idp)
|
|
160
|
+
console.log(entity.idp.getEntitySetting())*/
|
|
161
|
+
let soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
|
|
162
|
+
ID: id,
|
|
163
|
+
IssueInstant: new Date().toISOString(),
|
|
164
|
+
InResponseTo: metadata.inResponse ?? "",
|
|
165
|
+
Issuer: metadata.sp.getEntityID(),
|
|
166
|
+
AuthnRequest: rawSamlRequest
|
|
167
|
+
});
|
|
168
|
+
let rootSignSoap = libsaml.constructSAMLSignature({
|
|
169
|
+
isMessageSigned: true,
|
|
170
|
+
isBase64Output: false,
|
|
171
|
+
privateKey,
|
|
172
|
+
privateKeyPass,
|
|
173
|
+
signatureAlgorithm,
|
|
174
|
+
transformationAlgorithms,
|
|
175
|
+
rawSamlMessage: soapTemplate,
|
|
176
|
+
signingCert: metadata.sp.getX509Certificate('signing'),
|
|
177
|
+
signatureConfig: {
|
|
178
|
+
prefix: 'ds',
|
|
179
|
+
location: { reference: "//*[local-name()='Header']", action: 'after' },
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
authnRequest: rootSignSoap
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
throw new Error('ERR_GENERATE_REDIRECT_LOGIN_REQUEST_MISSING_METADATA');
|
|
187
|
+
}
|
|
103
188
|
/**
|
|
104
189
|
* @desc Redirect URL for login response
|
|
105
190
|
* @param {object} requestInfo corresponding request, used to obtain the id
|
|
@@ -130,7 +215,6 @@ function loginResponseRedirectURL(requestInfo, entity, user = {}, relayState, cu
|
|
|
130
215
|
// Five minutes later : nowtime + 5 * 60 * 1000 (in milliseconds)
|
|
131
216
|
const fiveMinutesLaterTime = new Date(nowTime.getTime() + 300_000);
|
|
132
217
|
const now = nowTime.toISOString();
|
|
133
|
-
console.log(`现在是北京时间:${nowTime.toLocaleString()}`);
|
|
134
218
|
const sessionIndex = 'session' + idpSetting.generateID(); // 这个是当前系统的会话索引,用于单点注销
|
|
135
219
|
const tenHoursLaterTime = new Date(nowTime.getTime());
|
|
136
220
|
tenHoursLaterTime.setHours(tenHoursLaterTime.getHours() + 10);
|
|
@@ -304,6 +388,7 @@ function logoutResponseRedirectURL(requestInfo, entity, relayState, customTagRep
|
|
|
304
388
|
throw new Error('ERR_GENERATE_REDIRECT_LOGOUT_RESPONSE_MISSING_METADATA');
|
|
305
389
|
}
|
|
306
390
|
const redirectBinding = {
|
|
391
|
+
loginRequestRedirectURLArt,
|
|
307
392
|
loginRequestRedirectURL,
|
|
308
393
|
loginResponseRedirectURL,
|
|
309
394
|
logoutRequestRedirectURL,
|
|
@@ -119,7 +119,6 @@ async function base64LoginResponse(requestInfo = {}, entity, user = {}, relaySta
|
|
|
119
119
|
// Five minutes later : nowtime + 5 * 60 * 1000 (in milliseconds)
|
|
120
120
|
const fiveMinutesLaterTime = new Date(nowTime.getTime() + 300_000);
|
|
121
121
|
const now = nowTime.toISOString();
|
|
122
|
-
console.log(`现在是北京时间:${nowTime.toLocaleString()}`);
|
|
123
122
|
const sessionIndex = 'session' + idpSetting.generateID(); // 这个是当前系统的会话索引,用于单点注销
|
|
124
123
|
const tenHoursLaterTime = new Date(nowTime.getTime());
|
|
125
124
|
tenHoursLaterTime.setHours(tenHoursLaterTime.getHours() + 10);
|
package/build/src/entity-idp.js
CHANGED
|
@@ -59,11 +59,7 @@ export class IdentityProvider extends Entity {
|
|
|
59
59
|
sp,
|
|
60
60
|
}, user, relayState, customTagReplacement, AttributeStatement);
|
|
61
61
|
default:
|
|
62
|
-
|
|
63
|
-
idp: this,
|
|
64
|
-
sp,
|
|
65
|
-
}, user, customTagReplacement, encryptThenSign, AttributeStatement);
|
|
66
|
-
/* throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING');*/
|
|
62
|
+
throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING');
|
|
67
63
|
}
|
|
68
64
|
return {
|
|
69
65
|
...context,
|
package/build/src/entity-sp.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file entity-sp.ts
|
|
3
|
-
* @author tngan
|
|
4
|
-
* @desc Declares the actions taken by service provider
|
|
5
|
-
*/
|
|
2
|
+
* @file entity-sp.ts
|
|
3
|
+
* @author tngan
|
|
4
|
+
* @desc Declares the actions taken by service provider
|
|
5
|
+
*/
|
|
6
6
|
import Entity from './entity.js';
|
|
7
|
+
import * as crypto from "node:crypto";
|
|
7
8
|
import { namespace } from './urn.js';
|
|
8
9
|
import redirectBinding from './binding-redirect.js';
|
|
9
10
|
import postBinding from './binding-post.js';
|
|
@@ -17,15 +18,15 @@ export default function (props) {
|
|
|
17
18
|
return new ServiceProvider(props);
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
|
-
* @desc Service provider can be configured using either metadata importing or spSetting
|
|
21
|
-
* @param {object} spSettingimport { FlowResult } from '../types/src/flow.d';
|
|
21
|
+
* @desc Service provider can be configured using either metadata importing or spSetting
|
|
22
|
+
* @param {object} spSettingimport { FlowResult } from '../types/src/flow.d';
|
|
22
23
|
|
|
23
|
-
*/
|
|
24
|
+
*/
|
|
24
25
|
export class ServiceProvider extends Entity {
|
|
25
26
|
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
* @desc Inherited from Entity
|
|
28
|
+
* @param {object} spSetting setting of service provider
|
|
29
|
+
*/
|
|
29
30
|
constructor(spSetting) {
|
|
30
31
|
const entitySetting = Object.assign({
|
|
31
32
|
authnRequestsSigned: false,
|
|
@@ -35,11 +36,11 @@ export class ServiceProvider extends Entity {
|
|
|
35
36
|
super(entitySetting, 'sp');
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
* @desc Generates the login request for developers to design their own method
|
|
40
|
+
* @param {IdentityProvider} idp object of identity provider
|
|
41
|
+
* @param {string} binding protocol binding
|
|
42
|
+
* @param {function} customTagReplacement used when developers have their own login response template
|
|
43
|
+
*/
|
|
43
44
|
createLoginRequest(idp, binding = 'redirect', customTagReplacement) {
|
|
44
45
|
const nsBinding = namespace.binding;
|
|
45
46
|
const protocol = nsBinding[binding];
|
|
@@ -51,14 +52,20 @@ export class ServiceProvider extends Entity {
|
|
|
51
52
|
case nsBinding.redirect:
|
|
52
53
|
return redirectBinding.loginRequestRedirectURL({ idp, sp: this }, customTagReplacement);
|
|
53
54
|
case nsBinding.post:
|
|
54
|
-
context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
55
|
+
context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
56
|
+
idp,
|
|
57
|
+
sp: this
|
|
58
|
+
}, customTagReplacement);
|
|
55
59
|
break;
|
|
56
60
|
case nsBinding.simpleSign:
|
|
57
61
|
// Object context = {id, context, signature, sigAlg}
|
|
58
62
|
context = simpleSignBinding.base64LoginRequest({ idp, sp: this }, customTagReplacement);
|
|
59
63
|
break;
|
|
60
64
|
case nsBinding.artifact:
|
|
61
|
-
context = artifactSignBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
65
|
+
context = artifactSignBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
66
|
+
idp,
|
|
67
|
+
sp: this
|
|
68
|
+
}, customTagReplacement);
|
|
62
69
|
break;
|
|
63
70
|
default:
|
|
64
71
|
// Will support artifact in the next release
|
|
@@ -72,11 +79,45 @@ export class ServiceProvider extends Entity {
|
|
|
72
79
|
};
|
|
73
80
|
}
|
|
74
81
|
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
* @desc Generates the Art login request for developers to design their own method
|
|
83
|
+
* @param {IdentityProvider} idp object of identity provider
|
|
84
|
+
* @param {string} binding protocol binding
|
|
85
|
+
* @param {function} customTagReplacement used when developers have their own login response template
|
|
86
|
+
*/
|
|
87
|
+
createLoginRequestArt(idp, binding = 'redirect', customTagReplacement) {
|
|
88
|
+
const nsBinding = namespace.binding;
|
|
89
|
+
const protocol = nsBinding[binding];
|
|
90
|
+
if (this.entityMeta.isAuthnRequestSigned() !== idp.entityMeta.isWantAuthnRequestsSigned()) {
|
|
91
|
+
throw new Error('ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG');
|
|
92
|
+
}
|
|
93
|
+
let context = null;
|
|
94
|
+
switch (protocol) {
|
|
95
|
+
case nsBinding.redirect:
|
|
96
|
+
return redirectBinding.loginRequestRedirectURLArt({ idp, sp: this }, customTagReplacement);
|
|
97
|
+
case nsBinding.post:
|
|
98
|
+
context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
99
|
+
idp,
|
|
100
|
+
sp: this,
|
|
101
|
+
soap: true
|
|
102
|
+
}, customTagReplacement);
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
// Will support artifact in the next release
|
|
106
|
+
throw new Error('ERR_SP_LOGIN_REQUEST_UNDEFINED_BINDING');
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...context,
|
|
110
|
+
relayState: this.entitySetting.relayState,
|
|
111
|
+
entityEndpoint: idp.entityMeta.getSingleSignOnService(binding),
|
|
112
|
+
type: 'SAMLRequest',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* @desc Validation of the parsed the URL parameters
|
|
117
|
+
* @param {IdentityProvider} idp object of identity provider
|
|
118
|
+
* @param {string} binding protocol binding
|
|
119
|
+
* @param {request} req request
|
|
120
|
+
*/
|
|
80
121
|
parseLoginResponse(idp, binding, request) {
|
|
81
122
|
const self = this;
|
|
82
123
|
return flow({
|
|
@@ -95,7 +136,7 @@ export class ServiceProvider extends Entity {
|
|
|
95
136
|
* @param {string} binding protocol binding
|
|
96
137
|
* @param {request} req request
|
|
97
138
|
*/
|
|
98
|
-
|
|
139
|
+
parseLoginResponseArt(idp, binding, request) {
|
|
99
140
|
const self = this;
|
|
100
141
|
return flow({
|
|
101
142
|
soap: true,
|
|
@@ -108,4 +149,55 @@ export class ServiceProvider extends Entity {
|
|
|
108
149
|
request: request
|
|
109
150
|
});
|
|
110
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* @desc generate Art id
|
|
154
|
+
*
|
|
155
|
+
* @param entityIDString
|
|
156
|
+
*/
|
|
157
|
+
createArt(entityIDString, endpointIndex = 0) {
|
|
158
|
+
let sourceEntityId = entityIDString ? entityIDString : this.entityMeta.getEntityID();
|
|
159
|
+
// 1. 固定类型代码 (0x0004 - 2字节)
|
|
160
|
+
const typeCode = Buffer.from([0x00, 0x04]);
|
|
161
|
+
// 2. 端点索引 (2字节,大端序)
|
|
162
|
+
if (endpointIndex < 0 || endpointIndex > 65535) {
|
|
163
|
+
throw new Error('Endpoint index must be between 0 and 65535');
|
|
164
|
+
}
|
|
165
|
+
const endpointBuf = Buffer.alloc(2);
|
|
166
|
+
endpointBuf.writeUInt16BE(endpointIndex);
|
|
167
|
+
// 3. Source ID - 实体ID的SHA-1哈希 (20字节)
|
|
168
|
+
const sourceId = crypto.createHash('sha1')
|
|
169
|
+
.update(sourceEntityId)
|
|
170
|
+
.digest();
|
|
171
|
+
// 4. Message Handler - 20字节随机值
|
|
172
|
+
const messageHandler = crypto.randomBytes(20);
|
|
173
|
+
// 组合所有组件 (2+2+20+20 = 44字节)
|
|
174
|
+
const artifact = Buffer.concat([typeCode, endpointBuf, sourceId, messageHandler]);
|
|
175
|
+
// 返回Base64编码的Artifact
|
|
176
|
+
return artifact.toString('base64');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* @desc generate Art id
|
|
180
|
+
* @param artifact
|
|
181
|
+
*/
|
|
182
|
+
parseArt(artifact) {
|
|
183
|
+
// 解码 Base64
|
|
184
|
+
const decoded = Buffer.from(artifact, 'base64');
|
|
185
|
+
// 确保长度正确(SAML 工件固定为 44 字节)
|
|
186
|
+
if (decoded.length !== 44) {
|
|
187
|
+
throw new Error(`Invalid artifact length: ${decoded.length}, expected 44 bytes`);
|
|
188
|
+
}
|
|
189
|
+
// 读取前 4 字节(TypeCode + EndpointIndex)
|
|
190
|
+
const typeCode = decoded.readUInt16BE(0);
|
|
191
|
+
const endpointIndex = decoded.readUInt16BE(2);
|
|
192
|
+
// 使用 Buffer.from() 替代 slice()
|
|
193
|
+
const sourceId = Buffer.from(decoded.buffer, // 底层 ArrayBuffer
|
|
194
|
+
decoded.byteOffset + 4, // 起始偏移量
|
|
195
|
+
20 // 长度
|
|
196
|
+
).toString('hex');
|
|
197
|
+
const messageHandle = Buffer.from(decoded.buffer, // 底层 ArrayBuffer
|
|
198
|
+
decoded.byteOffset + 24, // 起始偏移量
|
|
199
|
+
20 // 长度
|
|
200
|
+
).toString('hex');
|
|
201
|
+
return { typeCode, endpointIndex, sourceId, messageHandle };
|
|
202
|
+
}
|
|
111
203
|
}
|