scimgateway 5.4.4 → 5.5.1

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
@@ -16,6 +16,7 @@ Validated through IdP's:
16
16
 
17
17
  Latest news:
18
18
 
19
+ - Entra ID [Federated Identity Credentials](https://learn.microsoft.com/en-us/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0) is now supported. Identity federation allows SCIM Gateway to access Microsoft Entra protected resources without needing to manage secrets
19
20
  - External JWKS (JSON Web Key Set) is now supported by JWT Authentication. These are public and typically frequent rotated by modern identity providers
20
21
  - [Azure Relay](https://learn.microsoft.com/en-us/azure/azure-relay/relay-what-is-it) is now supported for secure and hassle-free outbound communication — with just one minute of configuration
21
22
  - [ETag](https://datatracker.ietf.org/doc/html/rfc7644#section-3.14) is now supported
@@ -418,7 +419,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
418
419
 
419
420
  - **auth.bearerJwtAzure** - Array of one or more JWT used by Azure SyncFabric. **tenantIdGUID** must be set to Entra ID Tenant ID.
420
421
 
421
- - **auth.bearerJwt** - Array of one or more standard JWT objects. Using **secret**, **publicKey** or **wellKnownUri** for signature verification. publicKey should be set to the filename of public key or certificate pem-file located in `<package-root>\config\certs` or absolute path being used. Clear text secret will become encrypted when gateway is started. For JWKS (JSON Web Key Set), the **wellKnownUri** must be set to identity provider well-known URI which will be used for lookup the jwks_uri key. **options.issuer** should normally be set for validation when using secret or publicKey. Other options may also be included according to jsonwebtoken npm package definition.
422
+ - **auth.bearerJwt** - Array of one or more standard JWT objects. Using **secret**, **publicKey** or **wellKnownUri** for signature verification. publicKey should be set to the filename of public key or certificate pem-file located in `<package-root>\config\certs` or absolute path being used. Clear text secret will become encrypted when gateway is started. For JWKS (JSON Web Key Set), the **wellKnownUri** must be set to identity provider well-known URI which will be used for lookup the jwks_uri key. **options.issuer** should normally be set for validation when using secret or publicKey, for JWKS the issuer will be included automatically. Other options may also be included according to the JWT standard.
422
423
 
423
424
  - **auth.bearerOAuth** - Array of one or more Client Credentials OAuth configuration objects. **`clientId`** and **`clientSecret`** are mandatory. clientSecret value will become encrypted when gateway is started. OAuth token request url is **/oauth/token** e.g. `http://localhost:8880/oauth/token`
424
425
 
@@ -716,7 +717,7 @@ Example Entra ID (plugin-entra-id) using certificate secret:
716
717
  "connection": {
717
718
  "baseUrls": [],
718
719
  "auth": {
719
- "type": "oauth",
720
+ "type": "oauthJwtBearer",
720
721
  "options": {
721
722
  "tenantIdGUID": "<tenantId>",
722
723
  "clientId": "<clientId>",
@@ -728,6 +729,27 @@ Example Entra ID (plugin-entra-id) using certificate secret:
728
729
  }
729
730
  }
730
731
 
732
+ Example Entra ID (plugin-entra-id) using federated credentials:
733
+
734
+ "connection": {
735
+ "baseUrls": [],
736
+ "auth": {
737
+ "type": "oauthJwtBearer",
738
+ "options": {
739
+ "tenantIdGUID": "<tenantId>",
740
+ "fedCred": {
741
+ "issuer": "<https://FQDN-scimgateway/oauth>",
742
+ "subject": "<entra id application object id - client id>",
743
+ "name": "<entra id federated credentials unique name>"
744
+ }
745
+ }
746
+ }
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"
751
+
752
+
731
753
  Example using general OAuth:
732
754
 
733
755
  "connection": {
@@ -1469,6 +1491,50 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1469
1491
 
1470
1492
  ## Change log
1471
1493
 
1494
+ ### v5.5.1
1495
+
1496
+ [Fixed]
1497
+
1498
+ - 401 Unauthorized response did include scim-formatted error message when using `helper-rest` and authentication `PassThrough`. 401 should not include scim-formatted error message
1499
+
1500
+ ### v5.5.0
1501
+
1502
+ [Improved]
1503
+
1504
+ - Entra ID [Federated Identity Credentials](https://learn.microsoft.com/en-us/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0) is now supported. Identity federation allows SCIM Gateway to access Microsoft Entra protected resources without needing to manage secrets
1505
+
1506
+ helper-rest includes options for federated credentials:
1507
+
1508
+ "auth {
1509
+ "type": "oauthJwtBearer",
1510
+ "options": {
1511
+ "tenantIdGUID": "<Entra ID tenantIdGUID",
1512
+ "fedCred": {
1513
+ "issuer": "<https://FQDN-scimgateway/oauth>",
1514
+ "subject": "<entra id application object id - client id>",
1515
+ "name": "<entra id federated credentials unique name>"
1516
+ }
1517
+ }
1518
+ }
1519
+
1520
+ Example:
1521
+
1522
+ "auth {
1523
+ "type": "oauthJwtBearer",
1524
+ "options": {
1525
+ "tenantIdGUID": "11111111-2222-3333-4444-555555555555",
1526
+ "fedCred": {
1527
+ "issuer": "https://scimgateway.my-company.com/oauth",
1528
+ "subject": "99999999-8888-7777-6666-555555555555",
1529
+ "name": "plugin-entra-id"
1530
+ }
1531
+ }
1532
+ }
1533
+
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.
1535
+
1536
+ 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
+
1472
1538
  ### v5.4.4
1473
1539
 
1474
1540
  [Improved]
package/bun.lock CHANGED
@@ -16,8 +16,7 @@
16
16
  "https-proxy-agent": "^7.0.6",
17
17
  "hyco-https": "^1.4.5",
18
18
  "is-in-subnet": "^4.0.1",
19
- "jsonwebtoken": "^9.0.2",
20
- "jwk-to-pem": "^2.0.7",
19
+ "jose": "^6.0.11",
21
20
  "ldapjs": "^3.0.7",
22
21
  "lokijs": "^1.5.12",
23
22
  "mongodb": "^6.16.0",
@@ -28,11 +27,10 @@
28
27
  "saml": "^3.0.1",
29
28
  },
30
29
  "devDependencies": {
31
- "@stylistic/eslint-plugin": "^4.4.0",
30
+ "@stylistic/eslint-plugin": "^5.1.0",
32
31
  "@types/bun": "latest",
33
32
  "@types/dot-object": "^2.1.6",
34
33
  "@types/jsonwebtoken": "^9.0.9",
35
- "@types/jwk-to-pem": "^2.0.3",
36
34
  "@types/node": "latest",
37
35
  "@types/nodemailer": "^6.4.17",
38
36
  "@types/passport": "^1.0.17",
@@ -143,7 +141,7 @@
143
141
 
144
142
  "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
145
143
 
146
- "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@4.4.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.32.1", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "estraverse": "^5.3.0", "picomatch": "^4.0.2" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-CEigAk7eOLyHvdgmpZsKFwtiqS2wFwI1fn4j09IU9GmD4euFM4jEBAViWeCqaNLlbX2k2+A/Fq9cje4HQBXuJQ=="],
144
+ "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/types": "^8.34.1", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.2" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-TJRJul4u/lmry5N/kyCU+7RWWOk0wyXN+BncRlDYBqpLFnzXkd7QGVfN7KewarFIXv0IX0jSF/Ksu7aHWEDeuw=="],
147
145
 
148
146
  "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
149
147
 
@@ -165,8 +163,6 @@
165
163
 
166
164
  "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
167
165
 
168
- "@types/jwk-to-pem": ["@types/jwk-to-pem@2.0.3", "", {}, "sha512-I/WFyFgk5GrNbkpmt14auGO3yFK1Wt4jXzkLuI+fDBNtO5ZI2rbymyGd6bKzfSBEuyRdM64ZUwxU1+eDcPSOEQ=="],
169
-
170
166
  "@types/ldapjs": ["@types/ldapjs@3.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-E2Tn1ltJDYBsidOT9QG4engaQeQzRQ9aYNxVmjCkD33F7cIeLPgrRDXAYs0O35mK2YDU20c/+ZkNjeAPRGLM0Q=="],
171
167
 
172
168
  "@types/lokijs": ["@types/lokijs@1.5.14", "", {}, "sha512-4Fic47BX3Qxr8pd12KT6/T1XWU8dOlJBIp1jGoMbaDbiEvdv50rAii+B3z1b/J2pvMywcVP+DBPGP5/lgLOKGA=="],
@@ -239,8 +235,6 @@
239
235
 
240
236
  "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
241
237
 
242
- "asn1.js": ["asn1.js@5.4.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "safer-buffer": "^2.1.0" } }, "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA=="],
243
-
244
238
  "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
245
239
 
246
240
  "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
@@ -259,14 +253,10 @@
259
253
 
260
254
  "bl": ["bl@6.1.0", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw=="],
261
255
 
262
- "bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="],
263
-
264
256
  "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
265
257
 
266
258
  "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
267
259
 
268
- "brorand": ["brorand@1.1.0", "", {}, "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="],
269
-
270
260
  "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="],
271
261
 
272
262
  "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
@@ -325,8 +315,6 @@
325
315
 
326
316
  "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
327
317
 
328
- "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="],
329
-
330
318
  "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
331
319
 
332
320
  "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -411,12 +399,8 @@
411
399
 
412
400
  "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
413
401
 
414
- "hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="],
415
-
416
402
  "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
417
403
 
418
- "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="],
419
-
420
404
  "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
421
405
 
422
406
  "https": ["https@1.0.0", "", {}, "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="],
@@ -465,6 +449,8 @@
465
449
 
466
450
  "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
467
451
 
452
+ "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
453
+
468
454
  "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="],
