scimgateway 5.0.11 → 5.0.12

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 CHANGED
@@ -18,7 +18,7 @@ Latest news:
18
18
 
19
19
  - Major version **v5.0.0** marks a shift to native TypeScript support and prioritizes [Bun](https://bun.sh/) over Node.js. This upgrade requires some modifications to existing plugins.
20
20
  - **BREAKING**: [SCIM Stream](https://elshaug.xyz/docs/scim-stream) is the modern way of user provisioning letting clients subscribe to messages instead of traditional IGA top-down provisioning. SCIM Gateway now offers enhanced functionality with support for message subscription and automated provisioning using SCIM Stream
21
- - Authentication PassThrough letting plugin pass authentication directly to endpoint for avoid maintaining secrets at the gateway
21
+ - Authentication PassThrough letting plugin pass authentication directly to endpoint for avoid maintaining secrets at the gateway. E.g., using Entra ID application OAuth
22
22
  - Supports OAuth Client Credentials authentication
23
23
  - Major version **v4.0.0** getUsers() and getGroups() replacing some deprecated methods. No limitations on filtering/sorting. Admin user access can be linked to specific baseEntities. New MongoDB plugin
24
24
  - ipAllowList for restricting access to allowlisted IP addresses or subnets e.g. Azure IP-range
@@ -1111,6 +1111,17 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1111
1111
 
1112
1112
  ## Change log
1113
1113
 
1114
+ ### v5.0.12
1115
+
1116
+ [Fixed]
1117
+
1118
+ - HelperRest doRequest() incorrect Auth PassThrough handling
1119
+
1120
+ [Improved]
1121
+
1122
+ - Dependencies bump
1123
+
1124
+
1114
1125
  ### v5.0.11
1115
1126
 
1116
1127
  [Fixed]
package/bun.lockb CHANGED
Binary file
@@ -43,31 +43,6 @@ export class HelperRest {
43
43
  if (errMsg) this.scimgateway.logError('undefined', errMsg)
44
44
  }
45
45
 
46
- /**
47
- * getClientIdentifier returns a unique client identifier having format user_secret
48
- * @param ctx having format { autorization: "<type>:xxxxx" }
49
- * @returns user_secret
50
- **/
51
- private getClientIdentifier(ctx: Record<string, any> | undefined): string {
52
- if (!ctx?.headers?.authorization) return 'undefined'
53
- const [user, secret] = this.getCtxAuth(ctx)
54
- return `${encodeURIComponent(user)}_${encodeURIComponent(secret)}` // user_password or undefined_password
55
- }
56
-
57
- /**
58
- * getCtxAuth returns [username, secret] based on Auth PassThrough autorization header included in ctx
59
- * @param ctx includes Auth PassThrough having format {headers:{autorization:"<type>:xxxxx"}}
60
- * @returns [username, secret]
61
- **/
62
- private getCtxAuth(ctx: Record<string, any> | undefined): any[] {
63
- if (!ctx?.headers?.authorization) return []
64
- const [authType, authToken] = (ctx.headers.authorization || '').split(' ') // [0] = 'Basic' or 'Bearer'
65
- let username, password
66
- if (authType === 'Basic') [username, password] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
67
- if (username) return [username, password] // basic auth
68
- else return [undefined, authToken] // bearer auth
69
- }
70
-
71
46
  /**
72
47
  * getAccessToken returns oauth accesstoken object
73
48
  * @param baseEntity
@@ -76,12 +51,11 @@ export class HelperRest {
76
51
  */
77
52
  public async getAccessToken(baseEntity: string, ctx?: Record<string, any> | undefined) { // public in case token is needed for other logic e.g. sending mail
78
53
  await this.lock.acquire()
79
- const clientIdentifier = this.getClientIdentifier(ctx)
80
54
  const d = Math.floor(Date.now() / 1000) // seconds (unix time)
81
- if (this._serviceClient[baseEntity] && this._serviceClient[baseEntity][clientIdentifier] && this._serviceClient[baseEntity][clientIdentifier].accessToken
82
- && (this._serviceClient[baseEntity][clientIdentifier].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
55
+ if (this._serviceClient[baseEntity] && this._serviceClient[baseEntity].accessToken
56
+ && (this._serviceClient[baseEntity].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
83
57
  this.lock.release()
84
- return this._serviceClient[baseEntity][clientIdentifier].accessToken
58
+ return this._serviceClient[baseEntity].accessToken
85
59
  }
86
60
 
87
61
  const action = 'getAccessToken'
@@ -213,20 +187,19 @@ export class HelperRest {
213
187
  //
214
188
  // path (no url) - default approach and client will be cached based on config
215
189
  //
216
- const clientIdentifier = this.getClientIdentifier(ctx)
217
- if (this._serviceClient[baseEntity] && this._serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist - token specific
190
+ if (this._serviceClient[baseEntity]) { // serviceClient already exist - token specific
218
191
  this.scimgateway.logDebug(baseEntity, `${action}: Using existing client`)
219
- if (this._serviceClient[baseEntity][clientIdentifier].accessToken) {
192
+ if (this._serviceClient[baseEntity].accessToken) {
220
193
  // check if token refresh is needed when using oauth
221
194
  const d = Math.floor(Date.now() / 1000) // seconds (unix time)
222
- if (this._serviceClient[baseEntity][clientIdentifier].accessToken.validTo < d + 30) { // less than 30 sec before token expiration
223
- this.scimgateway.logDebug(baseEntity, `${action}: Accesstoken about to expire in ${this._serviceClient[baseEntity][clientIdentifier].accessToken.validTo - d} seconds`)
195
+ if (this._serviceClient[baseEntity].accessToken.validTo < d + 30) { // less than 30 sec before token expiration
196
+ this.scimgateway.logDebug(baseEntity, `${action}: Accesstoken about to expire in ${this._serviceClient[baseEntity].accessToken.validTo - d} seconds`)
224
197
  try {
225
198
  const accessToken = await this.getAccessToken(baseEntity, ctx)
226
- this._serviceClient[baseEntity][clientIdentifier].accessToken = accessToken
227
- this._serviceClient[baseEntity][clientIdentifier].options.headers['Authorization'] = ` Bearer ${accessToken.access_token}`
199
+ this._serviceClient[baseEntity].accessToken = accessToken
200
+ this._serviceClient[baseEntity].options.headers['Authorization'] = ` Bearer ${accessToken.access_token}`
228
201
  } catch (err) {
229
- if (this._serviceClient[baseEntity]) delete this._serviceClient[baseEntity][clientIdentifier]
202
+ delete this._serviceClient[baseEntity]
230
203
  const newErr = err
231
204
  throw newErr
232
205
  }
@@ -260,53 +233,49 @@ export class HelperRest {
260
233
  }
261
234
 
262
235
  // Supporting no auth, header based auth (e.g., config {"options":{"headers":{"APIkey":"123"}}}),
263
- // basicAuth, bearerAuth, oauth, tokenAuth and auth PassTrough using request header authorization
264
- if (ctx?.headers?.authorization) { // Auth PassThrough using ctx header
265
- param.options.headers['Authorization'] = ctx.headers.authorization
266
- } else {
267
- switch (this.config_entity[baseEntity]?.connection?.auth?.type) {
268
- case 'basic':
269
- if (!this.config_entity[baseEntity]?.connection?.auth?.options?.username || !this.config_entity[baseEntity]?.connection?.auth?.options?.password) {
270
- const err = new Error(`auth type 'basic' - missing configuration entity.${baseEntity}.connection.auth.options.username/password`)
271
- throw err
272
- }
273
- param.options.headers['Authorization'] = 'Basic ' + Buffer.from(`${this.config_entity[baseEntity].connection.auth.options.username}:${this.config_entity[baseEntity].connection.auth.options.password}`).toString('base64')
274
- break
275
- case 'oauth':
276
- if (!this.config_entity[baseEntity]?.connection?.auth?.options?.clientId || !this.config_entity[baseEntity]?.connection?.auth?.options?.clientSecret) {
277
- const err = new Error(`auth type 'oauth' - missing configuration entity.${baseEntity}.connection.auth.options.clientId/clientSecret`)
278
- throw err
279
- }
280
- param.accessToken = await this.getAccessToken(baseEntity, ctx)
281
- param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
282
- break
283
- case 'token':
284
- if (!this.config_entity[baseEntity]?.connection?.auth?.options?.tokenUrl || !this.config_entity[baseEntity]?.connection?.auth?.options?.password) {
285
- const err = new Error(`missing configuration entity.${baseEntity}.connection.auth.options.tokenUrl/password`)
286
- throw err
287
- }
288
- param.accessToken = await this.getAccessToken(baseEntity, ctx)
289
- param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
290
- break
291
- case 'bearer':
292
- if (!this.config_entity[baseEntity]?.connection?.auth?.options?.token) {
293
- const err = new Error(`missing configuration entity.${baseEntity}.connection.auth.options.token`)
294
- throw err
295
- }
296
- param.options.headers['Authorization'] = 'Bearer ' + Buffer.from(this.config_entity[baseEntity].connection.auth.options.token).toString('base64')
297
- break
298
- case 'oauthSamlAssertion':
299
- if (!this.config_entity[baseEntity]?.connection?.auth?.options?.clientId || !this.config_entity[baseEntity]?.connection?.auth?.options?.companyId
300
- || !this.config_entity[baseEntity]?.connection?.auth?.options?.certificate?.key) {
301
- const err = new Error(`auth type 'oauthSamlAssertion' - missing configuration entity.${baseEntity}.connection.auth.options...`)
302
- throw err
303
- }
304
- param.accessToken = await this.getAccessToken(baseEntity, ctx)
305
- param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
306
- break
307
- default:
308
- // no auth
309
- }
236
+ // basicAuth, bearerAuth, oauth, tokenAuth, oauthSamlAssertion and auth PassTrough using request header authorization
237
+ switch (this.config_entity[baseEntity]?.connection?.auth?.type) {
238
+ case 'basic':
239
+ if (!this.config_entity[baseEntity]?.connection?.auth?.options?.username || !this.config_entity[baseEntity]?.connection?.auth?.options?.password) {
240
+ const err = new Error(`auth type 'basic' - missing configuration entity.${baseEntity}.connection.auth.options.username/password`)
241
+ throw err
242
+ }
243
+ param.options.headers['Authorization'] = 'Basic ' + Buffer.from(`${this.config_entity[baseEntity].connection.auth.options.username}:${this.config_entity[baseEntity].connection.auth.options.password}`).toString('base64')
244
+ break
245
+ case 'oauth':
246
+ if (!this.config_entity[baseEntity]?.connection?.auth?.options?.clientId || !this.config_entity[baseEntity]?.connection?.auth?.options?.clientSecret) {
247
+ const err = new Error(`auth type 'oauth' - missing configuration entity.${baseEntity}.connection.auth.options.clientId/clientSecret`)
248
+ throw err
249
+ }
250
+ param.accessToken = await this.getAccessToken(baseEntity, ctx)
251
+ param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
252
+ break
253
+ case 'token':
254
+ if (!this.config_entity[baseEntity]?.connection?.auth?.options?.tokenUrl || !this.config_entity[baseEntity]?.connection?.auth?.options?.password) {
255
+ const err = new Error(`missing configuration entity.${baseEntity}.connection.auth.options.tokenUrl/password`)
256
+ throw err
257
+ }
258
+ param.accessToken = await this.getAccessToken(baseEntity, ctx)
259
+ param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
260
+ break
261
+ case 'bearer':
262
+ if (!this.config_entity[baseEntity]?.connection?.auth?.options?.token) {
263
+ const err = new Error(`missing configuration entity.${baseEntity}.connection.auth.options.token`)
264
+ throw err
265
+ }
266
+ param.options.headers['Authorization'] = 'Bearer ' + Buffer.from(this.config_entity[baseEntity].connection.auth.options.token).toString('base64')
267
+ break
268
+ case 'oauthSamlAssertion':
269
+ if (!this.config_entity[baseEntity]?.connection?.auth?.options?.clientId || !this.config_entity[baseEntity]?.connection?.auth?.options?.companyId
270
+ || !this.config_entity[baseEntity]?.connection?.auth?.options?.certificate?.key) {
271
+ const err = new Error(`auth type 'oauthSamlAssertion' - missing configuration entity.${baseEntity}.connection.auth.options...`)
272
+ throw err
273
+ }
274
+ param.accessToken = await this.getAccessToken(baseEntity, ctx)
275
+ param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
276
+ break
277
+ default:
278
+ // no auth or PassTrough
310
279
  }
311
280
 
312
281
  // proxy
@@ -342,19 +311,21 @@ export class HelperRest {
342
311
  }
343
312
 
344
313
  if (!this._serviceClient[baseEntity]) this._serviceClient[baseEntity] = {}
345
- if (!this._serviceClient[baseEntity][clientIdentifier]) this._serviceClient[baseEntity][clientIdentifier] = {}
346
- this._serviceClient[baseEntity][clientIdentifier] = param // serviceClient created
314
+ this._serviceClient[baseEntity] = param // serviceClient created
347
315
 
348
- // OData support - note, not using [clientIdentifier]
316
+ // OData support
349
317
  this._serviceClient[baseEntity].nextLink = {} // OData pagination (Entra ID)
350
318
  this._serviceClient[baseEntity].nextLink.users = null
351
319
  this._serviceClient[baseEntity].nextLink.groups = null
352
320
  }
353
321
 
354
- const cli: any = utils.copyObj(this._serviceClient[baseEntity][clientIdentifier]) // client ready
322
+ if (ctx?.headers?.get) { // Auth PassThrough using ctx header
323
+ this._serviceClient[baseEntity].options.headers['Authorization'] = ctx.headers.get('authorization')
324
+ }
325
+ const cli: any = utils.copyObj(this._serviceClient[baseEntity]) // client ready
355
326
 
356
327
  // failover support
357
- path = this._serviceClient[baseEntity][clientIdentifier].baseUrl + path
328
+ path = this._serviceClient[baseEntity].baseUrl + path
358
329
  urlObj = new URL(path)
359
330
  cli.options.host = urlObj.hostname
360
331
  cli.options.port = urlObj.port
@@ -410,11 +381,10 @@ export class HelperRest {
410
381
  /**
411
382
  * updateServiceClient merges obj with _serviceClient
412
383
  * @param baseEntity
413
- * @param clientIdentifier
414
384
  * @param obj
415
385
  */
416
- private updateServiceClient(baseEntity: string, clientIdentifier: string, obj: any) {
417
- if (this._serviceClient[baseEntity] && this._serviceClient[baseEntity][clientIdentifier]) this._serviceClient[baseEntity][clientIdentifier] = utils.extendObj(this._serviceClient[baseEntity][clientIdentifier], obj)
386
+ private updateServiceClient(baseEntity: string, obj: any) {
387
+ if (this._serviceClient[baseEntity]) this._serviceClient[baseEntity] = utils.extendObj(this._serviceClient[baseEntity], obj)
418
388
  }
419
389
 
420
390
  /**
@@ -514,14 +484,13 @@ export class HelperRest {
514
484
  } catch (err: any) { // includes failover/retry logic based on config baseUrls array
515
485
  let statusCode
516
486
  try { statusCode = JSON.parse(err.message).statusCode } catch (e) { void 0 }
517
- const clientIdentifier = this.getClientIdentifier(ctx)
518
487
  if (err.message.includes('ratelimit')) { // have seen throttling not follow standard 429/retry-after, but instead using 500 and error message only
519
488
  if (!retryAfter) retryAfter = 60
520
489
  }
521
490
  if (!retryCount) retryCount = 0
522
491
  let urlObj
523
492
  try { urlObj = new URL(path) } catch (err) { void 0 }
524
- let isServiceClient = !urlObj && this._serviceClient[baseEntity] && this._serviceClient[baseEntity][clientIdentifier] && !this.lock.isLocked() // !isLocked to avoid retry ongoing doRequest with failing getAccessToken()
493
+ let isServiceClient = !urlObj && this._serviceClient[baseEntity] && !this.lock.isLocked() // !isLocked to avoid retry ongoing doRequest with failing getAccessToken()
525
494
  let oAuthTokeErr = statusCode === 401 && this.config_entity[baseEntity].connection?.auth?.type && this.config_entity[baseEntity].connection.auth.type.startsWith('oauth')
526
495
  if (isServiceClient && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ABORT_ERR' || err.code === 'ETIMEDOUT' || oAuthTokeErr || retryAfter)) {
527
496
  this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
@@ -531,27 +500,27 @@ export class HelperRest {
531
500
  resolve(null)
532
501
  }, retryAfter * 1000))
533
502
  }
534
- if (oAuthTokeErr && this._serviceClient[baseEntity]) delete this._serviceClient[baseEntity][clientIdentifier] // ensure new getAccessToken request - token used should not have been expired, but rejected for other reason e.g. token server restart and no persistent token store?
535
503
  if (retryCount < this.config_entity[baseEntity].connection.baseUrls.length) {
536
504
  retryCount++
537
- this.updateServiceClient(baseEntity, clientIdentifier, { baseUrl: this.config_entity[baseEntity].connection.baseUrls[retryCount - 1] })
505
+ this.updateServiceClient(baseEntity, { baseUrl: this.config_entity[baseEntity].connection.baseUrls[retryCount - 1] })
538
506
  this.scimgateway.logDebug(baseEntity, `${(this.config_entity[baseEntity].connection.baseUrls.length > 1) ? 'failover ' : ''}retry[${retryCount}] using baseUrl = ${this._serviceClient[baseEntity].baseUrl}`)
507
+ if (oAuthTokeErr) {
508
+ delete this._serviceClient[baseEntity] // ensure new getAccessToken request - token used should not have been expired, but rejected for other reason e.g. token server restart and no persistent token store?
509
+ }
539
510
  const ret = await this.doRequestHandler(baseEntity, method, path, body, ctx, opt, retryCount) // retry
540
511
  return ret // problem fixed
541
512
  } else {
542
513
  if (statusCode === 404) { // not logged as error e.g. getUser-manager
543
514
  this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
544
- } else this.scimgateway.logError(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} 11Error Response = ${err.message}`)
515
+ } else this.scimgateway.logError(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
545
516
  throw err
546
517
  }
547
518
  } else {
548
519
  if (statusCode === 404) { // not logged as error e.g. getUser-manager
549
520
  this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
550
521
  } else this.scimgateway.logError(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
551
- if (statusCode === 401 && this._serviceClient[baseEntity]) {
552
- delete this._serviceClient[baseEntity][clientIdentifier]
553
- }
554
- throw err // Symantec IM retries getUser 6 times on ECONNREFUSED
522
+ if (statusCode === 401) delete this._serviceClient[baseEntity]
523
+ throw err
555
524
  }
556
525
  }
557
526
  }
@@ -842,21 +842,16 @@ export class ScimGateway {
842
842
  ctx.response.status = 500
843
843
  return
844
844
  }
845
-
846
845
  let jsonBody = ctx.request.body
847
846
  try {
848
847
  if (!jsonBody) throw new Error('missing body')
849
- if (typeof jsonBody !== 'object') { // might have application/x-www-form-urlencoded body, but incorrect Content-Type header
848
+ if (typeof jsonBody !== 'object') { // might have application/x-www-form-urlencoded or multipart/form-data body, but incorrect Content-Type header
850
849
  logger.debug(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] continue request validation even though incorrect body vs header Content-Type: ${ctx.request.headers.get('content-type')}`)
851
- const arr = jsonBody.split('&')
852
- const body: Record<string, any> = {}
853
- arr.forEach((kv: string) => {
854
- const a = kv.split('=')
855
- if (a.length === 2) {
856
- body[a[0]] = decodeURIComponent(a[1])
857
- }
858
- })
859
- if (Object.keys(body).length < 1) throw new Error('body is not JSON nor application/x-www-form-urlencoded')
850
+ let body = utils.formUrlEncodedToJSON(jsonBody)
851
+ if (Object.keys(body).length < 1) {
852
+ body = utils.formDataMultipartToJSON(jsonBody)
853
+ if (Object.keys(body).length < 1) throw new Error('body is not JSON, application/x-www-form-urlencoded nor multipart/form-data')
854
+ }
860
855
  ctx.request.body = body // now json - ensure final info log will be masked
861
856
  jsonBody = body
862
857
  }
@@ -2310,14 +2305,9 @@ export class ScimGateway {
2310
2305
  } catch (err: any) {
2311
2306
  const contentType = request.headers.get('content-type')
2312
2307
  if (contentType && contentType.toLowerCase().startsWith('application/x-www-form-urlencoded')) {
2313
- const arr = bodyString.split('&') // "grant_type=client_credentials&client_id=id&client_secret=secret"
2314
- body = {}
2315
- arr.forEach((kv: string) => {
2316
- const a = kv.split('=')
2317
- if (a.length === 2) {
2318
- body[a[0]] = decodeURIComponent(a[1])
2319
- }
2320
- })
2308
+ body = utils.formUrlEncodedToJSON(bodyString)
2309
+ } else if (contentType && contentType.toLowerCase().startsWith('multipart/form-data')) {
2310
+ body = utils.formDataMultipartToJSON(bodyString)
2321
2311
  } else if (bodyString) body = bodyString
2322
2312
  }
2323
2313
 
package/lib/utils.ts CHANGED
@@ -613,3 +613,53 @@ export const createRandomPassword = function (len: number, ...set: string[]) {
613
613
  }
614
614
  return res
615
615
  }
616
+
617
+ /**
618
+ * formUrlEncodedToJSON converts application/x-www-form-urlencoded request body to json
619
+ * @param body http request body string
620
+ * @returns json formatted body
621
+ */
622
+ export const formUrlEncodedToJSON = function (body?: string): Record<string, any> {
623
+ if (!body) return {}
624
+ const arr = body.split('&') // "grant_type=client_credentials&client_id=id&client_secret=secret"
625
+ const json: Record<string, any> = {}
626
+ for (const kv of arr) {
627
+ const a = kv.split('=')
628
+ if (a.length === 2) {
629
+ try {
630
+ json[a[0]] = decodeURIComponent(a[1])
631
+ } catch (err) { return {} }
632
+ }
633
+ }
634
+ return json
635
+ }
636
+
637
+ /**
638
+ * formDataMultipartToJSON converts multipart/form-data request body to json - Content-Type: multipart/form-data;boundary="<some-delimiter>"
639
+ * @param body request body
640
+ * @param boundary the boundary/delimiter defiend in header Content-Type: multipart/form-data;boundary="<some-delimiter>"
641
+ * @returns json formatted body
642
+ */
643
+ export const formDataMultipartToJSON = function (body?: string, boundary?: string): Record<string, any> {
644
+ if (!body) return {} // --delimiter123\nContent-Disposition: form-data; name="field1"\n\nvalue1\n--delimiter123\nContent-Disposition: form-data; name="field2"; filename="example.txt"\n\nvalue2
645
+ if (!boundary) { // if boundary is missing, try to infer it
646
+ const inferredBoundary = body.match(/^--([^\r\n]+)/)
647
+ if (inferredBoundary) {
648
+ boundary = inferredBoundary[1]
649
+ } else return {} // No boundary found
650
+ }
651
+ const parts = body.split(`--${boundary}`).filter(part => part.trim() && !part.includes('--'))
652
+ const json: Record<string, any> = {}
653
+ for (const part of parts) {
654
+ const [headers, value] = part.split(/\r?\n\r?\n/)
655
+ const nameMatch = headers.match(/name="([^"]+)"/)
656
+ if (nameMatch) {
657
+ const key = nameMatch[1]
658
+ json[key] = value.trim()
659
+ try {
660
+ json[key] = decodeURIComponent(json[key])
661
+ } catch (err) { return {} }
662
+ }
663
+ }
664
+ return json
665
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.0.11",
3
+ "version": "5.0.12",
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)",
@@ -41,19 +41,19 @@
41
41
  "@types/tedious": "^4.0.14",
42
42
  "dot-object": "^2.1.5",
43
43
  "fold-to-ascii": "^5.0.1",
44
- "https-proxy-agent": "^7.0.4",
44
+ "https-proxy-agent": "^7.0.6",
45
45
  "is-in-subnet": "^4.0.1",
46
46
  "jsonwebtoken": "^9.0.2",
47
47
  "ldapjs": "^3.0.7",
48
48
  "lokijs": "^1.5.12",
49
- "mongodb": "^6.6.2",
49
+ "mongodb": "^6.12.0",
50
50
  "nats": "^2.28.2",
51
51
  "node-machine-id": "1.1.12",
52
- "nodemailer": "^6.9.13",
52
+ "nodemailer": "^6.9.16",
53
53
  "passport": "^0.7.0",
54
54
  "passport-azure-ad": "^4.3.5",
55
55
  "saml": "^3.0.1",
56
- "winston": "^3.13.0"
56
+ "winston": "^3.17.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "typescript": "^5.0.0"