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 +6 -0
- package/lib/scimgateway.ts +18 -8
- package/lib/utils-scim.ts +28 -28
- package/package.json +1 -1
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]
|
package/lib/scimgateway.ts
CHANGED
|
@@ -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')
|
|
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
|
|
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 =
|
|
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()
|
|
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])
|
|
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])
|
|
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