469
455
 
470
456
  "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
@@ -479,8 +465,6 @@
479
465
 
480
466
  "jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
481
467
 
482
- "jwk-to-pem": ["jwk-to-pem@2.0.7", "", { "dependencies": { "asn1.js": "^5.3.0", "elliptic": "^6.6.1", "safe-buffer": "^5.0.1" } }, "sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ=="],
483
-
484
468
  "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
485
469
 
486
470
  "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
@@ -525,10 +509,6 @@
525
509
 
526
510
  "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
527
511
 
528
- "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
529
-
530
- "minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="],
531
-
532
512
  "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
533
513
 
534
514
  "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -10,10 +10,11 @@
10
10
  import { HttpsProxyAgent } from 'https-proxy-agent'
11
11
  import { URL } from 'url'
12
12
  import { Buffer } from 'node:buffer'
13
+ import { createPublicKey, createPrivateKey, createHash } from 'node:crypto'
13
14
  import { samlAssertion } from './samlAssertion.ts'
14
- import * as jsonwebtoken from 'jsonwebtoken'
15
15
  import fs from 'node:fs'
16
16
  import querystring from 'querystring'
17
+ import * as jose from 'jose'
17
18
  import * as utils from './utils.ts'
18
19
 
19
20
  /**
@@ -148,66 +149,118 @@ export class HelperRest {
148
149
  break
149
150
 
150
151
  case 'oauthJwtBearer':
151
- let jwtClaims: jsonwebtoken.JwtPayload | Record<string, any> = {}
152
- let jwtOpts: jsonwebtoken.SignOptions = {}
152
+ let jwtClaims: jose.JWTPayload | Record<string, any>
153
+ let jwtHeaders: jose.JWTHeaderParameters
153
154
 
154
155
  if (tenantIdGUID) { // Microsoft Entra ID
155
- if (!this.config_entity[baseEntity]?.connection?.auth?.options?.tls?.cert) {
156
- throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - missing options.tls.key/cert configuration`)
157
- }
158
- let privateKey = this.config_entity[baseEntity]?.connection?.auth?.options?.tls?._key || ''
159
- let cert = this.config_entity[baseEntity]?.connection?.auth?.options?.tls?._cert || ''
160
- if (!privateKey || !cert) {
161
- privateKey = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.key, 'utf-8') || ''
162
- cert = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.cert, 'utf-8') || ''
163
- if (privateKey) this.config_entity[baseEntity].connection.auth.options.tls._key = privateKey
164
- if (cert) this.config_entity[baseEntity].connection.auth.options.tls._cert = cert
165
- }
166
- if (!privateKey || !cert) {
167
- throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - missing options.tls.key/cert file content`)
168
- }
169
-
170
- const jwtPayload: jsonwebtoken.JwtPayload = {
171
- sub: this.config_entity[baseEntity]?.connection?.auth?.options?.clientId,
172
- iss: this.config_entity[baseEntity]?.connection?.auth?.options?.clientId,
173
- aud: `https://login.microsoftonline.com/${tenantIdGUID}/v2.0`,
174
- iat: Math.floor(Date.now() / 1000) - 60,
175
- exp: Math.floor(Date.now() / 1000) + 3600,
176
- jti: crypto.randomUUID(),
177
- nbf: Math.floor(Date.now() / 1000) - 60,
178
- }
179
- jwtClaims = {
180
- ...jwtPayload,
181
- }
182
-
183
- const base64Thumbprint = utils.getBase64CertificateThumbprint(cert, 'sha1') // xt5=>sha1, x5t#S256=>sha256
184
- jwtOpts = {
185
- algorithm: 'RS256',
186
- header: {
187
- typ: 'JWT',
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
+ const now = Date.now()
176
+ 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
178
+ sub: this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.subject, // entra id application object id - client id
179
+ name: this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.name, // entra id federated credentials unique name e.g. plugin-entra-id
180
+ aud: 'api://AzureADTokenExchange', // entra id federated credentials audience
181
+ // below is not used by entra id federated credentials token-generation - could be skipped
182
+ iat: Math.floor(now / 1000) - 60,
183
+ exp: Math.floor(now / 1000) + 3600,
184
+ jti: crypto.randomUUID(),
185
+ nbf: Math.floor(now / 1000) - 60,
186
+ }
187
+ jwtClaims = {
188
+ ...jwtPayload,
189
+ }
190
+
191
+ const jwk = await jose.exportJWK(this.scimgateway.jwk[baseEntity][name].publicKey)
192
+ const kid = createHash('sha256') // kid required for JWKS
193
+ .update(JSON.stringify(jwk))
194
+ .digest('base64url')
195
+
196
+ jwtHeaders = {
188
197
  alg: 'RS256',
189
- x5t: base64Thumbprint,
190
- },
191
- }
192
-
193
- /* Microsoft recommended modern x5t#S256 does not work using self-signed certificate
194
- const base64Thumbprint = utils.getBase64CertificateThumbprint(cert, 'sha256')
195
- jwtOpts = {
196
- algorithm: 'PS256',
197
- header: {
198
+ typ: 'JWT',
199
+ kid,
200
+ }
201
+
202
+ form = {
203
+ grant_type: 'client_credentials',
204
+ scope: this.config_entity[baseEntity].connection.auth.options.scope, // "https://graph.microsoft.com/.default"
205
+ client_id: this.config_entity[baseEntity]?.connection?.auth?.options?.fedCred?.subject,
206
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
207
+ client_assertion: await new jose.SignJWT(jwtClaims)
208
+ .setProtectedHeader(jwtHeaders)
209
+ .sign(this.scimgateway.jwk[baseEntity][name].privateKey),
210
+ }
211
+ } else { // standard certificate
212
+ if (!this.config_entity[baseEntity]?.connection?.auth?.options?.tls?.cert) {
213
+ throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - missing options.tls.key/cert configuration`)
214
+ }
215
+ let privateKey = this.config_entity[baseEntity]?.connection?.auth?.options?.tls?._key || ''
216
+ let cert = this.config_entity[baseEntity]?.connection?.auth?.options?.tls?._cert || ''
217
+ let certPem = this.config_entity[baseEntity]?.connection?.auth?.options?.tls?._certPem || ''
218
+ if (!privateKey || !cert) {
219
+ const privateKeyPem = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.key, 'utf-8') || ''
220
+ certPem = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.cert, 'utf-8') || ''
221
+ if (privateKeyPem) {
222
+ privateKey = createPrivateKey(privateKeyPem) // PEM => KeyObject
223
+ this.config_entity[baseEntity].connection.auth.options.tls._key = privateKey
224
+ }
225
+ if (certPem) {
226
+ cert = createPublicKey(certPem)
227
+ this.config_entity[baseEntity].connection.auth.options.tls._cert = cert
228
+ this.config_entity[baseEntity].connection.auth.options.tls._certPem = certPem
229
+ }
230
+ }
231
+ if (!privateKey || !cert) {
232
+ throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - missing options.tls.key/cert file content`)
233
+ }
234
+
235
+ const jwtPayload: jose.JWTPayload = {
236
+ iss: this.config_entity[baseEntity]?.connection?.auth?.options?.clientId,
237
+ sub: this.config_entity[baseEntity]?.connection?.auth?.options?.clientId,
238
+ aud: `https://login.microsoftonline.com/${tenantIdGUID}/v2.0`,
239
+ iat: Math.floor(Date.now() / 1000) - 60,
240
+ exp: Math.floor(Date.now() / 1000) + 3600,
241
+ jti: crypto.randomUUID(),
242
+ nbf: Math.floor(Date.now() / 1000) - 60,
243
+ }
244
+ jwtClaims = {
245
+ ...jwtPayload,
246
+ }
247
+
248
+ const base64Thumbprint = utils.getBase64CertificateThumbprint(certPem, 'sha256') // x5t=>sha1, x5t#S256=>sha256
249
+ jwtHeaders = {
250
+ 'alg': 'RS256',
198
251
  'typ': 'JWT',
199
- 'alg': 'PS256',
200
- 'x5t#S256': base64Thumbprint,
201
- },
202
- }
203
- */
204
-
205
- form = {
206
- grant_type: 'client_credentials',
207
- scope: this.config_entity[baseEntity].connection.auth.options.scope, // "https://graph.microsoft.com/.default"
208
- client_id: this.config_entity[baseEntity]?.connection?.auth?.options?.clientId,
209
- client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
210
- client_assertion: jsonwebtoken.sign(jwtClaims, privateKey, jwtOpts),
252
+ 'x5t#S256': base64Thumbprint, // Microsoft recommend modern x5t#S256 over x5t
253
+ }
254
+
255
+ form = {
256
+ grant_type: 'client_credentials',
257
+ scope: this.config_entity[baseEntity].connection.auth.options.scope, // "https://graph.microsoft.com/.default"
258
+ client_id: this.config_entity[baseEntity]?.connection?.auth?.options?.clientId,
259
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
260
+ client_assertion: await new jose.SignJWT(jwtClaims)
261
+ .setProtectedHeader(jwtHeaders)
262
+ .sign(privateKey),
263
+ }
211
264
  }
