samlesa 4.3.5 → 4.5.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 +11 -8
- package/build/src/artifact.js +55 -0
- package/build/src/binding-artifact.js +477 -363
- package/build/src/binding-post.js +7 -3
- package/build/src/entity-idp.js +51 -3
- package/build/src/entity-sp.js +35 -30
- package/build/src/extractor.js +22 -4
- package/build/src/flow.js +190 -230
- package/build/src/libsamlSoap.js +88 -96
- package/build/src/saml2-enhancements-integration.js +5 -6
- package/build/src/saml2-enhancements.js +14 -15
- package/build/src/soap.js +34 -105
- package/package.json +10 -10
- package/types/src/artifact.d.ts +14 -0
- package/types/src/artifact.d.ts.map +1 -0
- package/types/src/binding-artifact.d.ts +92 -58
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/binding-post.d.ts +1 -1
- package/types/src/binding-post.d.ts.map +1 -1
- package/types/src/entity-idp.d.ts +42 -2
- package/types/src/entity-idp.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts +16 -17
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsamlSoap.d.ts +9 -2
- package/types/src/libsamlSoap.d.ts.map +1 -1
- package/types/src/saml2-enhancements-integration.d.ts.map +1 -1
- package/types/src/saml2-enhancements.d.ts.map +1 -1
- package/types/src/soap.d.ts +5 -25
- package/types/src/soap.d.ts.map +1 -1
- package/types/src/types.d.ts +3 -0
- package/types/src/types.d.ts.map +1 -1
package/build/src/flow.js
CHANGED
|
@@ -24,6 +24,154 @@ function getDefaultExtractorFields(parserType, assertion) {
|
|
|
24
24
|
throw new Error('ERR_UNDEFINED_PARSERTYPE');
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
function collectServiceLocations(entityMeta, serviceKey) {
|
|
28
|
+
const serviceConfig = entityMeta?.meta?.[serviceKey];
|
|
29
|
+
if (!serviceConfig) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const collectFromEntry = (entry) => {
|
|
33
|
+
if (!entry) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
if (typeof entry === 'string') {
|
|
37
|
+
return entry.trim() ? [entry.trim()] : [];
|
|
38
|
+
}
|
|
39
|
+
if (typeof entry !== 'object') {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const directLocation = entry.location ?? entry.Location;
|
|
43
|
+
if (typeof directLocation === 'string' && directLocation.trim()) {
|
|
44
|
+
return [directLocation.trim()];
|
|
45
|
+
}
|
|
46
|
+
return Object.values(entry)
|
|
47
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
48
|
+
.map(value => value.trim())
|
|
49
|
+
.filter(value => /^https?:/i.test(value));
|
|
50
|
+
};
|
|
51
|
+
if (Array.isArray(serviceConfig)) {
|
|
52
|
+
return serviceConfig.flatMap(collectFromEntry);
|
|
53
|
+
}
|
|
54
|
+
if (typeof serviceConfig === 'object') {
|
|
55
|
+
const directLocations = collectFromEntry(serviceConfig);
|
|
56
|
+
if (directLocations.length > 0) {
|
|
57
|
+
return directLocations;
|
|
58
|
+
}
|
|
59
|
+
return Object.values(serviceConfig).flatMap(collectFromEntry);
|
|
60
|
+
}
|
|
61
|
+
return collectFromEntry(serviceConfig);
|
|
62
|
+
}
|
|
63
|
+
function matchesKnownEndpoint(endpoint, knownEndpoints) {
|
|
64
|
+
return typeof endpoint === 'string' && endpoint.length > 0 && knownEndpoints.includes(endpoint);
|
|
65
|
+
}
|
|
66
|
+
function validateIssuer(parserType, extractedProperties, expectedIssuer) {
|
|
67
|
+
const messageIssuer = extractedProperties?.issuer;
|
|
68
|
+
if (parserType === ParserType.SAMLResponse) {
|
|
69
|
+
const responseIssuer = extractedProperties?.responseIssuer;
|
|
70
|
+
if (responseIssuer && responseIssuer !== expectedIssuer) {
|
|
71
|
+
return 'ERR_UNMATCH_ISSUER';
|
|
72
|
+
}
|
|
73
|
+
if (messageIssuer && messageIssuer !== expectedIssuer) {
|
|
74
|
+
return 'ERR_UNMATCH_ISSUER';
|
|
75
|
+
}
|
|
76
|
+
if (responseIssuer && messageIssuer && responseIssuer !== messageIssuer) {
|
|
77
|
+
return 'ERR_UNMATCH_ISSUER';
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (messageIssuer && messageIssuer !== expectedIssuer) {
|
|
82
|
+
return 'ERR_UNMATCH_ISSUER';
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function validateResponseTimes(parserType, extractedProperties, self) {
|
|
87
|
+
if (parserType !== ParserType.SAMLResponse) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (extractedProperties?.sessionIndex?.sessionNotOnOrAfter &&
|
|
91
|
+
!verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
92
|
+
return 'ERR_EXPIRED_SESSION';
|
|
93
|
+
}
|
|
94
|
+
if (extractedProperties?.conditions &&
|
|
95
|
+
!verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
96
|
+
return 'ERR_CONDITION_UNCONFIRMED';
|
|
97
|
+
}
|
|
98
|
+
if (extractedProperties?.subjectConfirmation?.notOnOrAfter &&
|
|
99
|
+
!verifyTime(undefined, extractedProperties.subjectConfirmation.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
100
|
+
return 'ERR_SUBJECT_UNCONFIRMED';
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function validateEndpointConstraints(parserType, extractedProperties, self, from) {
|
|
105
|
+
const requestData = extractedProperties?.request ?? {};
|
|
106
|
+
const responseData = extractedProperties?.response ?? {};
|
|
107
|
+
const subjectConfirmation = extractedProperties?.subjectConfirmation ?? {};
|
|
108
|
+
switch (parserType) {
|
|
109
|
+
case ParserType.SAMLRequest: {
|
|
110
|
+
const validSsoUrls = collectServiceLocations(self?.entityMeta, 'singleSignOnService');
|
|
111
|
+
if (requestData.destination &&
|
|
112
|
+
validSsoUrls.length > 0 &&
|
|
113
|
+
!matchesKnownEndpoint(requestData.destination, validSsoUrls)) {
|
|
114
|
+
return 'ERR_INVALID_DESTINATION';
|
|
115
|
+
}
|
|
116
|
+
const assertionConsumerServiceUrl = requestData.assertionConsumerServiceUrl ??
|
|
117
|
+
requestData.assertionConsumerServiceURL;
|
|
118
|
+
const validAcsUrls = collectServiceLocations(from?.entityMeta, 'assertionConsumerService');
|
|
119
|
+
if (assertionConsumerServiceUrl &&
|
|
120
|
+
validAcsUrls.length > 0 &&
|
|
121
|
+
!matchesKnownEndpoint(assertionConsumerServiceUrl, validAcsUrls)) {
|
|
122
|
+
return 'ERR_INVALID_ASSERTION_CONSUMER_SERVICE';
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
case ParserType.SAMLResponse: {
|
|
127
|
+
const validAcsUrls = collectServiceLocations(self?.entityMeta, 'assertionConsumerService');
|
|
128
|
+
if (validAcsUrls.length === 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (!matchesKnownEndpoint(responseData.destination, validAcsUrls)) {
|
|
132
|
+
return 'ERR_INVALID_DESTINATION';
|
|
133
|
+
}
|
|
134
|
+
if (!matchesKnownEndpoint(subjectConfirmation.recipient, validAcsUrls)) {
|
|
135
|
+
return 'ERR_INVALID_RECIPIENT';
|
|
136
|
+
}
|
|
137
|
+
if (responseData.destination !== subjectConfirmation.recipient) {
|
|
138
|
+
return 'ERR_DESTINATION_RECIPIENT_MISMATCH';
|
|
139
|
+
}
|
|
140
|
+
if (responseData.inResponseTo &&
|
|
141
|
+
subjectConfirmation.inResponseTo &&
|
|
142
|
+
responseData.inResponseTo !== subjectConfirmation.inResponseTo) {
|
|
143
|
+
return 'ERR_UNMATCH_IN_RESPONSE_TO';
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
case ParserType.LogoutRequest:
|
|
148
|
+
case ParserType.LogoutResponse: {
|
|
149
|
+
const validSloUrls = collectServiceLocations(self?.entityMeta, 'singleLogoutService');
|
|
150
|
+
const destination = parserType === ParserType.LogoutRequest
|
|
151
|
+
? requestData.destination
|
|
152
|
+
: responseData.destination;
|
|
153
|
+
if (destination &&
|
|
154
|
+
validSloUrls.length > 0 &&
|
|
155
|
+
!matchesKnownEndpoint(destination, validSloUrls)) {
|
|
156
|
+
return 'ERR_INVALID_LOGOUT_DESTINATION';
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
default:
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function runCommonValidation(parserType, extractedProperties, self, from) {
|
|
165
|
+
const issuerError = validateIssuer(parserType, extractedProperties, from.entityMeta.getEntityID());
|
|
166
|
+
if (issuerError) {
|
|
167
|
+
return issuerError;
|
|
168
|
+
}
|
|
169
|
+
const timeError = validateResponseTimes(parserType, extractedProperties, self);
|
|
170
|
+
if (timeError) {
|
|
171
|
+
return timeError;
|
|
172
|
+
}
|
|
173
|
+
return validateEndpointConstraints(parserType, extractedProperties, self, from);
|
|
174
|
+
}
|
|
27
175
|
// proceed the redirect binding flow
|
|
28
176
|
async function redirectFlow(options) {
|
|
29
177
|
const { request, parserType, self, checkSignature = true, from } = options;
|
|
@@ -87,39 +235,12 @@ async function redirectFlow(options) {
|
|
|
87
235
|
}
|
|
88
236
|
parseResult.sigAlg = decodeSigAlg;
|
|
89
237
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const issuer = targetEntityMetadata.getEntityID();
|
|
94
|
-
const extractedProperties = parseResult.extract;
|
|
95
|
-
// unmatched issuer
|
|
96
|
-
if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
|
|
97
|
-
&& extractedProperties
|
|
98
|
-
&& extractedProperties.issuer !== issuer) {
|
|
99
|
-
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
100
|
-
}
|
|
101
|
-
// invalid session time
|
|
102
|
-
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
103
|
-
if (parserType === 'SAMLResponse'
|
|
104
|
-
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
105
|
-
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
106
|
-
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
107
|
-
}
|
|
108
|
-
// invalid time
|
|
109
|
-
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
110
|
-
if (parserType === 'SAMLResponse'
|
|
111
|
-
&& extractedProperties.conditions
|
|
112
|
-
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
113
|
-
return Promise.reject('ERR_CONDITION_UNCONFIRMED');
|
|
114
|
-
}
|
|
115
|
-
if (parserType === 'SAMLResponse') {
|
|
116
|
-
let destination = extractedProperties?.response?.destination;
|
|
117
|
-
let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
|
|
118
|
-
return item?.location === destination;
|
|
119
|
-
});
|
|
120
|
-
if (isExit?.length === 0) {
|
|
238
|
+
const validationError = runCommonValidation(parserType, parseResult.extract, self, from);
|
|
239
|
+
if (validationError) {
|
|
240
|
+
if (validationError === 'ERR_INVALID_DESTINATION') {
|
|
121
241
|
return Promise.reject('ERR_Destination_URL');
|
|
122
242
|
}
|
|
243
|
+
return Promise.reject(validationError);
|
|
123
244
|
}
|
|
124
245
|
return Promise.resolve(parseResult);
|
|
125
246
|
}
|
|
@@ -170,25 +291,35 @@ async function postFlow(options) {
|
|
|
170
291
|
};
|
|
171
292
|
// 检查验证结果
|
|
172
293
|
if (!verificationResult.status) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
if (verificationResult.encrypted && !verificationResult.decrypted) {
|
|
182
|
-
return Promise.reject('ERR_FAIL_TO_DECRYPT_ASSERTION');
|
|
294
|
+
if (!checkSignature &&
|
|
295
|
+
parserType === urlParams.samlRequest &&
|
|
296
|
+
!verificationResult.isMessageSigned &&
|
|
297
|
+
!verificationResult.isAssertionSigned &&
|
|
298
|
+
!verificationResult.encrypted) {
|
|
299
|
+
verificationResult.status = true;
|
|
300
|
+
verificationResult.samlContent = samlContent;
|
|
183
301
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
302
|
+
else {
|
|
303
|
+
// 如果验证失败,根据具体情况返回错误
|
|
304
|
+
/** 需要判断是不是 */
|
|
305
|
+
if (verificationResult.isMessageSigned && !verificationResult.MessageSignatureStatus) {
|
|
306
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_MESSAGE_SIGNATURE');
|
|
307
|
+
}
|
|
308
|
+
if (verificationResult.isAssertionSigned && !verificationResult.AssertionSignatureStatus) {
|
|
309
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ASSERTION_SIGNATURE');
|
|
310
|
+
}
|
|
311
|
+
if (verificationResult.encrypted && !verificationResult.decrypted) {
|
|
312
|
+
return Promise.reject('ERR_FAIL_TO_DECRYPT_ASSERTION');
|
|
313
|
+
}
|
|
314
|
+
if (!verificationResult.isMessageSigned && verificationResult.type === 'LogoutRequest') {
|
|
315
|
+
return Promise.reject('ERR_LogoutRequest_Need_Signature');
|
|
316
|
+
}
|
|
317
|
+
if (!verificationResult.isMessageSigned && verificationResult.type === 'LogoutResponse') {
|
|
318
|
+
return Promise.reject('ERR_LogoutResponse_Need_Signature');
|
|
319
|
+
}
|
|
320
|
+
// 通用验证失败
|
|
321
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
|
|
189
322
|
}
|
|
190
|
-
// 通用验证失败
|
|
191
|
-
return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
|
|
192
323
|
}
|
|
193
324
|
// 更新samlContent为验证后的版本(可能已解密)
|
|
194
325
|
samlContent = verificationResult.samlContent;
|
|
@@ -204,121 +335,9 @@ async function postFlow(options) {
|
|
|
204
335
|
samlContent: samlContent,
|
|
205
336
|
extract: extract(samlContent, extractorFields),
|
|
206
337
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const targetEntityMetadata = from.entityMeta;
|
|
211
|
-
const issuer = targetEntityMetadata.getEntityID();
|
|
212
|
-
const extractedProperties = parseResult.extract;
|
|
213
|
-
// unmatched issuer
|
|
214
|
-
if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
|
|
215
|
-
&& extractedProperties
|
|
216
|
-
&& extractedProperties.issuer !== issuer) {
|
|
217
|
-
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
218
|
-
}
|
|
219
|
-
// invalid session time
|
|
220
|
-
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
221
|
-
if (parserType === 'SAMLResponse'
|
|
222
|
-
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
223
|
-
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
224
|
-
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
225
|
-
}
|
|
226
|
-
// invalid time
|
|
227
|
-
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
228
|
-
if (parserType === 'SAMLResponse'
|
|
229
|
-
&& extractedProperties.conditions
|
|
230
|
-
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
231
|
-
return Promise.reject('ERR_CONDITION_UNCONFIRMED');
|
|
232
|
-
}
|
|
233
|
-
// invalid subjectConfirmation time
|
|
234
|
-
// invalid time
|
|
235
|
-
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
236
|
-
if (parserType === 'SAMLResponse'
|
|
237
|
-
&& extractedProperties.subjectConfirmation
|
|
238
|
-
&& !verifyTime(undefined, extractedProperties.subjectConfirmation.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
239
|
-
return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
|
|
240
|
-
}
|
|
241
|
-
// ============================
|
|
242
|
-
// VALIDATE Destination & Recipient
|
|
243
|
-
// ============================
|
|
244
|
-
const { type } = verificationResult;
|
|
245
|
-
const { response, subjectConfirmation } = extractedProperties || {};
|
|
246
|
-
// 获取 SP 配置的所有合法 ACS URLs(用于比对)
|
|
247
|
-
const validACSUrls = (self?.entityMeta?.meta?.assertionConsumerService || [])
|
|
248
|
-
.map((item) => item.location)
|
|
249
|
-
.filter(Boolean);
|
|
250
|
-
/**
|
|
251
|
-
* Helper: Check if a given URL is in the list of valid ACS endpoints
|
|
252
|
-
*/
|
|
253
|
-
function isValidACSEndpoint(url) {
|
|
254
|
-
return url != null && validACSUrls.includes(url);
|
|
255
|
-
}
|
|
256
|
-
// 根据消息类型执行不同的验证
|
|
257
|
-
switch (type) {
|
|
258
|
-
case 'Response': // SAML Response (Login)
|
|
259
|
-
{
|
|
260
|
-
// 1. 验证协议层 Destination(必须匹配 ACS)
|
|
261
|
-
const destination = response?.destination;
|
|
262
|
-
if (!isValidACSEndpoint(destination)) {
|
|
263
|
-
return Promise.reject('ERR_INVALID_DESTINATION');
|
|
264
|
-
}
|
|
265
|
-
// 2. 验证断言层 Recipient(必须匹配 ACS,且通常应等于 Destination)
|
|
266
|
-
const recipient = subjectConfirmation?.recipient;
|
|
267
|
-
if (!isValidACSEndpoint(recipient)) {
|
|
268
|
-
return Promise.reject('ERR_INVALID_RECIPIENT');
|
|
269
|
-
}
|
|
270
|
-
// 可选:强制 Destination === Recipient(推荐)
|
|
271
|
-
if (destination !== recipient) {
|
|
272
|
-
// 注意:某些 IdP 可能不严格一致,但安全起见建议开启
|
|
273
|
-
return Promise.reject('ERR_DESTINATION_RECIPIENT_MISMATCH');
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
break;
|
|
277
|
-
case 'LogoutRequest': // IdP 发起的单点登出
|
|
278
|
-
{
|
|
279
|
-
// LogoutRequest 是 IdP → SP,SP 是接收方
|
|
280
|
-
// 必须验证 Destination 是否为 SP 的 SLO endpoint(Single Logout Service)
|
|
281
|
-
const destination = response?.destination; // 注意:LogoutRequest 的 root 元素是 <samlp:LogoutRequest>
|
|
282
|
-
/** 必须检查是否为对象*/
|
|
283
|
-
let singleLogoutService = [];
|
|
284
|
-
if (self?.entityMeta?.meta?.singleLogoutService?.binding) {
|
|
285
|
-
// @ts-ignore
|
|
286
|
-
singleLogoutService = [self?.entityMeta?.meta?.singleLogoutService];
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
singleLogoutService = self?.entityMeta?.meta?.singleLogoutService;
|
|
290
|
-
}
|
|
291
|
-
const validSLOUrls = singleLogoutService
|
|
292
|
-
.map((item) => item.location)
|
|
293
|
-
.filter(Boolean);
|
|
294
|
-
if (destination && !validSLOUrls.includes(destination)) {
|
|
295
|
-
return Promise.reject('ERR_INVALID_LOGOUT_DESTINATION');
|
|
296
|
-
}
|
|
297
|
-
// LogoutRequest 通常**不包含 Assertion**,所以无 Recipient
|
|
298
|
-
// 如果有嵌套断言(罕见),可额外处理,但一般不需要
|
|
299
|
-
}
|
|
300
|
-
break;
|
|
301
|
-
case 'LogoutResponse': // SP → IdP 的登出响应
|
|
302
|
-
{
|
|
303
|
-
// LogoutResponse 是 SP → IdP,IdP 是接收方
|
|
304
|
-
// 此时 SP 是发送方,**不应验证 Destination 是否属于自身**
|
|
305
|
-
// 而应由 IdP 验证。因此 SP 端通常**跳过 Destination 验证**
|
|
306
|
-
// 但如果你作为 SP 也要校验(比如防止发错),可对比 IdP 的 SLO URL
|
|
307
|
-
// —— 但你的 entityMeta 是 SP 自身,没有 IdP 的 SLO,所以一般不验
|
|
308
|
-
// ✅ 所以:LogoutResponse 在 SP 端通常**无需验证 Destination/Recipient**
|
|
309
|
-
}
|
|
310
|
-
break;
|
|
311
|
-
case 'AuthnRequest': // SP → IdP 的认证请求
|
|
312
|
-
{
|
|
313
|
-
// AuthnRequest 是 SP 发出的,不是接收的
|
|
314
|
-
// 此验证逻辑运行在 SP 接收响应时,**不会收到 AuthnRequest**
|
|
315
|
-
// 所以这个 case 实际不会触发,保留仅为完整性
|
|
316
|
-
}
|
|
317
|
-
break;
|
|
318
|
-
case 'Unknown':
|
|
319
|
-
default:
|
|
320
|
-
// 未知类型,保守拒绝
|
|
321
|
-
return Promise.reject('ERR_UNKNOWN_SAML_MESSAGE_TYPE');
|
|
338
|
+
const validationError = runCommonValidation(parserType, parseResult.extract, self, from);
|
|
339
|
+
if (validationError) {
|
|
340
|
+
return Promise.reject(validationError);
|
|
322
341
|
}
|
|
323
342
|
return Promise.resolve({
|
|
324
343
|
...parseResult,
|
|
@@ -389,44 +408,12 @@ async function postArtifactFlow(options) {
|
|
|
389
408
|
samlContent: samlContent,
|
|
390
409
|
extract: extract(samlContent, extractorFields),
|
|
391
410
|
};
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const targetEntityMetadata = from.entityMeta;
|
|
396
|
-
const issuer = targetEntityMetadata.getEntityID();
|
|
397
|
-
const extractedProperties = parseResult.extract;
|
|
398
|
-
// unmatched issuer
|
|
399
|
-
if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
|
|
400
|
-
&& extractedProperties
|
|
401
|
-
&& extractedProperties.issuer !== issuer) {
|
|
402
|
-
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
403
|
-
}
|
|
404
|
-
// invalid session time
|
|
405
|
-
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
406
|
-
if (parserType === 'SAMLResponse'
|
|
407
|
-
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
408
|
-
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
409
|
-
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
410
|
-
}
|
|
411
|
-
// invalid time
|
|
412
|
-
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
413
|
-
if (parserType === 'SAMLResponse'
|
|
414
|
-
&& extractedProperties.conditions
|
|
415
|
-
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
416
|
-
return Promise.reject('ERR_CONDITION_UNCONFIRMED');
|
|
417
|
-
}
|
|
418
|
-
//valid destination
|
|
419
|
-
//There is no validation of the response here. The upper-layer application
|
|
420
|
-
// should verify the result by itself to see if the destination is equal to the SP acs and
|
|
421
|
-
// whether the response.id is used to prevent replay attacks.
|
|
422
|
-
if (parserType === 'SAMLResponse') {
|
|
423
|
-
let destination = extractedProperties?.response?.destination;
|
|
424
|
-
let isExit = self?.entityMeta?.meta?.assertionConsumerService?.filter((item) => {
|
|
425
|
-
return item?.location === destination;
|
|
426
|
-
});
|
|
427
|
-
if (isExit?.length === 0) {
|
|
411
|
+
const validationError = runCommonValidation(parserType, parseResult.extract, self, from);
|
|
412
|
+
if (validationError) {
|
|
413
|
+
if (validationError === 'ERR_INVALID_DESTINATION') {
|
|
428
414
|
return Promise.reject('ERR_Destination_URL');
|
|
429
415
|
}
|
|
416
|
+
return Promise.reject(validationError);
|
|
430
417
|
}
|
|
431
418
|
return Promise.resolve(parseResult);
|
|
432
419
|
}
|
|
@@ -491,39 +478,12 @@ async function postSimpleSignFlow(options) {
|
|
|
491
478
|
}
|
|
492
479
|
parseResult.sigAlg = sigAlg;
|
|
493
480
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const issuer = targetEntityMetadata.getEntityID();
|
|
498
|
-
const extractedProperties = parseResult.extract;
|
|
499
|
-
// unmatched issuer
|
|
500
|
-
if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
|
|
501
|
-
&& extractedProperties
|
|
502
|
-
&& extractedProperties.issuer !== issuer) {
|
|
503
|
-
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
504
|
-
}
|
|
505
|
-
// invalid session time
|
|
506
|
-
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
507
|
-
if (parserType === 'SAMLResponse'
|
|
508
|
-
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
509
|
-
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
510
|
-
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
511
|
-
}
|
|
512
|
-
// invalid time
|
|
513
|
-
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
514
|
-
if (parserType === 'SAMLResponse'
|
|
515
|
-
&& extractedProperties.conditions
|
|
516
|
-
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
517
|
-
return Promise.reject('ERR_CONDITION_UNCONFIRMED');
|
|
518
|
-
}
|
|
519
|
-
if (parserType === 'SAMLResponse') {
|
|
520
|
-
let destination = extractedProperties?.response?.destination;
|
|
521
|
-
let isExit = self?.entityMeta?.meta?.assertionConsumerService?.filter((item) => {
|
|
522
|
-
return item?.location === destination;
|
|
523
|
-
});
|
|
524
|
-
if (isExit?.length === 0) {
|
|
481
|
+
const validationError = runCommonValidation(parserType, parseResult.extract, self, from);
|
|
482
|
+
if (validationError) {
|
|
483
|
+
if (validationError === 'ERR_INVALID_DESTINATION') {
|
|
525
484
|
return Promise.reject('ERR_Destination_URL');
|
|
526
485
|
}
|
|
486
|
+
return Promise.reject(validationError);
|
|
527
487
|
}
|
|
528
488
|
return Promise.resolve(parseResult);
|
|
529
489
|
}
|