samlesa 2.16.0 → 2.16.5
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/build/src/binding-artifact.js +333 -0
- package/build/src/entity-sp.js +23 -0
- package/build/src/flow.js +235 -1
- package/build/src/libsaml.js +254 -1
- package/build/src/metadata-idp.js +22 -0
- package/build/src/metadata-sp.js +17 -15
- package/build/src/metadata.js +52 -31
- package/build/src/schema/env.xsd +100 -0
- package/build/src/schemaValidator.js +29 -1
- package/build/src/soap.js +25 -0
- package/build/src/urn.js +5 -3
- package/package.json +3 -2
- package/types/src/binding-artifact.d.ts +48 -0
- package/types/src/binding-artifact.d.ts.map +1 -0
- package/types/src/entity-sp.d.ts +7 -0
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +13 -0
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/metadata-idp.d.ts +6 -0
- package/types/src/metadata-idp.d.ts.map +1 -1
- package/types/src/metadata-sp.d.ts.map +1 -1
- package/types/src/metadata.d.ts +34 -27
- package/types/src/metadata.d.ts.map +1 -1
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/soap.d.ts +2 -0
- package/types/src/soap.d.ts.map +1 -0
- package/types/src/urn.d.ts +2 -0
- package/types/src/urn.d.ts.map +1 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file binding-post.ts
|
|
3
|
+
* @author tngan
|
|
4
|
+
* @desc Binding-level API, declare the functions using POST binding
|
|
5
|
+
*/
|
|
6
|
+
import { wording, StatusCode } from './urn.js';
|
|
7
|
+
import libsaml from './libsaml.js';
|
|
8
|
+
import utility, { get } from './utility.js';
|
|
9
|
+
const binding = wording.binding;
|
|
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
|
+
*/
|
|
16
|
+
function base64LoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
17
|
+
const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta };
|
|
18
|
+
const spSetting = entity.sp.entitySetting;
|
|
19
|
+
let id = '';
|
|
20
|
+
if (metadata && metadata.idp && metadata.sp) {
|
|
21
|
+
const base = metadata.idp.getSingleSignOnService(binding.post);
|
|
22
|
+
let rawSamlRequest;
|
|
23
|
+
if (spSetting.loginRequestTemplate && customTagReplacement) {
|
|
24
|
+
const info = customTagReplacement(spSetting.loginRequestTemplate.context);
|
|
25
|
+
id = get(info, 'id', null);
|
|
26
|
+
rawSamlRequest = get(info, 'context', null);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const nameIDFormat = spSetting.nameIDFormat;
|
|
30
|
+
const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
|
|
31
|
+
id = spSetting.generateID();
|
|
32
|
+
rawSamlRequest = libsaml.replaceTagsByValue(libsaml.defaultLoginRequestTemplate.context, {
|
|
33
|
+
ID: id,
|
|
34
|
+
Destination: base,
|
|
35
|
+
Issuer: metadata.sp.getEntityID(),
|
|
36
|
+
IssueInstant: new Date().toISOString(),
|
|
37
|
+
AssertionConsumerServiceURL: metadata.sp.getAssertionConsumerService(binding.post),
|
|
38
|
+
EntityID: metadata.sp.getEntityID(),
|
|
39
|
+
AllowCreate: spSetting.allowCreate,
|
|
40
|
+
NameIDFormat: selectedNameIDFormat
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
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
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// No need to embeded XML signature
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
context: utility.base64Encode(rawSamlRequest),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
throw new Error('ERR_GENERATE_POST_LOGIN_REQUEST_MISSING_METADATA');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @desc Generate a base64 encoded login response
|
|
72
|
+
* @param {object} requestInfo corresponding request, used to obtain the id
|
|
73
|
+
* @param {object} entity object includes both idp and sp
|
|
74
|
+
* @param {object} user current logged user (e.g. req.user)
|
|
75
|
+
* @param {function} customTagReplacement used when developers have their own login response template
|
|
76
|
+
* @param {boolean} encryptThenSign whether or not to encrypt then sign first (if signing). Defaults to sign-then-encrypt
|
|
77
|
+
* @param AttributeStatement
|
|
78
|
+
*/
|
|
79
|
+
async function base64LoginResponse(requestInfo = {}, entity, user = {}, customTagReplacement, encryptThenSign = false, AttributeStatement = []) {
|
|
80
|
+
const idpSetting = entity.idp.entitySetting;
|
|
81
|
+
const spSetting = entity.sp.entitySetting;
|
|
82
|
+
const id = idpSetting.generateID();
|
|
83
|
+
const metadata = {
|
|
84
|
+
idp: entity.idp.entityMeta,
|
|
85
|
+
sp: entity.sp.entityMeta,
|
|
86
|
+
};
|
|
87
|
+
const nameIDFormat = idpSetting.nameIDFormat;
|
|
88
|
+
const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
|
|
89
|
+
if (metadata && metadata.idp && metadata.sp) {
|
|
90
|
+
const base = metadata.sp.getAssertionConsumerService(binding.post);
|
|
91
|
+
let rawSamlResponse;
|
|
92
|
+
const nowTime = new Date();
|
|
93
|
+
const spEntityID = metadata.sp.getEntityID();
|
|
94
|
+
const oneMinutesLaterTime = new Date(nowTime.getTime());
|
|
95
|
+
oneMinutesLaterTime.setMinutes(oneMinutesLaterTime.getMinutes() + 5);
|
|
96
|
+
const OneMinutesLater = oneMinutesLaterTime.toISOString();
|
|
97
|
+
const now = nowTime.toISOString();
|
|
98
|
+
const acl = metadata.sp.getAssertionConsumerService(binding.post);
|
|
99
|
+
const sessionIndex = 'session' + idpSetting.generateID(); // 这个是当前系统的会话索引,用于单点注销
|
|
100
|
+
const tenHoursLaterTime = new Date(nowTime.getTime());
|
|
101
|
+
tenHoursLaterTime.setHours(tenHoursLaterTime.getHours() + 10);
|
|
102
|
+
const tenHoursLater = tenHoursLaterTime.toISOString();
|
|
103
|
+
const tvalue = {
|
|
104
|
+
ID: id,
|
|
105
|
+
AssertionID: idpSetting.generateID(),
|
|
106
|
+
Destination: base,
|
|
107
|
+
Audience: spEntityID,
|
|
108
|
+
EntityID: spEntityID,
|
|
109
|
+
SubjectRecipient: acl,
|
|
110
|
+
Issuer: metadata.idp.getEntityID(),
|
|
111
|
+
IssueInstant: now,
|
|
112
|
+
AssertionConsumerServiceURL: acl,
|
|
113
|
+
StatusCode: StatusCode.Success,
|
|
114
|
+
// can be customized
|
|
115
|
+
ConditionsNotBefore: now,
|
|
116
|
+
ConditionsNotOnOrAfter: OneMinutesLater,
|
|
117
|
+
SubjectConfirmationDataNotOnOrAfter: OneMinutesLater,
|
|
118
|
+
NameIDFormat: selectedNameIDFormat,
|
|
119
|
+
NameID: user?.NameID || '',
|
|
120
|
+
InResponseTo: get(requestInfo, 'extract.request.id', ''),
|
|
121
|
+
AuthnStatement: `<saml:AuthnStatement AuthnInstant="${now}" SessionNotOnOrAfter="${tenHoursLater}" SessionIndex="${sessionIndex}"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>`,
|
|
122
|
+
AttributeStatement: libsaml.attributeStatementBuilder(AttributeStatement),
|
|
123
|
+
};
|
|
124
|
+
if (idpSetting.loginResponseTemplate && customTagReplacement) {
|
|
125
|
+
const template = customTagReplacement(idpSetting.loginResponseTemplate.context);
|
|
126
|
+
rawSamlResponse = get(template, 'context', null);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
if (requestInfo !== null) {
|
|
130
|
+
tvalue.InResponseTo = requestInfo?.extract?.request?.id ?? '';
|
|
131
|
+
}
|
|
132
|
+
rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate.context, tvalue);
|
|
133
|
+
}
|
|
134
|
+
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm } = idpSetting;
|
|
135
|
+
const config = {
|
|
136
|
+
privateKey,
|
|
137
|
+
privateKeyPass,
|
|
138
|
+
signatureAlgorithm,
|
|
139
|
+
signingCert: metadata.idp.getX509Certificate('signing'),
|
|
140
|
+
isBase64Output: false,
|
|
141
|
+
};
|
|
142
|
+
// step: sign assertion ? -> encrypted ? -> sign message ?
|
|
143
|
+
if (metadata.sp.isWantAssertionsSigned()) {
|
|
144
|
+
// console.debug('sp wants assertion signed');
|
|
145
|
+
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
146
|
+
...config,
|
|
147
|
+
rawSamlMessage: rawSamlResponse,
|
|
148
|
+
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
149
|
+
referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']",
|
|
150
|
+
signatureConfig: {
|
|
151
|
+
prefix: 'ds',
|
|
152
|
+
location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']", action: 'after' },
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// console.debug('after assertion signed', rawSamlResponse);
|
|
157
|
+
// SAML response must be signed sign message first, then encrypt
|
|
158
|
+
if (!encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
|
|
159
|
+
// console.debug('sign then encrypt and sign entire message');
|
|
160
|
+
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
161
|
+
...config,
|
|
162
|
+
rawSamlMessage: rawSamlResponse,
|
|
163
|
+
isMessageSigned: true,
|
|
164
|
+
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
165
|
+
signatureConfig: spSetting.signatureConfig || {
|
|
166
|
+
prefix: 'ds',
|
|
167
|
+
location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Issuer']", action: 'after' },
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// console.debug('after message signed', rawSamlResponse);
|
|
172
|
+
if (idpSetting.isAssertionEncrypted) {
|
|
173
|
+
// console.debug('idp is configured to do encryption');
|
|
174
|
+
const context = await libsaml.encryptAssertion(entity.idp, entity.sp, rawSamlResponse);
|
|
175
|
+
if (encryptThenSign) {
|
|
176
|
+
//need to decode it
|
|
177
|
+
rawSamlResponse = utility.base64Decode(context);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
return Promise.resolve({ id, context });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//sign after encrypting
|
|
184
|
+
if (encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
|
|
185
|
+
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
186
|
+
...config,
|
|
187
|
+
rawSamlMessage: rawSamlResponse,
|
|
188
|
+
isMessageSigned: true,
|
|
189
|
+
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
190
|
+
signatureConfig: spSetting.signatureConfig || {
|
|
191
|
+
prefix: 'ds',
|
|
192
|
+
location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Issuer']", action: 'after' },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return Promise.resolve({
|
|
197
|
+
id,
|
|
198
|
+
context: utility.base64Encode(rawSamlResponse),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
throw new Error('ERR_GENERATE_POST_LOGIN_RESPONSE_MISSING_METADATA');
|
|
202
|
+
}
|
|
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
|
+
};
|
|
261
|
+
}
|
|
262
|
+
throw new Error('ERR_GENERATE_POST_LOGOUT_REQUEST_MISSING_METADATA');
|
|
263
|
+
}
|
|
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) {
|
|
272
|
+
const metadata = {
|
|
273
|
+
init: entity.init.entityMeta,
|
|
274
|
+
target: entity.target.entityMeta,
|
|
275
|
+
};
|
|
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;
|
|
284
|
+
}
|
|
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);
|
|
297
|
+
}
|
|
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
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
id,
|
|
322
|
+
context: utility.base64Encode(rawSamlResponse),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
throw new Error('ERR_GENERATE_POST_LOGOUT_RESPONSE_MISSING_METADATA');
|
|
326
|
+
}
|
|
327
|
+
const artifactSignBinding = {
|
|
328
|
+
base64LoginRequest,
|
|
329
|
+
base64LoginResponse,
|
|
330
|
+
base64LogoutRequest,
|
|
331
|
+
base64LogoutResponse,
|
|
332
|
+
};
|
|
333
|
+
export default artifactSignBinding;
|
package/build/src/entity-sp.js
CHANGED
|
@@ -8,6 +8,7 @@ import { namespace } from './urn.js';
|
|
|
8
8
|
import redirectBinding from './binding-redirect.js';
|
|
9
9
|
import postBinding from './binding-post.js';
|
|
10
10
|
import simpleSignBinding from './binding-simplesign.js';
|
|
11
|
+
import artifactSignBinding from './binding-artifact.js';
|
|
11
12
|
import { flow } from './flow.js';
|
|
12
13
|
/*
|
|
13
14
|
* @desc interface function
|
|
@@ -56,6 +57,9 @@ export class ServiceProvider extends Entity {
|
|
|
56
57
|
// Object context = {id, context, signature, sigAlg}
|
|
57
58
|
context = simpleSignBinding.base64LoginRequest({ idp, sp: this }, customTagReplacement);
|
|
58
59
|
break;
|
|
60
|
+
case nsBinding.artifact:
|
|
61
|
+
context = artifactSignBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", { idp, sp: this }, customTagReplacement);
|
|
62
|
+
break;
|
|
59
63
|
default:
|
|
60
64
|
// Will support artifact in the next release
|
|
61
65
|
throw new Error('ERR_SP_LOGIN_REQUEST_UNDEFINED_BINDING');
|
|
@@ -85,4 +89,23 @@ export class ServiceProvider extends Entity {
|
|
|
85
89
|
request: request
|
|
86
90
|
});
|
|
87
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* @desc request SamlResponse by Arc id
|
|
94
|
+
* @param {IdentityProvider} idp object of identity provider
|
|
95
|
+
* @param {string} binding protocol binding
|
|
96
|
+
* @param {request} req request
|
|
97
|
+
*/
|
|
98
|
+
artifactResolveResponse(idp, binding, request) {
|
|
99
|
+
const self = this;
|
|
100
|
+
return flow({
|
|
101
|
+
soap: true,
|
|
102
|
+
from: idp,
|
|
103
|
+
self: self,
|
|
104
|
+
checkSignature: true, // saml response must have signature
|
|
105
|
+
parserType: 'SAMLResponse',
|
|
106
|
+
type: 'login',
|
|
107
|
+
binding: binding,
|
|
108
|
+
request: request
|
|
109
|
+
});
|
|
110
|
+
}
|
|
88
111
|
}
|
package/build/src/flow.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { base64Decode } from './utility.js';
|
|
2
2
|
import { verifyTime } from './validator.js';
|
|
3
3
|
import libsaml from './libsaml.js';
|
|
4
|
+
import * as uuid from 'uuid';
|
|
5
|
+
import { select } from 'xpath';
|
|
6
|
+
import { DOMParser } from '@xmldom/xmldom';
|
|
7
|
+
import { sendArtifactResolve } from "./soap.js";
|
|
4
8
|
import { extract, loginRequestFields, loginResponseFields, logoutRequestFields, logoutResponseFields, logoutResponseStatusFields, loginResponseStatusFields } from './extractor.js';
|
|
5
9
|
import { BindingNamespace, ParserType, wording, StatusCode } from './urn.js';
|
|
6
10
|
const bindDict = wording.binding;
|
|
@@ -122,6 +126,237 @@ async function redirectFlow(options) {
|
|
|
122
126
|
}
|
|
123
127
|
// proceed the post flow
|
|
124
128
|
async function postFlow(options) {
|
|
129
|
+
const { soap = false, request, from, self, parserType, checkSignature = true } = options;
|
|
130
|
+
const { body } = request;
|
|
131
|
+
const direction = libsaml.getQueryParamByType(parserType);
|
|
132
|
+
let encodedRequest = '';
|
|
133
|
+
let samlContent = '';
|
|
134
|
+
if (soap === false) {
|
|
135
|
+
encodedRequest = body[direction];
|
|
136
|
+
// @ts-ignore
|
|
137
|
+
samlContent = String(base64Decode(encodedRequest));
|
|
138
|
+
}
|
|
139
|
+
/** 增加判断是不是Soap 工件绑定*/
|
|
140
|
+
if (soap) {
|
|
141
|
+
const metadata = {
|
|
142
|
+
idp: from.entityMeta,
|
|
143
|
+
sp: self.entityMeta,
|
|
144
|
+
};
|
|
145
|
+
const spSetting = self.entitySetting;
|
|
146
|
+
let ID = '_' + uuid.v4();
|
|
147
|
+
let url = metadata.idp.getArtifactResolutionService(bindDict.soap);
|
|
148
|
+
let samlSoapRaw = libsaml.replaceTagsByValue(libsaml.defaultArtifactResolveTemplate.context, {
|
|
149
|
+
ID: request?.messageHandle,
|
|
150
|
+
Destination: url,
|
|
151
|
+
Issuer: metadata.sp.getEntityID(),
|
|
152
|
+
IssueInstant: new Date().toISOString(),
|
|
153
|
+
Art: request.Art
|
|
154
|
+
});
|
|
155
|
+
if (metadata.idp.isWantAuthnRequestsSigned()) {
|
|
156
|
+
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
|
|
157
|
+
let signatureSoap = libsaml.constructSAMLSignature({
|
|
158
|
+
referenceTagXPath: "//*[local-name(.)='ArtifactResolve']",
|
|
159
|
+
isMessageSigned: false,
|
|
160
|
+
isBase64Output: false,
|
|
161
|
+
transformationAlgorithms: transformationAlgorithms,
|
|
162
|
+
privateKey,
|
|
163
|
+
privateKeyPass,
|
|
164
|
+
signatureAlgorithm,
|
|
165
|
+
rawSamlMessage: samlSoapRaw,
|
|
166
|
+
signingCert: metadata.sp.getX509Certificate('signing'),
|
|
167
|
+
signatureConfig: {
|
|
168
|
+
prefix: 'ds',
|
|
169
|
+
location: {
|
|
170
|
+
reference: "//*[local-name(.)='Issuer']",
|
|
171
|
+
action: 'after'
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
let data = await sendArtifactResolve(url, signatureSoap);
|
|
176
|
+
/* console.log(signatureSoap)
|
|
177
|
+
console.log("签过名的")*/
|
|
178
|
+
console.log(data);
|
|
179
|
+
console.log("keycloak数据----------------------");
|
|
180
|
+
samlContent = data;
|
|
181
|
+
}
|
|
182
|
+
// No need to embeded XML signature
|
|
183
|
+
}
|
|
184
|
+
const verificationOptions = {
|
|
185
|
+
metadata: from.entityMeta,
|
|
186
|
+
signatureAlgorithm: from.entitySetting.requestSignatureAlgorithm,
|
|
187
|
+
};
|
|
188
|
+
/** 断言是否加密应根据响应里面的字段判断*/
|
|
189
|
+
let decryptRequired = from.entitySetting.isAssertionEncrypted;
|
|
190
|
+
let extractorFields = [];
|
|
191
|
+
// validate the xml first
|
|
192
|
+
/* let res = await libsaml.isValidXml(samlContent).catch((error)=>{
|
|
193
|
+
console.log(error);
|
|
194
|
+
console.log("验证和结果-----------------------")
|
|
195
|
+
console.log("验证和结果-----------------------")
|
|
196
|
+
console.log("验证和结果-----------------------")
|
|
197
|
+
console.log("验证和结果-----------------------")
|
|
198
|
+
console.log("验证和结果-----------------------")
|
|
199
|
+
console.log("验证和结果-----------------------")
|
|
200
|
+
console.log("验证和结果-----------------------")
|
|
201
|
+
});
|
|
202
|
+
console.log(res);
|
|
203
|
+
console.log("验证和结果-----------------------")*/
|
|
204
|
+
if (parserType !== urlParams.samlResponse) {
|
|
205
|
+
extractorFields = getDefaultExtractorFields(parserType, null);
|
|
206
|
+
}
|
|
207
|
+
// check status based on different scenarios
|
|
208
|
+
/* await checkStatus(samlContent, parserType);*/
|
|
209
|
+
/**检查签名顺序 */
|
|
210
|
+
/* if (
|
|
211
|
+
checkSignature &&
|
|
212
|
+
from.entitySetting.messageSigningOrder === MessageSignatureOrder.ETS
|
|
213
|
+
) {
|
|
214
|
+
console.log("===============我走的这里=========================")
|
|
215
|
+
const [verified, verifiedAssertionNode,isDecryptRequired] = libsaml.verifySignature(samlContent, verificationOptions);
|
|
216
|
+
console.log(verified);
|
|
217
|
+
console.log("verified")
|
|
218
|
+
decryptRequired = isDecryptRequired
|
|
219
|
+
if (!verified) {
|
|
220
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
221
|
+
}
|
|
222
|
+
if (!decryptRequired) {
|
|
223
|
+
extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
|
|
224
|
+
}
|
|
225
|
+
}*/
|
|
226
|
+
if (soap === true) {
|
|
227
|
+
const [verified, verifiedAssertionNode, isDecryptRequired] = libsaml.verifySignatureSoap(samlContent, verificationOptions);
|
|
228
|
+
decryptRequired = isDecryptRequired;
|
|
229
|
+
if (!verified) {
|
|
230
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
231
|
+
}
|
|
232
|
+
if (!decryptRequired) {
|
|
233
|
+
console.log("-------------------走到了这里----------------------");
|
|
234
|
+
extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
|
|
235
|
+
}
|
|
236
|
+
if (parserType === 'SAMLResponse' && decryptRequired) {
|
|
237
|
+
// 1. 解密断言
|
|
238
|
+
const [decryptedSAML, decryptedAssertion] = await libsaml.decryptAssertionSoap(self, samlContent);
|
|
239
|
+
console.log(decryptedAssertion);
|
|
240
|
+
console.log("解密数据-----------------------------");
|
|
241
|
+
// 2. 检查解密后的断言是否包含签名
|
|
242
|
+
const assertionDoc = new DOMParser().parseFromString(decryptedAssertion, 'text/xml');
|
|
243
|
+
const assertionSignatureNodes = select("./*[local-name()='Signature']", assertionDoc.documentElement);
|
|
244
|
+
// 3. 如果存在签名则验证
|
|
245
|
+
if (assertionSignatureNodes.length > 0) {
|
|
246
|
+
// 3.1 创建新的验证选项(保持原配置)
|
|
247
|
+
const assertionVerificationOptions = {
|
|
248
|
+
...verificationOptions,
|
|
249
|
+
isAssertion: true // 添加标识表示正在验证断言
|
|
250
|
+
};
|
|
251
|
+
// 3.2 验证断言签名
|
|
252
|
+
const [assertionVerified, result] = libsaml.verifySignatureSoap(decryptedAssertion, assertionVerificationOptions);
|
|
253
|
+
console.log(assertionVerified);
|
|
254
|
+
console.log(result);
|
|
255
|
+
console.log("验证机结果--------------");
|
|
256
|
+
if (!assertionVerified) {
|
|
257
|
+
console.error("解密后的断言签名验证失败");
|
|
258
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ASSERTION_SIGNATURE');
|
|
259
|
+
}
|
|
260
|
+
if (assertionVerified) {
|
|
261
|
+
// @ts-ignore
|
|
262
|
+
samlContent = result;
|
|
263
|
+
extractorFields = getDefaultExtractorFields(parserType, result);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
samlContent = decryptedAssertion;
|
|
268
|
+
extractorFields = getDefaultExtractorFields(parserType, decryptedAssertion);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (soap === false) {
|
|
273
|
+
const [verified, verifiedAssertionNode, isDecryptRequired] = libsaml.verifySignature(samlContent, verificationOptions);
|
|
274
|
+
decryptRequired = isDecryptRequired;
|
|
275
|
+
if (!verified) {
|
|
276
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
277
|
+
}
|
|
278
|
+
if (!decryptRequired) {
|
|
279
|
+
extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
|
|
280
|
+
}
|
|
281
|
+
if (parserType === 'SAMLResponse' && decryptRequired) {
|
|
282
|
+
const result = await libsaml.decryptAssertion(self, samlContent);
|
|
283
|
+
samlContent = result[0];
|
|
284
|
+
extractorFields = getDefaultExtractorFields(parserType, result[1]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// verify the signatures (the response is signed then encrypted, then decrypt first then verify)
|
|
288
|
+
/* if (
|
|
289
|
+
checkSignature &&
|
|
290
|
+
from.entitySetting.messageSigningOrder === MessageSignatureOrder.STE
|
|
291
|
+
) {
|
|
292
|
+
const [verified, verifiedAssertionNode,isDecryptRequired] = libsaml.verifySignature(samlContent, verificationOptions);
|
|
293
|
+
decryptRequired = isDecryptRequired
|
|
294
|
+
if (verified) {
|
|
295
|
+
extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
|
|
296
|
+
} else {
|
|
297
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_STE_SIGNATURE');
|
|
298
|
+
}
|
|
299
|
+
}*/
|
|
300
|
+
const parseResult = {
|
|
301
|
+
samlContent: samlContent,
|
|
302
|
+
extract: extract(samlContent, extractorFields),
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Validation part: validate the context of response after signature is verified and decrypted (optional)
|
|
306
|
+
*/
|
|
307
|
+
const targetEntityMetadata = from.entityMeta;
|
|
308
|
+
const issuer = targetEntityMetadata.getEntityID();
|
|
309
|
+
const extractedProperties = parseResult.extract;
|
|
310
|
+
console.log(extractedProperties);
|
|
311
|
+
console.log(parseResult);
|
|
312
|
+
console.log("解析结果----------------------------------");
|
|
313
|
+
console.log("签发这-----------");
|
|
314
|
+
// unmatched issuer
|
|
315
|
+
if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
|
|
316
|
+
&& extractedProperties
|
|
317
|
+
&& extractedProperties.issuer !== issuer) {
|
|
318
|
+
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
319
|
+
}
|
|
320
|
+
// invalid session time
|
|
321
|
+
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
322
|
+
if (parserType === 'SAMLResponse'
|
|
323
|
+
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
324
|
+
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
325
|
+
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
326
|
+
}
|
|
327
|
+
// invalid time
|
|
328
|
+
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
329
|
+
if (parserType === 'SAMLResponse'
|
|
330
|
+
&& extractedProperties.conditions
|
|
331
|
+
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
332
|
+
return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
|
|
333
|
+
}
|
|
334
|
+
//valid destination
|
|
335
|
+
//There is no validation of the response here. The upper-layer application
|
|
336
|
+
// should verify the result by itself to see if the destination is equal to the SP acs and
|
|
337
|
+
// whether the response.id is used to prevent replay attacks.
|
|
338
|
+
/*
|
|
339
|
+
let destination = extractedProperties?.response?.destination
|
|
340
|
+
let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
|
|
341
|
+
return item?.Location === destination
|
|
342
|
+
})
|
|
343
|
+
if (isExit?.length === 0) {
|
|
344
|
+
return Promise.reject('ERR_Destination_URL');
|
|
345
|
+
}
|
|
346
|
+
if (parserType === 'SAMLResponse') {
|
|
347
|
+
let destination = extractedProperties?.response?.destination
|
|
348
|
+
let isExit = self.entitySetting?.assertionConsumerService?.filter((item: { Location: any; }) => {
|
|
349
|
+
return item?.Location === destination
|
|
350
|
+
})
|
|
351
|
+
if (isExit?.length === 0) {
|
|
352
|
+
return Promise.reject('ERR_Destination_URL');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
*/
|
|
356
|
+
return Promise.resolve(parseResult);
|
|
357
|
+
}
|
|
358
|
+
// proceed the post Artifact flow
|
|
359
|
+
async function postArtifactFlow(options) {
|
|
125
360
|
const { request, from, self, parserType, checkSignature = true } = options;
|
|
126
361
|
const { body } = request;
|
|
127
362
|
const direction = libsaml.getQueryParamByType(parserType);
|
|
@@ -339,7 +574,6 @@ function checkStatus(content, parserType) {
|
|
|
339
574
|
? loginResponseStatusFields
|
|
340
575
|
: logoutResponseStatusFields;
|
|
341
576
|
const { top, second } = extract(content, fields);
|
|
342
|
-
console.log(top, second);
|
|
343
577
|
// only resolve when top-tier status code is success
|
|
344
578
|
if (top === StatusCode.Success) {
|
|
345
579
|
return Promise.resolve('OK');
|