scimgateway 5.5.1 → 5.5.3

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
@@ -42,7 +42,7 @@ Latest news:
42
42
 
43
43
  ## Overview
44
44
 
45
- SCIM Gateway facilitates user management using the standardized REST-based SCIM 1.1 or 2.0 protocol, offering easier, more powerful, and consistent provisioning while avoiding vendor lock-in. Acting as a translator for incoming SCIM requests, the gateway seamlessly enables CRUD functionality (create, read, update, and delete) for users and groups. By implementing endpoint-specific protocols, it ensures provisioning across diverse destinations. With the gateway, your destinations effectively become SCIM endpoints, streamlining integration and simplifying user management.
45
+ SCIM Gateway facilitates user management using the standardized REST-based SCIM 1.1 or 2.0 protocol, offering easier, more powerful, and consistent provisioning while avoiding vendor lock-in. Acting as a translator for incoming SCIM requests, the gateway seamlessly enables CRUD functionality (create, read, update, and delete) for users and groups. By implementing endpoint-specific protocols, it ensures provisioning across diverse destinations. With the gateway, your destinations become SCIM-compatible interfaces, streamlining integration and simplifying user management.
46
46
 
47
47
 
48
48
  ![](https://jelhub.github.io/images/ScimGateway.svg)
@@ -738,16 +738,16 @@ Example Entra ID (plugin-entra-id) using federated credentials:
738
738
  "options": {
739
739
  "tenantIdGUID": "<tenantId>",
740
740
  "fedCred": {
741
- "issuer": "<https://FQDN-scimgateway/oauth>",
741
+ "issuer": "<https://FQDN-scimgateway>",
742
742
  "subject": "<entra id application object id - client id>",
743
743
  "name": "<entra id federated credentials unique name>"
744
744
  }
745
745
  }
746
746
  }
747
747
  }
748
- // Note: Federated credentials defined for the application in Entra ID must match the corresponding `issuer`, `subject`, and `name`
749
- // example issuer: "https://scimgateway.my-company.com/oauth" and base URL must be reachable from the internet
750
- // exampole name: "plugin-entra-id"
748
+ // Note, fedCred configuration must match corresponding configuration in Entra ID Application - Certificates & Secrets - Federated credentials - scenario "Other issuer"
749
+ // example issuer: "https://scimgateway.my-company.com" note, this scimgateway base URL must be reachable from the internet
750
+ // example name: "plugin-entra-id"
751
751
 
752
752
 
753
753
  Example using general OAuth:
