scimgateway 6.2.5 → 6.2.7
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/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/bun.lock +95 -91
- package/config/plugin-entra-id.json +616 -616
- package/lib/azure-license-mapping.json +17 -0
- package/lib/helper-rest.ts +102 -51
- package/lib/plugin-entra-id.ts +43 -40
- package/lib/saml/LICENSE +21 -0
- package/lib/saml/README.md +66 -0
- package/lib/saml/index.js +2 -0
- package/lib/saml/saml11.js +197 -0
- package/lib/saml/saml20.js +205 -0
- package/lib/saml/utils.js +94 -0
- package/lib/saml/xml/encrypt.js +56 -0
- package/lib/saml/xml/sign.js +104 -0
- package/lib/samlAssertion.ts +1 -2
- package/lib/scimgateway.ts +2 -0
- package/package.json +13 -13
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
var utils = require('./utils');
|
|
2
|
+
var Parser = require('@xmldom/xmldom').DOMParser;
|
|
3
|
+
var xmlenc = require('xml-encryption');
|
|
4
|
+
var moment = require('moment');
|
|
5
|
+
var async = require('async');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
var EncryptXml = require('./xml/encrypt');
|
|
9
|
+
var SignXml = require('./xml/sign');
|
|
10
|
+
|
|
11
|
+
var saml11Template = '<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" MajorVersion="1" MinorVersion="1" AssertionID="" IssueInstant="">' +
|
|
12
|
+
'<saml:Conditions>' +
|
|
13
|
+
'<saml:AudienceRestrictionCondition />' +
|
|
14
|
+
'</saml:Conditions>' +
|
|
15
|
+
'<saml:AttributeStatement>' +
|
|
16
|
+
'<saml:Subject>' +
|
|
17
|
+
'<saml:NameIdentifier Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" />' +
|
|
18
|
+
'<saml:SubjectConfirmation>' +
|
|
19
|
+
'<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>' +
|
|
20
|
+
'</saml:SubjectConfirmation>' +
|
|
21
|
+
'</saml:Subject>' +
|
|
22
|
+
'</saml:AttributeStatement>' +
|
|
23
|
+
'<saml:AuthenticationStatement AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password">' +
|
|
24
|
+
'<saml:Subject>' +
|
|
25
|
+
'<saml:NameIdentifier Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">' +
|
|
26
|
+
'</saml:NameIdentifier>' +
|
|
27
|
+
'<saml:SubjectConfirmation>' +
|
|
28
|
+
'<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>' +
|
|
29
|
+
'</saml:SubjectConfirmation>' +
|
|
30
|
+
'</saml:Subject>' +
|
|
31
|
+
'</saml:AuthenticationStatement>' +
|
|
32
|
+
'</saml:Assertion>';
|
|
33
|
+
|
|
34
|
+
var newSaml11Document = utils.factoryForNode(saml11Template);
|
|
35
|
+
|
|
36
|
+
var NAMESPACE = 'urn:oasis:names:tc:SAML:1.0:assertion';
|
|
37
|
+
|
|
38
|
+
function extractSaml11Options(opts) {
|
|
39
|
+
return {
|
|
40
|
+
uid: opts.uid,
|
|
41
|
+
issuer: opts.issuer,
|
|
42
|
+
lifetimeInSeconds: opts.lifetimeInSeconds,
|
|
43
|
+
audiences: opts.audiences,
|
|
44
|
+
attributes: opts.attributes,
|
|
45
|
+
nameIdentifier: opts.nameIdentifier,
|
|
46
|
+
nameIdentifierFormat: opts.nameIdentifierFormat,
|
|
47
|
+
subjectConfirmationMethod: opts.subjectConfirmationMethod,
|
|
48
|
+
holderOfKeyProofSecret: opts.holderOfKeyProofSecret,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates a signed SAML 1.1 assertion from the given options.
|
|
54
|
+
*/
|
|
55
|
+
exports.create = function(options, callback) {
|
|
56
|
+
return createAssertion(extractSaml11Options(options), {
|
|
57
|
+
signXml: SignXml.fromSignXmlOptions(Object.assign({
|
|
58
|
+
xpathToNodeBeforeSignature: "//*[local-name(.)='AuthenticationStatement']",
|
|
59
|
+
signatureIdAttribute: 'AssertionID'
|
|
60
|
+
}, options)),
|
|
61
|
+
encryptXml: EncryptXml.fromEncryptXmlOptions(options)
|
|
62
|
+
}, callback);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates an unsigned SAML 1.1 assertion from the given options.
|
|
67
|
+
*/
|
|
68
|
+
exports.createUnsignedAssertion = function(options, callback) {
|
|
69
|
+
return createAssertion(extractSaml11Options(options), {
|
|
70
|
+
signXml: SignXml.unsigned,
|
|
71
|
+
encryptXml: EncryptXml.fromEncryptXmlOptions(options)
|
|
72
|
+
}, callback);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createAssertion(options, strategies, callback) {
|
|
76
|
+
var doc;
|
|
77
|
+
try {
|
|
78
|
+
doc = newSaml11Document();
|
|
79
|
+
} catch(err){
|
|
80
|
+
return utils.reportError(err, callback);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
doc.documentElement.setAttribute('AssertionID', '_' + (options.uid || utils.uid(32)));
|
|
84
|
+
if (options.issuer)
|
|
85
|
+
doc.documentElement.setAttribute('Issuer', options.issuer);
|
|
86
|
+
|
|
87
|
+
var now = moment.utc();
|
|
88
|
+
doc.documentElement.setAttribute('IssueInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
|
89
|
+
var conditions = doc.documentElement.getElementsByTagName('saml:Conditions');
|
|
90
|
+
|
|
91
|
+
if (options.lifetimeInSeconds) {
|
|
92
|
+
conditions[0].setAttribute('NotBefore', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
|
93
|
+
conditions[0].setAttribute('NotOnOrAfter', moment(now).add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (options.audiences) {
|
|
97
|
+
var audiences = options.audiences instanceof Array ? options.audiences : [options.audiences];
|
|
98
|
+
audiences.forEach(function (audience) {
|
|
99
|
+
var element = doc.createElementNS(NAMESPACE, 'saml:Audience');
|
|
100
|
+
element.textContent = audience;
|
|
101
|
+
var audienceCondition = conditions[0].getElementsByTagNameNS(NAMESPACE, 'AudienceRestrictionCondition')[0];
|
|
102
|
+
audienceCondition.appendChild(element);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (options.attributes) {
|
|
107
|
+
var statement = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'AttributeStatement')[0];
|
|
108
|
+
Object.keys(options.attributes).forEach(function(prop) {
|
|
109
|
+
if(typeof options.attributes[prop] === 'undefined') return;
|
|
110
|
+
|
|
111
|
+
var name = prop.indexOf('/') > -1 ? prop.substring(prop.lastIndexOf('/') + 1) : prop;
|
|
112
|
+
var namespace = prop.indexOf('/') > -1 ? prop.substring(0, prop.lastIndexOf('/')) : '';
|
|
113
|
+
var attributeElement = doc.createElementNS(NAMESPACE, 'saml:Attribute');
|
|
114
|
+
attributeElement.setAttribute('AttributeNamespace', namespace);
|
|
115
|
+
attributeElement.setAttribute('AttributeName', name);
|
|
116
|
+
var values = options.attributes[prop] instanceof Array ? options.attributes[prop] : [options.attributes[prop]];
|
|
117
|
+
values.forEach(function (value) {
|
|
118
|
+
var valueElement = doc.createElementNS(NAMESPACE, 'saml:AttributeValue');
|
|
119
|
+
valueElement.textContent = value;
|
|
120
|
+
attributeElement.appendChild(valueElement);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (values && values.length > 0) {
|
|
124
|
+
statement.appendChild(attributeElement);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
doc.getElementsByTagName('saml:AuthenticationStatement')[0]
|
|
130
|
+
.setAttribute('AuthenticationInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
|
131
|
+
|
|
132
|
+
var nameID = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'NameIdentifier')[0];
|
|
133
|
+
|
|
134
|
+
if (options.nameIdentifier) {
|
|
135
|
+
nameID.textContent = options.nameIdentifier;
|
|
136
|
+
|
|
137
|
+
doc.getElementsByTagName('saml:AuthenticationStatement')[0]
|
|
138
|
+
.getElementsByTagName('saml:NameIdentifier')[0]
|
|
139
|
+
.textContent = options.nameIdentifier;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (options.nameIdentifierFormat) {
|
|
143
|
+
var nameIDs = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'NameIdentifier');
|
|
144
|
+
nameIDs[0].setAttribute('Format', options.nameIdentifierFormat);
|
|
145
|
+
nameIDs[1].setAttribute('Format', options.nameIdentifierFormat);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (strategies.encryptXml === EncryptXml.unencrypted) {
|
|
149
|
+
var signed = strategies.signXml(doc);
|
|
150
|
+
return strategies.encryptXml(signed, callback);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// encryption is turned on,
|
|
154
|
+
var proofSecret;
|
|
155
|
+
async.waterfall([
|
|
156
|
+
function (cb) {
|
|
157
|
+
if (!options.subjectConfirmationMethod && options.subjectConfirmationMethod !== 'holder-of-key')
|
|
158
|
+
return cb();
|
|
159
|
+
|
|
160
|
+
crypto.randomBytes(32, function(err, randomBytes) {
|
|
161
|
+
proofSecret = randomBytes;
|
|
162
|
+
addSubjectConfirmation(strategies.encryptXml.encryptOptions, doc, options.holderOfKeyProofSecret || randomBytes, cb);
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
function(cb) {
|
|
166
|
+
strategies.signXml(doc, cb);
|
|
167
|
+
},
|
|
168
|
+
function(signed, cb) {
|
|
169
|
+
strategies.encryptXml(signed, cb);
|
|
170
|
+
}
|
|
171
|
+
], function (err, result) {
|
|
172
|
+
if (err) return callback(err);
|
|
173
|
+
callback(null, result, proofSecret);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function addSubjectConfirmation(encryptOptions, doc, randomBytes, callback) {
|
|
178
|
+
xmlenc.encryptKeyInfo(randomBytes, encryptOptions, function(err, keyinfo) {
|
|
179
|
+
if (err) return callback(err);
|
|
180
|
+
var subjectConfirmationNodes = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'SubjectConfirmation');
|
|
181
|
+
|
|
182
|
+
for (var i=0; i<subjectConfirmationNodes.length; i++) {
|
|
183
|
+
var keyinfoDom;
|
|
184
|
+
try {
|
|
185
|
+
keyinfoDom = new Parser().parseFromString(keyinfo);
|
|
186
|
+
} catch(error){
|
|
187
|
+
return utils.reportError(error, callback);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
var method = subjectConfirmationNodes[i].getElementsByTagNameNS(NAMESPACE, 'ConfirmationMethod')[0];
|
|
191
|
+
method.textContent = 'urn:oasis:names:tc:SAML:1.0:cm:holder-of-key';
|
|
192
|
+
subjectConfirmationNodes[i].appendChild(keyinfoDom.documentElement);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
callback();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
var async = require('async');
|
|
2
|
+
var moment = require('moment');
|
|
3
|
+
var xmlNameValidator = require('xml-name-validator');
|
|
4
|
+
var is_uri = require('valid-url').is_uri;
|
|
5
|
+
|
|
6
|
+
var EncryptXml = require('./xml/encrypt');
|
|
7
|
+
var SignXml = require('./xml/sign');
|
|
8
|
+
var utils = require('./utils');
|
|
9
|
+
|
|
10
|
+
var saml20Template = '<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="" IssueInstant="">' +
|
|
11
|
+
'<saml:Issuer></saml:Issuer>' +
|
|
12
|
+
'<saml:Subject>' +
|
|
13
|
+
'<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" />' +
|
|
14
|
+
'<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">' +
|
|
15
|
+
'<saml:SubjectConfirmationData />' +
|
|
16
|
+
'</saml:SubjectConfirmation>' +
|
|
17
|
+
'</saml:Subject>' +
|
|
18
|
+
'<saml:Conditions />' +
|
|
19
|
+
'<saml:AuthnStatement AuthnInstant="">' +
|
|
20
|
+
'<saml:AuthnContext>' +
|
|
21
|
+
'<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>' +
|
|
22
|
+
'</saml:AuthnContext>' +
|
|
23
|
+
'</saml:AuthnStatement>' +
|
|
24
|
+
'</saml:Assertion>';
|
|
25
|
+
|
|
26
|
+
var newSaml20Document = utils.factoryForNode(saml20Template);
|
|
27
|
+
|
|
28
|
+
var NAMESPACE = 'urn:oasis:names:tc:SAML:2.0:assertion';
|
|
29
|
+
|
|
30
|
+
function getAttributeType(value){
|
|
31
|
+
switch(typeof value) {
|
|
32
|
+
case "string":
|
|
33
|
+
return 'xs:string';
|
|
34
|
+
case "boolean":
|
|
35
|
+
return 'xs:boolean';
|
|
36
|
+
case "number":
|
|
37
|
+
return 'xs:double';
|
|
38
|
+
default:
|
|
39
|
+
return 'xs:anyType';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getNameFormat(name){
|
|
44
|
+
if (is_uri(name)){
|
|
45
|
+
return 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (xmlNameValidator.name(name).success){
|
|
49
|
+
return 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractSaml20Options(opts) {
|
|
56
|
+
return {
|
|
57
|
+
uid: opts.uid,
|
|
58
|
+
issuer: opts.issuer,
|
|
59
|
+
lifetimeInSeconds: opts.lifetimeInSeconds,
|
|
60
|
+
audiences: opts.audiences,
|
|
61
|
+
recipient: opts.recipient,
|
|
62
|
+
inResponseTo: opts.inResponseTo,
|
|
63
|
+
attributes: opts.attributes,
|
|
64
|
+
includeAttributeNameFormat: (typeof opts.includeAttributeNameFormat !== 'undefined') ? opts.includeAttributeNameFormat : true,
|
|
65
|
+
typedAttributes: (typeof opts.typedAttributes !== 'undefined') ? opts.typedAttributes : true,
|
|
66
|
+
sessionIndex: opts.sessionIndex,
|
|
67
|
+
nameIdentifier: opts.nameIdentifier,
|
|
68
|
+
nameIdentifierFormat: opts.nameIdentifierFormat,
|
|
69
|
+
authnContextClassRef: opts.authnContextClassRef
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a signed SAML 2.0 assertion from the given options.
|
|
75
|
+
*/
|
|
76
|
+
exports.create = function createSignedAssertion(options, callback) {
|
|
77
|
+
return createAssertion(extractSaml20Options(options), {
|
|
78
|
+
signXml: SignXml.fromSignXmlOptions(Object.assign({
|
|
79
|
+
xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']",
|
|
80
|
+
signatureIdAttribute: 'ID'
|
|
81
|
+
}, options)),
|
|
82
|
+
encryptXml: EncryptXml.fromEncryptXmlOptions(options)
|
|
83
|
+
}, callback);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates an unsigned SAML 2.0 assertion from the given options.
|
|
88
|
+
*/
|
|
89
|
+
exports.createUnsignedAssertion = function createUnsignedAssertion(options, callback) {
|
|
90
|
+
return createAssertion(extractSaml20Options(options), {
|
|
91
|
+
signXml: SignXml.unsigned,
|
|
92
|
+
encryptXml: EncryptXml.fromEncryptXmlOptions(options)
|
|
93
|
+
}, callback);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function createAssertion(options, strategies, callback) {
|
|
97
|
+
var doc = newSaml20Document();
|
|
98
|
+
|
|
99
|
+
doc.documentElement.setAttribute('ID', '_' + (options.uid || utils.uid(32)));
|
|
100
|
+
if (options.issuer) {
|
|
101
|
+
var issuer = doc.documentElement.getElementsByTagName('saml:Issuer');
|
|
102
|
+
issuer[0].textContent = options.issuer;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var now = moment.utc();
|
|
106
|
+
doc.documentElement.setAttribute('IssueInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
|
107
|
+
var conditions = doc.documentElement.getElementsByTagName('saml:Conditions');
|
|
108
|
+
var confirmationData = doc.documentElement.getElementsByTagName('saml:SubjectConfirmationData');
|
|
109
|
+
|
|
110
|
+
if (options.lifetimeInSeconds) {
|
|
111
|
+
conditions[0].setAttribute('NotBefore', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
|
112
|
+
conditions[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
|
113
|
+
|
|
114
|
+
confirmationData[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.audiences) {
|
|
118
|
+
var audienceRestriction = doc.createElementNS(NAMESPACE, 'saml:AudienceRestriction');
|
|
119
|
+
var audiences = options.audiences instanceof Array ? options.audiences : [options.audiences];
|
|
120
|
+
audiences.forEach(function (audience) {
|
|
121
|
+
var element = doc.createElementNS(NAMESPACE, 'saml:Audience');
|
|
122
|
+
element.textContent = audience;
|
|
123
|
+
audienceRestriction.appendChild(element);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
conditions[0].appendChild(audienceRestriction);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (options.recipient)
|
|
130
|
+
confirmationData[0].setAttribute('Recipient', options.recipient);
|
|
131
|
+
|
|
132
|
+
if (options.inResponseTo)
|
|
133
|
+
confirmationData[0].setAttribute('InResponseTo', options.inResponseTo);
|
|
134
|
+
|
|
135
|
+
if (options.attributes) {
|
|
136
|
+
var statement = doc.createElementNS(NAMESPACE, 'saml:AttributeStatement');
|
|
137
|
+
statement.setAttribute('xmlns:xs', 'http://www.w3.org/2001/XMLSchema');
|
|
138
|
+
statement.setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
|
|
139
|
+
doc.documentElement.appendChild(statement);
|
|
140
|
+
Object.keys(options.attributes).forEach(function(prop) {
|
|
141
|
+
if(typeof options.attributes[prop] === 'undefined') return;
|
|
142
|
+
|
|
143
|
+
var attributeElement = doc.createElementNS(NAMESPACE, 'saml:Attribute');
|
|
144
|
+
attributeElement.setAttribute('Name', prop);
|
|
145
|
+
|
|
146
|
+
if (options.includeAttributeNameFormat){
|
|
147
|
+
attributeElement.setAttribute('NameFormat', getNameFormat(prop));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
var values = options.attributes[prop] instanceof Array ? options.attributes[prop] : [options.attributes[prop]];
|
|
151
|
+
values.forEach(function (value) {
|
|
152
|
+
if (typeof value !== 'undefined') {
|
|
153
|
+
var valueElement = doc.createElementNS(NAMESPACE, 'saml:AttributeValue');
|
|
154
|
+
valueElement.setAttribute('xsi:type', options.typedAttributes ? getAttributeType(value) : 'xs:anyType');
|
|
155
|
+
valueElement.textContent = value;
|
|
156
|
+
attributeElement.appendChild(valueElement);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (values && values.filter(function(i){ return typeof i !== 'undefined'; }).length > 0) {
|
|
161
|
+
statement.appendChild(attributeElement);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
doc.getElementsByTagName('saml:AuthnStatement')[0]
|
|
167
|
+
.setAttribute('AuthnInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
|
168
|
+
|
|
169
|
+
if (options.sessionIndex) {
|
|
170
|
+
doc.getElementsByTagName('saml:AuthnStatement')[0]
|
|
171
|
+
.setAttribute('SessionIndex', options.sessionIndex);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
var nameID = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'NameID')[0];
|
|
175
|
+
|
|
176
|
+
if (options.nameIdentifier) {
|
|
177
|
+
nameID.textContent = options.nameIdentifier;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (options.nameIdentifierFormat) {
|
|
181
|
+
nameID.setAttribute('Format', options.nameIdentifierFormat);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if( options.authnContextClassRef ) {
|
|
185
|
+
doc.getElementsByTagName('saml:AuthnContextClassRef')[0]
|
|
186
|
+
.textContent = options.authnContextClassRef;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (strategies.encryptXml === EncryptXml.unencrypted) {
|
|
190
|
+
var signed = strategies.signXml(doc);
|
|
191
|
+
return strategies.encryptXml(signed, callback);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async.waterfall([
|
|
195
|
+
function(cb) {
|
|
196
|
+
strategies.signXml(doc, cb);
|
|
197
|
+
},
|
|
198
|
+
function(signed, cb) {
|
|
199
|
+
strategies.encryptXml(signed, cb);
|
|
200
|
+
}
|
|
201
|
+
], function (err, result) {
|
|
202
|
+
if (err) return callback(err);
|
|
203
|
+
callback(null, result);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const Parser = require('@xmldom/xmldom').DOMParser;
|
|
2
|
+
|
|
3
|
+
exports.pemToCert = function(pem) {
|
|
4
|
+
const cert = /-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/g.exec(pem.toString());
|
|
5
|
+
if (cert && cert.length > 0) {
|
|
6
|
+
return cert[1].replace(/[\n|\r\n]/g, '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
exports.reportError = function(err, callback){
|
|
13
|
+
if (callback){
|
|
14
|
+
setImmediate(function(){
|
|
15
|
+
callback(err);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return a unique identifier with the given `len`.
|
|
22
|
+
*
|
|
23
|
+
* utils.uid(10);
|
|
24
|
+
* // => "FDaS435D2z"
|
|
25
|
+
*
|
|
26
|
+
* @param {Number} len
|
|
27
|
+
* @return {String}
|
|
28
|
+
* @api private
|
|
29
|
+
*/
|
|
30
|
+
exports.uid = function(len) {
|
|
31
|
+
const buf = []
|
|
32
|
+
, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
33
|
+
, charlen = chars.length;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < len; ++i) {
|
|
36
|
+
buf.push(chars[getRandomInt(0, charlen - 1)]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return buf.join('');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
exports.removeWhitespace = function(xml) {
|
|
43
|
+
return xml
|
|
44
|
+
.replace(/\r\n/g, '')
|
|
45
|
+
.replace(/\n/g, '')
|
|
46
|
+
.replace(/>(\s*)</g, '><') //unindent
|
|
47
|
+
.trim();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return a random int, used by `utils.uid()`
|
|
52
|
+
*
|
|
53
|
+
* @param {Number} min
|
|
54
|
+
* @param {Number} max
|
|
55
|
+
* @return {Number}
|
|
56
|
+
* @api private
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
function getRandomInt(min, max) {
|
|
60
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns a function that can be called to create a new Node from an inline template string.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} templateString the XML template content
|
|
67
|
+
* @return {function(): Node}
|
|
68
|
+
*/
|
|
69
|
+
exports.factoryForNode = function factoryForNode(templateString) {
|
|
70
|
+
const prototypeDoc = new Parser().parseFromString(templateString);
|
|
71
|
+
|
|
72
|
+
return function () {
|
|
73
|
+
return prototypeDoc.cloneNode(true);
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Standardizes PEM content to match the spec (best effort)
|
|
79
|
+
*
|
|
80
|
+
* @param pem {Buffer} The PEM content to standardize
|
|
81
|
+
* @returns {Buffer} The standardized PEM. Original will be returned unmodified if the content is not PEM.
|
|
82
|
+
*/
|
|
83
|
+
exports.fixPemFormatting = function (pem) {
|
|
84
|
+
let pemEntries = pem.toString().matchAll(/([-]{5}[^-\r\n]+[-]{5})([^-]*)([-]{5}[^-\r\n]+[-]{5})/g);
|
|
85
|
+
let fixedPem = ''
|
|
86
|
+
for (const pemParts of pemEntries) {
|
|
87
|
+
fixedPem = fixedPem.concat(`${pemParts[1]}\n${pemParts[2].replaceAll(/[\r\n]/g, '')}\n${pemParts[3]}\n`)
|
|
88
|
+
}
|
|
89
|
+
if (fixedPem.length === 0) {
|
|
90
|
+
return pem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Buffer.from(fixedPem)
|
|
94
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const xmlenc = require('xml-encryption');
|
|
2
|
+
|
|
3
|
+
const utils = require('../utils');
|
|
4
|
+
|
|
5
|
+
exports.fromEncryptXmlOptions = function (options) {
|
|
6
|
+
if (!options.encryptionCert) {
|
|
7
|
+
return this.unencrypted;
|
|
8
|
+
} else {
|
|
9
|
+
const encryptOptions = {
|
|
10
|
+
rsa_pub: options.encryptionPublicKey,
|
|
11
|
+
pem: options.encryptionCert,
|
|
12
|
+
encryptionAlgorithm: options.encryptionAlgorithm || 'http://www.w3.org/2009/xmlenc11#aes256-gcm',
|
|
13
|
+
keyEncryptionAlgorithm: options.keyEncryptionAlgorithm || 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p',
|
|
14
|
+
disallowEncryptionWithInsecureAlgorithm: options?.disallowEncryptionWithInsecureAlgorithm !== false,
|
|
15
|
+
warnInsecureAlgorithm: options?.warnOnInsecureEncryptionAlgorithm !== false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// expose the encryptOptions as these are needed when adding the SubjectConfirmation
|
|
19
|
+
return Object.assign(this.encrypted(encryptOptions), { encryptOptions: encryptOptions });
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
exports.unencrypted = function (xml, callback) {
|
|
24
|
+
if (callback) {
|
|
25
|
+
return setImmediate(callback, null, xml);
|
|
26
|
+
} else {
|
|
27
|
+
return xml;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
exports.encrypted = function (encryptOptions) {
|
|
32
|
+
return function encrypt(xml, callback) {
|
|
33
|
+
xmlenc.encrypt(xml, encryptOptions, function (err, encrypted) {
|
|
34
|
+
if (err) {
|
|
35
|
+
// Attempt to fix errors and retry
|
|
36
|
+
xmlenc.encrypt(
|
|
37
|
+
xml,
|
|
38
|
+
{
|
|
39
|
+
...encryptOptions,
|
|
40
|
+
rsa_pub: utils.fixPemFormatting(encryptOptions.rsa_pub),
|
|
41
|
+
pem: utils.fixPemFormatting(encryptOptions.pem),
|
|
42
|
+
},
|
|
43
|
+
function (retryErr, retryEncrypted) {
|
|
44
|
+
if (retryErr) {
|
|
45
|
+
return callback(retryErr);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
callback(null, utils.removeWhitespace(retryEncrypted));
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
callback(null, utils.removeWhitespace(encrypted));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const utils = require('../utils');
|
|
2
|
+
const SignedXml = require('xml-crypto').SignedXml;
|
|
3
|
+
|
|
4
|
+
const algorithms = {
|
|
5
|
+
signature: {
|
|
6
|
+
'rsa-sha256': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
|
7
|
+
'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
|
|
8
|
+
},
|
|
9
|
+
digest: {
|
|
10
|
+
'sha256': 'http://www.w3.org/2001/04/xmlenc#sha256',
|
|
11
|
+
'sha1': 'http://www.w3.org/2000/09/xmldsig#sha1'
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
exports.fromSignXmlOptions = function (options) {
|
|
16
|
+
if (!options.key)
|
|
17
|
+
throw new Error('Expect a private key in pem format');
|
|
18
|
+
|
|
19
|
+
if (!options.cert)
|
|
20
|
+
throw new Error('Expect a public key cert in pem format');
|
|
21
|
+
|
|
22
|
+
if (!options.xpathToNodeBeforeSignature)
|
|
23
|
+
throw new Error('xpathToNodeBeforeSignature is required')
|
|
24
|
+
|
|
25
|
+
const key = options.key;
|
|
26
|
+
const pem = options.cert;
|
|
27
|
+
const signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256';
|
|
28
|
+
const digestAlgorithm = options.digestAlgorithm || 'sha256';
|
|
29
|
+
const signatureNamespacePrefix = (function (prefix) {
|
|
30
|
+
// 0.10.1 added prefix, but we want to name it signatureNamespacePrefix - This is just to keep supporting prefix
|
|
31
|
+
return typeof prefix === 'string' ? prefix : '';
|
|
32
|
+
})(options.signatureNamespacePrefix || options.prefix);
|
|
33
|
+
const xpathToNodeBeforeSignature = options.xpathToNodeBeforeSignature;
|
|
34
|
+
const idAttribute = options.signatureIdAttribute;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {Document} doc
|
|
38
|
+
* @param {Function} [callback]
|
|
39
|
+
* @return {string}
|
|
40
|
+
*/
|
|
41
|
+
return function signXmlDocument(doc, callback) {
|
|
42
|
+
function sign(key) {
|
|
43
|
+
const unsigned = exports.unsigned(doc);
|
|
44
|
+
const cert = utils.pemToCert(pem);
|
|
45
|
+
|
|
46
|
+
const sig = new SignedXml(null, {
|
|
47
|
+
signatureAlgorithm: algorithms.signature[signatureAlgorithm],
|
|
48
|
+
idAttribute: idAttribute
|
|
49
|
+
});
|
|
50
|
+
sig.addReference("//*[local-name(.)='Assertion']",
|
|
51
|
+
["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"],
|
|
52
|
+
algorithms.digest[digestAlgorithm]);
|
|
53
|
+
|
|
54
|
+
sig.signingKey = key;
|
|
55
|
+
|
|
56
|
+
sig.keyInfoProvider = {
|
|
57
|
+
getKeyInfo: function (key, prefix) {
|
|
58
|
+
prefix = prefix ? prefix + ':' : prefix;
|
|
59
|
+
return "<" + prefix + "X509Data><" + prefix + "X509Certificate>" + cert + "</" + prefix + "X509Certificate></" + prefix + "X509Data>";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
sig.computeSignature(unsigned, {
|
|
64
|
+
location: {reference: xpathToNodeBeforeSignature, action: 'after'},
|
|
65
|
+
prefix: signatureNamespacePrefix
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return sig.getSignedXml();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let signed
|
|
72
|
+
try {
|
|
73
|
+
try {
|
|
74
|
+
signed = sign(key)
|
|
75
|
+
} catch (err) {
|
|
76
|
+
signed = sign(utils.fixPemFormatting(key))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (callback) {
|
|
80
|
+
setImmediate(callback, null, signed);
|
|
81
|
+
} else {
|
|
82
|
+
return signed;
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
if (callback) {
|
|
86
|
+
setImmediate(callback, e)
|
|
87
|
+
}
|
|
88
|
+
throw e
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* @param {Document} doc
|
|
94
|
+
* @param {Function} [callback]
|
|
95
|
+
* @return {string}
|
|
96
|
+
*/
|
|
97
|
+
exports.unsigned = function (doc, callback) {
|
|
98
|
+
const xml = utils.removeWhitespace(doc.toString());
|
|
99
|
+
if (callback) {
|
|
100
|
+
setImmediate(callback, null, xml)
|
|
101
|
+
} else {
|
|
102
|
+
return xml;
|
|
103
|
+
}
|
|
104
|
+
}
|
package/lib/samlAssertion.ts
CHANGED
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
// SOFTWARE.
|
|
27
27
|
//
|
|
28
28
|
|
|
29
|
-
// @
|
|
30
|
-
import { Saml20 as saml } from 'saml'
|
|
29
|
+
import { Saml20 as saml } from './saml/index.js' // The `saml` entry in `package.json` must be kept — it provides the sub-dependencies (@xmldom/xmldom, moment, xml-crypto, etc.) that this local copy requires.
|
|
31
30
|
import crypto from 'node:crypto'
|
|
32
31
|
|
|
33
32
|
export const samlAssertionUtils = {
|
package/lib/scimgateway.ts
CHANGED
|
@@ -25,6 +25,8 @@ import * as utils from './utils.ts'
|
|
|
25
25
|
import * as utilsScim from './utils-scim.ts'
|
|
26
26
|
import * as stream from './scim-stream.js'
|
|
27
27
|
export * from './helper-rest.ts'
|
|
28
|
+
import LicenseData from './azure-license-mapping.json'
|
|
29
|
+
export { LicenseData }
|
|
28
30
|
// @ts-expect-error: cannot find declaration
|
|
29
31
|
import hycoPkg from 'hyco-https'
|
|
30
32
|
|