scimgateway 5.0.10 → 5.0.11

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,13 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1111
1111
 
1112
1112
  ## Change log
1113
1113
 
1114
+ ### v5.0.11
1115
+
1116
+ [Fixed]
1117
+
1118
+ - OAuth token response on error missing error_description in v5
1119
+ - HelperRest doRequest() now also includes retry logic on invalid token that has not expired - will renew token
1120
+
1114
1121
  ### v5.0.10
1115
1122
 
1116
1123
  [Improved]
@@ -166,13 +166,11 @@ export class HelperRest {
166
166
  const response = await this.doRequest(baseEntity, method, tokenUrl, form, ctx, connOpt)
167
167
  if (!response.body) {
168
168
  const err = new Error(`[${action}] No data retrieved from: ${method} ${tokenUrl}`)
169
- this.lock.release()
170
169
  throw (err)
171
170
  }
172
171
  const jbody = response.body
173
172
  if (jbody.error) {
174
173
  const err = new Error(`[${action}] Error message: ${jbody.error_description}`)
175
- this.lock.release()
176
174
  throw (err)
177
175
  }
178
176
  if (this.config_entity[baseEntity]?.connection?.auth?.type === 'token') { // in case response using token instead of access_token
@@ -180,7 +178,6 @@ export class HelperRest {
180
178
  else if (jbody.accessToken) jbody.access_token = jbody.accessToken
181
179
  }
182
180
  if (!jbody.access_token) {
183
- this.lock.release()
184
181
  const err = new Error(`[${action}] Error message: Retrieved invalid token response`)
185
182
  throw (err)
186
183
  }
@@ -229,7 +226,7 @@ export class HelperRest {
229
226
  this._serviceClient[baseEntity][clientIdentifier].accessToken = accessToken
230
227
  this._serviceClient[baseEntity][clientIdentifier].options.headers['Authorization'] = ` Bearer ${accessToken.access_token}`
231
228
  } catch (err) {
232
- delete this._serviceClient[baseEntity][clientIdentifier]
229
+ if (this._serviceClient[baseEntity]) delete this._serviceClient[baseEntity][clientIdentifier]
233
230
  const newErr = err
234
231
  throw newErr
235
232
  }
@@ -517,9 +514,6 @@ export class HelperRest {
517
514
  } catch (err: any) { // includes failover/retry logic based on config baseUrls array
518
515
  let statusCode
519
516
  try { statusCode = JSON.parse(err.message).statusCode } catch (e) { void 0 }
520
- if (statusCode === 404) { // not logged as error, let caller decide e.g. getUser-manager
521
- this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
522
- } else this.scimgateway.logError(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
523
517
  const clientIdentifier = this.getClientIdentifier(ctx)
524
518
  if (err.message.includes('ratelimit')) { // have seen throttling not follow standard 429/retry-after, but instead using 500 and error message only
525
519
  if (!retryAfter) retryAfter = 60
@@ -527,13 +521,17 @@ export class HelperRest {
527
521
  if (!retryCount) retryCount = 0
528
522
  let urlObj
529
523
  try { urlObj = new URL(path) } catch (err) { void 0 }
530
- if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ABORT_ERR' || err.code === 'ETIMEDOUT' || retryAfter)) {
524
+ let isServiceClient = !urlObj && this._serviceClient[baseEntity] && this._serviceClient[baseEntity][clientIdentifier] && !this.lock.isLocked() // !isLocked to avoid retry ongoing doRequest with failing getAccessToken()
525
+ let oAuthTokeErr = statusCode === 401 && this.config_entity[baseEntity].connection?.auth?.type && this.config_entity[baseEntity].connection.auth.type.startsWith('oauth')
526
+ if (isServiceClient && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ABORT_ERR' || err.code === 'ETIMEDOUT' || oAuthTokeErr || retryAfter)) {
527
+ this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
531
528
  if (retryAfter) {
532
529
  this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} throttle/ratelimit error - awaiting ${retryAfter} seconds before automatic retry`)
533
530
  await new Promise(resolve => setTimeout(function () {
534
531
  resolve(null)
535
532
  }, retryAfter * 1000))
536
533
  }
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?
537
535
  if (retryCount < this.config_entity[baseEntity].connection.baseUrls.length) {
538
536
  retryCount++
539
537
  this.updateServiceClient(baseEntity, clientIdentifier, { baseUrl: this.config_entity[baseEntity].connection.baseUrls[retryCount - 1] })
@@ -541,13 +539,19 @@ export class HelperRest {
541
539
  const ret = await this.doRequestHandler(baseEntity, method, path, body, ctx, opt, retryCount) // retry
542
540
  return ret // problem fixed
543
541
  } else {
542
+ if (statusCode === 404) { // not logged as error e.g. getUser-manager
543
+ 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}`)
544
545
  throw err
545
546
  }
546
547
  } else {
548
+ if (statusCode === 404) { // not logged as error e.g. getUser-manager
549
+ this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
550
+ } else this.scimgateway.logError(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
547
551
  if (statusCode === 401 && this._serviceClient[baseEntity]) {
548
552
  delete this._serviceClient[baseEntity][clientIdentifier]
549
553
  }
550
- throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED)
554
+ throw err // Symantec IM retries getUser 6 times on ECONNREFUSED
551
555
  }
552
556
  }
553
557
  }
@@ -698,7 +698,6 @@ export class ScimGateway {
698
698
  }
699
699
  if (tokenObj.baseEntities) {
700
700
  if (Array.isArray(tokenObj.baseEntities) && tokenObj.baseEntities.length > 0) {
701
- if (!baseEntity) return reject(new Error(`baseEntity=${baseEntity} not allowed for this bearerOAuth according to bearerOAuth configuration baseEntitites=${tokenObj.baseEntities}`))
702
701
  if (!tokenObj.baseEntities.includes(baseEntity)) return reject(new Error(`baseEntity=${baseEntity} not allowed for this bearerOAuth according to bearerOAuth configuration baseEntitites=${tokenObj.baseEntities}`))
703
702
  }
704
703
  }
@@ -706,7 +705,11 @@ export class ScimGateway {
706
705
  return resolve(true)
707
706
  } else {
708
707
  for (let i = 0; i < arr.length; i++) { // resolve if token memory store have been cleared because of a gateway restart
709
- if (utils.getEncrypted(authToken, arr[i].client_secret) === arr[i].client_secret && !arr[i].isTokenRequested) {
708
+ if (arr[i].isTokenRequested || !arr[i].clientSecret) continue
709
+ if (arr[i].baseEntities && Array.isArray(arr[i].baseEntities) && arr[i].baseEntities.length > 0) {
710
+ if (!arr[i].baseEntities.includes(baseEntity)) continue
711
+ }
712
+ if (utils.getEncrypted(authToken, arr[i].clientSecret) === arr[i].clientSecret) {
710
713
  arr[i].isTokenRequested = true // flagged as true to not allow repeated resolvements because token will also be cleared when expired
711
714
  const baseEntities = utils.copyObj(arr[i].baseEntities)
712
715
  let expires
@@ -774,8 +777,21 @@ export class ScimGateway {
774
777
  if (ctx.request.url !== '/favicon.ico') logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${err.message}`)
775
778
  return false
776
779
  } catch (err: any) {
777
- if (authType === 'Bearer') ctx.response.headers.set('WWW-Authenticate', 'Bearer realm=""')
778
- else ctx.response.headers.set('WWW-Authenticate', 'Basic realm=""')
780
+ if (authType === 'Bearer') {
781
+ let str = 'realm=""'
782
+ if (err?.name === 'invalid_token') {
783
+ str += `, error="${err.name}"`
784
+ if (err.message) {
785
+ str += `, error_description="${err.message}"`
786
+ const errMsg = {
787
+ error: err.name,
788
+ error_description: err.message,
789
+ }
790
+ ctx.response.body = JSON.stringify(errMsg)
791
+ }
792
+ }
793
+ ctx.response.headers.set('WWW-Authenticate', `Bearer ${str}`)
794
+ } else ctx.response.headers.set('WWW-Authenticate', 'Basic realm=""')
779
795
  if (pwErrCount < 3) {
780
796
  pwErrCount += 1
781
797
  logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ctx.request.url} ${err.message}`)
package/lib/utils.ts CHANGED
@@ -14,18 +14,20 @@ import fs from 'node:fs'
14
14
  import path from 'node:path'
15
15
  import { EventEmitter } from 'node:events'
16
16
 
17
- // mutual exclusion ref: https://thecodebarbarian.com/mutual-exclusion-patterns-with-node-promises
17
+ /** Lock implements mutual exclusion
18
+ * reference: https://thecodebarbarian.com/mutual-exclusion-patterns-with-node-promises
19
+ */
18
20
  export class Lock {
19
- private _locked: boolean
21
+ private _locked = false
20
22
  private _ee: any
21
23
  constructor() {
22
24
  this._locked = false
23
25
  this._ee = new EventEmitter()
24
26
  }
25
27
 
28
+ /** If nobody has the lock, take it and resolve immediately else wait until released */
26
29
  acquire() {
27
30
  return new Promise((resolve) => {
28
- // If nobody has the lock, take it and resolve immediately
29
31
  if (!this._locked) {
30
32
  // Safe because JS doesn't interrupt you on synchronous operations,
31
33
  // so no need for compare-and-swap or anything like that.
@@ -45,11 +47,16 @@ export class Lock {
45
47
  })
46
48
  }
47
49
 
50
+ /** Release the lock immediately */
48
51
  release() {
49
- // Release the lock immediately
50
52
  this._locked = false
51
53
  setImmediate(() => this._ee.emit('release'))
52
54
  }
55
+
56
+ /** Return status of lock true/false */
57
+ isLocked(): boolean {
58
+ return this._locked
59
+ }
53
60
  }
54
61
 
55
62
  export const getSecret = function (dotNotationAttr: string, configFile: string) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.0.10",
3
+ "version": "5.0.11",
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)",