scimgateway 6.2.6 → 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 +3 -0
- package/README.md +1 -1
- package/config/plugin-entra-id.json +616 -616
- package/lib/plugin-entra-id.ts +4 -8
- 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 +1 -1
package/lib/plugin-entra-id.ts
CHANGED
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
import path from 'node:path'
|
|
77
77
|
|
|
78
78
|
// start - mandatory plugin initialization
|
|
79
|
-
import { ScimGateway, HelperRest } from 'scimgateway'
|
|
79
|
+
import { ScimGateway, HelperRest, LicenseData } from 'scimgateway'
|
|
80
80
|
const scimgateway = new ScimGateway()
|
|
81
81
|
const helper = new HelperRest(scimgateway)
|
|
82
82
|
const config = scimgateway.getConfig()
|
|
@@ -93,18 +93,14 @@ const permission: Record<string, any> = {}
|
|
|
93
93
|
|
|
94
94
|
// load Azure license mapping JSON-file having skuPartNumber and corresponding user-friendly name
|
|
95
95
|
let fs: typeof import('fs')
|
|
96
|
-
let licenseMapping: Record<string, any> =
|
|
96
|
+
let licenseMapping: Record<string, any> = LicenseData
|
|
97
97
|
async function loadLicenseMapping() {
|
|
98
98
|
try {
|
|
99
99
|
if (!fs) fs = (await import('fs'))
|
|
100
|
+
// runtime override: if file exists in pluginDir, use it instead of the embedded default
|
|
100
101
|
let mappingPath = path.join(scimgateway.pluginDir, 'azure-license-mapping.json')
|
|
101
102
|
if (fs.existsSync(mappingPath)) {
|
|
102
103
|
licenseMapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8'))
|
|
103
|
-
} else {
|
|
104
|
-
mappingPath = path.join(scimgateway.gwDir, 'azure-license-mapping.json')
|
|
105
|
-
if (fs.existsSync(mappingPath)) {
|
|
106
|
-
licenseMapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8'))
|
|
107
|
-
}
|
|
108
104
|
}
|
|
109
105
|
} catch (err) {
|
|
110
106
|
scimgateway.logDebug('plugin-entra-id', `Error loading license mapping: ${err}`)
|
|
@@ -251,7 +247,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
251
247
|
}
|
|
252
248
|
} else selectAttributes = userSelectAttributes
|
|
253
249
|
|
|
254
|
-
if (
|
|
250
|
+
if (permission[baseEntity]?.signInActivity === false) { // remove signInActivity
|
|
255
251
|
const index = selectAttributes.indexOf('signInActivity')
|
|
256
252
|
if (index > -1) {
|
|
257
253
|
selectAttributes.splice(index, 1)
|
package/lib/saml/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 Auth0, Inc. <support@auth0.com> (http://auth0.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Local `saml` Package
|
|
2
|
+
|
|
3
|
+
This folder contains a local copy of the [node-saml](https://github.com/auth0/node-saml) package (v4.0.0), originally authored by Auth0.
|
|
4
|
+
Licensed under the MIT License — see [LICENSE](./LICENSE).
|
|
5
|
+
|
|
6
|
+
## Why This Exists
|
|
7
|
+
|
|
8
|
+
When compiling scimgateway plugins to standalone binaries using `bun build --compile`, the original `saml` npm package fails at runtime on any machine other than the build machine.
|
|
9
|
+
|
|
10
|
+
**Root cause:** The original `saml` package uses `fs.readFileSync` with `__dirname`-resolved paths to load XML template files (`saml11.template` and `saml20.template`) at module initialization:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
// Original saml/lib/saml11.js
|
|
14
|
+
var newSaml11Document = utils.factoryForNode(path.join(__dirname, 'saml11.template'));
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// Original saml/lib/utils.js
|
|
19
|
+
exports.factoryForNode = function factoryForNode(pathToTemplate) {
|
|
20
|
+
const template = fs.readFileSync(pathToTemplate); // fails in compiled binary
|
|
21
|
+
...
|
|
22
|
+
};
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When Bun compiles the binary, `__dirname` gets baked in as the **absolute path from the build machine** (e.g. `/Users/xxx/.../node_modules/saml/lib/`). The `.template` files are not embedded in the binary, so the `readFileSync` call fails with `ENOENT` when the binary runs elsewhere.
|
|
26
|
+
|
|
27
|
+
## What Was Changed
|
|
28
|
+
|
|
29
|
+
Only one functional change was made — **templates are inlined as string constants** instead of being read from disk at runtime.
|
|
30
|
+
|
|
31
|
+
### `utils.js`
|
|
32
|
+
- Removed `const fs = require('fs')` — no longer needed
|
|
33
|
+
- Changed `factoryForNode(pathToTemplate)` to `factoryForNode(templateString)` — accepts an XML string directly instead of a file path
|
|
34
|
+
|
|
35
|
+
### `saml11.js` and `saml20.js`
|
|
36
|
+
- Removed `path.join(__dirname, '*.template')` file references
|
|
37
|
+
- Template XML is inlined as a string constant passed to `factoryForNode()`
|
|
38
|
+
- Removed `var path = require('path')` (no longer needed)
|
|
39
|
+
|
|
40
|
+
### `xml/encrypt.js` and `xml/sign.js`
|
|
41
|
+
- Copied unchanged from the original package
|
|
42
|
+
|
|
43
|
+
### `index.js`
|
|
44
|
+
- Copied unchanged from the original package
|
|
45
|
+
|
|
46
|
+
## Import Change
|
|
47
|
+
|
|
48
|
+
In `lib/samlAssertion.ts`:
|
|
49
|
+
```diff
|
|
50
|
+
-import { Saml20 as saml } from 'saml'
|
|
51
|
+
+import { Saml20 as saml } from './saml/index.js'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Dependencies
|
|
55
|
+
|
|
56
|
+
The sub-dependencies used by this code remain as npm packages (not copied):
|
|
57
|
+
- `@xmldom/xmldom`
|
|
58
|
+
- `moment`
|
|
59
|
+
- `xml-crypto`
|
|
60
|
+
- `xml-encryption`
|
|
61
|
+
- `valid-url`
|
|
62
|
+
- `xml-name-validator`
|
|
63
|
+
- `async`
|
|
64
|
+
- `xpath`
|
|
65
|
+
|
|
66
|
+
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.
|
|
@@ -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
|
+
};
|