@@ -972,8 +972,6 @@ On Linux systems we may also run SCIM Gateway as a Docker image (using docker-co
972
972
  **docker-ce
973
973
  docker-compose**
974
974
 
975
-
976
-
977
975
  - Install SCIM Gateway within your own package and copy provided docker files:
978
976
 
979
977
  mkdir /opt/my-scimgateway
@@ -988,6 +986,7 @@ docker-compose**
988
986
  **DataDockerfile** <== Handles volume mapping
989
987
  **docker-compose-debug.yml** <== Debugging
990
988
  **docker-compose-mssql.yml** <== Example including MSSQL docker image
989
+ **.dockerignore** <== Files to exclude from the build context
991
990
 
992
991
  - Create a scimgateway user on your Linux VM.
993
992
 
@@ -1491,6 +1490,26 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1491
1490
 
1492
1491
  ## Change log
1493
1492
 
1493
+ ### v5.5.3
1494
+
1495
+ [Fixed]
1496
+ - Docker - fixed `docker build` error introduced in v5.5.0 (using bun.lock instead of binary bun.lockb)
1497
+
1498
+ [Improved]
1499
+ - plugin-mssql - attribute externalId included
1500
+ - .dockerignore - new docker configuration file, contains files to be excluded from the build context
1501
+
1502
+ ### v5.5.2
1503
+
1504
+ [Improved]
1505
+
1506
+ - Entra ID Federated Identity Credentials introduced in v5.5.0, the issuer configuration should be scimgateway base URL
1507
+ old: `"issuer": "<https://FQDN-scimgateway>/oauth"`
1508
+ new: `"issuer": "<https://FQDN-scimgateway>"`
1509
+
1510
+ Change log v5.5.0 have been corrected with the new issuer having base URL only
1511
+
1512
+
1494
1513
  ### v5.5.1
1495
1514
 
1496
1515
  [Fixed]
@@ -1510,7 +1529,7 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1510
1529
  "options": {
1511
1530
  "tenantIdGUID": "<Entra ID tenantIdGUID",
1512
1531
  "fedCred": {
1513
- "issuer": "<https://FQDN-scimgateway/oauth>",
1532
+ "issuer": "<https://FQDN-scimgateway>",
1514
1533
  "subject": "<entra id application object id - client id>",
1515
1534
  "name": "<entra id federated credentials unique name>"
1516
1535
  }
@@ -1524,14 +1543,14 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1524
1543
  "options": {
1525
1544
  "tenantIdGUID": "11111111-2222-3333-4444-555555555555",
1526
1545
  "fedCred": {
1527
- "issuer": "https://scimgateway.my-company.com/oauth",
1546
+ "issuer": "https://scimgateway.my-company.com",
1528
1547
  "subject": "99999999-8888-7777-6666-555555555555",
1529
1548
  "name": "plugin-entra-id"
1530
1549
  }
1531
1550
  }
1532
1551
  }
1533
1552
 
1534
- Note: Federated credentials defined for the application in Entra ID must match the corresponding `issuer`, `subject`, and `name` values defined in the SCIM Gateway endpoint configuration. An example of this can be using `plugin-entra-id` and other plugins that interact with endpoints or applications protected by Entra ID.
1553
+ Note: Federated credentials (scenario "Other issuer") defined for the application in Entra ID must match the corresponding `issuer`, `subject`, and `name` values defined in the SCIM Gateway endpoint configuration. An example of this can be using `plugin-entra-id` and other plugins that interact with endpoints or applications protected by Entra ID.
1535
1554
 
1536
1555
  Also note: SCIM Gateway must be reachable from the internet (as defined by the `issuer` URL). This requires allowing inbound internet communication — or alternatively, Azure Relay can be used for outbound-only communication.
1537
1556
 
@@ -0,0 +1,27 @@
1
+ .DS_Store
2
+ dist/
3
+ client_deploy/
4
+ typings/
5
+ /typings.json
6
+ /jsconfig.json
7
+ /npm-debug.log
8
+ /uml.txt
9
+ /.dockerignore
10
+ /docker-compose*.yml
11
+ /Dockerfile
12
+ /DataDockerfile
13
+ /sqlserver_data/
14
+ /node_modules/
15
+ /.vscode/
16
+ /dbinit/
17
+ /logs/
18
+ /config/docker
19
+ /config/approles
20
+ /config/resources
21
+ /eslint.config.js
22
+ /.travis.yml
23
+ /.gitignore
24
+ /.gitattributes
25
+ /.git/
26
+ /.github/
27
+ /test/
@@ -2,7 +2,7 @@
2
2
  # To run: docker run -d --name scimgateway-data -t scimgateway-data:latest
3
3
 
4
4
  FROM busybox:latest
5
- LABEL maintainer="Jeffrey Gilbert"
5
+ LABEL maintainer="https://elshaug.xyz"
6
6
 
7
7
  RUN mkdir -p /home/scimgateway/config
8
8
  VOLUME /home/scimgateway/config
@@ -1,3 +1,5 @@
1
+ # Thanks to Charles Watson <cwatsonx@costco.com> and Jeffrey Gilbert for the base of Docker implementation and inspiration.
2
+ #
1
3
  # Depending on your system you may need to prefix the commands below with sudo.
2
4
  #
3
5
  # To build: docker build --force-rm=true -t <projectName>:1.0.0 .
@@ -14,7 +16,7 @@
14
16
  FROM oven/bun:slim AS base
15
17
 
16
18
  # Declare who maintains this Dockerfile
17
- LABEL maintainer="Charles Watson <cwatsonx@costco.com>"
19
+ LABEL maintainer="https://elshaug.xyz"
18
20
 
19
21
  # Add a Process ID 1 Safety Net. Specific to debian.
20
22
  ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
@@ -25,7 +27,7 @@ WORKDIR /home/scimgateway
25
27
  ENV NODE_HOME=/home/scimgateway
26
28
 
27
29
  # Add your project info
28
- ADD ./package.json ./bun.lockb $NODE_HOME
30
+ ADD ./package.json ./bun.lock $NODE_HOME
29
31
 
30
32
  # Install dependencies (exclude test stuff for dependencies)
31
33
  RUN . ~/.bashrc && cd $NODE_HOME && bun install --production --frozen-lockfile
@@ -154,27 +154,9 @@ export class HelperRest {
154
154
 
155
155
  if (tenantIdGUID) { // Microsoft Entra ID
156
156
  if (this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.issuer) { // federated credentials
157
- const name = JSON.stringify(this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.name) // ensure not using none valid json key
158
- if (!this.scimgateway.jwk) this.scimgateway.jwk = {}
159
- if (!this.scimgateway.jwk[baseEntity]) this.scimgateway.jwk[baseEntity] = {}
160
- if (!this.scimgateway.jwk[baseEntity][name]) {
161
- const { publicKey, privateKey } = await jose.generateKeyPair('RS256')
162
- this.scimgateway.jwk[baseEntity][name] = { publicKey, privateKey }
163
- const ttl = 5 * 60 // 5 minutes
164
- ;(async () => {
165
- // rotate - delete JWK after 5 minutes, will be regenerated on next token request
166
- // entra id only lookup well-known uri and corresponding jwks_uri on token request validation if kid not found in entra cached JWKS
167
- setTimeout(async () => {
168
- delete this.scimgateway.jwk[baseEntity][name]
169
- }, ttl * 1000)
170
- })()
171
- }
172
-
173
- this.scimgateway.jwk[baseEntity].issuer = this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.issuer // updates .well-known
174
-
175
157
  const now = Date.now()
176
158
  const jwtPayload: jose.JWTPayload = {
177
- iss: this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.issuer, // entra id federated credentials issuer e.g. https://scimgateway.my-company.com/oauth
159
+ iss: this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.issuer, // entra id federated credentials issuer - scimgateway base URL, e.g. https://scimgateway.my-company.com
178
160
  sub: this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.subject, // entra id application object id - client id
179
161
  name: this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.name, // entra id federated credentials unique name e.g. plugin-entra-id
180
162
  aud: 'api://AzureADTokenExchange', // entra id federated credentials audience
@@ -188,7 +170,8 @@ export class HelperRest {
188
170
  ...jwtPayload,
189
171
  }
190
172
 
191
- const jwk = await jose.exportJWK(this.scimgateway.jwk[baseEntity][name].publicKey)
173
+ const { publicKey, privateKey } = await jose.generateKeyPair('RS256')
174
+ const jwk = await jose.exportJWK(publicKey)
192
175
  const kid = createHash('sha256') // kid required for JWKS
193
176
  .update(JSON.stringify(jwk))
194
177
  .digest('base64url')
@@ -206,8 +189,22 @@ export class HelperRest {
206
189
  client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
207
190
  client_assertion: await new jose.SignJWT(jwtClaims)
208
191
  .setProtectedHeader(jwtHeaders)
209
- .sign(this.scimgateway.jwk[baseEntity][name].privateKey),
192
+ .sign(privateKey),
193
+ }
194
+
195
+ // keep JWK for 5 minutes, will be regenerated on next token request
196
+ // entra id only lookup well-known uri and corresponding jwks_uri on token request validation if kid not found in entra cached JWKS
197
+ if (!this.scimgateway.jwk) this.scimgateway.jwk = {}
198
+ if (!this.scimgateway.jwk[kid]) {
199
+ this.scimgateway.jwk[kid] = { publicKey, privateKey }
200
+ const ttl = 5 * 60
201
+ ;(async () => {
202
+ setTimeout(async () => {
203
+ delete this.scimgateway.jwk[kid]
204
+ }, ttl * 1000)
205
+ })()
210
206
  }
207
+ this.scimgateway.jwk.issuer = this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.issuer // all baseEntities should use same issuer
211
208
  } else { // standard certificate
212
209
  if (!this.config_entity[baseEntity]?.connection?.auth?.options?.tls?.cert) {
213
210
  throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - missing options.tls.key/cert configuration`)
@@ -918,13 +915,13 @@ export class HelperRest {
918
915
  * }
919
916
  *
920
917
  * // Microsoft Entra ID - using Federated credentials
921
- * // Note, fedCred configuration must match corresponding configuration in Entra ID Application - Federation credentials
918
+ * // Note, fedCred configuration must match corresponding configuration in Entra ID Application - Certificates & Secrets - Federated credentials - scenario "Other issuer"
922
919
  * {
923
920
  * "type": "oauthJwtBearer",
924
921
  * "options": {
925
922
  * "tenantIdGUID": "<Entra ID tenantIdGUID",
926
923
  * "fedCred": {
927
- * "issuer": "<https://FQDN-scimgateway/oauth>", // e.g. https://scimgateway.my-company.com/oauth
924
+ * "issuer": "<https://FQDN-scimgateway", // scimgateway base URL, e.g. https://scimgateway.my-company.com
928
925
  * "subject": "<entra id application object id - client id>",
929
926
  * "name": "<entra id federated credentials unique name>" // e.g. plugin-entra-id
930
927
  * }
@@ -121,6 +121,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
121
121
  for (const user of users) {
122
122
  const scimUser = {
123
123
  id: user.UserID.value ? user.UserID.value : undefined,
124
+ externalId: user.UserID.value ? user.UserID.value : undefined,
124
125
  userName: user.UserID.value ? user.UserID.value : undefined,
125
126
  active: user.Enabled.value === 'true' || false,
126
127
  name: {
@@ -292,6 +293,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
292
293
  for (const group of groups) {
293
294
  const scimGroup: Record<string, any> = {
294
295
  id: group.GroupID.value ? group.GroupID.value : undefined,
296
+ externalId: group.GroupID.value ? group.GroupID.value : undefined,
295
297
  displayName: group.GroupID.value ? group.GroupID.value : undefined,
296
298
  active: group.Enabled.value === 'true' || false,
297
299
  members: [],
@@ -333,15 +333,17 @@ export class ScimGateway {
333
333
  pluginDir = '.' // only support running binary in current directory (path to binary can't be found)
334
334
  configDir = './config'
335
335
  }
336
- const configFile = path.join(`${configDir}`, `${pluginName}.json`) // config name prefix same as pluging name prefix
336
+ const configFile = path.join(configDir, `${pluginName}.json`) // config name prefix same as pluging name prefix
337
337
  const gwName = path.basename(fileURLToPath(import.meta.url)).split('.')[0] // prefix of current file - using fileURLToPath because using "__filename" is not supported by nodejs typescript
338
338
  const gwPath = path.dirname(fileURLToPath(import.meta.url))
339
339
 
340
340
  this.config = {}
341
341
  // exposed outside class
342
+ this.gwName = gwName
342
343
  this.pluginName = pluginName
343
344
  this.configDir = configDir
344
345
  this.configFile = configFile
346
+ this.authPassThroughAllowed = false // set to true by plugin if using Auth PassThrough
345
347
  this.countries = (() => {
346
348
  try {
347
349
  return JSON.parse(fs.readFileSync(path.join(gwPath, 'countries.json')).toString())
@@ -382,14 +384,7 @@ export class ScimGateway {
382
384
  logger.error(`${gwName}[${pluginName}] stopping...`)
383
385
  throw (new Error('Using exception to stop further asynchronous code execution (ensure synchronous logger flush to logfile and exit program), please ignore this one...'))
384
386
  }
385
-
386
387
  this.logger = logger
387
- // exposed to plugin
388
- this.gwName = gwName
389
- this.pluginName = pluginName
390
- this.configDir = configDir
391
- this.configFile = configFile
392
- this.authPassThroughAllowed = false // set to true by plugin if using Auth PassThrough
393
388
 
394
389
  const oAuthTokenExpire = 3600 // seconds
395
390
  let pwErrCount = 0
@@ -486,7 +481,7 @@ export class ScimGateway {
486
481
  getMethod: 'getAppRoles',
487
482
  }
488
483
  /** handlers supported url paths */
489
- const handlers = ['users', 'groups', 'bulk', 'serviceplans', 'approles', 'api', 'schemas', 'resourcetypes', 'serviceproviderconfig', 'serviceproviderconfigs', 'oauth', 'logger']
484
+ const handlers = ['users', 'groups', 'bulk', 'serviceplans', 'approles', 'api', 'schemas', 'resourcetypes', 'serviceproviderconfig', 'serviceproviderconfigs', 'oauth', '.well-known', 'logger']
490
485
 
491
486
  try {
492
487
  if (!fs.existsSync(configDir + '/wsdls')) fs.mkdirSync(configDir + '/wsdls')
@@ -972,53 +967,44 @@ export class ScimGateway {
972
967
  )
973
968
  }
974
969
 
975
- // oauth well-known: /oauth/.well-known/openid-configuration
970
+ // oauth well-known: /.well-known/openid-configuration
976
971
  // this.jwk is managed by helper-rest oauthJwtBearer - Entra ID Federated Identity
977
- // {issuer: <scimgateway-baseUrl>/oauth, <federated-identity-unique-name>: {privateKey, publicKey}}
978
- // example issuer: https://scimgateway.my-company.com/oauth
972
+ // { issuer: <scimgateway-baseUrl>, kid: { privateKey, publicKey } }
973
+ // example issuer: https://scimgateway.my-company.com
979
974
  const getHandlerOauthWellKnown = async (ctx: Context) => {
980
- const baseEntity = ctx.routeObj.baseEntity
981
- logger.debug(`${gwName}[${pluginName}][${baseEntity}] [oauth] .well-known request`)
982
-
983
- if (!this.jwk || !this.jwk[baseEntity] || !this.jwk[baseEntity].issuer) {
975
+ logger.debug(`${gwName}[${pluginName}] [oauth] .well-known request`)
976
+ if (!this.jwk || (Object.keys(this.jwk).length < 1)) {
984
977
  ctx.response.body = '{}'
985
978
  ctx.response.status = 200
986
979
  return ctx
987
980
  }
988
-
989
- const issuer = this.jwk[baseEntity].issuer // dynamic set by helper-rest oauthJwtBearer e.g. 'https://scimgateway.my-company.com/oauth'
981
+ const issuer = this.jwk.issuer
990
982
  let body = {
991
983
  issuer,
992
- jwks_uri: issuer + '/certs',
984
+ jwks_uri: issuer + '/.well-known/jwks.json',
993
985
  }
994
986
  ctx.response.body = JSON.stringify(body)
995
987
  ctx.response.status = 200
996
988
  }
997
989
 
998
- // oauth JWKS: /oauth/certs
990
+ // oauth JWKS: /.well-known/jwks.json
999
991
  // this.jwk is managed by helper-rest oauthJwtBearer - Entra ID Federated Identity
1000
- // {issuer: <scimgateway-baseUrl>/oauth, <federated-identity-unique-name>: {privateKey, publicKey}}
1001
- const getHandlerOauthCerts = async (ctx: Context) => {
1002
- const baseEntity = ctx.routeObj.baseEntity
1003
- logger.debug(`${gwName}[${pluginName}][${baseEntity}] [oauth] jwks_uri certs request`)
1004
-
1005
- if (!this.jwk || !this.jwk[baseEntity]) {
992
+ // { issuer: <scimgateway-baseUrl>, kid: { privateKey, publicKey } }
993
+ const getHandlerOauthJwks = async (ctx: Context) => {
994
+ logger.debug(`${gwName}[${pluginName}] [oauth] jwks_uri request`)
995
+ if (!this.jwk || (Object.keys(this.jwk).length < 1)) {
1006
996
  ctx.response.body = '{"keys":[]}'
1007
997
  ctx.response.status = 200
1008
998
  return ctx
1009
999
  }
1010
-
1011
1000
  const keys: Array<Record<string, any>> = []
1012
- for (const name in this.jwk[baseEntity]) {
1013
- const keyObj = this.jwk[baseEntity][name]
1014
- if (typeof keyObj !== 'object' || keyObj === null) continue // skip issuer
1015
- const jwk = await jose.exportJWK(this.jwk[baseEntity][name].publicKey)
1016
- jwk.kid = createHash('sha256') // needed for JWKS
1017
- .update(JSON.stringify(jwk))
1018
- .digest('base64url')
1001
+ for (const kid in this.jwk) {
1002
+ const keyObj = this.jwk[kid]
1003
+ if (typeof keyObj !== 'object' || keyObj === null) continue
1004
+ const jwk = await jose.exportJWK(this.jwk[kid].publicKey)
1005
+ jwk.kid = kid // needed for JWKS
1019
1006
  keys.push(jwk)
1020
1007
  }
1021
-
1022
1008
  let body = {
1023
1009
  keys,
1024
1010
  }
@@ -2673,13 +2659,13 @@ export class ScimGateway {
2673
2659
  return ctx
2674
2660
  }
2675
2661
  }
2676
- if (ctx.request.method === 'GET' && ctx.path.endsWith('/oauth/.well-known/openid-configuration')) {
2662
+ if (ctx.request.method === 'GET' && ctx.path.endsWith('/.well-known/openid-configuration')) {
2677
2663
  await getHandlerOauthWellKnown(ctx)
2678
2664
  if (!ctx.response.status) ctx.response.status = 404
2679
2665
  return ctx
2680
2666
  }
2681
- if (ctx.request.method === 'GET' && ctx.path.endsWith('/oauth/certs')) {
2682
- await getHandlerOauthCerts(ctx)
2667
+ if (ctx.request.method === 'GET' && ctx.path.endsWith('/.well-known/jwks.json')) {
2668
+ await getHandlerOauthJwks(ctx)
2683
2669
  if (!ctx.response.status) ctx.response.status = 404
2684
2670
  return ctx
2685
2671
  }
@@ -3093,8 +3079,8 @@ export class ScimGateway {
3093
3079
  let request = new Request(new URL(req.url ?? '', `${protocol}://${req.headers.host}`), {
3094
3080
  method: req.method,
3095
3081
  headers: new Headers(req.headers as any),
3082
+ // @ts-expect-error ignore incompatible types
3096
3083
  body: body,
3097
- // @ts-expect-error duplex not defined in RequestInit interface
3098
3084
  duplex: body ? 'half' : undefined,
3099
3085
  }) as Request & { raw: IncomingMessage }
3100
3086
  request.raw = req
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.5.1",
3
+ "version": "5.5.3",
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)",