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.
@@ -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 (!permission[baseEntity]?.signInActivity) { // remove signInActivity
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)
@@ -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,2 @@
1
+ exports.Saml11 = require('./saml11');
2
+ exports.Saml20 = require('./saml20');
@@ -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
+ };