scimgateway 5.4.3 → 5.5.0
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 +89 -5
- package/bun.lock +5 -3
- package/config/plugin-api.json +1 -0
- package/config/plugin-entra-id.json +1 -0
- package/config/plugin-ldap.json +1 -0
- package/config/plugin-loki.json +1 -0
- package/config/plugin-mongodb.json +1 -0
- package/config/plugin-mssql.json +1 -0
- package/config/plugin-saphana.json +1 -0
- package/config/plugin-scim.json +1 -0
- package/config/plugin-soap.json +1 -0
- package/lib/helper-rest.ts +162 -82
- package/lib/logger.ts +1 -1
- package/lib/scimgateway.ts +241 -106
- package/lib/utils.ts +7 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ 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
|
|
20
|
+
- External JWKS (JSON Web Key Set) is now supported by JWT Authentication. These are public and typically frequent rotated by modern identity providers
|
|
19
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
|
|
20
22
|
- [ETag](https://datatracker.ietf.org/doc/html/rfc7644#section-3.14) is now supported
|
|
21
23
|
- [Bulk Operations](https://datatracker.ietf.org/doc/html/rfc7644#section-3.7) is now supported
|
|
@@ -417,7 +419,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
417
419
|
|
|
418
420
|
- **auth.bearerJwtAzure** - Array of one or more JWT used by Azure SyncFabric. **tenantIdGUID** must be set to Entra ID Tenant ID.
|
|
419
421
|
|
|
420
|
-
- **auth.bearerJwt** - Array of one or more standard JWT objects. Using **secret** or **
|
|
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.
|
|
421
423
|
|
|
422
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`
|
|
423
425
|
|
|
@@ -715,7 +717,7 @@ Example Entra ID (plugin-entra-id) using certificate secret:
|
|
|
715
717
|
"connection": {
|
|
716
718
|
"baseUrls": [],
|
|
717
719
|
"auth": {
|
|
718
|
-
"type": "
|
|
720
|
+
"type": "oauthJwtBearer",
|
|
719
721
|
"options": {
|
|
720
722
|
"tenantIdGUID": "<tenantId>",
|
|
721
723
|
"clientId": "<clientId>",
|
|
@@ -727,6 +729,27 @@ Example Entra ID (plugin-entra-id) using certificate secret:
|
|
|
727
729
|
}
|
|
728
730
|
}
|
|
729
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 must be reachable from the internet
|
|
750
|
+
// exampole name: "plugin-entra-id"
|
|
751
|
+
|
|
752
|
+
|
|
730
753
|
Example using general OAuth:
|
|
731
754
|
|
|
732
755
|
"connection": {
|
|
@@ -816,7 +839,7 @@ const messageHandler = async (message: string) => {
|
|
|
816
839
|
console.log(message)
|
|
817
840
|
}
|
|
818
841
|
|
|
819
|
-
async function
|
|
842
|
+
async function startup() {
|
|
820
843
|
while (true) {
|
|
821
844
|
try {
|
|
822
845
|
const resp = await fetch(url, { headers });
|
|
@@ -847,7 +870,7 @@ async function startSSE() {
|
|
|
847
870
|
}
|
|
848
871
|
}
|
|
849
872
|
|
|
850
|
-
|
|
873
|
+
startup()
|
|
851
874
|
```
|
|
852
875
|
|
|
853
876
|
### Configuration notes - Azure Relay
|
|
@@ -1466,7 +1489,68 @@ In code editor (e.g., Visual Studio Code), method details and documentation are
|
|
|
1466
1489
|
MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
1467
1490
|
|
|
1468
1491
|
|
|
1469
|
-
## Change log
|
|
1492
|
+
## Change log
|
|
1493
|
+
|
|
1494
|
+
### v5.5.0
|
|
1495
|
+
|
|
1496
|
+
[Improved]
|
|
1497
|
+
|
|
1498
|
+
- 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
|
|
1499
|
+
|
|
1500
|
+
helper-rest includes options for federated credentials:
|
|
1501
|
+
|
|
1502
|
+
"auth {
|
|
1503
|
+
"type": "oauthJwtBearer",
|
|
1504
|
+
"options": {
|
|
1505
|
+
"tenantIdGUID": "<Entra ID tenantIdGUID",
|
|
1506
|
+
"fedCred": {
|
|
1507
|
+
"issuer": "<https://FQDN-scimgateway/oauth>",
|
|
1508
|
+
"subject": "<entra id application object id - client id>",
|
|
1509
|
+
"name": "<entra id federated credentials unique name>"
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
Example:
|
|
1515
|
+
|
|
1516
|
+
"auth {
|
|
1517
|
+
"type": "oauthJwtBearer",
|
|
1518
|
+
"options": {
|
|
1519
|
+
"tenantIdGUID": "11111111-2222-3333-4444-555555555555",
|
|
1520
|
+
"fedCred": {
|
|
1521
|
+
"issuer": "https://scimgateway.my-company.com/oauth>",
|
|
1522
|
+
"subject": "99999999-8888-7777-6666-555555555555",
|
|
1523
|
+
"name": "plugin-entra-id"
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
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.
|
|
1529
|
+
|
|
1530
|
+
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.
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
### v5.4.4
|
|
1534
|
+
|
|
1535
|
+
[Improved]
|
|
1536
|
+
|
|
1537
|
+
- External JWKS (JSON Web Key Set) is now supported by JWT Authentication. These are public and typically frequent rotated by modern identity providers
|
|
1538
|
+
|
|
1539
|
+
JKWS is enabled by setting scimgateway.auth.bearerJwt[].wellKnownUri to the identity provider's well-known URI
|
|
1540
|
+
|
|
1541
|
+
Keycloak example:
|
|
1542
|
+
|
|
1543
|
+
auth: {
|
|
1544
|
+
"bearerJwt": [
|
|
1545
|
+
{
|
|
1546
|
+
"wellKnownUri": "https://keycloak.example.com/realms/example-realm/.well-known/openid-configuration",
|
|
1547
|
+
"options": {
|
|
1548
|
+
...
|
|
1549
|
+
},
|
|
1550
|
+
...
|
|
1551
|
+
}
|
|
1552
|
+
]
|
|
1553
|
+
}
|
|
1470
1554
|
|
|
1471
1555
|
### v5.4.3
|
|
1472
1556
|
|
package/bun.lock
CHANGED
|
@@ -16,7 +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
|
-
"
|
|
19
|
+
"jose": "^6.0.11",
|
|
20
20
|
"ldapjs": "^3.0.7",
|
|
21
21
|
"lokijs": "^1.5.12",
|
|
22
22
|
"mongodb": "^6.16.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"saml": "^3.0.1",
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@stylistic/eslint-plugin": "^
|
|
30
|
+
"@stylistic/eslint-plugin": "^5.1.0",
|
|
31
31
|
"@types/bun": "latest",
|
|
32
32
|
"@types/dot-object": "^2.1.6",
|
|
33
33
|
"@types/jsonwebtoken": "^9.0.9",
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
|
|
142
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=="],
|
|
143
143
|
|
|
144
|
-
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@
|
|
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=="],
|
|
145
145
|
|
|
146
146
|
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
|
147
147
|
|
|
@@ -449,6 +449,8 @@
|
|
|
449
449
|
|
|
450
450
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
|
451
451
|
|
|
452
|
+
"jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
|
|
453
|
+
|
|
452
454
|
"js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="],
|
|
453
455
|
|
|
454
456
|
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
package/config/plugin-api.json
CHANGED
package/config/plugin-ldap.json
CHANGED
package/config/plugin-loki.json
CHANGED
package/config/plugin-mssql.json
CHANGED
package/config/plugin-scim.json
CHANGED
package/config/plugin-soap.json
CHANGED
package/lib/helper-rest.ts
CHANGED
|
@@ -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:
|
|
152
|
-
let
|
|
152
|
+
let jwtClaims: jose.JWTPayload | Record<string, any>
|
|
153
|
+
let jwtHeaders: jose.JWTHeaderParameters
|
|
153
154
|
|
|
154
155
|
if (tenantIdGUID) { // Microsoft Entra ID
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
'
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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:
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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:
|
|
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)
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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:
|
|
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 (!
|
|
414
|
-
|
|
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
|
-
|
|
713
|
-
|
|
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": {
|