scimgateway 5.0.9 → 5.0.10

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
@@ -1111,6 +1111,12 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1111
1111
 
1112
1112
  ## Change log
1113
1113
 
1114
+ ### v5.0.10
1115
+
1116
+ [Improved]
1117
+
1118
+ - OAuth token request now accept missing or invalid Content-Type header
1119
+
1114
1120
  ### v5.0.9
1115
1121
 
1116
1122
  [Improved]
@@ -830,7 +830,20 @@ export class ScimGateway {
830
830
  let jsonBody = ctx.request.body
831
831
  try {
832
832
  if (!jsonBody) throw new Error('missing body')
833
- if (typeof jsonBody !== 'object') throw new Error('body is not JSON')
833
+ if (typeof jsonBody !== 'object') { // might have application/x-www-form-urlencoded body, but incorrect Content-Type header
834
+ 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')}`)
835
+ const arr = jsonBody.split('&')
836
+ const body: Record<string, any> = {}
837
+ arr.forEach((kv: string) => {
838
+ const a = kv.split('=')
839
+ if (a.length === 2) {
840
+ body[a[0]] = decodeURIComponent(a[1])
841
+ }
842
+ })
843
+ if (Object.keys(body).length < 1) throw new Error('body is not JSON nor application/x-www-form-urlencoded')
844
+ ctx.request.body = body // now json - ensure final info log will be masked
845
+ jsonBody = body
846
+ }
834
847
  jsonBody = utils.copyObj(jsonBody) // no changes to original
835
848
  } catch (err: any) {
836
849
  logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] [oauth] token request error: ${err.message}`)
@@ -875,7 +888,7 @@ export class ScimGateway {
875
888
  }
876
889
  if (!token) {
877
890
  err = 'invalid_client'
878
- errDescr = 'incorrect or missing clientId/clientSecret'
891
+ errDescr = 'incorrect or missing client_id/client_secret'
879
892
  if (pwErrCount < 3) {
880
893
  pwErrCount += 1
881
894
  } else { // delay brute force attempts
@@ -1008,7 +1021,6 @@ export class ScimGateway {
1008
1021
  }
1009
1022
  }
1010
1023
 
1011
- userObj = utilsScim.addPrimaryAttrs(userObj)
1012
1024
  scimdata = utils.stripObj(userObj, ctx.query.attributes, ctx.query.excludedAttributes)
1013
1025
  scimdata = utilsScim.addSchemas(scimdata, isScimv2, handle.description, undefined)
1014
1026
 
@@ -1259,7 +1271,6 @@ export class ScimGateway {
1259
1271
  if (this.config.scimgateway.scim.skipMetaLocation) location = undefined
1260
1272
  else if (ctx.query.attributes || (ctx.query.excludedAttributes && ctx.query.excludedAttributes.includes('meta'))) location = undefined
1261
1273
  for (let i = 0; i < scimdata.Resources.length; i++) {
1262
- scimdata.Resources[i] = utilsScim.addPrimaryAttrs(scimdata.Resources[i])
1263
1274
  scimdata.Resources[i] = utils.stripObj(scimdata.Resources[i], ctx.query.attributes, ctx.query.excludedAttributes)
1264
1275
  }
1265
1276
  scimdata = utilsScim.addResources(scimdata, ctx.query.startIndex, ctx.query.sortBy, ctx.query.sortOrder)
@@ -1440,7 +1451,6 @@ export class ScimGateway {
1440
1451
  ctx.response.headers.set('Location', location)
1441
1452
  }
1442
1453
  delete jsonBody.password
1443
- jsonBody = utilsScim.addPrimaryAttrs(jsonBody)
1444
1454
  jsonBody = utilsScim.addSchemas(jsonBody, isScimv2, handle.description, undefined)
1445
1455
  ctx.response.status = 201
1446
1456
  ctx.response.body = JSON.stringify(jsonBody)
@@ -1658,7 +1668,7 @@ export class ScimGateway {
1658
1668
  const location = ctx.origin + ctx.path
1659
1669
  ctx.response.headers.set('Location', location)
1660
1670
  }
1661
- const userObj = utilsScim.addPrimaryAttrs(scimdata.Resources[0])
1671
+ const userObj = scimdata.Resources[0]
1662
1672
  scimdata = utils.stripObj(userObj, ctx.query.attributes, ctx.query.excludedAttributes)
1663
1673
  scimdata = utilsScim.addSchemas(scimdata, isScimv2, handle.description, undefined)
1664
1674
  ctx.response.status = 200
@@ -2283,13 +2293,13 @@ export class ScimGateway {
2283
2293
  body = JSON.parse(bodyString)
2284
2294
  } catch (err: any) {
2285
2295
  const contentType = request.headers.get('content-type')
2286
- if (contentType && contentType.toLowerCase() === 'application/x-www-form-urlencoded') {
2296
+ if (contentType && contentType.toLowerCase().startsWith('application/x-www-form-urlencoded')) {
2287
2297
  const arr = bodyString.split('&') // "grant_type=client_credentials&client_id=id&client_secret=secret"
2288
2298
  body = {}
2289
2299
  arr.forEach((kv: string) => {
2290
2300
  const a = kv.split('=')
2291
2301
  if (a.length === 2) {
2292
- body[a[0]] = a[1]
2302
+ body[a[0]] = decodeURIComponent(a[1])
2293
2303
  }
2294
2304
  })
2295
2305
  } else if (bodyString) body = bodyString
package/lib/utils-scim.ts CHANGED
@@ -793,7 +793,19 @@ export function addSchemas(data: Record<string, any>, isScimv2: boolean, type?:
793
793
  } else if (key === 'password') delete data.Resources[i].password // exclude password, null and empty object/array
794
794
  else if (data.Resources[i][key] === null) delete data.Resources[i][key]
795
795
  else if (JSON.stringify(data.Resources[i][key]) === '{}') delete data.Resources[i][key]
796
- else if (Array.isArray(data.Resources[i][key]) && data.Resources[i][key].length < 1) delete data.Resources[i][key]
796
+ else if (Array.isArray(data.Resources[i][key])) {
797
+ if (data.Resources[i][key].length < 1) delete data.Resources[i][key]
798
+ else if (key !== 'members' && key !== 'groups') { // any primary attribute should be boolean
799
+ for (let j = 0; j < data.Resources[i][key].length; j++) {
800
+ let el = data.Resources[i][key][j]
801
+ if (typeof el !== 'object') break
802
+ if (el.type && el.primary && typeof el.primary === 'string') {
803
+ if (el.primary.toLowerCase() === 'true') el.primary = true
804
+ else if (el.primary.toLowerCase() === 'false') el.primary = false
805
+ }
806
+ }
807
+ }
808
+ }
797
809
  }
798
810
  if (Object.keys(data.Resources[i]).length === 0) {
799
811
  data.Resources.splice(i, 1) // delete
@@ -827,37 +839,25 @@ export function addSchemas(data: Record<string, any>, isScimv2: boolean, type?:
827
839
  } else if (key === 'password') delete data.password // exclude password, null and empty object/array
828
840
  else if (data[key] === null) delete data[key]
829
841
  else if (JSON.stringify(data[key]) === '{}') delete data[key]
830
- else if (Array.isArray(data[key]) && data[key].length < 1) delete data[key]
842
+ else if (Array.isArray(data[key])) {
843
+ if (data[key].length < 1) delete data[key]
844
+ else if (key !== 'members' && key !== 'groups') { // any primary attribute should be boolean
845
+ for (let j = 0; j < data[key].length; j++) {
846
+ let el = data[key][j]
847
+ if (typeof el !== 'object') break
848
+ if (el.type && el.primary && typeof el.primary === 'string') {
849
+ if (el.primary.toLowerCase() === 'true') el.primary = true
850
+ else if (el.primary.toLowerCase() === 'false') el.primary = false
851
+ }
852
+ }
853
+ }
854
+ }
831
855
  }
832
856
  }
833
857
 
834
858
  return data
835
859
  }
836
860
 
837
- // addPrimaryAttrs cheks for primary attributes (only for roles) and add them as standalone attributes
838
- // some IdP's may check for these e.g. Azure
839
- // e.g. {roles: [{value: "val1", primary: "True"}]}
840
- // gives:
841
- // { roles: [{value: "val1", primary: "True"}],
842
- // roles[primary eq "True"].value: "val1",
843
- // roles[primary eq "True"].primary: "True"}]
844
- // }
845
- export function addPrimaryAttrs(obj: Record<string, any>) {
846
- const key = 'roles'
847
- if (!obj || typeof obj !== 'object') return obj
848
- if (!obj[key] || !Array.isArray(obj[key])) return obj
849
- const o = utils.copyObj(obj)
850
- const index = o[key].findIndex((el: Record<string, any>) => (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')))
851
- if (index >= 0) {
852
- const prim = o[key][index]
853
- for (const k in prim) {
854
- const primKey = `${key}[primary eq ${typeof prim.primary === 'string' ? `"${prim.primary}"` : prim.primary}].${k}` // roles[primary eq true].value / roles[primary eq "True"].value``
855
- o[primKey] = prim[k] // { roles[primary eq true].value : "some-value" }
856
- }
857
- }
858
- return o
859
- }
860
-
861
861
  //
862
862
  // SCIM error formatting
863
863
  //
@@ -900,7 +900,7 @@ export function jsonErr(scimVersion: string | number, pluginName: string, htmlEr
900
900
 
901
901
  if (scimVersion !== '2.0' && scimVersion !== 2) { // v1.1
902
902
  errJson
903
- = {
903
+ = {
904
904
  Errors: [
905
905
  {
906
906
  description: msg,
@@ -910,7 +910,7 @@ export function jsonErr(scimVersion: string | number, pluginName: string, htmlEr
910
910
  }
911
911
  } else { // v2.0
912
912
  errJson
913
- = {
913
+ = {
914
914
  schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
915
915
  scimType,
916
916
  detail: msg,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.0.9",
3
+ "version": "5.0.10",
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)",