scimgateway 5.1.4 → 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 +211 -82
- package/lib/helper-rest.ts +85 -72
- package/lib/scimgateway.ts +6 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Latest news:
|
|
|
36
36
|
|
|
37
37
|
## Overview
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
SCIM Gateway facilitates user management using the standardized REST-based SCIM 1.1 or 2.0 protocol, offering easier, more powerful, and consistent provisioning while avoiding vendor lock-in. Acting as a translator for incoming SCIM requests, the gateway seamlessly enables CRUD functionality (create, read, update, and delete) for users and groups. By implementing endpoint-specific protocols, it ensures precise and efficient provisioning across diverse destinations. With the gateway, your diverse destinations effectively become SCIM endpoints, streamlining integration and simplifying user management.
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|

|
|
@@ -150,7 +150,7 @@ If internet connection is blocked, we could install on another machine and copy
|
|
|
150
150
|
|
|
151
151
|
>Tip, take a look at bun test scripts located in `node_modules\scimgateway\test\lib`
|
|
152
152
|
|
|
153
|
-
> If using Node.js instead of Bun, scimgateway must be downloaded from github
|
|
153
|
+
> If using Node.js instead of Bun, scimgateway must be downloaded from github because Node.js does not support native typescript used by modules. Startup will then be:
|
|
154
154
|
node --experimental-strip-types c:\my-scimgateway\index.ts
|
|
155
155
|
|
|
156
156
|
#### Upgrade SCIM Gateway
|
|
@@ -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
|
|
|
@@ -1147,7 +1263,7 @@ If using REST, we could also include the HelperRest:
|
|
|
1147
1263
|
...
|
|
1148
1264
|
// end - mandatory plugin initialization
|
|
1149
1265
|
|
|
1150
|
-
Plugins should include following SCIM methods:
|
|
1266
|
+
Plugins should include following SCIM Gateway methods:
|
|
1151
1267
|
|
|
1152
1268
|
* scimgateway.getUsers()
|
|
1153
1269
|
* scimgateway.createUser()
|
|
@@ -1175,6 +1291,19 @@ 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
|
+
|
|
1301
|
+
### v5.1.5
|
|
1302
|
+
|
|
1303
|
+
[Improved]
|
|
1304
|
+
|
|
1305
|
+
- 404 NOT_FOUND is now logged as a warning instead of error
|
|
1306
|
+
|
|
1178
1307
|
### v5.1.4
|
|
1179
1308
|
|
|
1180
1309
|
[Fixed]
|
|
@@ -1199,7 +1328,7 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
|
|
|
1199
1328
|
"options": {
|
|
1200
1329
|
"tenantIdGUID": "Entra ID Tenant ID (GUID)",
|
|
1201
1330
|
"clientId": "<application clientId>",
|
|
1202
|
-
"
|
|
1331
|
+
"tls": { // files located in ./config/certs
|
|
1203
1332
|
"key": "key.pem",
|
|
1204
1333
|
"cert": "cert.pem"
|
|
1205
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
|
@@ -305,7 +305,6 @@ export class ScimGateway {
|
|
|
305
305
|
|
|
306
306
|
constructor() {
|
|
307
307
|
const funcHandler: any = {}
|
|
308
|
-
const startTime = utils.timestamp()
|
|
309
308
|
let requester: string = ''
|
|
310
309
|
{
|
|
311
310
|
let _prepareStackTrace = Error.prepareStackTrace
|
|
@@ -374,7 +373,6 @@ export class ScimGateway {
|
|
|
374
373
|
|
|
375
374
|
const oAuthTokenExpire = 3600 // seconds
|
|
376
375
|
let pwErrCount = 0
|
|
377
|
-
let requestCounter = 0
|
|
378
376
|
let isMailLock = false
|
|
379
377
|
let ipAllowListChecker: any
|
|
380
378
|
let server: any
|
|
@@ -554,9 +552,9 @@ export class ScimGateway {
|
|
|
554
552
|
if (authType === 'Basic') [userName] = (Buffer.from(authToken, 'base64').toString() || '').split(':')
|
|
555
553
|
if (!userName && authType === 'Bearer') userName = 'token'
|
|
556
554
|
if (ctx.response.status && (ctx.response.status < 200 || ctx.response.status > 299)) {
|
|
557
|
-
logger.
|
|
555
|
+
if (ctx.response.status === 404) logger.warn(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ellapsed} ${ctx.ip} ${userName} ${ctx.response.status} ${ctx.request.method} ${ctx.request.url} Inbound=${JSON.stringify(ctx.request.body)} Outbound=${ctx.response.body}${(this.config.scimgateway.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
|
|
556
|
+
else logger.error(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ellapsed} ${ctx.ip} ${userName} ${ctx.response.status} ${ctx.request.method} ${ctx.request.url} Inbound=${JSON.stringify(ctx.request.body)} Outbound=${ctx.response.body}${(this.config.scimgateway.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
|
|
558
557
|
} else logger.info(`${gwName}[${pluginName}][${ctx?.routeObj?.baseEntity}] ${ellapsed} ${ctx.ip} ${ctx.response.status} ${userName} ${ctx.request.method} ${ctx.request.url} Inbound=${JSON.stringify(ctx.request.body)} Outbound=${ctx.response.body}${(this.config.scimgateway.log.loglevel.file === 'debug' && ctx.request.url !== '/ping') ? '\n' : ''}`)
|
|
559
|
-
requestCounter += 1 // logged on exit (not win process termination)
|
|
560
558
|
}
|
|
561
559
|
|
|
562
560
|
// start auth methods - used by auth
|
|
@@ -2540,12 +2538,12 @@ export class ScimGateway {
|
|
|
2540
2538
|
isMailLock = false
|
|
2541
2539
|
}, (this.config.scimgateway.email.emailOnError.sendInterval || 15) * 1000 * 60)
|
|
2542
2540
|
|
|
2543
|
-
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>`
|
|
2544
2542
|
const msgObj = {
|
|
2545
2543
|
from: this.config.scimgateway.email.emailOnError.from,
|
|
2546
2544
|
to: this.config.scimgateway.email.emailOnError.to,
|
|
2547
2545
|
cc: this.config.scimgateway.email.emailOnError.cc,
|
|
2548
|
-
subject: this.config.scimgateway.email.emailOnError.subject
|
|
2546
|
+
subject: this.config.scimgateway.email.emailOnError.subject || 'SCIM Gateway error message',
|
|
2549
2547
|
content: msgHtml,
|
|
2550
2548
|
}
|
|
2551
2549
|
this.sendMail(msgObj, true)
|
|
@@ -2558,9 +2556,6 @@ export class ScimGateway {
|
|
|
2558
2556
|
}
|
|
2559
2557
|
}
|
|
2560
2558
|
logger.debug(`${gwName}[${pluginName}] received terminate/kill signal - closing connections and exit`)
|
|
2561
|
-
logger.setLoglevelConsole('info')
|
|
2562
|
-
logger.setLoglevelFile('info')
|
|
2563
|
-
logger.info(`${gwName}[${pluginName}] pheww... ${requestCounter} requests have been processed in the period ${startTime} - ${utils.timestamp()}\n`)
|
|
2564
2559
|
logger.close()
|
|
2565
2560
|
if (server) {
|
|
2566
2561
|
if (typeof Bun !== 'undefined') {
|
|
@@ -2875,9 +2870,7 @@ export class ScimGateway {
|
|
|
2875
2870
|
|
|
2876
2871
|
const path = `/users/${msgObj.from}/sendMail`
|
|
2877
2872
|
try {
|
|
2878
|
-
|
|
2879
|
-
await this.helperRest.doRequest('undefined', 'POST', path, emailMessage, null, { connection: { auth: { type: 'oauthJwtBearer' } } })
|
|
2880
|
-
} else await this.helperRest.doRequest('undefined', 'POST', path, emailMessage)
|
|
2873
|
+
await this.helperRest.doRequest('undefined', 'POST', path, emailMessage)
|
|
2881
2874
|
logger.debug(`${gwName}[${pluginName}] sendMail subject '${msgObj.subject}' sent to: ${msgObj.to}${(msgObj.cc) ? ',' + msgObj.cc : ''}`)
|
|
2882
2875
|
} catch (err: any) {
|
|
2883
2876
|
logger.error(`${gwName}[${pluginName}] sendMail subject '${msgObj.subject}' sending failed: ${err.message}`)
|
|
@@ -2899,7 +2892,7 @@ Content-Transfer-Encoding: quoted-printable
|
|
|
2899
2892
|
const emailMessage = { raw: encodedMessage }
|
|
2900
2893
|
const path = `/gmail/v1/users/${msgObj.from}/messages/send`
|
|
2901
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
|
|
2902
|
-
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 } } } } })
|
|
2903
2896
|
logger.debug(`${gwName}[${pluginName}] sendMail subject '${msgObj.subject}' sent to: ${msgObj.to}${(msgObj.cc) ? ',' + msgObj.cc : ''}`)
|
|
2904
2897
|
} catch (err: any) {
|
|
2905
2898
|
logger.error(`${gwName}[${pluginName}] sendMail subject '${msgObj.subject}' sending failed: ${err.message}`)
|
package/package.json
CHANGED