212
265
  } else if (serviceAccountKeyFile) { // Google - using Service Account key json-file
213
266
  if (!this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload?.scope || !this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload?.subject) {
@@ -228,10 +281,10 @@ export class HelperRest {
228
281
  }
229
282
 
230
283
  tokenUrl = gkey.token_uri // https://oauth2.googleapis.com/token
231
- const privateKey = gkey.private_key
232
- const jwtPayload: jsonwebtoken.JwtPayload = {
233
- sub: this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload?.subject, // gmail sender mail-address: noreply@mycompany.com
284
+ const privateKey = createPrivateKey(gkey.private_key) // PEM => KeyObject
285
+ const jwtPayload: jose.JWTPayload = {
234
286
  iss: gkey.client_email, // service account email/user
287
+ sub: this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload?.subject, // gmail sender mail-address: noreply@mycompany.com
235
288
  aud: gkey.token_uri,
236
289
  iat: Math.floor(Date.now() / 1000) - 60, // issued at
237
290
  exp: Math.floor(Date.now() / 1000) + 3600, // expiration time
@@ -240,17 +293,17 @@ export class HelperRest {
240
293
  ...jwtPayload,
241
294
  scope: this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload?.scope, // https://www.googleapis.com/auth/gmail.send
242
295
  }
243
- jwtOpts = {
244
- algorithm: 'RS256',
245
- header: {
246
- typ: 'JWT',
247
- alg: 'RS256',
248
- kid: gkey.client_id,
249
- },
296
+ jwtHeaders = {
297
+ alg: 'RS256',
298
+ typ: 'JWT',
299
+ kid: gkey.client_id,
250
300
  }
301
+
251
302
  form = {
252
303
  grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
253
- assertion: jsonwebtoken.sign(jwtClaims, privateKey, jwtOpts),
304
+ assertion: await new jose.SignJWT(jwtClaims)
305
+ .setProtectedHeader(jwtHeaders)
306
+ .sign(privateKey),
254
307
  }
255
308
  } else {
256
309
  // standard JWT - requires all configuation: tokenUrl, jwtPayload and tls.key
@@ -266,27 +319,29 @@ export class HelperRest {
266
319
  let privateKey = this.config_entity[baseEntity]?.connection?.auth?.options?.tls?._key || ''
267
320
  if (!privateKey) {
268
321
  privateKey = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.key, 'utf-8') || ''
269
- if (privateKey) this.config_entity[baseEntity].connection.auth.options.tls._key = privateKey
322
+ if (privateKey) {
323
+ privateKey = createPrivateKey(privateKey)
324
+ this.config_entity[baseEntity].connection.auth.options.tls._key = privateKey
325
+ }
270
326
  }
271
327
 
272
- let jwtPayload = this.config_entity[baseEntity].connection.auth.options.jwtPayload
328
+ let jwtPayload: jose.JWTPayload = this.config_entity[baseEntity].connection.auth.options.jwtPayload
273
329
  if (!jwtPayload.iat) jwtPayload.iat = Math.floor(Date.now() / 1000) - 60
274
330
  if (!jwtPayload.exp) jwtPayload.exp = Math.floor(Date.now() / 1000) + 3600
275
331
 
276
332
  jwtClaims = {
277
333
  ...jwtPayload,
278
334
  }
279
- jwtOpts = {
280
- algorithm: 'RS256',
281
- header: {
282
- typ: 'JWT',
283
- alg: 'RS256',
284
- },
335
+ jwtHeaders = {
336
+ alg: 'RS256',
337
+ typ: 'JWT',
285
338
  }
286
339
 
287
340
  form = {
288
341
  grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
289
- assertion: jsonwebtoken.sign(jwtClaims, privateKey, jwtOpts),
342
+ assertion: await new jose.SignJWT(jwtClaims)
343
+ .setProtectedHeader(jwtHeaders)
344
+ .sign(privateKey),
290
345
  }
291
346
  }
292
347
 
@@ -410,8 +465,8 @@ export class HelperRest {
410
465
  if (opt?.connection) { // allow overriding/extending configuration connection by caller argument opt.connection
411
466
  let org = this.config_entity[baseEntity]?.connection
412
467
  orgConnection = utils.copyObj(org)
413
- if (!orgConnection) orgConnection = {}
414
- orgConnection = utils.extendObj(orgConnection, opt.connection)
468
+ if (!org) org = {}
469
+ org = utils.extendObj(org, opt.connection)
415
470
  }
416
471
 
417
472
  // may use configuration type='oauth' and auto corrected to 'oauthJwtBearer'
@@ -699,6 +754,7 @@ export class HelperRest {
699
754
  try { urlObj = new URL(path) } catch (err) { void 0 }
700
755
  let isServiceClient = !urlObj && this._serviceClient[baseEntity] && !this.lock.isLocked() // !isLocked to avoid retry ongoing doRequest with failing getAccessToken()
701
756
  let oAuthTokeErr = statusCode === 401 && this.config_entity[baseEntity].connection?.auth?.type && this.config_entity[baseEntity].connection.auth.type.startsWith('oauth')
757
+
702
758
  if (isServiceClient && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ABORT_ERR' || err.code === 'ETIMEDOUT' || statusCode === 504 || oAuthTokeErr || retryAfter)) {
703
759
  this.scimgateway.logDebug(baseEntity, `doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`)
704
760
  if (retryAfter) {
@@ -709,8 +765,10 @@ export class HelperRest {
709
765
  }
710
766
  if (retryCount < this.config_entity[baseEntity].connection.baseUrls.length) {
711
767
  retryCount++
712
- this.updateServiceClient(baseEntity, { baseUrl: this.config_entity[baseEntity].connection.baseUrls[retryCount - 1] })
713
- this.scimgateway.logDebug(baseEntity, `${(this.config_entity[baseEntity].connection.baseUrls.length > 1) ? 'failover ' : ''}retry[${retryCount}] using baseUrl = ${this._serviceClient[baseEntity].baseUrl}`)
768
+ if (isServiceClient) {
769
+ this.updateServiceClient(baseEntity, { baseUrl: this.config_entity[baseEntity].connection.baseUrls[retryCount - 1] })
770
+ this.scimgateway.logDebug(baseEntity, `${(this.config_entity[baseEntity].connection.baseUrls.length > 1) ? 'failover ' : ''}retry[${retryCount}] using baseUrl = ${this._serviceClient[baseEntity].baseUrl}`)
771
+ }
714
772
  if (oAuthTokeErr) {
715
773
  delete this._serviceClient[baseEntity] // 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?
716
774
  }
@@ -778,6 +836,7 @@ export class HelperRest {
778
836
  * type=**"basic"** having auth.options:
779
837
  * ```
780
838
  * {
839
+ * "type": "basic",
781
840
  * "options": {
782
841
  * "username": "<username>",
783
842
  * "password": "<password>"
@@ -788,6 +847,7 @@ export class HelperRest {
788
847
  * type=**"oauth"** having auth.options:
789
848
  * ```
790
849
  * {
850
+ * "type": "oauth",
791
851
  * "options": {
792
852
  * "tenantIdGUID": "<Entra ID tenantIdGUID", // Entra ID authentication - if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/beta]
793
853
  * "tokenUrl": "<tokenUrl>", // must be set if not using tenantIdGUID
@@ -800,6 +860,7 @@ export class HelperRest {
800
860
  * type=**"token"** having auth.options:
801
861
  * ```
802
862
  * {
863
+ * "type": "token",
803
864
  * "options": {
804
865
  * "tokenUrl": "<url for requesting token">
805
866
  * "username": "<user name for token request>"
@@ -811,6 +872,7 @@ export class HelperRest {
811
872
  * type=**"bearer"** having auth.options:
812
873
  * ```
813
874
  * {
875
+ * "type": "bearer",
814
876
  * "options": {
815
877
  * "token": "<bearer token to be used">
816
878
  * }
@@ -820,6 +882,7 @@ export class HelperRest {
820
882
  * type=**"oauthSamlBearer"** having auth.options:
821
883
  * ```
822
884
  * {
885
+ * "type": "oauthSamlBearer",
823
886
  * "options": {
824
887
  * "tokenUrl": "<tokenUrl>",
825
888
  * "samlPayload": {
@@ -841,8 +904,9 @@ export class HelperRest {
841
904
  *
842
905
  * type=**"oauthJwtBearer"** having auth.options:
843
906
  * ```
844
- * // Microsoft Entra ID
907
+ * // Microsoft Entra ID - using certificate
845
908
  * {
909
+ * "type": "oauthJwtBearer",
846
910
  * "options": {
847
911
  * "tenantIdGUID": "<Entra ID tenantIdGUID", // Entra ID authentication, if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/beta]
848
912
  * "clientId": "<clientId>",
@@ -853,8 +917,23 @@ export class HelperRest {
853
917
  * }
854
918
  * }
855
919
  *
920
+ * // Microsoft Entra ID - using Federated credentials
921
+ * // Note, fedCred configuration must match corresponding configuration in Entra ID Application - Federation credentials
922
+ * {
923
+ * "type": "oauthJwtBearer",
924
+ * "options": {
925
+ * "tenantIdGUID": "<Entra ID tenantIdGUID",
926
+ * "fedCred": {
927
+ * "issuer": "<https://FQDN-scimgateway/oauth>", // e.g. https://scimgateway.my-company.com/oauth
928
+ * "subject": "<entra id application object id - client id>",
929
+ * "name": "<entra id federated credentials unique name>" // e.g. plugin-entra-id
930
+ * }
931
+ * }
932
+ * }
933
+ *
856
934
  * // Google Cloud Platform - GCP
857
935
  * {
936
+ * "type": "oauthJwtBearer",
858
937
  * "options": {
859
938
  * "serviceAccountKeyFile": "<Google Service Account key file name>", // located in ./config/certs. If baseUrls not defined, baseUrls automatically set to [https://www.googleapis.com]
860
939
  * "scope": "<jwt-scope>",
@@ -864,6 +943,7 @@ export class HelperRest {
864
943
  *
865
944
  * // General JWT API
866
945
  * {
946
+ * "type": "oauthJwtBearer",
867
947
  * "options": {
868
948
  * "tokenUrl": "<tokenUrl",
869
949
  * "tls": {
package/lib/logger.ts CHANGED
@@ -131,7 +131,7 @@ export class Logger {
131
131
 
132
132
  let customMask = this.customMasking || []
133
133
  if (!Array.isArray(customMask)) customMask = []
134
- const jsonMaskKeys = ['password', 'access_token', 'client_secret', 'assertion', 'client_assertion']
134
+ const jsonMaskKeys = ['password', 'access_token', 'client_secret', 'assertion', 'client_assertion', 'refresh_token']
135
135
  const jsonJoinedKeys = jsonMaskKeys.concat(customMask).join('|')
136
136
  const xmlMaskKeys = ['credentials', 'PasswordText', 'PasswordDigest', 'password']
137
137
  const xmlJoinedKeys = xmlMaskKeys.concat(customMask).join('"?|') + '"?'