samlesa 3.4.2 → 3.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 +260 -25
- package/build/src/binding-artifact.js +194 -285
- package/build/src/entity-idp.js +16 -1
- package/build/src/entity-sp.js +19 -17
- package/build/src/extractor.js +25 -5
- package/build/src/flow.js +1 -8
- package/build/src/schemaValidator.js +78 -63
- package/build/src/urn.js +109 -11
- package/build/src/utility.js +71 -0
- package/package.json +88 -75
- package/types/src/binding-artifact.d.ts +53 -25
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/entity-idp.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts +12 -14
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/extractor.d.ts +2 -1
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/schemaValidator.d.ts +18 -1
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/urn.d.ts +61 -5
- package/types/src/urn.d.ts.map +1 -1
- package/types/src/utility.d.ts +17 -0
- package/types/src/utility.d.ts.map +1 -1
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file binding-
|
|
2
|
+
* @file binding-artifact.ts
|
|
3
3
|
* @author tngan
|
|
4
|
-
* @desc Binding-level API
|
|
4
|
+
* @desc Binding-level API for SAML 2.0 Artifact Binding
|
|
5
|
+
* @see https://docs.oasis-open.org/security/saml/v2.0/saml-bind-2.0-os.pdf
|
|
5
6
|
*/
|
|
6
7
|
import { checkStatus } from "./flow.js";
|
|
7
8
|
import { ParserType, StatusCode, wording } from './urn.js';
|
|
9
|
+
import * as crypto from "node:crypto";
|
|
8
10
|
import libsaml from './libsaml.js';
|
|
9
11
|
import libsamlSoap from './libsamlSoap.js';
|
|
10
12
|
import utility, { get } from './utility.js';
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import postBinding from './binding-post.js';
|
|
13
15
|
import { artifactResolveFields, extract, loginRequestFields, loginResponseFields, logoutRequestFields, logoutResponseFields } from "./extractor.js";
|
|
14
16
|
import { verifyTime } from "./validator.js";
|
|
15
17
|
import { sendArtifactResolve } from "./soap.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const binding = wording.binding;
|
|
19
|
+
/**
|
|
20
|
+
* Get default extractor fields based on parser type
|
|
21
|
+
*/
|
|
20
22
|
function getDefaultExtractorFields(parserType, assertion) {
|
|
21
23
|
switch (parserType) {
|
|
22
24
|
case ParserType.SAMLRequest:
|
|
23
25
|
return loginRequestFields;
|
|
24
26
|
case ParserType.SAMLResponse:
|
|
25
27
|
if (!assertion) {
|
|
26
|
-
// unexpected hit
|
|
27
28
|
throw new Error('ERR_EMPTY_ASSERTION');
|
|
28
29
|
}
|
|
29
30
|
return loginResponseFields(assertion);
|
|
@@ -35,25 +36,46 @@ function getDefaultExtractorFields(parserType, assertion) {
|
|
|
35
36
|
throw new Error('ERR_UNDEFINED_PARSERTYPE');
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
const binding = wording.binding;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* @param
|
|
43
|
-
* @param
|
|
40
|
+
* Generate a SAML 2.0 compliant Artifact ID
|
|
41
|
+
* Format: [TypeCode: 2 bytes] + [EndpointIndex: 2 bytes] + [SourceID: 20 bytes] + [MessageHandle: 20 bytes]
|
|
42
|
+
* @param issuerId The entity ID of the issuing party (IdP)
|
|
43
|
+
* @param endpointIndex The index of the destination endpoint (default is 1 for Artifact Resolution Service)
|
|
44
|
+
* @returns The Base64 encoded Artifact ID string
|
|
45
|
+
*/
|
|
46
|
+
export function generateArtifactId(issuerId, endpointIndex = 1) {
|
|
47
|
+
// SourceID: 20 bytes SHA-1 of issuer ID
|
|
48
|
+
const issuerHash = crypto.createHash('sha1').update(issuerId).digest();
|
|
49
|
+
const sourceId = issuerHash.subarray(0, 20);
|
|
50
|
+
// TypeCode: 0x0004 (SAML 2.0 Artifact Type)
|
|
51
|
+
const typeCode = Buffer.from([0x00, 0x04]);
|
|
52
|
+
// EndpointIndex: 2 bytes Big Endian
|
|
53
|
+
const indexBuffer = Buffer.alloc(2);
|
|
54
|
+
indexBuffer.writeUInt16BE(endpointIndex, 0);
|
|
55
|
+
// MessageHandle: 20 random bytes
|
|
56
|
+
const messageHandle = crypto.randomBytes(20);
|
|
57
|
+
// Concatenate: 2 + 2 + 20 + 20 = 44 bytes
|
|
58
|
+
const artifactBytes = Buffer.concat([typeCode, indexBuffer, sourceId, messageHandle]);
|
|
59
|
+
// Base64 encode
|
|
60
|
+
return artifactBytes.toString('base64');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @desc Generate a SOAP-encoded login request for Artifact binding
|
|
64
|
+
* @param {string} referenceTagXPath reference uri
|
|
65
|
+
* @param {object} entity object includes both idp and sp
|
|
66
|
+
* @param {function} customTagReplacement used when developers have their own login request template
|
|
67
|
+
* @returns {BindingContext}
|
|
44
68
|
*/
|
|
45
69
|
function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
46
70
|
const metadata = {
|
|
47
71
|
idp: entity.idp.entityMeta,
|
|
48
72
|
sp: entity.sp.entityMeta,
|
|
49
|
-
inResponse: entity
|
|
50
|
-
relayState: entity
|
|
73
|
+
inResponse: entity.inResponse,
|
|
74
|
+
relayState: entity.relayState
|
|
51
75
|
};
|
|
52
76
|
const spSetting = entity.sp.entitySetting;
|
|
53
77
|
let id = '';
|
|
54
|
-
let
|
|
55
|
-
let soapTemplate = '';
|
|
56
|
-
let Response = '';
|
|
78
|
+
let soapId = spSetting.generateID();
|
|
57
79
|
if (metadata && metadata.idp && metadata.sp) {
|
|
58
80
|
const base = metadata.idp.getSingleSignOnService(binding.post);
|
|
59
81
|
let rawSamlRequest;
|
|
@@ -78,12 +100,13 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
|
78
100
|
});
|
|
79
101
|
}
|
|
80
102
|
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
|
|
103
|
+
let signedAuthnRequest;
|
|
81
104
|
if (metadata.idp.isWantAuthnRequestsSigned()) {
|
|
82
|
-
|
|
105
|
+
signedAuthnRequest = libsaml.constructSAMLSignature({
|
|
83
106
|
referenceTagXPath,
|
|
84
|
-
privateKey,
|
|
107
|
+
privateKey: privateKey,
|
|
85
108
|
privateKeyPass,
|
|
86
|
-
signatureAlgorithm,
|
|
109
|
+
signatureAlgorithm: signatureAlgorithm,
|
|
87
110
|
transformationAlgorithms,
|
|
88
111
|
rawSamlMessage: rawSamlRequest,
|
|
89
112
|
isBase64Output: false,
|
|
@@ -91,35 +114,29 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
|
91
114
|
signatureConfig: spSetting.signatureConfig || {
|
|
92
115
|
prefix: 'ds',
|
|
93
116
|
location: {
|
|
94
|
-
reference: "/*[local-name(.)='AuthnRequest']
|
|
117
|
+
reference: "/*[local-name(.)='AuthnRequest']/*[local-name(.)='Issuer']",
|
|
95
118
|
action: 'after'
|
|
96
119
|
},
|
|
97
120
|
}
|
|
98
121
|
});
|
|
99
|
-
soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
|
|
100
|
-
ID: id2,
|
|
101
|
-
IssueInstant: new Date().toISOString(),
|
|
102
|
-
InResponseTo: metadata.inResponse ?? "",
|
|
103
|
-
Issuer: metadata.sp.getEntityID(),
|
|
104
|
-
AuthnRequest: Response
|
|
105
|
-
});
|
|
106
122
|
}
|
|
107
123
|
else {
|
|
108
|
-
|
|
109
|
-
ID: id2,
|
|
110
|
-
IssueInstant: new Date().toISOString(),
|
|
111
|
-
InResponseTo: metadata.inResponse ?? "",
|
|
112
|
-
Issuer: metadata.sp.getEntityID(),
|
|
113
|
-
AuthnRequest: rawSamlRequest
|
|
114
|
-
});
|
|
124
|
+
signedAuthnRequest = rawSamlRequest;
|
|
115
125
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
// Construct SOAP envelope with ArtifactResponse containing AuthnRequest
|
|
127
|
+
const soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
|
|
128
|
+
ID: soapId,
|
|
129
|
+
IssueInstant: new Date().toISOString(),
|
|
130
|
+
InResponseTo: metadata.inResponse ?? "",
|
|
131
|
+
Issuer: metadata.sp.getEntityID(),
|
|
132
|
+
AuthnRequest: signedAuthnRequest
|
|
133
|
+
});
|
|
134
|
+
// Sign the SOAP envelope
|
|
135
|
+
const signedSoap = libsaml.constructSAMLSignature({
|
|
119
136
|
referenceTagXPath: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']",
|
|
120
|
-
privateKey,
|
|
137
|
+
privateKey: privateKey,
|
|
121
138
|
privateKeyPass,
|
|
122
|
-
signatureAlgorithm,
|
|
139
|
+
signatureAlgorithm: signatureAlgorithm,
|
|
123
140
|
transformationAlgorithms,
|
|
124
141
|
rawSamlMessage: soapTemplate,
|
|
125
142
|
isBase64Output: false,
|
|
@@ -133,161 +150,105 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
|
133
150
|
}
|
|
134
151
|
},
|
|
135
152
|
});
|
|
153
|
+
return {
|
|
154
|
+
id: soapId,
|
|
155
|
+
context: signedSoap,
|
|
156
|
+
};
|
|
136
157
|
}
|
|
137
158
|
throw new Error('ERR_GENERATE_POST_LOGIN_REQUEST_MISSING_METADATA');
|
|
138
159
|
}
|
|
139
160
|
/**
|
|
140
|
-
* @desc Generate a
|
|
141
|
-
* @param
|
|
142
|
-
* @
|
|
143
|
-
* @param {object} user current logged user (e.g. req.user)
|
|
144
|
-
* @param {function} customTagReplacement used when developers have their own login response template
|
|
145
|
-
* @param {boolean} encryptThenSign whether or not to encrypt then sign first (if signing). Defaults to sign-then-encrypt
|
|
146
|
-
* @param AttributeStatement
|
|
161
|
+
* @desc Generate a SOAP-encoded login response for Artifact binding
|
|
162
|
+
* @param {Base64LoginResponseParams} params parameters for generating login response
|
|
163
|
+
* @returns {BindingContext}
|
|
147
164
|
*/
|
|
148
|
-
async function soapLoginResponse(
|
|
149
|
-
const
|
|
150
|
-
const spSetting = entity.sp.entitySetting;
|
|
151
|
-
const id = idpSetting.generateID();
|
|
165
|
+
async function soapLoginResponse(params) {
|
|
166
|
+
const { entity } = params;
|
|
152
167
|
const metadata = {
|
|
153
168
|
idp: entity.idp.entityMeta,
|
|
154
169
|
sp: entity.sp.entityMeta,
|
|
155
170
|
};
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (metadata && metadata.idp && metadata.sp) {
|
|
159
|
-
const base = metadata.sp.getAssertionConsumerService(binding.post);
|
|
160
|
-
let rawSamlResponse;
|
|
161
|
-
const nowTime = new Date();
|
|
162
|
-
const spEntityID = metadata.sp.getEntityID();
|
|
163
|
-
const oneMinutesLaterTime = new Date(nowTime.getTime());
|
|
164
|
-
oneMinutesLaterTime.setMinutes(oneMinutesLaterTime.getMinutes() + 5);
|
|
165
|
-
const OneMinutesLater = oneMinutesLaterTime.toISOString();
|
|
166
|
-
const now = nowTime.toISOString();
|
|
167
|
-
const acl = metadata.sp.getAssertionConsumerService(binding.post);
|
|
168
|
-
const sessionIndex = 'session' + idpSetting.generateID(); // 这个是当前系统的会话索引,用于单点注销
|
|
169
|
-
const tenHoursLaterTime = new Date(nowTime.getTime());
|
|
170
|
-
tenHoursLaterTime.setHours(tenHoursLaterTime.getHours() + 10);
|
|
171
|
-
const tenHoursLater = tenHoursLaterTime.toISOString();
|
|
172
|
-
const tvalue = {
|
|
173
|
-
ID: id,
|
|
174
|
-
AssertionID: idpSetting.generateID(),
|
|
175
|
-
Destination: base,
|
|
176
|
-
Audience: spEntityID,
|
|
177
|
-
EntityID: spEntityID,
|
|
178
|
-
SubjectRecipient: acl,
|
|
179
|
-
Issuer: metadata.idp.getEntityID(),
|
|
180
|
-
IssueInstant: now,
|
|
181
|
-
AssertionConsumerServiceURL: acl,
|
|
182
|
-
StatusCode: StatusCode.Success,
|
|
183
|
-
// can be customized
|
|
184
|
-
ConditionsNotBefore: now,
|
|
185
|
-
ConditionsNotOnOrAfter: OneMinutesLater,
|
|
186
|
-
SubjectConfirmationDataNotOnOrAfter: OneMinutesLater,
|
|
187
|
-
NameIDFormat: selectedNameIDFormat,
|
|
188
|
-
NameID: user?.NameID || '',
|
|
189
|
-
InResponseTo: get(requestInfo, 'extract.request.id', ''),
|
|
190
|
-
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>`,
|
|
191
|
-
AttributeStatement: libsaml.attributeStatementBuilder(AttributeStatement),
|
|
192
|
-
};
|
|
193
|
-
if (idpSetting.loginResponseTemplate && customTagReplacement) {
|
|
194
|
-
const template = customTagReplacement(idpSetting.loginResponseTemplate.context);
|
|
195
|
-
rawSamlResponse = get(template, 'context', null);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
if (requestInfo !== null) {
|
|
199
|
-
tvalue.InResponseTo = requestInfo?.extract?.request?.id ?? '';
|
|
200
|
-
}
|
|
201
|
-
rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate.context, tvalue);
|
|
202
|
-
}
|
|
203
|
-
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm } = idpSetting;
|
|
204
|
-
const config = {
|
|
205
|
-
privateKey,
|
|
206
|
-
privateKeyPass,
|
|
207
|
-
signatureAlgorithm,
|
|
208
|
-
signingCert: metadata.idp.getX509Certificate('signing'),
|
|
209
|
-
isBase64Output: false,
|
|
210
|
-
};
|
|
211
|
-
// step: sign assertion ? -> encrypted ? -> sign message ?
|
|
212
|
-
if (metadata.sp.isWantAssertionsSigned()) {
|
|
213
|
-
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
214
|
-
...config,
|
|
215
|
-
rawSamlMessage: rawSamlResponse,
|
|
216
|
-
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
217
|
-
referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']",
|
|
218
|
-
signatureConfig: {
|
|
219
|
-
prefix: 'ds',
|
|
220
|
-
location: {
|
|
221
|
-
reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']",
|
|
222
|
-
action: 'after'
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
// console.debug('after assertion signed', rawSamlResponse);
|
|
228
|
-
// SAML response must be signed sign message first, then encrypt
|
|
229
|
-
if (!encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
|
|
230
|
-
// console.debug('sign then encrypt and sign entire message');
|
|
231
|
-
// @ts-ignore
|
|
232
|
-
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
233
|
-
...config,
|
|
234
|
-
rawSamlMessage: rawSamlResponse,
|
|
235
|
-
isMessageSigned: true,
|
|
236
|
-
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
237
|
-
signatureConfig: spSetting.signatureConfig || {
|
|
238
|
-
prefix: 'ds',
|
|
239
|
-
location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Issuer']", action: 'after' },
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
// console.debug('after message signed', rawSamlResponse);
|
|
244
|
-
if (idpSetting.isAssertionEncrypted) {
|
|
245
|
-
// console.debug('idp is configured to do encryption');
|
|
246
|
-
const context = await libsaml.encryptAssertion(entity.idp, entity.sp, rawSamlResponse);
|
|
247
|
-
if (encryptThenSign) {
|
|
248
|
-
//need to decode it
|
|
249
|
-
rawSamlResponse = utility.base64Decode(context);
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
return Promise.resolve({ id, context });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
//sign after encrypting
|
|
256
|
-
if (encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
|
|
257
|
-
// @ts-ignore
|
|
258
|
-
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
259
|
-
...config,
|
|
260
|
-
rawSamlMessage: rawSamlResponse,
|
|
261
|
-
isMessageSigned: true,
|
|
262
|
-
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
263
|
-
signatureConfig: spSetting.signatureConfig || {
|
|
264
|
-
prefix: 'ds',
|
|
265
|
-
location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Issuer']", action: 'after' },
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
return Promise.resolve({
|
|
270
|
-
id,
|
|
271
|
-
context: utility.base64Encode(rawSamlResponse),
|
|
272
|
-
});
|
|
171
|
+
if (!metadata.idp || !metadata.sp) {
|
|
172
|
+
throw new Error('ERR_GENERATE_ARTIFACT_MISSING_METADATA');
|
|
273
173
|
}
|
|
274
|
-
|
|
174
|
+
// Generate the base SAML Response using POST binding logic
|
|
175
|
+
const samlResponseResult = await postBinding.base64LoginResponse(params);
|
|
176
|
+
const samlResponseXml = utility.base64Decode(samlResponseResult.context);
|
|
177
|
+
// Generate the SAML 2.0 Artifact ID
|
|
178
|
+
const artifactId = generateArtifactId(metadata.idp.getEntityID());
|
|
179
|
+
// Prepare config for SOAP signing
|
|
180
|
+
const idpSetting = entity.idp.entitySetting;
|
|
181
|
+
const spSetting = entity.sp.entitySetting;
|
|
182
|
+
const config = {
|
|
183
|
+
privateKey: idpSetting.privateKey,
|
|
184
|
+
privateKeyPass: idpSetting.privateKeyPass,
|
|
185
|
+
signatureAlgorithm: idpSetting.requestSignatureAlgorithm,
|
|
186
|
+
signingCert: metadata.idp.getX509Certificate('signing'),
|
|
187
|
+
isBase64Output: false,
|
|
188
|
+
};
|
|
189
|
+
// Construct the SOAP Envelope containing the ArtifactResponse with SAML Response
|
|
190
|
+
const soapBodyContent = `<samlp:ArtifactResponse
|
|
191
|
+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
|
192
|
+
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
|
193
|
+
ID="${samlResponseResult.id}_artifact_resp"
|
|
194
|
+
InResponseTo="${params.requestInfo?.extract?.request?.id || ''}"
|
|
195
|
+
Version="2.0"
|
|
196
|
+
IssueInstant="${new Date().toISOString()}">
|
|
197
|
+
<saml2:Issuer>${metadata.idp.getEntityID()}</saml2:Issuer>
|
|
198
|
+
<samlp:Status>
|
|
199
|
+
<samlp:StatusCode Value="${StatusCode.Success}"/>
|
|
200
|
+
</samlp:Status>
|
|
201
|
+
${samlResponseXml}
|
|
202
|
+
</samlp:ArtifactResponse>`;
|
|
203
|
+
const soapEnvelope = `
|
|
204
|
+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
205
|
+
<soap:Header/>
|
|
206
|
+
<soap:Body>${soapBodyContent}</soap:Body>
|
|
207
|
+
</soap:Envelope>`;
|
|
208
|
+
// Sign the SOAP Envelope
|
|
209
|
+
const signedSoapEnvelope = libsaml.constructSAMLSignature({
|
|
210
|
+
...config,
|
|
211
|
+
rawSamlMessage: soapEnvelope,
|
|
212
|
+
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
213
|
+
isMessageSigned: true,
|
|
214
|
+
referenceTagXPath: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']",
|
|
215
|
+
signatureConfig: {
|
|
216
|
+
prefix: 'ds',
|
|
217
|
+
location: {
|
|
218
|
+
reference: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']/*[local-name(.)='Status']",
|
|
219
|
+
action: 'before'
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
// Return the Artifact ID and the Base64 encoded signed SOAP message
|
|
224
|
+
return {
|
|
225
|
+
id: artifactId,
|
|
226
|
+
context: utility.base64Encode(signedSoapEnvelope),
|
|
227
|
+
};
|
|
275
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* @desc Parse and validate Artifact Resolve request
|
|
231
|
+
* @param {object} params
|
|
232
|
+
* @param {IdentityProvider} params.idp Identity Provider instance
|
|
233
|
+
* @param {ServiceProvider} params.sp Service Provider instance
|
|
234
|
+
* @param {string} params.xml SOAP request XML string
|
|
235
|
+
* @returns {Promise}
|
|
236
|
+
*/
|
|
276
237
|
async function parseLoginRequestResolve(params) {
|
|
277
|
-
|
|
238
|
+
const { idp, sp, xml } = params;
|
|
278
239
|
const verificationOptions = {
|
|
279
240
|
metadata: idp.entityMeta,
|
|
280
241
|
signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
|
|
281
242
|
};
|
|
282
|
-
|
|
243
|
+
// Validate XML
|
|
244
|
+
let res = await libsaml.isValidXml(xml, true).catch(() => {
|
|
283
245
|
return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
|
|
284
246
|
});
|
|
285
247
|
if (res !== true) {
|
|
286
248
|
return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
|
|
287
249
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
let [verify, xmlString, isEncrypted, noSignature] = await libsamlSoap.verifyAndDecryptSoapMessage(xml, verificationOptions);
|
|
250
|
+
// Verify and decrypt SOAP message
|
|
251
|
+
let [verify, xmlString] = await libsamlSoap.verifyAndDecryptSoapMessage(xml, verificationOptions);
|
|
291
252
|
if (!verify) {
|
|
292
253
|
return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE');
|
|
293
254
|
}
|
|
@@ -295,25 +256,32 @@ async function parseLoginRequestResolve(params) {
|
|
|
295
256
|
samlContent: xmlString,
|
|
296
257
|
extract: extract(xmlString, artifactResolveFields),
|
|
297
258
|
};
|
|
298
|
-
|
|
299
|
-
* Validation part: validate the context of response after signature is verified and decrypted (optional)
|
|
300
|
-
*/
|
|
259
|
+
// Validation
|
|
301
260
|
const targetEntityMetadata = sp.entityMeta;
|
|
302
261
|
const issuer = targetEntityMetadata.getEntityID();
|
|
303
262
|
const extractedProperties = parseResult.extract;
|
|
304
|
-
//
|
|
263
|
+
// Check issuer
|
|
305
264
|
if (extractedProperties.issuer !== issuer) {
|
|
306
265
|
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
307
266
|
}
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
267
|
+
// Check time validity (5 minutes from issue instant)
|
|
268
|
+
const issueInstant = new Date(extractedProperties.request.issueInstant);
|
|
269
|
+
const expiryTime = new Date(issueInstant.getTime() + 5 * 60 * 1000);
|
|
270
|
+
if (!verifyTime(undefined, expiryTime.toISOString(), sp.entitySetting.clockDrifts)) {
|
|
311
271
|
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
312
272
|
}
|
|
313
273
|
return Promise.resolve(parseResult);
|
|
314
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* @desc Parse and validate Artifact Resolve response
|
|
277
|
+
* @param {object} params
|
|
278
|
+
* @param {IdentityProvider} params.idp Identity Provider instance
|
|
279
|
+
* @param {ServiceProvider} params.sp Service Provider instance
|
|
280
|
+
* @param {string} params.art Artifact string
|
|
281
|
+
* @returns {Promise}
|
|
282
|
+
*/
|
|
315
283
|
async function parseLoginResponseResolve(params) {
|
|
316
|
-
|
|
284
|
+
const { idp, sp, art } = params;
|
|
317
285
|
const metadata = {
|
|
318
286
|
idp: idp.entityMeta,
|
|
319
287
|
sp: sp.entityMeta,
|
|
@@ -322,14 +290,11 @@ async function parseLoginResponseResolve(params) {
|
|
|
322
290
|
metadata: idp.entityMeta,
|
|
323
291
|
signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
|
|
324
292
|
};
|
|
325
|
-
|
|
326
|
-
/** 断言是否加密应根据响应里面的字段判断*/
|
|
327
|
-
let decryptRequired = idp.entitySetting.isAssertionEncrypted;
|
|
328
|
-
let extractorFields = [];
|
|
329
|
-
let samlContent = '';
|
|
293
|
+
const parserType = ParserType.SAMLResponse;
|
|
330
294
|
const spSetting = sp.entitySetting;
|
|
331
|
-
|
|
332
|
-
|
|
295
|
+
const ID = '_' + randomUUID();
|
|
296
|
+
const url = metadata.idp.getArtifactResolutionService('soap');
|
|
297
|
+
// Construct ArtifactResolve request
|
|
333
298
|
let samlSoapRaw = libsaml.replaceTagsByValue(libsaml.defaultArtifactResolveTemplate.context, {
|
|
334
299
|
ID: ID,
|
|
335
300
|
Destination: url,
|
|
@@ -337,31 +302,36 @@ async function parseLoginResponseResolve(params) {
|
|
|
337
302
|
IssueInstant: new Date().toISOString(),
|
|
338
303
|
Art: art
|
|
339
304
|
});
|
|
305
|
+
let samlContent;
|
|
306
|
+
// Send signed or unsigned ArtifactResolve based on IdP configuration
|
|
340
307
|
if (!metadata.idp.isWantAuthnRequestsSigned()) {
|
|
341
308
|
samlContent = await sendArtifactResolve(url, samlSoapRaw);
|
|
342
|
-
//
|
|
343
|
-
// validate the xml
|
|
309
|
+
// Validate XML
|
|
344
310
|
try {
|
|
345
311
|
await libsaml.isValidXml(samlContent, true);
|
|
346
312
|
}
|
|
347
|
-
catch
|
|
313
|
+
catch {
|
|
348
314
|
return Promise.reject('ERR_INVALID_XML');
|
|
349
315
|
}
|
|
350
316
|
await checkStatus(samlContent, parserType, true);
|
|
317
|
+
// Verify and decrypt SOAP response
|
|
318
|
+
const [verified, verifiedAssertionNode, isDecryptRequired] = await libsamlSoap.verifyAndDecryptSoapMessage(samlContent, verificationOptions);
|
|
319
|
+
if (!verified) {
|
|
320
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
321
|
+
}
|
|
322
|
+
samlContent = verifiedAssertionNode;
|
|
351
323
|
}
|
|
352
|
-
|
|
324
|
+
else {
|
|
353
325
|
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
|
|
354
|
-
|
|
355
|
-
|
|
326
|
+
// Sign the ArtifactResolve request
|
|
327
|
+
const signatureSoap = libsaml.constructSAMLSignature({
|
|
356
328
|
referenceTagXPath: "//*[local-name(.)='ArtifactResolve']",
|
|
357
329
|
isMessageSigned: false,
|
|
358
330
|
isBase64Output: false,
|
|
359
331
|
transformationAlgorithms: transformationAlgorithms,
|
|
360
|
-
|
|
361
|
-
privateKey,
|
|
332
|
+
privateKey: privateKey,
|
|
362
333
|
privateKeyPass,
|
|
363
|
-
|
|
364
|
-
signatureAlgorithm,
|
|
334
|
+
signatureAlgorithm: signatureAlgorithm,
|
|
365
335
|
rawSamlMessage: samlSoapRaw,
|
|
366
336
|
signingCert: metadata.sp.getX509Certificate('signing'),
|
|
367
337
|
signatureConfig: {
|
|
@@ -373,26 +343,23 @@ async function parseLoginResponseResolve(params) {
|
|
|
373
343
|
}
|
|
374
344
|
});
|
|
375
345
|
samlContent = await sendArtifactResolve(url, signatureSoap);
|
|
376
|
-
//
|
|
377
|
-
// validate the xml
|
|
346
|
+
// Validate XML
|
|
378
347
|
try {
|
|
379
348
|
await libsaml.isValidXml(samlContent, true);
|
|
380
349
|
}
|
|
381
|
-
catch
|
|
350
|
+
catch {
|
|
382
351
|
return Promise.reject('ERR_INVALID_XML');
|
|
383
352
|
}
|
|
384
353
|
await checkStatus(samlContent, parserType, true);
|
|
385
|
-
|
|
386
|
-
|
|
354
|
+
// Verify and decrypt SOAP response
|
|
355
|
+
const [verified1, verifiedAssertionNode1, isDecryptRequired1] = await libsamlSoap.verifyAndDecryptSoapMessage(samlContent, verificationOptions);
|
|
387
356
|
if (!verified1) {
|
|
388
357
|
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
389
358
|
}
|
|
390
359
|
samlContent = verifiedAssertionNode1;
|
|
391
|
-
//
|
|
392
|
-
const verificationResult = await libsaml.verifySignature(samlContent, verificationOptions,
|
|
393
|
-
// 检查验证结果
|
|
360
|
+
// Verify SAML signature and decrypt if needed
|
|
361
|
+
const verificationResult = await libsaml.verifySignature(samlContent, verificationOptions, sp);
|
|
394
362
|
if (!verificationResult.status) {
|
|
395
|
-
// 如果验证失败,根据具体情况返回错误
|
|
396
363
|
if (verificationResult.isMessageSigned && !verificationResult.MessageSignatureStatus) {
|
|
397
364
|
return Promise.reject('ERR_FAIL_TO_VERIFY_MESSAGE_SIGNATURE');
|
|
398
365
|
}
|
|
@@ -402,109 +369,51 @@ async function parseLoginResponseResolve(params) {
|
|
|
402
369
|
if (verificationResult.encrypted && !verificationResult.decrypted) {
|
|
403
370
|
return Promise.reject('ERR_FAIL_TO_DECRYPT_ASSERTION');
|
|
404
371
|
}
|
|
405
|
-
// 通用验证失败
|
|
406
372
|
return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
|
|
407
373
|
}
|
|
408
|
-
// 更新samlContent为验证后的版本(可能已解密)
|
|
409
374
|
samlContent = verificationResult.samlContent;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const parseResult = {
|
|
419
|
-
samlContent: samlContent,
|
|
420
|
-
extract: extract(samlContent, extractorFields),
|
|
421
|
-
};
|
|
422
|
-
/**
|
|
423
|
-
* Validation part: validate the context of response after signature is verified and decrypted (optional)
|
|
424
|
-
*/
|
|
425
|
-
const targetEntityMetadata = idp.entityMeta;
|
|
426
|
-
const issuer = targetEntityMetadata.getEntityID();
|
|
427
|
-
const extractedProperties = parseResult.extract;
|
|
428
|
-
// unmatched issuer
|
|
429
|
-
if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
|
|
430
|
-
&& extractedProperties
|
|
431
|
-
&& extractedProperties.issuer !== issuer) {
|
|
432
|
-
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
433
|
-
}
|
|
434
|
-
// invalid session time
|
|
435
|
-
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
436
|
-
if (parserType === 'SAMLResponse'
|
|
437
|
-
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
438
|
-
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, sp.entitySetting.clockDrifts)) {
|
|
439
|
-
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
440
|
-
}
|
|
441
|
-
// invalid time
|
|
442
|
-
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
443
|
-
if (parserType === 'SAMLResponse'
|
|
444
|
-
&& extractedProperties.conditions
|
|
445
|
-
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, sp.entitySetting.clockDrifts)) {
|
|
446
|
-
return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
|
|
447
|
-
}
|
|
448
|
-
return Promise.resolve(parseResult);
|
|
375
|
+
}
|
|
376
|
+
// Extract fields
|
|
377
|
+
let extractorFields;
|
|
378
|
+
if (parserType === 'SAMLResponse') {
|
|
379
|
+
extractorFields = getDefaultExtractorFields(parserType, samlContent);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
extractorFields = getDefaultExtractorFields(parserType, null);
|
|
449
383
|
}
|
|
450
384
|
const parseResult = {
|
|
451
385
|
samlContent: samlContent,
|
|
452
386
|
extract: extract(samlContent, extractorFields),
|
|
453
387
|
};
|
|
454
|
-
|
|
455
|
-
* Validation part: validate the context of response after signature is verified and decrypted (optional)
|
|
456
|
-
*/
|
|
388
|
+
// Validation
|
|
457
389
|
const targetEntityMetadata = idp.entityMeta;
|
|
458
390
|
const issuer = targetEntityMetadata.getEntityID();
|
|
459
391
|
const extractedProperties = parseResult.extract;
|
|
460
|
-
//
|
|
461
|
-
if (
|
|
392
|
+
// Check issuer
|
|
393
|
+
if (parserType === ParserType.SAMLResponse
|
|
462
394
|
&& extractedProperties
|
|
463
395
|
&& extractedProperties.issuer !== issuer) {
|
|
464
396
|
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
465
397
|
}
|
|
466
|
-
//
|
|
467
|
-
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
398
|
+
// Check session time
|
|
468
399
|
if (parserType === 'SAMLResponse'
|
|
469
400
|
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
470
401
|
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, sp.entitySetting.clockDrifts)) {
|
|
471
402
|
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
472
403
|
}
|
|
473
|
-
//
|
|
474
|
-
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
404
|
+
// Check conditions time
|
|
475
405
|
if (parserType === 'SAMLResponse'
|
|
476
406
|
&& extractedProperties.conditions
|
|
477
407
|
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, sp.entitySetting.clockDrifts)) {
|
|
478
408
|
return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
|
|
479
409
|
}
|
|
480
|
-
//valid destination
|
|
481
|
-
//There is no validation of the response here. The upper-layer application
|
|
482
|
-
// should verify the result by itself to see if the destination is equal to the SP acs and
|
|
483
|
-
// whether the response.id is used to prevent replay attacks.
|
|
484
|
-
/*
|
|
485
|
-
let destination = extractedProperties?.response?.destination
|
|
486
|
-
let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
|
|
487
|
-
return item?.Location === destination
|
|
488
|
-
})
|
|
489
|
-
if (isExit?.length === 0) {
|
|
490
|
-
return Promise.reject('ERR_Destination_URL');
|
|
491
|
-
}
|
|
492
|
-
if (parserType === 'SAMLResponse') {
|
|
493
|
-
let destination = extractedProperties?.response?.destination
|
|
494
|
-
let isExit = self.entitySetting?.assertionConsumerService?.filter((item: { Location: any; }) => {
|
|
495
|
-
return item?.Location === destination
|
|
496
|
-
})
|
|
497
|
-
if (isExit?.length === 0) {
|
|
498
|
-
return Promise.reject('ERR_Destination_URL');
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
*/
|
|
502
410
|
return Promise.resolve(parseResult);
|
|
503
411
|
}
|
|
504
|
-
const
|
|
505
|
-
parseLoginRequestResolve,
|
|
412
|
+
const artifactBinding = {
|
|
506
413
|
soapLoginRequest,
|
|
507
|
-
parseLoginResponseResolve,
|
|
508
414
|
soapLoginResponse,
|
|
415
|
+
parseLoginRequestResolve,
|
|
416
|
+
parseLoginResponseResolve,
|
|
417
|
+
generateArtifactId,
|
|
509
418
|
};
|
|
510
|
-
export default
|
|
419
|
+
export default artifactBinding;
|