scimgateway 5.0.7 → 5.0.9
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 +28 -15
- package/bun.lockb +0 -0
- package/config/plugin-api.json +2 -2
- package/config/plugin-entra-id.json +2 -2
- package/config/plugin-ldap.json +2 -2
- package/config/plugin-loki.json +2 -2
- package/config/plugin-mongodb.json +2 -2
- package/config/plugin-mssql.json +2 -2
- package/config/plugin-saphana.json +2 -2
- package/config/plugin-scim.json +2 -2
- package/config/plugin-soap.json +2 -2
- package/lib/helper-rest.ts +72 -16
- package/lib/logger.ts +1 -1
- package/lib/plugin-mssql.ts +30 -53
- package/lib/samlAssertion.ts +220 -0
- package/lib/scimgateway.ts +23 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -258,8 +258,8 @@ Below shows an example of config\plugin-saphana.json
|
|
|
258
258
|
],
|
|
259
259
|
"bearerOAuth": [
|
|
260
260
|
{
|
|
261
|
-
"
|
|
262
|
-
"
|
|
261
|
+
"clientId": null,
|
|
262
|
+
"clientSecret": null,
|
|
263
263
|
"readOnly": false,
|
|
264
264
|
"baseEntities": []
|
|
265
265
|
}
|
|
@@ -398,7 +398,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
398
398
|
|
|
399
399
|
- **auth.bearerJwt** - Array of one or more standard JWT objects. Using **secret** or **publicKey** for signature verification. publicKey should be set to the filename of public key or certificate pem-file located in `<package-root>\config\certs` or absolute path being used. Clear text secret will become encrypted when gateway is started. **options.issuer** is mandatory. Other options may also be included according to jsonwebtoken npm package definition.
|
|
400
400
|
|
|
401
|
-
- **auth.bearerOAuth** - Array of one or more Client Credentials OAuth configuration objects. **`
|
|
401
|
+
- **auth.bearerOAuth** - Array of one or more Client Credentials OAuth configuration objects. **`clientId`** and **`clientSecret`** are mandatory. clientSecret value will become encrypted when gateway is started. OAuth token request url is **/oauth/token** e.g. http://localhost:8880/oauth/token
|
|
402
402
|
|
|
403
403
|
- **auth.passThrough** - Setting **auth.passThrough.enabled=true** will bypass SCIM Gateway authentication. Gateway will instead pass ctx containing authentication header to the plugin. Plugin could then use this information for endpoint authentication and we don't have any password/token stored at the gateway. Note, this also requires plugin binary having `scimgateway.authPassThroughAllowed = true` and endpoint logic for handling/passing ctx.request.header.authorization
|
|
404
404
|
|
|
@@ -462,18 +462,18 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
462
462
|
|
|
463
463
|
Configuration notes when using default configuration oauth and tenantIdGUID - Microsoft Exchange Online (ExO):
|
|
464
464
|
|
|
465
|
-
- Entra ID application must have application permissions
|
|
466
|
-
- To prevent the sending of emails from any defined mailboxes, an ExO
|
|
465
|
+
- Entra ID application must have application permissions `Mail.Send`
|
|
466
|
+
- To prevent the sending of emails from any defined mailboxes, an ExO `ApplicationAccessPolicy` must be defined through PowerShell.
|
|
467
467
|
|
|
468
468
|
First create a mail-enabled security-group that only includes those users (mailboxes) the application is allowed to send from
|
|
469
|
-
Note,
|
|
469
|
+
Note, `mail enabled security group` cannot be created from portal, only from admin or admin.exchange console
|
|
470
470
|
|
|
471
471
|
##Connect to Exchange
|
|
472
472
|
Install-Module -Name ExchangeOnlineManagement
|
|
473
473
|
Connect-ExchangeOnline
|
|
474
474
|
|
|
475
475
|
##Create ApplicationAccessPolicy
|
|
476
|
-
New-ApplicationAccessPolicy -AppId
|
|
476
|
+
New-ApplicationAccessPolicy -AppId <AppClientID> -PolicyScopeGroupId <MailEnabledSecurityGrpId> -AccessRight RestrictAccess -Description "Restrict app to specific mailboxes"
|
|
477
477
|
|
|
478
478
|
- **stream** - See [SCIM Stream](https://elshaug.xyz/docs/scim-stream) for configuration details
|
|
479
479
|
|
|
@@ -1111,6 +1111,21 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1111
1111
|
|
|
1112
1112
|
## Change log
|
|
1113
1113
|
|
|
1114
|
+
### v5.0.9
|
|
1115
|
+
|
|
1116
|
+
[Improved]
|
|
1117
|
+
|
|
1118
|
+
- HelperRest doRequest() now support configuration auth type `oauthSamlAssertion` for OAuth SAML token assertion. Please see code editor method IntelliSense for details
|
|
1119
|
+
|
|
1120
|
+
### v5.0.8
|
|
1121
|
+
|
|
1122
|
+
[Fixed]
|
|
1123
|
+
|
|
1124
|
+
- Ensure Bun compatibility with Azure Reverse Proxy for large and long running response
|
|
1125
|
+
- HelperRest was not compatible with Node.js
|
|
1126
|
+
- plugin-mssql, some error handling should not throw an error
|
|
1127
|
+
- Configuration files updated according to the v5 configuration syntax of `scimgateway.auth.bearerOAuth` - `clientId/clientSecret` now replacing deprecated `client_id/client_secret`
|
|
1128
|
+
|
|
1114
1129
|
### v5.0.7
|
|
1115
1130
|
|
|
1116
1131
|
[Improved]
|
|
@@ -1155,9 +1170,7 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1155
1170
|
|
|
1156
1171
|
|
|
1157
1172
|
**new configuration:**
|
|
1158
|
-
Using Microsoft Exchange Online and oauth authencation which also is default and recommended by Microsoft
|
|
1159
|
-
For other mail servers and options like SMTP AUTH (basic/oauth), please see configuration description
|
|
1160
|
-
Plugin may also send mail using method scimgateway.sendMail()
|
|
1173
|
+
Using Microsoft Exchange Online and oauth authencation which also is default and recommended by Microsoft. For other mail servers and options like SMTP AUTH (basic/oauth), please see configuration description. Plugin may also send mail using method scimgateway.sendMail()
|
|
1161
1174
|
|
|
1162
1175
|
{
|
|
1163
1176
|
"scimgateway": {
|
|
@@ -1184,18 +1197,18 @@ Plugin may also send mail using method scimgateway.sendMail()
|
|
|
1184
1197
|
|
|
1185
1198
|
Configuration notes when using oauth and tenantIdGUID - Microsoft Exchange Online (ExO):
|
|
1186
1199
|
|
|
1187
|
-
- Entra ID application must have application permissions
|
|
1188
|
-
- To prevent the sending of emails from any defined mailboxes, an ExO
|
|
1200
|
+
- Entra ID application must have application permissions `Mail.Send`
|
|
1201
|
+
- To prevent the sending of emails from any defined mailboxes, an ExO `ApplicationAccessPolicy` must be defined through PowerShell.
|
|
1189
1202
|
|
|
1190
1203
|
First create a mail-enabled security-group that only includes those users (mailboxes) the application is allowed to send from
|
|
1191
|
-
Note,
|
|
1204
|
+
Note, `mail enabled security group` cannot be created from portal, only from admin or admin.exchange console
|
|
1192
1205
|
|
|
1193
1206
|
##Connect to Exchange
|
|
1194
1207
|
Install-Module -Name ExchangeOnlineManagement
|
|
1195
1208
|
Connect-ExchangeOnline
|
|
1196
1209
|
|
|
1197
1210
|
##Create ApplicationAccessPolicy
|
|
1198
|
-
New-ApplicationAccessPolicy -AppId
|
|
1211
|
+
New-ApplicationAccessPolicy -AppId <AppClientID> -PolicyScopeGroupId <MailEnabledSecurityGrpId> -AccessRight RestrictAccess -Description "Restrict app to specific mailboxes"
|
|
1199
1212
|
|
|
1200
1213
|
|
|
1201
1214
|
### v5.0.5
|
|
@@ -1308,7 +1321,7 @@ Besides going from JavaScript to TypeScript, following can be mentioned:
|
|
|
1308
1321
|
|
|
1309
1322
|
* Use scimgateway.HelperRest() for REST functionlity, also supports Auth PassThrough
|
|
1310
1323
|
* scimgateway.endpointMapper() may be used for inbound/outbound attribute mappings
|
|
1311
|
-
* In general when using TypeScript, variables should be type
|
|
1324
|
+
* In general when using TypeScript, variables should be type-defined: `let isDone: boolean = false`, `catch (err: any)`, ...
|
|
1312
1325
|
|
|
1313
1326
|
### v4.5.12
|
|
1314
1327
|
|
package/bun.lockb
CHANGED
|
Binary file
|
package/config/plugin-api.json
CHANGED
package/config/plugin-ldap.json
CHANGED
package/config/plugin-loki.json
CHANGED
package/config/plugin-mssql.json
CHANGED
package/config/plugin-scim.json
CHANGED
package/config/plugin-soap.json
CHANGED
package/lib/helper-rest.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { HttpsProxyAgent } from 'https-proxy-agent'
|
|
2
2
|
import { URL } from 'url'
|
|
3
3
|
import { Buffer } from 'node:buffer'
|
|
4
|
+
import { samlAssertion } from './samlAssertion.ts' // prereq: saml
|
|
4
5
|
import fs from 'node:fs'
|
|
5
6
|
import querystring from 'querystring'
|
|
6
7
|
import * as utils from './utils.ts'
|
|
7
|
-
import ScimGateway from 'scimgateway'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* HelperRest includes function doRequest() for
|
|
10
|
+
* HelperRest includes function doRequest() for executing REST calls
|
|
11
11
|
*/
|
|
12
12
|
export class HelperRest {
|
|
13
13
|
private lock = new utils.Lock()
|
|
14
14
|
private _serviceClient: Record<string, any> = {}
|
|
15
15
|
private config_entity: any
|
|
16
|
-
private scimgateway:
|
|
16
|
+
private scimgateway: any
|
|
17
17
|
private idleTimeout: number
|
|
18
18
|
private graphUrl = 'https://graph.microsoft.com/beta' // beta instead of 'v1.0' gives all user attributes when no $select
|
|
19
19
|
|
|
20
|
-
constructor(scimgateway:
|
|
21
|
-
if (!
|
|
20
|
+
constructor(scimgateway: any, optionalEntities?: Record<string, any>) {
|
|
21
|
+
if (!scimgateway || !scimgateway.gwName) throw new Error('HelperRest initialization error: argument scimgateway is not of type ScimGateway')
|
|
22
22
|
this.scimgateway = scimgateway
|
|
23
23
|
this.idleTimeout = (scimgateway as any)?.config?.scimgateway.idleTimeout || 120
|
|
24
24
|
this.idleTimeout = this.idleTimeout - 1
|
|
@@ -118,6 +118,31 @@ export class HelperRest {
|
|
|
118
118
|
}
|
|
119
119
|
break
|
|
120
120
|
|
|
121
|
+
case 'oauthSamlAssertion':
|
|
122
|
+
tokenUrl = this.config_entity[baseEntity].connection.auth.options.tokenUrl
|
|
123
|
+
const context = null
|
|
124
|
+
const cert = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.certificate.cert).toString()
|
|
125
|
+
const key = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.certificate.key).toString()
|
|
126
|
+
|
|
127
|
+
const issuer = `scimgateway/${this.scimgateway.pluginName}`
|
|
128
|
+
const lifetime = 3600
|
|
129
|
+
const clientId = this.config_entity[baseEntity].connection.auth.options.clientId
|
|
130
|
+
const nameId = this.config_entity[baseEntity].connection.auth.options.userId
|
|
131
|
+
const userIdentifierFormat = 'userName'
|
|
132
|
+
const tokenEndpoint = tokenUrl
|
|
133
|
+
const audience = `scimgateway/${this.scimgateway.pluginName}`
|
|
134
|
+
const delay = 1
|
|
135
|
+
|
|
136
|
+
const assertion = await samlAssertion.run(context, cert, key, issuer, lifetime, clientId, nameId, userIdentifierFormat, tokenEndpoint, audience, delay)
|
|
137
|
+
form = {
|
|
138
|
+
token_url: tokenUrl,
|
|
139
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
|
|
140
|
+
client_id: clientId,
|
|
141
|
+
company_id: this.config_entity[baseEntity].connection.auth.options.companyId,
|
|
142
|
+
assertion: assertion,
|
|
143
|
+
}
|
|
144
|
+
break
|
|
145
|
+
|
|
121
146
|
default:
|
|
122
147
|
this.lock.release()
|
|
123
148
|
throw new Error(`getAccessToken() none supported entity.${baseEntity}.connection.auth.type: '${this.config_entity[baseEntity]?.connection?.auth?.type}'`)
|
|
@@ -176,7 +201,7 @@ export class HelperRest {
|
|
|
176
201
|
* @param baseEntity baseEntity
|
|
177
202
|
* @param method GET/PATCH/PUT/DELETE
|
|
178
203
|
* @param path e.g., /Users having baseUrl from configuration added, or full url e.g. https://mycompany.com/Users
|
|
179
|
-
* @param opt optional, connection
|
|
204
|
+
* @param opt optional, connection options
|
|
180
205
|
* @param ctx optional, ctx included if using Auth PassThrough
|
|
181
206
|
* @returns client.options needed for connect
|
|
182
207
|
*/
|
|
@@ -255,7 +280,7 @@ export class HelperRest {
|
|
|
255
280
|
const err = new Error(`auth type 'oauth' - missing configuration entity.${baseEntity}.connection.auth.options.clientId/clientSecret`)
|
|
256
281
|
throw err
|
|
257
282
|
}
|
|
258
|
-
param.accessToken = await this.getAccessToken(baseEntity, ctx)
|
|
283
|
+
param.accessToken = await this.getAccessToken(baseEntity, ctx)
|
|
259
284
|
param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
|
|
260
285
|
break
|
|
261
286
|
case 'token':
|
|
@@ -263,7 +288,7 @@ export class HelperRest {
|
|
|
263
288
|
const err = new Error(`missing configuration entity.${baseEntity}.connection.auth.options.tokenUrl/password`)
|
|
264
289
|
throw err
|
|
265
290
|
}
|
|
266
|
-
param.accessToken = await this.getAccessToken(baseEntity, ctx)
|
|
291
|
+
param.accessToken = await this.getAccessToken(baseEntity, ctx)
|
|
267
292
|
param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
|
|
268
293
|
break
|
|
269
294
|
case 'bearer':
|
|
@@ -273,6 +298,15 @@ export class HelperRest {
|
|
|
273
298
|
}
|
|
274
299
|
param.options.headers['Authorization'] = 'Bearer ' + Buffer.from(this.config_entity[baseEntity].connection.auth.options.token).toString('base64')
|
|
275
300
|
break
|
|
301
|
+
case 'oauthSamlAssertion':
|
|
302
|
+
if (!this.config_entity[baseEntity]?.connection?.auth?.options?.clientId || !this.config_entity[baseEntity]?.connection?.auth?.options?.companyId
|
|
303
|
+
|| !this.config_entity[baseEntity]?.connection?.auth?.options?.certificate?.key) {
|
|
304
|
+
const err = new Error(`auth type 'oauthSamlAssertion' - missing configuration entity.${baseEntity}.connection.auth.options...`)
|
|
305
|
+
throw err
|
|
306
|
+
}
|
|
307
|
+
param.accessToken = await this.getAccessToken(baseEntity, ctx)
|
|
308
|
+
param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
|
|
309
|
+
break
|
|
276
310
|
default:
|
|
277
311
|
// no auth
|
|
278
312
|
}
|
|
@@ -426,6 +460,7 @@ export class HelperRest {
|
|
|
426
460
|
const timeout = setTimeout(() => controller.abort(), options.abortTimeout ? options.abortTimeout * 1000 : this.idleTimeout * 1000) // 120 seconds default abort timeout
|
|
427
461
|
options.signal = signal
|
|
428
462
|
const url = `${options.protocol}//${options.host}${options.port ? ':' + options.port : ''}${options.path}`
|
|
463
|
+
if (path.includes(')?$') && !options.headers['Accept-Encoding']) options.headers['Accept-Encoding'] = 'identity' // workaround for bun fetch error: "Decompression error: ShortRead" - have seen this error using OData with "<some-path>('xxx')?$expand=" or "<some-path>('xxx')?$select=" ref: https://github.com/oven-sh/bun/issues/8017
|
|
429
464
|
// execute request
|
|
430
465
|
const f = await fetch(url, options)
|
|
431
466
|
clearTimeout(timeout)
|
|
@@ -534,15 +569,20 @@ export class HelperRest {
|
|
|
534
569
|
* "entity": {
|
|
535
570
|
* "undefined": {
|
|
536
571
|
* "connection": {
|
|
537
|
-
* "baseUrls": [
|
|
572
|
+
* "baseUrls": [ // ignored when using option tenantIdGUID
|
|
538
573
|
* "<baseUrl>", // "https://host1.company.com:8880",
|
|
539
574
|
* "<baseUrl2>" // optional using several baseUrls for failover
|
|
540
575
|
* ],
|
|
541
576
|
* "auth": {
|
|
542
|
-
* "type": "<type>"
|
|
577
|
+
* "type": "<type>",
|
|
543
578
|
* "options": { <auth.options> }
|
|
544
579
|
* },
|
|
545
580
|
* "options": { <connection.options> }
|
|
581
|
+
* "proxy": {
|
|
582
|
+
* "host": "<host>", // http://proxy-host:1234
|
|
583
|
+
* "username": "<username>", // username if authentication is required
|
|
584
|
+
* "password": "<password>" // password if authentication is required
|
|
585
|
+
* }
|
|
546
586
|
* }
|
|
547
587
|
* }
|
|
548
588
|
* }
|
|
@@ -551,11 +591,11 @@ export class HelperRest {
|
|
|
551
591
|
* ```
|
|
552
592
|
* type defines authentication being used
|
|
553
593
|
* if type not defined, no authentication used
|
|
554
|
-
* valid type is: `basic`, `oauth`, `token` or `
|
|
594
|
+
* valid type is: `basic`, `oauth`, `token`, `bearer` or `oauthSamlAssertion`
|
|
555
595
|
*
|
|
556
596
|
* for each valid type there are different auth.options
|
|
557
597
|
*
|
|
558
|
-
* type=**basic
|
|
598
|
+
* type=**"basic"** having auth.options:
|
|
559
599
|
* ```
|
|
560
600
|
* {
|
|
561
601
|
* "options": {
|
|
@@ -565,11 +605,11 @@ export class HelperRest {
|
|
|
565
605
|
* }
|
|
566
606
|
* ```
|
|
567
607
|
*
|
|
568
|
-
* type=**oauth
|
|
608
|
+
* type=**"oauth"** having auth.options:
|
|
569
609
|
* ```
|
|
570
610
|
* {
|
|
571
611
|
* "options": {
|
|
572
|
-
* "tenantIdGUID": "<Entra ID tenantIdGUID", //
|
|
612
|
+
* "tenantIdGUID": "<Entra ID tenantIdGUID", // simplified configuration for using Microsoft Graph API
|
|
573
613
|
* "tokenUrl": "<tokenUrl>", // not used when tenantIdGUID defined
|
|
574
614
|
* "clientId": "<clientId",
|
|
575
615
|
* "clientSecret": "<clientSecret>"
|
|
@@ -577,7 +617,7 @@ export class HelperRest {
|
|
|
577
617
|
* }
|
|
578
618
|
* ```
|
|
579
619
|
*
|
|
580
|
-
* type=**token
|
|
620
|
+
* type=**"token"** having auth.options:
|
|
581
621
|
* ```
|
|
582
622
|
* {
|
|
583
623
|
* "options": {
|
|
@@ -588,7 +628,7 @@ export class HelperRest {
|
|
|
588
628
|
* }
|
|
589
629
|
* ```
|
|
590
630
|
*
|
|
591
|
-
* type=**bearer
|
|
631
|
+
* type=**"bearer"** having auth.options:
|
|
592
632
|
* ```
|
|
593
633
|
* {
|
|
594
634
|
* "options": {
|
|
@@ -597,6 +637,22 @@ export class HelperRest {
|
|
|
597
637
|
* }
|
|
598
638
|
* ```
|
|
599
639
|
*
|
|
640
|
+
* type=**"oauthSamlAssertion"** having auth.options:
|
|
641
|
+
* ```
|
|
642
|
+
* {
|
|
643
|
+
* "options": {
|
|
644
|
+
* "tokenUrl": "<tokenUrl>",
|
|
645
|
+
* "clientId": "<clientId>",
|
|
646
|
+
* "companyId": "<companyId>",
|
|
647
|
+
* "userId": "<userId>",
|
|
648
|
+
* "certificate": {
|
|
649
|
+
* "key": "<key-file-name>", // location: config/certs
|
|
650
|
+
* "cert": "<cert-file-name>", // location: config/certs
|
|
651
|
+
* }
|
|
652
|
+
* }
|
|
653
|
+
* }
|
|
654
|
+
* ```
|
|
655
|
+
*
|
|
600
656
|
* **connection.options** can be set according to web-standard fetch client options
|
|
601
657
|
* examples:
|
|
602
658
|
* ```
|
package/lib/logger.ts
CHANGED
|
@@ -96,7 +96,7 @@ export class Log {
|
|
|
96
96
|
customMaskXml = customMasking.join('"?|')
|
|
97
97
|
customMaskXml = '|' + customMaskXml + '"?'
|
|
98
98
|
}
|
|
99
|
-
this.reJson = `^.*"(password|access_token|client_secret${customMaskJson})" ?: ?"([^"]+)".*`
|
|
99
|
+
this.reJson = `^.*"(password|access_token|client_secret|assertion${customMaskJson})" ?: ?"([^"]+)".*`
|
|
100
100
|
this.reXml = `^.*(credentials"?|PasswordText"?|PasswordDigest"?|password"?${customMaskXml})>([^<]+)</.*`
|
|
101
101
|
|
|
102
102
|
const trans: any = [
|
package/lib/plugin-mssql.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Purpose: SQL user-provisioning
|
|
7
7
|
//
|
|
8
8
|
// Prereq:
|
|
9
|
-
// CREATE TABLE [
|
|
9
|
+
// CREATE TABLE [Users] (
|
|
10
10
|
// [UserID] VARCHAR(50) NOT NULL,
|
|
11
11
|
// [Enabled] VARCHAR(50) NULL,
|
|
12
12
|
// [Password] VARCHAR(50) NULL,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// PRIMARY KEY ([UserID])
|
|
20
20
|
// );
|
|
21
21
|
//
|
|
22
|
-
// CREATE TABLE [
|
|
22
|
+
// CREATE TABLE [Groups] (
|
|
23
23
|
// [GroupID] VARCHAR(50) NOT NULL,
|
|
24
24
|
// [Enabled] VARCHAR(50) NULL,
|
|
25
25
|
// CONSTRAINT [PK_Group]
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
// PRIMARY KEY ([GroupID],[UserID]),
|
|
34
34
|
// CONSTRAINT [FK_U2G_Group]
|
|
35
35
|
// FOREIGN KEY ([GroupID])
|
|
36
|
-
// REFERENCES [
|
|
36
|
+
// REFERENCES [Groups]([GroupID])
|
|
37
37
|
// ON DELETE CASCADE,
|
|
38
38
|
// CONSTRAINT [FK_U2G_Users]
|
|
39
39
|
// FOREIGN KEY ([UserID])
|
|
40
|
-
// REFERENCES [
|
|
40
|
+
// REFERENCES [Users]([UserID])
|
|
41
41
|
// ON DELETE CASCADE
|
|
42
42
|
// );
|
|
43
43
|
//
|
|
@@ -66,7 +66,7 @@ import { Connection, Request } from 'tedious'
|
|
|
66
66
|
const ScimGateway: typeof import('scimgateway').ScimGateway = await (async () => {
|
|
67
67
|
try {
|
|
68
68
|
return (await import('scimgateway')).ScimGateway
|
|
69
|
-
} catch (err) {
|
|
69
|
+
} catch (err: any) {
|
|
70
70
|
const source = './scimgateway.ts'
|
|
71
71
|
return (await import(source)).ScimGateway
|
|
72
72
|
}
|
|
@@ -113,17 +113,13 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
113
113
|
if (!sqlQuery) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
|
|
114
114
|
|
|
115
115
|
try {
|
|
116
|
-
return await new Promise(async (resolve
|
|
116
|
+
return await new Promise(async (resolve) => {
|
|
117
117
|
const ret: any = { // itemsPerPage will be set by scimgateway
|
|
118
118
|
Resources: [],
|
|
119
119
|
totalResults: null,
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
const users: any[] = await query(sqlQuery, ctx).catch(
|
|
123
|
-
const e = new Error(`${action} error: ${err.message}`)
|
|
124
|
-
return reject(e)
|
|
125
|
-
})
|
|
126
|
-
|
|
122
|
+
const users: any[] = await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
127
123
|
for (const user of users) {
|
|
128
124
|
const scimUser = {
|
|
129
125
|
id: user.UserID.value ? user.UserID.value : undefined,
|
|
@@ -155,7 +151,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
|
|
|
155
151
|
scimgateway.logDebug(baseEntity, `handling "${action}" userObj=${JSON.stringify(userObj)}`)
|
|
156
152
|
|
|
157
153
|
try {
|
|
158
|
-
return await new Promise(async (resolve
|
|
154
|
+
return await new Promise(async (resolve) => {
|
|
159
155
|
if (!userObj.name) userObj.name = {}
|
|
160
156
|
if (!userObj.emails) userObj.emails = { work: {} }
|
|
161
157
|
if (!userObj.phoneNumbers) userObj.phoneNumbers = { work: {} }
|
|
@@ -174,10 +170,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
|
|
|
174
170
|
const sqlQuery = `insert into [Users] (UserID, Enabled, Password, FirstName, MiddleName, LastName, Email, MobilePhone)
|
|
175
171
|
values (${insert.UserID}, ${insert.Enabled}, ${insert.Password}, ${insert.FirstName}, ${insert.MiddleName}, ${insert.LastName}, ${insert.Email}, ${insert.MobilePhone})`
|
|
176
172
|
|
|
177
|
-
await query(sqlQuery, ctx).catch(
|
|
178
|
-
const e = new Error(`${action} error: ${err.message}`)
|
|
179
|
-
return reject(e)
|
|
180
|
-
})
|
|
173
|
+
await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
181
174
|
|
|
182
175
|
resolve(null)
|
|
183
176
|
}) // Promise
|
|
@@ -194,12 +187,9 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
|
|
|
194
187
|
scimgateway.logDebug(baseEntity, `handling "${action}" id=${id}`)
|
|
195
188
|
|
|
196
189
|
try {
|
|
197
|
-
return await new Promise(async (resolve
|
|
190
|
+
return await new Promise(async (resolve) => {
|
|
198
191
|
const sqlQuery = `delete from [Users] where UserID = '${id}'`
|
|
199
|
-
await query(sqlQuery, ctx).catch(
|
|
200
|
-
const e = new Error(`${action} error: ${err.message}`)
|
|
201
|
-
return reject(e)
|
|
202
|
-
})
|
|
192
|
+
await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
203
193
|
|
|
204
194
|
resolve(null)
|
|
205
195
|
}) // Promise
|
|
@@ -216,7 +206,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
216
206
|
scimgateway.logDebug(baseEntity, `handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
|
|
217
207
|
|
|
218
208
|
try {
|
|
219
|
-
return await new Promise(async (resolve
|
|
209
|
+
return await new Promise(async (resolve) => {
|
|
220
210
|
if (!attrObj.name) attrObj.name = {}
|
|
221
211
|
if (!attrObj.emails) attrObj.emails = { work: {} }
|
|
222
212
|
if (!attrObj.phoneNumbers) attrObj.phoneNumbers = { work: {} }
|
|
@@ -252,10 +242,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
|
|
|
252
242
|
sql = sql.substr(0, sql.length - 1) // remove trailing ","
|
|
253
243
|
|
|
254
244
|
const sqlQuery = `update [Users] set ${sql} where UserID like '${id}'`
|
|
255
|
-
await query(sqlQuery, ctx).catch(
|
|
256
|
-
const e = new Error(`${action} error: ${err.message}`)
|
|
257
|
-
return reject(e)
|
|
258
|
-
})
|
|
245
|
+
await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
259
246
|
|
|
260
247
|
resolve(null)
|
|
261
248
|
}) // Promise
|
|
@@ -296,16 +283,13 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
296
283
|
if (!sqlQuery) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
|
|
297
284
|
|
|
298
285
|
try {
|
|
299
|
-
return await new Promise(async (resolve
|
|
286
|
+
return await new Promise(async (resolve) => {
|
|
300
287
|
const ret: any = { // itemsPerPage will be set by scimgateway
|
|
301
288
|
Resources: [],
|
|
302
289
|
totalResults: null,
|
|
303
290
|
}
|
|
304
291
|
|
|
305
|
-
const groups = await query(sqlQuery, ctx).catch(
|
|
306
|
-
const e = new Error(`${action} error: ${err.message}`)
|
|
307
|
-
return reject(e)
|
|
308
|
-
})
|
|
292
|
+
const groups: any[] = await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
309
293
|
|
|
310
294
|
for (const group of groups) {
|
|
311
295
|
const scimGroup: Record<string, any> = {
|
|
@@ -316,7 +300,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
|
|
|
316
300
|
}
|
|
317
301
|
|
|
318
302
|
const sqlQuery = `select UserID from [Users2Group] where GroupID = '${scimGroup.id}'`
|
|
319
|
-
const members = await query(sqlQuery, ctx).catch(
|
|
303
|
+
const members = await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
320
304
|
for (const member of members) {
|
|
321
305
|
const scimMember = {
|
|
322
306
|
value: member.UserID.value,
|
|
@@ -343,21 +327,20 @@ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
|
|
|
343
327
|
scimgateway.logDebug(baseEntity, `handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
|
|
344
328
|
|
|
345
329
|
try {
|
|
346
|
-
return await new Promise(async (resolve
|
|
330
|
+
return await new Promise(async (resolve) => {
|
|
347
331
|
const insert = {
|
|
348
332
|
GroupID: `'${groupObj.displayName}'`,
|
|
349
333
|
Enabled: (groupObj.active) ? `'${groupObj.active}'` : '\'false\'',
|
|
350
334
|
}
|
|
351
335
|
|
|
352
336
|
const sqlQuery = `insert into [Groups] (GroupID, Enabled) values (${insert.GroupID}, ${insert.Enabled})`
|
|
353
|
-
await query(sqlQuery, ctx).catch(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
})
|
|
337
|
+
await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
338
|
+
|
|
339
|
+
if (Array.isArray(groupObj.members) && groupObj.members) {
|
|
340
|
+
for (const member of groupObj.members) {
|
|
341
|
+
const sqlQuery = `insert into [Users2Group] (UserID, GroupID) values ('${member.value}', ${insert.GroupID})`
|
|
342
|
+
await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
343
|
+
}
|
|
361
344
|
}
|
|
362
345
|
|
|
363
346
|
resolve(null)
|
|
@@ -375,12 +358,9 @@ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
|
|
|
375
358
|
scimgateway.logDebug(baseEntity, `handling "${action}" id=${id}`)
|
|
376
359
|
|
|
377
360
|
try {
|
|
378
|
-
return await new Promise(async (resolve
|
|
361
|
+
return await new Promise(async (resolve) => {
|
|
379
362
|
const sqlQuery = `delete from [Groups] where GroupID = '${id}'`
|
|
380
|
-
await query(sqlQuery, ctx).catch(
|
|
381
|
-
const e = new Error(`${action} error: ${err.message}`)
|
|
382
|
-
return reject(e)
|
|
383
|
-
})
|
|
363
|
+
await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
384
364
|
|
|
385
365
|
resolve(null)
|
|
386
366
|
}) // Promise
|
|
@@ -409,25 +389,22 @@ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
|
|
|
409
389
|
// This BLINDLY inserts all user/groups and gracefully breaks on PK violation
|
|
410
390
|
// for each existing membership
|
|
411
391
|
if (Array.isArray(attrObj.members) && attrObj.members) {
|
|
412
|
-
attrObj.members
|
|
392
|
+
for (const member of attrObj.members) {
|
|
413
393
|
if (member.operation == 'delete') {
|
|
414
394
|
queries.push(`delete from [Users2Group] where GroupID='${id}' and UserID='${member.value}'`)
|
|
415
395
|
} else {
|
|
416
396
|
queries.push(`insert into [Users2Group] (UserID, GroupID) values ('${member.value}','${id}')`)
|
|
417
397
|
}
|
|
418
|
-
}
|
|
398
|
+
}
|
|
419
399
|
}
|
|
420
400
|
|
|
421
401
|
const sqlQuery = queries.join(';')
|
|
422
402
|
|
|
423
403
|
try {
|
|
424
|
-
return await new Promise(async (resolve
|
|
404
|
+
return await new Promise(async (resolve) => {
|
|
425
405
|
if (sqlQuery) {
|
|
426
406
|
scimgateway.logDebug(baseEntity, `sqlQuery: ${sqlQuery}`)
|
|
427
|
-
await query(sqlQuery, ctx).catch(
|
|
428
|
-
const e = new Error(`${action} error: ${err.message}`)
|
|
429
|
-
return reject(e)
|
|
430
|
-
})
|
|
407
|
+
await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
|
|
431
408
|
}
|
|
432
409
|
|
|
433
410
|
resolve(null)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Purpose: create SAML token assertion that can be used by OAuth token request having grant type saml2-bearer
|
|
3
|
+
// Based on: https://github.com/edersouza38/insomnia-plugin-sfsf-samlassertion
|
|
4
|
+
//
|
|
5
|
+
// MIT License
|
|
6
|
+
//
|
|
7
|
+
// Copyright (c) 2023 edersouza38
|
|
8
|
+
//
|
|
9
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
// in the Software without restriction, including without limitation the rights
|
|
12
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
// furnished to do so, subject to the following conditions:
|
|
15
|
+
//
|
|
16
|
+
// The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
// copies or substantial portions of the Software.
|
|
18
|
+
//
|
|
19
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
// SOFTWARE.
|
|
26
|
+
//
|
|
27
|
+
|
|
28
|
+
// @ts-expect-error type declaration file not found
|
|
29
|
+
import { Saml20 as saml } from 'saml'
|
|
30
|
+
import crypto from 'node:crypto'
|
|
31
|
+
|
|
32
|
+
export const samlAssertionUtils = {
|
|
33
|
+
formatPrivateKey: function (input: string) {
|
|
34
|
+
// Validate PEM keys:
|
|
35
|
+
const keyArmor = /-----(BEGIN |END )(.*?) KEY-----/g
|
|
36
|
+
let v: any = [...input.matchAll(keyArmor)]
|
|
37
|
+
if (v.length > 0) {
|
|
38
|
+
if (v.length !== 2 || v[0][2] !== v[1][2] || v[0][2] !== 'PRIVATE') {
|
|
39
|
+
throw new Error('Invalid PEM private key. Make sure that the armoring is consistent and the PEM key is from the type "PRIVATE".')
|
|
40
|
+
}
|
|
41
|
+
return input.replace(/\r?\n|\r/g, '')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Verify whether key was generated directly in SFSF:
|
|
45
|
+
const d = Buffer.from(input, 'base64').toString('utf-8')
|
|
46
|
+
v = d.split('###')
|
|
47
|
+
if (v.length === 2) {
|
|
48
|
+
input = v[0]
|
|
49
|
+
}
|
|
50
|
+
return `-----BEGIN PRIVATE KEY-----${input}-----END PRIVATE KEY-----`
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
formatCertificate: function (input: string) {
|
|
54
|
+
// Validate PEM keys:
|
|
55
|
+
const keyArmor = /-----(BEGIN |END )(.*?)-----/g
|
|
56
|
+
let v: any = [...input.matchAll(keyArmor)]
|
|
57
|
+
if (v.length > 0) {
|
|
58
|
+
if (v.length !== 2 || v[0][2] !== v[1][2]) {
|
|
59
|
+
throw new Error('Invalid PEM certificate. Make sure that the armoring is consistent.')
|
|
60
|
+
}
|
|
61
|
+
return input.replace(/\r?\n|\r/g, '')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Verify whether key was generated directly in SFSF:
|
|
65
|
+
const d = Buffer.from(input, 'base64').toString('utf-8')
|
|
66
|
+
v = d.split('###')
|
|
67
|
+
if (v.length === 2) {
|
|
68
|
+
input = v[0]
|
|
69
|
+
}
|
|
70
|
+
return `-----BEGIN CERTIFICATE-----${input}-----END CERTIFICATE-----`
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
delay: function (time: number) {
|
|
74
|
+
return new Promise(resolve => setTimeout(resolve, time))
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
userIdentifierFormat: {
|
|
78
|
+
userId: 'userId',
|
|
79
|
+
userName: 'userName',
|
|
80
|
+
eMail: 'e-Mail',
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const samlAssertion = {
|
|
85
|
+
name: 'samlAssertionSFSF',
|
|
86
|
+
displayName: 'SAML Assertion - SFSF',
|
|
87
|
+
description: 'Create a SAML Assertion for SFSF OAuth2SAMLAssertion flow.',
|
|
88
|
+
args: [
|
|
89
|
+
{
|
|
90
|
+
displayName: 'X.509 Certificate',
|
|
91
|
+
description: 'X.509 Certificate used to identify the SAML IdP',
|
|
92
|
+
type: 'string',
|
|
93
|
+
placeholder: '-----BEGIN CERTIFICATE-----',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
displayName: 'Private Key',
|
|
97
|
+
description: 'Private Key used to sign the SAML Assertion',
|
|
98
|
+
type: 'string',
|
|
99
|
+
placeholder: '-----BEGIN PRIVATE KEY-----',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
displayName: 'SAML Issuer',
|
|
103
|
+
description: 'Name of the IdP issuing the SAML Assertion',
|
|
104
|
+
type: 'string',
|
|
105
|
+
defaultValue: 'local.insomnia.com',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
displayName: 'Lifetime in seconds',
|
|
109
|
+
description: 'Lifetime of the SAML Assertion in seconds',
|
|
110
|
+
type: 'number',
|
|
111
|
+
defaultValue: 600,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
displayName: 'Client Id',
|
|
115
|
+
description: 'Registered Client Id in SFSF',
|
|
116
|
+
type: 'string',
|
|
117
|
+
placeholder: 'OWE1Yzg0NTMyOGJlY2M4NWRiZGFiMGE3MTI5MA',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
displayName: 'User Identifier',
|
|
121
|
+
description: 'User Identifier',
|
|
122
|
+
type: 'string',
|
|
123
|
+
placeholder: 'Username',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
displayName: 'User Identifier Format',
|
|
127
|
+
description: 'User Identifier Format',
|
|
128
|
+
type: 'enum',
|
|
129
|
+
placeholder: 'User Identifier Format',
|
|
130
|
+
defaultValue: samlAssertionUtils.userIdentifierFormat.userId,
|
|
131
|
+
options: [
|
|
132
|
+
{
|
|
133
|
+
displayName: 'User ID',
|
|
134
|
+
value: samlAssertionUtils.userIdentifierFormat.userId,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
displayName: 'Username',
|
|
138
|
+
value: samlAssertionUtils.userIdentifierFormat.userName,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
displayName: 'E-Mail',
|
|
142
|
+
value: samlAssertionUtils.userIdentifierFormat.eMail,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
displayName: 'OAuth Token Endpoint',
|
|
148
|
+
description: 'SFSF OAuth Token Endpoint',
|
|
149
|
+
type: 'string',
|
|
150
|
+
placeholder: 'Username',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
displayName: 'Audience',
|
|
154
|
+
description: 'Audience of the SAML Assertion',
|
|
155
|
+
type: 'string',
|
|
156
|
+
defaultValue: 'www.successfactors.com',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
displayName: 'Delay (Seconds)',
|
|
160
|
+
description: 'Useful when the request is reaching the endpoint before the "validNotBefore" date from SAML assertion.',
|
|
161
|
+
type: 'number',
|
|
162
|
+
defaultValue: 0,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
|
|
166
|
+
async run(
|
|
167
|
+
context: any,
|
|
168
|
+
cert: any,
|
|
169
|
+
key: any,
|
|
170
|
+
issuer: any,
|
|
171
|
+
lifetime: any,
|
|
172
|
+
clientId: any,
|
|
173
|
+
nameId: any,
|
|
174
|
+
userIdentifierFormat: any,
|
|
175
|
+
tokenEndpoint: any,
|
|
176
|
+
audience: any,
|
|
177
|
+
delay: any,
|
|
178
|
+
) {
|
|
179
|
+
const samlAttributes: Record<string, any> = {
|
|
180
|
+
api_key: clientId,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let nameIdentifierFormat
|
|
184
|
+
= 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'
|
|
185
|
+
|
|
186
|
+
switch (userIdentifierFormat) {
|
|
187
|
+
case samlAssertionUtils.userIdentifierFormat.eMail:
|
|
188
|
+
nameIdentifierFormat
|
|
189
|
+
= 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
|
|
190
|
+
break
|
|
191
|
+
case samlAssertionUtils.userIdentifierFormat.userName:
|
|
192
|
+
samlAttributes.use_username = 'true'
|
|
193
|
+
break
|
|
194
|
+
default:
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
const options = {
|
|
198
|
+
cert: samlAssertionUtils.formatCertificate(cert),
|
|
199
|
+
key: samlAssertionUtils.formatPrivateKey(key),
|
|
200
|
+
issuer: issuer,
|
|
201
|
+
lifetimeInSeconds: lifetime,
|
|
202
|
+
audiences: audience,
|
|
203
|
+
attributes: samlAttributes,
|
|
204
|
+
nameIdentifier: nameId,
|
|
205
|
+
nameIdentifierFormat: nameIdentifierFormat,
|
|
206
|
+
recipient: tokenEndpoint,
|
|
207
|
+
sessionIndex: '_' + crypto.randomUUID(),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const assertionBuff = Buffer.from(saml.create(options))
|
|
211
|
+
const assertion = assertionBuff.toString('base64')
|
|
212
|
+
|
|
213
|
+
if (delay > 0) {
|
|
214
|
+
await samlAssertionUtils.delay(delay * 1000)
|
|
215
|
+
}
|
|
216
|
+
return assertion
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default samlAssertion
|
package/lib/scimgateway.ts
CHANGED
|
@@ -2367,13 +2367,25 @@ export class ScimGateway {
|
|
|
2367
2367
|
if (!ctx.response.body) ctx.response.body = 'NOT_FOUND'
|
|
2368
2368
|
break
|
|
2369
2369
|
}
|
|
2370
|
-
const
|
|
2371
|
-
if (
|
|
2370
|
+
const body = ctx.response.body
|
|
2371
|
+
if (body) {
|
|
2372
2372
|
try {
|
|
2373
|
-
JSON.parse(
|
|
2374
|
-
response.headers.set('content-type', 'application/scim+json; charset=utf-8')
|
|
2373
|
+
JSON.parse(body)
|
|
2374
|
+
ctx.response.headers.set('content-type', 'application/scim+json; charset=utf-8')
|
|
2375
2375
|
} catch (err) { void 0 }
|
|
2376
2376
|
}
|
|
2377
|
+
let response: Response
|
|
2378
|
+
if (typeof Bun !== 'undefined') {
|
|
2379
|
+
const stream = new ReadableStream({ // ensure Bun compatibility with Azure Reverse Proxy for large and long running response - header set by Bun: Transfer-Encoding: 'chunked'
|
|
2380
|
+
start(controller) {
|
|
2381
|
+
controller.enqueue(body)
|
|
2382
|
+
controller.close()
|
|
2383
|
+
},
|
|
2384
|
+
})
|
|
2385
|
+
response = new Response(body ? stream : body, { status: ctx.response.status, headers: ctx.response.headers })
|
|
2386
|
+
} else {
|
|
2387
|
+
response = new Response(body, { status: ctx.response.status, headers: ctx.response.headers })
|
|
2388
|
+
}
|
|
2377
2389
|
logResult(ctx)
|
|
2378
2390
|
return response
|
|
2379
2391
|
}
|
|
@@ -2698,6 +2710,13 @@ export class ScimGateway {
|
|
|
2698
2710
|
this.logger.info(`${this.pluginName}[${baseEntity}] ${msg}`)
|
|
2699
2711
|
}
|
|
2700
2712
|
|
|
2713
|
+
/**
|
|
2714
|
+
* logWarn logs warning message
|
|
2715
|
+
**/
|
|
2716
|
+
logWarn(baseEntity: string | undefined, msg: string) {
|
|
2717
|
+
this.logger.warn(`${this.pluginName}[${baseEntity}] ${msg}`)
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2701
2720
|
/**
|
|
2702
2721
|
* logError logs error message
|
|
2703
2722
|
**/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scimgateway",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
|
|
6
6
|
"author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"nodemailer": "^6.9.13",
|
|
53
53
|
"passport": "^0.7.0",
|
|
54
54
|
"passport-azure-ad": "^4.3.5",
|
|
55
|
+
"saml": "^3.0.1",
|
|
55
56
|
"winston": "^3.13.0"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|