scimgateway 5.1.5 → 5.1.6
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 +202 -79
- package/lib/helper-rest.ts +85 -72
- package/lib/scimgateway.ts +4 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -465,7 +465,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
465
465
|
|
|
466
466
|
- **endpoint** - Contains endpoint specific configuration according to customized **plugin code**.
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
### Configuration notes - general
|
|
469
469
|
|
|
470
470
|
- Custom Schemas, ServiceProviderConfig and ResourceType can be used if `./lib/scimdef-v2.json or scimdef-v1.json` exists. Original scimdef-v2.json/scimdef-v1.json can be copied from node_modules/scimgateway/lib to your plugin/lib and customized.
|
|
471
471
|
- Using reverse proxy and we want ipAllowList and correct meta.location response, following headers must be set by proxy: `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host`
|
|
@@ -521,88 +521,204 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
|
|
|
521
521
|
"plugin-soap.endpoint.password": "secret"
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
### Configuration notes - Email, using Microsoft Exchange Online (ExO)
|
|
524
525
|
|
|
525
|
-
-
|
|
526
|
-
|
|
527
|
-
- Entra ID application must have application permissions `Mail.Send`
|
|
528
|
-
- To prevent the sending of emails from any defined mailboxes, an ExO `ApplicationAccessPolicy` must be defined through PowerShell.
|
|
529
|
-
|
|
530
|
-
First create a mail-enabled security-group that only includes those users (mailboxes) the application is allowed to send from
|
|
531
|
-
Note, `mail enabled security group` cannot be created from portal, only from admin or admin.exchange console
|
|
532
|
-
|
|
533
|
-
##Connect to Exchange
|
|
534
|
-
Install-Module -Name ExchangeOnlineManagement
|
|
535
|
-
Connect-ExchangeOnline
|
|
536
|
-
|
|
537
|
-
##Create ApplicationAccessPolicy
|
|
538
|
-
New-ApplicationAccessPolicy -AppId <AppClientID> -PolicyScopeGroupId <MailEnabledSecurityGrpId> -AccessRight RestrictAccess -Description "Restrict app to specific mailboxes"
|
|
539
|
-
|
|
540
|
-
- Email, using Google Workspace Gmail
|
|
541
|
-
|
|
542
|
-
- https://console.cloud.google.com
|
|
543
|
-
- IAM & Admin > Service Accounts > Create Service Account
|
|
544
|
-
- Name=email-sender
|
|
545
|
-
- Create and Continue
|
|
546
|
-
- Grant this service account access to project - not needed
|
|
547
|
-
- Grant users access to this service - not needed
|
|
548
|
-
- IAM & Admin > Service Accounts > "email-sender" account > Keys
|
|
549
|
-
- Add Key > Create new key > JSON
|
|
550
|
-
- download json Service Account Key file, refere to configuration `email.auth.options.serviceAccountKeyFile`
|
|
551
|
-
|
|
552
|
-
- https://admin.google.com
|
|
553
|
-
- Security > Access and data control > API controls
|
|
554
|
-
- Manage Domain Wide Delegation > Add new
|
|
555
|
-
- Client ID = id of service account created
|
|
556
|
-
- OAuth scope = `https://www.googleapis.com/auth/gmail.send`
|
|
557
|
-
|
|
558
|
-
- https://admin.google.com
|
|
559
|
-
- Billing > Subscriptions - verify Google Workspace license
|
|
560
|
-
- Directory > Users > "user"
|
|
561
|
-
- Licenses > Edit > enable Google Workspace license
|
|
562
|
-
`email.emailOnError.from` mail address must have Google Workspace license
|
|
563
|
-
|
|
564
|
-
- Gateway chainging and chainingBaseUrl configuration
|
|
565
|
-
|
|
566
|
-
By configuring the `chainingBaseUrl`, it is possible to chain multiple gateways in sequence, such as `gateway1->gateway2->gateway3->endpoint`. In this setup, gateway behave much like a reverse proxy, validating authorization at each step unless PassThrough mode is enabled. Chaining is also supported in stream subscriber mode
|
|
526
|
+
- Entra ID application must have application permissions `Mail.Send`
|
|
527
|
+
- To prevent the sending of emails from any defined mailboxes, an ExO `ApplicationAccessPolicy` must be defined through PowerShell.
|
|
567
528
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
529
|
+
First create a mail-enabled security-group that only includes those users (mailboxes) the application is allowed to send from
|
|
530
|
+
Note, `mail enabled security group` cannot be created from portal, only from admin or admin.exchange console
|
|
531
|
+
|
|
532
|
+
##Connect to Exchange
|
|
533
|
+
Install-Module -Name ExchangeOnlineManagement
|
|
534
|
+
Connect-ExchangeOnline
|
|
535
|
+
|
|
536
|
+
##Create ApplicationAccessPolicy
|
|
537
|
+
New-ApplicationAccessPolicy -AppId <AppClientID> -PolicyScopeGroupId <MailEnabledSecurityGrpId> -AccessRight RestrictAccess -Description "Restrict app to specific mailboxes"
|
|
538
|
+
|
|
539
|
+
### Configuration notes - Email, using Google Workspace Gmail
|
|
540
|
+
|
|
541
|
+
- https://console.cloud.google.com
|
|
542
|
+
- IAM & Admin > Service Accounts > Create Service Account
|
|
543
|
+
- Name=email-sender
|
|
544
|
+
- Create and Continue
|
|
545
|
+
- Grant this service account access to project - not needed
|
|
546
|
+
- Grant users access to this service - not needed
|
|
547
|
+
- IAM & Admin > Service Accounts > "email-sender" account > Keys
|
|
548
|
+
- Add Key > Create new key > JSON
|
|
549
|
+
- download json Service Account Key file, refere to configuration `email.auth.options.serviceAccountKeyFile`
|
|
550
|
+
|
|
551
|
+
- https://admin.google.com
|
|
552
|
+
- Security > Access and data control > API controls
|
|
553
|
+
- Manage Domain Wide Delegation > Add new
|
|
554
|
+
- Client ID = id of service account created
|
|
555
|
+
- OAuth scope = `https://www.googleapis.com/auth/gmail.send`
|
|
556
|
+
|
|
557
|
+
- https://admin.google.com
|
|
558
|
+
- Billing > Subscriptions - verify Google Workspace license
|
|
559
|
+
- Directory > Users > "user"
|
|
560
|
+
- Licenses > Edit > enable Google Workspace license
|
|
561
|
+
`email.emailOnError.from` mail address must have Google Workspace license
|
|
562
|
+
|
|
563
|
+
### Configuration notes - Gateway chainging and chainingBaseUrl
|
|
564
|
+
|
|
565
|
+
By configuring the `chainingBaseUrl`, it is possible to chain multiple gateways in sequence, such as `gateway1->gateway2->gateway3->endpoint`. In this setup, gateway behave much like a reverse proxy, validating authorization at each step unless PassThrough mode is enabled. Chaining is also supported in stream subscriber mode
|
|
566
|
+
|
|
567
|
+
{
|
|
568
|
+
"scimgateway": {
|
|
569
|
+
...
|
|
570
|
+
"chainingBaseUrl": "https:\\gateway2:8880",
|
|
571
|
+
...
|
|
572
|
+
"auth": {
|
|
573
|
+
...
|
|
574
|
+
"passThrough": {
|
|
575
|
+
"enabled": false,
|
|
576
|
+
"readOnly": false,
|
|
577
|
+
"baseEntities": []
|
|
578
|
+
}
|
|
583
579
|
...
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
...
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
Using above configuration example on gateway1, incoming requests will be routed to `https:\\gateway2:8880`
|
|
587
|
+
|
|
588
|
+
The plugin and its associated authentication configuration can mirror the setup running on the final gateway. However, in chaining mode, the plugin binary is used solely for initializing and configuring the gateway. This allows for the use of a simplified `plugin-<name>.ts` binary containing only the essential mandatory components:
|
|
588
589
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
590
|
+
// start - mandatory plugin initialization
|
|
591
|
+
const ScimGateway: typeof import('scimgateway').ScimGateway = await (async () => {
|
|
592
|
+
try {
|
|
593
|
+
return (await import('scimgateway')).ScimGateway
|
|
594
|
+
} catch (err) {
|
|
595
|
+
const source = './scimgateway.ts'
|
|
596
|
+
return (await import(source)).ScimGateway
|
|
597
|
+
}
|
|
598
|
+
})()
|
|
599
|
+
const scimgateway = new ScimGateway()
|
|
600
|
+
const config = scimgateway.getConfig()
|
|
601
|
+
scimgateway.authPassThroughAllowed = false
|
|
602
|
+
// end - mandatory plugin initialization
|
|
603
|
+
|
|
604
|
+
Using `scimgateway.authPassThroughAllowed = true` and `plugin-<name>.json` configuration `scimgateway.auth.passThrough=true` enables Authentication PassTrhough
|
|
605
|
+
|
|
606
|
+
### Configuration notes - HelperRest used by plugins
|
|
607
|
+
For REST endpoints, plugins may use HelperRest to simplify authentication and communication
|
|
608
|
+
doRequest() executes REST request and return response
|
|
609
|
+
`doRequest(<baseEntity>, <method>, <path>, <body>, <ctx>, <options>)`
|
|
610
|
+
|
|
611
|
+
* baseEntity - 'undefined' if not used and must correspond with endpoint configuration that defines baseUrls and connection options.
|
|
612
|
+
* method - GET, PATCH, PUT, DELETE
|
|
613
|
+
* path - either full url or just the path that will be added to baseUrl. Using full url will override baseUrl. Using path is preferred because of auth caching logic and simplicity
|
|
614
|
+
* body - optional body to be used
|
|
615
|
+
* ctx - optional, passing authorization header if Auth PassThrough is enabled
|
|
616
|
+
* opt - optional, connection options that will extend/override any endpoint.entity.undefined.connection definitions
|
|
617
|
+
|
|
618
|
+
Configuration showing connection settings:
|
|
619
|
+
|
|
620
|
+
{
|
|
621
|
+
"scimgateway": {
|
|
622
|
+
...
|
|
623
|
+
}
|
|
624
|
+
"endpoint": {
|
|
625
|
+
"entity": {
|
|
626
|
+
"undefined": {
|
|
627
|
+
"connection": {
|
|
628
|
+
"baseUrls": [],
|
|
629
|
+
"auth": {
|
|
630
|
+
"type": "xxx",
|
|
631
|
+
"options": {
|
|
632
|
+
...
|
|
633
|
+
"jwtPayload": {},
|
|
634
|
+
"samlPayload": {},
|
|
635
|
+
"tls": {} // files located in ./config/certs
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
"options": {
|
|
639
|
+
"headers": {},
|
|
640
|
+
"tls": {} // files located in ./config/certs
|
|
641
|
+
},
|
|
642
|
+
"proxy": {}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
* baseUrls - Endpoint URL. Several may be defined for failower. There are retry logic on connection failures
|
|
651
|
+
* auth.type - defines authentication being used: `basic`, `oauth`, `token`, `bearer`, `oauthSamlBearer` or `oauthJwtBearer`
|
|
652
|
+
* auth.options - for each valid type there are different options. tenantIdGUID is special for Entra ID and serviceAccountKeyFile is special for Google. Using these will simplify and reduce options to be included. Also note we do not need to include baseUrls when using tenantIdGUID/serviceAccountKeyFile as long as endpoint is Entra ID (Microsoft Graph) or Google.
|
|
653
|
+
|
|
654
|
+
Example using basic auth:
|
|
655
|
+
|
|
656
|
+
"connection": {
|
|
657
|
+
"baseUrls": [
|
|
658
|
+
"https://localhost:8880"
|
|
659
|
+
],
|
|
660
|
+
"auth": {
|
|
661
|
+
"type": "basic",
|
|
662
|
+
"options": {
|
|
663
|
+
"username": "gwadmin",
|
|
664
|
+
"password": "password"
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
"options": {
|
|
668
|
+
"tls": {
|
|
669
|
+
"rejectUnauthorized": false,
|
|
670
|
+
"ca": "ca.pem"
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
Example Entra ID (plugin-entra-id) using clientId/clientSecret:
|
|
676
|
+
|
|
677
|
+
"connection": {
|
|
678
|
+
"baseUrls": [],
|
|
679
|
+
"auth": {
|
|
680
|
+
"type": "oauth",
|
|
681
|
+
"options": {
|
|
682
|
+
"tenantIdGUID": "<tenantId>",
|
|
683
|
+
"clientId": "<clientId",
|
|
684
|
+
"clientSecret": "<clientSecret>"
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
Example Entra ID (plugin-entra-id) using certificate secret:
|
|
690
|
+
|
|
691
|
+
"connection": {
|
|
692
|
+
"baseUrls": [],
|
|
693
|
+
"auth": {
|
|
694
|
+
"type": "oauth",
|
|
695
|
+
"options": {
|
|
696
|
+
"tenantIdGUID": "<tenantId>",
|
|
697
|
+
"clientId": "<clientId",
|
|
698
|
+
"tls": {
|
|
699
|
+
"key": "key.pem",
|
|
700
|
+
"cert": "cert.pem"
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
Example using general OAuth:
|
|
707
|
+
|
|
708
|
+
"connection": {
|
|
709
|
+
"baseUrls": ["endpointUrl"],
|
|
710
|
+
"auth": {
|
|
711
|
+
"type": "oauth",
|
|
712
|
+
"options": {
|
|
713
|
+
"tokenUrl": "<tokenUrl>"
|
|
714
|
+
"clientId": "<clientId",
|
|
715
|
+
"clientSecret": "<clientSecret>"
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
Please see code editor method HelperRest doRequest() IntelliSense for type and option details
|
|
604
721
|
|
|
605
|
-
Using `scimgateway.authPassThroughAllowed = true` and `plugin-<name>.json` configuration `scimgateway.auth.passThrough=true` enables Authentication PassTrhough
|
|
606
722
|
|
|
607
723
|
## Manual startup
|
|
608
724
|
|
|
@@ -1175,6 +1291,13 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1175
1291
|
|
|
1176
1292
|
## Change log
|
|
1177
1293
|
|
|
1294
|
+
### v5.1.6
|
|
1295
|
+
|
|
1296
|
+
[Improved]
|
|
1297
|
+
|
|
1298
|
+
- HelperRest, payload/claims configuration now defined in auth.options.jwtPayload and auth.options.samlPayload. Previously all was defiend in auth.options
|
|
1299
|
+
- README configuration notes updated
|
|
1300
|
+
|
|
1178
1301
|
### v5.1.5
|
|
1179
1302
|
|
|
1180
1303
|
[Improved]
|
|
@@ -1205,7 +1328,7 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1205
1328
|
"options": {
|
|
1206
1329
|
"tenantIdGUID": "Entra ID Tenant ID (GUID)",
|
|
1207
1330
|
"clientId": "<application clientId>",
|
|
1208
|
-
"
|
|
1331
|
+
"tls": { // files located in ./config/certs
|
|
1209
1332
|
"key": "key.pem",
|
|
1210
1333
|
"cert": "cert.pem"
|
|
1211
1334
|
}
|
package/lib/helper-rest.ts
CHANGED
|
@@ -42,7 +42,7 @@ export class HelperRest {
|
|
|
42
42
|
if (this.config_entity[baseEntity]?.connection) {
|
|
43
43
|
connectionFound = true
|
|
44
44
|
const type = this.config_entity[baseEntity].connection?.auth?.type
|
|
45
|
-
if (type === 'oauthJwtBearer' || type === 'oauth') {
|
|
45
|
+
if (type === 'oauthJwtBearer' || type === 'oauth') {
|
|
46
46
|
// set default baseUrls for Entra ID and Google if not already defined
|
|
47
47
|
if (this.config_entity[baseEntity]?.connection?.auth?.options?.tenantIdGUID) { // Entra ID, setting baseUrls to graph
|
|
48
48
|
if (!this.config_entity[baseEntity].connection.baseUrls) {
|
|
@@ -83,29 +83,32 @@ export class HelperRest {
|
|
|
83
83
|
|
|
84
84
|
const action = 'getAccessToken'
|
|
85
85
|
|
|
86
|
+
const serviceAccountKeyFile = this.config_entity[baseEntity]?.connection?.auth?.options?.serviceAccountKeyFile
|
|
87
|
+
const tenantIdGUID = this.config_entity[baseEntity]?.connection?.auth?.options?.tenantIdGUID
|
|
86
88
|
let tokenUrl: string
|
|
87
|
-
let form:
|
|
89
|
+
let form: Record<string, any>
|
|
88
90
|
let resource = ''
|
|
89
91
|
|
|
92
|
+
try {
|
|
93
|
+
const urlObj = new URL(this.config_entity[baseEntity].connection.baseUrls[0])
|
|
94
|
+
resource = urlObj.origin
|
|
95
|
+
} catch (err) { void 0 }
|
|
96
|
+
if (tenantIdGUID) {
|
|
97
|
+
tokenUrl = `https://login.microsoftonline.com/${tenantIdGUID}/oauth2/v2.0/token`
|
|
98
|
+
if (resource) this.config_entity[baseEntity].connection.auth.options.scope = resource + '/.default' // "https://graph.microsoft.com/.default"
|
|
99
|
+
} else tokenUrl = this.config_entity[baseEntity].connection.auth.options.tokenUrl
|
|
100
|
+
|
|
90
101
|
try {
|
|
91
102
|
switch (this.config_entity[baseEntity]?.connection?.auth?.type) {
|
|
92
103
|
case 'oauth':
|
|
93
|
-
try {
|
|
94
|
-
const urlObj = new URL(this.config_entity[baseEntity].connection.baseUrls[0])
|
|
95
|
-
resource = urlObj.origin
|
|
96
|
-
} catch (err) { void 0 }
|
|
97
|
-
if (this.config_entity[baseEntity].connection.auth?.options?.tenantIdGUID) { // Azure
|
|
98
|
-
tokenUrl = `https://login.microsoftonline.com/${this.config_entity[baseEntity].connection.auth.options.tenantIdGUID}/oauth2/token`
|
|
99
|
-
} else {
|
|
100
|
-
tokenUrl = this.config_entity[baseEntity].connection.auth.options.tokenUrl
|
|
101
|
-
}
|
|
102
104
|
form = {
|
|
103
105
|
grant_type: 'client_credentials',
|
|
104
106
|
client_id: this.config_entity[baseEntity].connection.auth.options.clientId,
|
|
105
107
|
client_secret: this.config_entity[baseEntity].connection.auth.options.clientSecret,
|
|
106
|
-
scope: this.config_entity[baseEntity].connection.auth.options.scope || null,
|
|
107
|
-
resource: resource || null, // "https://graph.microsoft.com"
|
|
108
108
|
}
|
|
109
|
+
if (this.config_entity[baseEntity].connection.auth.options.scope) form.scope = this.config_entity[baseEntity].connection.auth.options.scope // required using Entra ID /oauth2/v2.0/token
|
|
110
|
+
if (this.config_entity[baseEntity].connection.auth.options.resource) resource = this.config_entity[baseEntity].connection.auth.options.resource // required using Entra ID /oauth2/token
|
|
111
|
+
|
|
109
112
|
break
|
|
110
113
|
|
|
111
114
|
case 'token':
|
|
@@ -119,23 +122,26 @@ export class HelperRest {
|
|
|
119
122
|
case 'oauthSamlBearer':
|
|
120
123
|
tokenUrl = this.config_entity[baseEntity].connection.auth.options.tokenUrl
|
|
121
124
|
const context = null
|
|
122
|
-
const cert = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.
|
|
123
|
-
const key = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.
|
|
124
|
-
|
|
125
|
-
const issuer = `scimgateway/${this.scimgateway.pluginName}`
|
|
126
|
-
const lifetime = 3600
|
|
127
|
-
const clientId = this.config_entity[baseEntity].connection.auth.options.clientId
|
|
128
|
-
const nameId = this.config_entity[baseEntity].connection.auth.options.userId
|
|
129
|
-
const userIdentifierFormat = 'userName'
|
|
125
|
+
const cert = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.cert).toString()
|
|
126
|
+
const key = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.key).toString()
|
|
127
|
+
|
|
130
128
|
const tokenEndpoint = tokenUrl
|
|
131
|
-
const audience = `scimgateway/${this.scimgateway.pluginName}`
|
|
132
129
|
const delay = 1
|
|
133
130
|
|
|
131
|
+
// mandatory: clientId, companyId and nameId
|
|
132
|
+
const clientId = this.config_entity[baseEntity].connection.auth.options.samlPayload.clientId
|
|
133
|
+
const companyId = this.config_entity[baseEntity].connection.auth.options.samlPayload.companyId
|
|
134
|
+
const nameId = this.config_entity[baseEntity].connection.auth.options.samlPayload.nameId
|
|
135
|
+
const userIdentifierFormat = this.config_entity[baseEntity].connection.auth.options.samlPayload.userIdentifierFormat || 'userName'
|
|
136
|
+
const lifetime = this.config_entity[baseEntity].connection.auth.options.samlPayload.lifetime || 3600
|
|
137
|
+
const issuer = this.config_entity[baseEntity].connection.auth.options.samlPayload.clientId || `https://scimgateway.${this.scimgateway.pluginName}.com`
|
|
138
|
+
const audience = this.config_entity[baseEntity].connection.auth.options.samlPayload.audience || `scimgateway/${this.scimgateway.pluginName}`
|
|
139
|
+
|
|
134
140
|
form = {
|
|
135
141
|
token_url: tokenUrl,
|
|
136
142
|
grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
|
|
137
143
|
client_id: clientId,
|
|
138
|
-
company_id:
|
|
144
|
+
company_id: companyId,
|
|
139
145
|
assertion: await samlAssertion.run(context, cert, key, issuer, lifetime, clientId, nameId, userIdentifierFormat, tokenEndpoint, audience, delay),
|
|
140
146
|
}
|
|
141
147
|
break
|
|
@@ -143,25 +149,21 @@ export class HelperRest {
|
|
|
143
149
|
case 'oauthJwtBearer':
|
|
144
150
|
let jwtClaims: jsonwebtoken.JwtPayload | Record<string, any> = {}
|
|
145
151
|
let jwtOpts: jsonwebtoken.SignOptions = {}
|
|
146
|
-
const serviceAccountKeyFile = this.config_entity[baseEntity]?.connection?.auth?.options?.serviceAccountKeyFile
|
|
147
|
-
const tenantIdGUID = this.config_entity[baseEntity]?.connection?.auth?.options?.tenantIdGUID
|
|
148
152
|
|
|
149
|
-
if (tenantIdGUID) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - missing options.certificate.key/cert configuration`)
|
|
153
|
+
if (tenantIdGUID) { // Microsoft Entra ID
|
|
154
|
+
if (!this.config_entity[baseEntity]?.connection?.auth?.options?.tls?.cert) {
|
|
155
|
+
throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - missing options.tls.key/cert configuration`)
|
|
153
156
|
}
|
|
154
|
-
|
|
155
|
-
let
|
|
156
|
-
let cert = this.config_entity[baseEntity]?.connection?.auth?.options?.certificate?._cert || ''
|
|
157
|
+
let privateKey = this.config_entity[baseEntity]?.connection?.auth?.options?.tls?._key || ''
|
|
158
|
+
let cert = this.config_entity[baseEntity]?.connection?.auth?.options?.tls?._cert || ''
|
|
157
159
|
if (!privateKey || !cert) {
|
|
158
|
-
privateKey = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.
|
|
159
|
-
cert = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.
|
|
160
|
-
if (privateKey) this.config_entity[baseEntity].connection.auth.options.
|
|
161
|
-
if (cert) this.config_entity[baseEntity].connection.auth.options.
|
|
160
|
+
privateKey = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.key, 'utf-8') || ''
|
|
161
|
+
cert = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.cert, 'utf-8') || ''
|
|
162
|
+
if (privateKey) this.config_entity[baseEntity].connection.auth.options.tls._key = privateKey
|
|
163
|
+
if (cert) this.config_entity[baseEntity].connection.auth.options.tls._cert = cert
|
|
162
164
|
}
|
|
163
165
|
if (!privateKey || !cert) {
|
|
164
|
-
throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - missing options.
|
|
166
|
+
throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - missing options.tls.key/cert file content`)
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
const jwtPayload: jsonwebtoken.JwtPayload = {
|
|
@@ -199,23 +201,16 @@ export class HelperRest {
|
|
|
199
201
|
}
|
|
200
202
|
*/
|
|
201
203
|
|
|
202
|
-
let scope = 'https://graph.microsoft.com/.default'
|
|
203
|
-
try {
|
|
204
|
-
const urlObj = new URL(this.config_entity[baseEntity].connection.baseUrls[0])
|
|
205
|
-
scope = urlObj.origin + '/.default' // for application exposed api's and included permissions use: api://${this.config_entity[baseEntity]?.connection?.auth?.options?.clientId}/.default
|
|
206
|
-
} catch (err) { void 0 }
|
|
207
|
-
|
|
208
204
|
form = {
|
|
209
|
-
scope,
|
|
210
205
|
grant_type: 'client_credentials',
|
|
206
|
+
scope: this.config_entity[baseEntity].connection.auth.options.scope, // "https://graph.microsoft.com/.default"
|
|
211
207
|
client_id: this.config_entity[baseEntity]?.connection?.auth?.options?.clientId,
|
|
212
208
|
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
|
213
209
|
client_assertion: jsonwebtoken.sign(jwtClaims, privateKey, jwtOpts),
|
|
214
210
|
}
|
|
215
|
-
} else if (serviceAccountKeyFile) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const err = new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - using auth.options 'serviceAccountKeyFile' requires mandatory configuration entity.${baseEntity}.connection.auth.options.scope/subject`)
|
|
211
|
+
} else if (serviceAccountKeyFile) { // Google - using Service Account key json-file
|
|
212
|
+
if (!this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload?.scope || !this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload?.subject) {
|
|
213
|
+
const err = new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' - using auth.options 'serviceAccountKeyFile' requires mandatory configuration entity.${baseEntity}.connection.auth.options.jwtPayload.scope/subject`)
|
|
219
214
|
throw err
|
|
220
215
|
}
|
|
221
216
|
let gkey: Record<string, any> = this.config_entity[baseEntity]?.connection?.auth?.options?._gkey
|
|
@@ -234,7 +229,7 @@ export class HelperRest {
|
|
|
234
229
|
tokenUrl = gkey.token_uri // https://oauth2.googleapis.com/token
|
|
235
230
|
const privateKey = gkey.private_key
|
|
236
231
|
const jwtPayload: jsonwebtoken.JwtPayload = {
|
|
237
|
-
sub: this.config_entity[baseEntity]?.connection?.auth?.options?.subject, //
|
|
232
|
+
sub: this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload?.subject, // gmail sender mail-address: noreply@mycompany.com
|
|
238
233
|
iss: gkey.client_email, // service account email/user
|
|
239
234
|
aud: gkey.token_uri,
|
|
240
235
|
iat: Math.floor(Date.now() / 1000) - 60, // issued at
|
|
@@ -242,7 +237,7 @@ export class HelperRest {
|
|
|
242
237
|
}
|
|
243
238
|
jwtClaims = {
|
|
244
239
|
...jwtPayload,
|
|
245
|
-
scope: this.config_entity[baseEntity]?.connection?.auth?.options?.scope, // https://www.googleapis.com/auth/gmail.send
|
|
240
|
+
scope: this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload?.scope, // https://www.googleapis.com/auth/gmail.send
|
|
246
241
|
}
|
|
247
242
|
jwtOpts = {
|
|
248
243
|
algorithm: 'RS256',
|
|
@@ -257,23 +252,23 @@ export class HelperRest {
|
|
|
257
252
|
assertion: jsonwebtoken.sign(jwtClaims, privateKey, jwtOpts),
|
|
258
253
|
}
|
|
259
254
|
} else {
|
|
260
|
-
// standard JWT - requires all configuation: tokenUrl,
|
|
255
|
+
// standard JWT - requires all configuation: tokenUrl, jwtPayload and tls.key
|
|
261
256
|
if (!this.config_entity[baseEntity]?.connection?.auth?.options?.tokenUrl
|
|
262
|
-
|| !this.config_entity[baseEntity]?.connection?.auth?.options?.
|
|
263
|
-
|| typeof this.config_entity[baseEntity]?.connection?.auth?.options?.
|
|
264
|
-
throw new Error(`auth.type '${this.config_entity[baseEntity]?.connection?.auth?.type}' (no tenantIdGUID/serviceAccountKeyFile using raw) - missing configuration entity.${baseEntity}.connection.auth.options.tokenUrl/
|
|
257
|
+
|| !this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload
|
|
258
|
+
|| typeof this.config_entity[baseEntity]?.connection?.auth?.options?.jwtPayload !== 'object') {
|
|
259
|
+
throw new Error(`auth.type '${this.config_entity[baseEntity]?.connection?.auth?.type}' (no tenantIdGUID/serviceAccountKeyFile using raw) - missing configuration entity.${baseEntity}.connection.auth.options.tokenUrl/jwtPayload`)
|
|
265
260
|
}
|
|
266
|
-
if (!this.config_entity[baseEntity]?.connection?.auth?.options?.
|
|
267
|
-
throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' (no tenantIdGUID/serviceAccountKeyFile using raw) - missing options.
|
|
261
|
+
if (!this.config_entity[baseEntity]?.connection?.auth?.options?.tls?.key) {
|
|
262
|
+
throw new Error(`auth type '${this.config_entity[baseEntity]?.connection?.auth?.type}' (no tenantIdGUID/serviceAccountKeyFile using raw) - missing options.tls.key configuration`)
|
|
268
263
|
}
|
|
269
264
|
tokenUrl = this.config_entity[baseEntity].connection.auth.options.tokenUrl
|
|
270
|
-
let privateKey = this.config_entity[baseEntity]?.connection?.auth?.options?.
|
|
265
|
+
let privateKey = this.config_entity[baseEntity]?.connection?.auth?.options?.tls?._key || ''
|
|
271
266
|
if (!privateKey) {
|
|
272
|
-
privateKey = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.
|
|
273
|
-
if (privateKey) this.config_entity[baseEntity].connection.auth.options.
|
|
267
|
+
privateKey = fs.readFileSync(this.config_entity[baseEntity].connection.auth.options.tls.key, 'utf-8') || ''
|
|
268
|
+
if (privateKey) this.config_entity[baseEntity].connection.auth.options.tls._key = privateKey
|
|
274
269
|
}
|
|
275
270
|
|
|
276
|
-
let jwtPayload = this.config_entity[baseEntity].connection.auth.options.
|
|
271
|
+
let jwtPayload = this.config_entity[baseEntity].connection.auth.options.jwtPayload
|
|
277
272
|
if (!jwtPayload.iat) jwtPayload.iat = Math.floor(Date.now() / 1000) - 60
|
|
278
273
|
if (!jwtPayload.exp) jwtPayload.exp = Math.floor(Date.now() / 1000) + 3600
|
|
279
274
|
|
|
@@ -419,6 +414,18 @@ export class HelperRest {
|
|
|
419
414
|
org = utils.extendObj(org, opt.connection)
|
|
420
415
|
}
|
|
421
416
|
|
|
417
|
+
// may use configuration type='oauth' and auto corrected to 'oauthJwtBearer'
|
|
418
|
+
if (this.config_entity[baseEntity]?.connection?.auth?.type == 'oauth') {
|
|
419
|
+
if (this.config_entity[baseEntity].connection.auth?.options?.tenantIdGUID) {
|
|
420
|
+
if (this.config_entity[baseEntity].connection.auth.options?.tls?.cert
|
|
421
|
+
&& this.config_entity[baseEntity].connection.auth.options?.tls?.key
|
|
422
|
+
&& this.config_entity[baseEntity].connection.auth.options.clientId
|
|
423
|
+
) this.config_entity[baseEntity].connection.auth.type = 'oauthJwtBearer'
|
|
424
|
+
} else if (this.config_entity[baseEntity]?.connection?.auth?.options?.serviceAccountKeyFile) {
|
|
425
|
+
this.config_entity[baseEntity].connection.auth.type = 'oauthJwtBearer'
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
422
429
|
switch (this.config_entity[baseEntity]?.connection?.auth?.type) {
|
|
423
430
|
case 'basic':
|
|
424
431
|
if (!this.config_entity[baseEntity]?.connection?.auth?.options?.username || !this.config_entity[baseEntity]?.connection?.auth?.options?.password) {
|
|
@@ -451,9 +458,9 @@ export class HelperRest {
|
|
|
451
458
|
param.options.headers['Authorization'] = 'Bearer ' + Buffer.from(this.config_entity[baseEntity].connection.auth.options.token).toString('base64')
|
|
452
459
|
break
|
|
453
460
|
case 'oauthSamlBearer':
|
|
454
|
-
if (!this.config_entity[baseEntity]?.connection?.auth?.options?.clientId || !this.config_entity[baseEntity]?.connection?.auth?.options?.companyId
|
|
455
|
-
|| !this.config_entity[baseEntity]?.connection?.auth?.options?.
|
|
456
|
-
const err = new Error(`auth.type 'oauthSamlBearer' - missing configuration entity.${baseEntity}.connection.auth.options
|
|
461
|
+
if (!this.config_entity[baseEntity]?.connection?.auth?.options?.samlPayload?.clientId || !this.config_entity[baseEntity]?.connection?.auth?.options?.samlPayload?.companyId
|
|
462
|
+
|| !this.config_entity[baseEntity]?.connection?.auth?.options?.tls?.key) {
|
|
463
|
+
const err = new Error(`auth.type 'oauthSamlBearer' - missing configuration entity.${baseEntity}.connection.auth.options.tls and/or options.samlPayload.clientId/companyId`)
|
|
457
464
|
throw err
|
|
458
465
|
}
|
|
459
466
|
param.accessToken = await this.getAccessToken(baseEntity, ctx)
|
|
@@ -462,7 +469,7 @@ export class HelperRest {
|
|
|
462
469
|
case 'oauthJwtBearer':
|
|
463
470
|
// auth.options.tenantIdGUID => Microsoft Entra ID
|
|
464
471
|
// auth.options.serviceAccountKeyFile => Google Service Account
|
|
465
|
-
// also support custom using tokenUrl/
|
|
472
|
+
// also support custom using tokenUrl/jwtPayload
|
|
466
473
|
param.accessToken = await this.getAccessToken(baseEntity, ctx)
|
|
467
474
|
param.options.headers['Authorization'] = `Bearer ${param.accessToken.access_token}`
|
|
468
475
|
break
|
|
@@ -764,7 +771,7 @@ export class HelperRest {
|
|
|
764
771
|
* ```
|
|
765
772
|
* type defines authentication being used
|
|
766
773
|
* if type not defined, no authentication used
|
|
767
|
-
* valid type is: `basic`, `oauth`, `token`, `bearer` or `
|
|
774
|
+
* valid type is: `basic`, `oauth`, `token`, `bearer`, `oauthSamlBearer` or `oauthJwtBearer`
|
|
768
775
|
*
|
|
769
776
|
* for each valid type there are different auth.options
|
|
770
777
|
*
|
|
@@ -815,10 +822,16 @@ export class HelperRest {
|
|
|
815
822
|
* {
|
|
816
823
|
* "options": {
|
|
817
824
|
* "tokenUrl": "<tokenUrl>",
|
|
818
|
-
* "
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
825
|
+
* "samlPayload": {
|
|
826
|
+
* "clientId": "<clientId>",
|
|
827
|
+
* "companyId": "<companyId>",
|
|
828
|
+
* "nameId": "<nameId>",
|
|
829
|
+
* "lifetime": "<optional>"
|
|
830
|
+
* "issuer": "<optional>",
|
|
831
|
+
* "userIdentifierFormat": "<optional>",
|
|
832
|
+
* "audience": "<optional>"
|
|
833
|
+
* },
|
|
834
|
+
* "tls": {
|
|
822
835
|
* "key": "<key-file-name>", // location: config/certs
|
|
823
836
|
* "cert": "<cert-file-name>", // location: config/certs
|
|
824
837
|
* }
|
|
@@ -833,7 +846,7 @@ export class HelperRest {
|
|
|
833
846
|
* "options": {
|
|
834
847
|
* "tenantIdGUID": "<Entra ID tenantIdGUID", // Entra ID authentication, if baseUrls not defined, baseUrls automatically set to [https://graph.microsoft.com/beta]
|
|
835
848
|
* "clientId": "<clientId>",
|
|
836
|
-
* "
|
|
849
|
+
* "tls": { // files located in ./config/certs
|
|
837
850
|
* "key": "key.pem",
|
|
838
851
|
* "cert": "cert.pem"
|
|
839
852
|
* }
|
|
@@ -853,10 +866,10 @@ export class HelperRest {
|
|
|
853
866
|
* {
|
|
854
867
|
* "options": {
|
|
855
868
|
* "tokenUrl": "<tokenUrl",
|
|
856
|
-
* "
|
|
869
|
+
* "tls": {
|
|
857
870
|
* "key": "<signing-key-file-name>" // key.pem file located in ./config/certs
|
|
858
871
|
* },
|
|
859
|
-
* "
|
|
872
|
+
* "jwtPayload": {
|
|
860
873
|
* "sub": "<subject>",
|
|
861
874
|
* "iss": "<issuer>",
|
|
862
875
|
* "aud": "<audience>",
|
package/lib/scimgateway.ts
CHANGED
|
@@ -2538,12 +2538,12 @@ export class ScimGateway {
|
|
|
2538
2538
|
isMailLock = false
|
|
2539
2539
|
}, (this.config.scimgateway.email.emailOnError.sendInterval || 15) * 1000 * 60)
|
|
2540
2540
|
|
|
2541
|
-
const msgHtml = `<html><body><pre style="font-family: monospace; white-space: pre-wrap;">${msg}</pre><br/><p><strong>This is an automatically generated email - please do NOT reply to this email
|
|
2541
|
+
const msgHtml = `<html><body><pre style="font-family: monospace; white-space: pre-wrap;">${msg}</pre><br/><p><strong>This is an automatically generated email - please do NOT reply to this email</strong></p></body></html>`
|
|
2542
2542
|
const msgObj = {
|
|
2543
2543
|
from: this.config.scimgateway.email.emailOnError.from,
|
|
2544
2544
|
to: this.config.scimgateway.email.emailOnError.to,
|
|
2545
2545
|
cc: this.config.scimgateway.email.emailOnError.cc,
|
|
2546
|
-
subject: this.config.scimgateway.email.emailOnError.subject
|
|
2546
|
+
subject: this.config.scimgateway.email.emailOnError.subject || 'SCIM Gateway error message',
|
|
2547
2547
|
content: msgHtml,
|
|
2548
2548
|
}
|
|
2549
2549
|
this.sendMail(msgObj, true)
|
|
@@ -2870,9 +2870,7 @@ export class ScimGateway {
|
|
|
2870
2870
|
|
|
2871
2871
|
const path = `/users/${msgObj.from}/sendMail`
|
|
2872
2872
|
try {
|
|
2873
|
-
|
|
2874
|
-
await this.helperRest.doRequest('undefined', 'POST', path, emailMessage, null, { connection: { auth: { type: 'oauthJwtBearer' } } })
|
|
2875
|
-
} else await this.helperRest.doRequest('undefined', 'POST', path, emailMessage)
|
|
2873
|
+
await this.helperRest.doRequest('undefined', 'POST', path, emailMessage)
|
|
2876
2874
|
logger.debug(`${gwName}[${pluginName}] sendMail subject '${msgObj.subject}' sent to: ${msgObj.to}${(msgObj.cc) ? ',' + msgObj.cc : ''}`)
|
|
2877
2875
|
} catch (err: any) {
|
|
2878
2876
|
logger.error(`${gwName}[${pluginName}] sendMail subject '${msgObj.subject}' sending failed: ${err.message}`)
|
|
@@ -2894,7 +2892,7 @@ Content-Transfer-Encoding: quoted-printable
|
|
|
2894
2892
|
const emailMessage = { raw: encodedMessage }
|
|
2895
2893
|
const path = `/gmail/v1/users/${msgObj.from}/messages/send`
|
|
2896
2894
|
try { // using opt connection argument type=oauthJwtBearer and options scope/subject because we want to keep simplified email.auth.type=oauth and options serviceAccountKeyFile
|
|
2897
|
-
await this.helperRest.doRequest('undefined', 'POST', path, emailMessage, null, { connection: { auth: { type: 'oauthJwtBearer', options: { scope: 'https://www.googleapis.com/auth/gmail.send', subject: msgObj.from } } } })
|
|
2895
|
+
await this.helperRest.doRequest('undefined', 'POST', path, emailMessage, null, { connection: { auth: { type: 'oauthJwtBearer', options: { jwtPayload: { scope: 'https://www.googleapis.com/auth/gmail.send', subject: msgObj.from } } } } })
|
|
2898
2896
|
logger.debug(`${gwName}[${pluginName}] sendMail subject '${msgObj.subject}' sent to: ${msgObj.to}${(msgObj.cc) ? ',' + msgObj.cc : ''}`)
|
|
2899
2897
|
} catch (err: any) {
|
|
2900
2898
|
logger.error(`${gwName}[${pluginName}] sendMail subject '${msgObj.subject}' sending failed: ${err.message}`)
|
package/package.json
CHANGED