scimgateway 5.5.1 → 5.5.2

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
@@ -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:
@@ -1491,6 +1491,17 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1491
1491
 
1492
1492
  ## Change log
1493
1493
 
1494
+ ### v5.5.2
1495
+
1496
+ [Improved]
1497
+
1498
+ - Entra ID Federated Identity Credentials introduced in v5.5.0, the issuer configuration should be scimgateway base URL
1499
+ old: `"issuer": "<https://FQDN-scimgateway>/oauth"`
1500
+ new: `"issuer": "<https://FQDN-scimgateway>"`
1501
+
1502
+ Change log v5.5.0 have been corrected with the new issuer having base URL only
1503
+
1504
+
1494
1505
  ### v5.5.1
1495
1506
 
1496
1507
  [Fixed]
@@ -1510,7 +1521,7 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1510
1521
  "options": {
1511
1522
  "tenantIdGUID": "<Entra ID tenantIdGUID",
1512
1523
  "fedCred": {
1513
- "issuer": "<https://FQDN-scimgateway/oauth>",
1524
+ "issuer": "<https://FQDN-scimgateway>",
1514
1525
  "subject": "<entra id application object id - client id>",
1515
1526
  "name": "<entra id federated credentials unique name>"
1516
1527
  }
@@ -1524,14 +1535,14 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1524
1535
  "options": {
1525
1536
  "tenantIdGUID": "11111111-2222-3333-4444-555555555555",
1526
1537
  "fedCred": {
1527
- "issuer": "https://scimgateway.my-company.com/oauth",
1538
+ "issuer": "https://scimgateway.my-company.com",
1528
1539
  "subject": "99999999-8888-7777-6666-555555555555",
1529
1540
  "name": "plugin-entra-id"
1530
1541
  }
1531
1542
  }
1532
1543
  }
1533
1544
 
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.
1545
+ 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
1546
 
1536
1547
  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
1548
 
@@ -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
  * }
@@ -486,7 +486,7 @@ export class ScimGateway {
486
486
  getMethod: 'getAppRoles',
487
487
  }
488
488
  /** handlers supported url paths */
489
- const handlers = ['users', 'groups', 'bulk', 'serviceplans', 'approles', 'api', 'schemas', 'resourcetypes', 'serviceproviderconfig', 'serviceproviderconfigs', 'oauth', 'logger']
489
+ const handlers = ['users', 'groups', 'bulk', 'serviceplans', 'approles', 'api', 'schemas', 'resourcetypes', 'serviceproviderconfig', 'serviceproviderconfigs', 'oauth', '.well-known', 'logger']
490
490
 
491
491
  try {
492
492
  if (!fs.existsSync(configDir + '/wsdls')) fs.mkdirSync(configDir + '/wsdls')
@@ -972,53 +972,44 @@ export class ScimGateway {
972
972
  )
973
973
  }
974
974
 
975
- // oauth well-known: /oauth/.well-known/openid-configuration
975
+ // oauth well-known: /.well-known/openid-configuration
976
976
  // 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
977
+ // { issuer: <scimgateway-baseUrl>, kid: { privateKey, publicKey } }
978
+ // example issuer: https://scimgateway.my-company.com
979
979
  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) {
980
+ logger.debug(`${gwName}[${pluginName}] [oauth] .well-known request`)
981
+ if (!this.jwk || (Object.keys(this.jwk).length < 1)) {
984
982
  ctx.response.body = '{}'
985
983
  ctx.response.status = 200
986
984
  return ctx
987
985
  }
988
-
989
- const issuer = this.jwk[baseEntity].issuer // dynamic set by helper-rest oauthJwtBearer e.g. 'https://scimgateway.my-company.com/oauth'
986
+ const issuer = this.jwk.issuer
990
987
  let body = {
991
988
  issuer,
992
- jwks_uri: issuer + '/certs',
989
+ jwks_uri: issuer + '/.well-known/jwks.json',
993
990
  }
994
991
  ctx.response.body = JSON.stringify(body)
995
992
  ctx.response.status = 200
996
993
  }
997
994
 
998
- // oauth JWKS: /oauth/certs
995
+ // oauth JWKS: /.well-known/jwks.json
999
996
  // 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]) {
997
+ // { issuer: <scimgateway-baseUrl>, kid: { privateKey, publicKey } }
998
+ const getHandlerOauthJwks = async (ctx: Context) => {
999
+ logger.debug(`${gwName}[${pluginName}] [oauth] jwks_uri request`)
1000
+ if (!this.jwk || (Object.keys(this.jwk).length < 1)) {
1006
1001
  ctx.response.body = '{"keys":[]}'
1007
1002
  ctx.response.status = 200
1008
1003
  return ctx
1009
1004
  }
1010
-
1011
1005
  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')
1006
+ for (const kid in this.jwk) {
1007
+ const keyObj = this.jwk[kid]
1008
+ if (typeof keyObj !== 'object' || keyObj === null) continue
1009
+ const jwk = await jose.exportJWK(this.jwk[kid].publicKey)
1010
+ jwk.kid = kid // needed for JWKS
1019
1011
  keys.push(jwk)
1020
1012
  }
1021
-
1022
1013
  let body = {
1023
1014
  keys,
1024
1015
  }
@@ -2673,13 +2664,13 @@ export class ScimGateway {
2673
2664
  return ctx
2674
2665
  }
2675
2666
  }
2676
- if (ctx.request.method === 'GET' && ctx.path.endsWith('/oauth/.well-known/openid-configuration')) {
2667
+ if (ctx.request.method === 'GET' && ctx.path.endsWith('/.well-known/openid-configuration')) {
2677
2668
  await getHandlerOauthWellKnown(ctx)
2678
2669
  if (!ctx.response.status) ctx.response.status = 404
2679
2670
  return ctx
2680
2671
  }
2681
- if (ctx.request.method === 'GET' && ctx.path.endsWith('/oauth/certs')) {
2682
- await getHandlerOauthCerts(ctx)
2672
+ if (ctx.request.method === 'GET' && ctx.path.endsWith('/.well-known/jwks.json')) {
2673
+ await getHandlerOauthJwks(ctx)
2683
2674
  if (!ctx.response.status) ctx.response.status = 404
2684
2675
  return ctx
2685
2676
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.5.1",
3
+ "version": "5.5.2",
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)",