Repository: camunda/camunda-modeler (JavaScript)

Issue:
Add OAuth scope field to deploy modal
_Related to https://github.com/camunda/product-hub/issues/2089._

### Problem you would like to solve

As a user, I want to be able to deploy to Zeebe clusters that use OAuth2 as authentication mechanism with a provider that requires a `scope` in the token request, e.g. Microsoft Entra ID.

### Proposed solution

Add a field "Scope" (or better "OAuth Scope" and rename "Audience" to "OAuth Audience") to the deploy modal which gets picked up by the Zeebe client for acquiring a token.

### Alternatives considered

None

### Additional context

- Web Modeler is also missing the field, see https://github.com/camunda/web-modeler/issues/8027
- https://github.com/camunda-community-hub/zeebe-client-node-js has to provide support for passing a `scope` within the token request, see https://github.com/camunda-community-hub/camunda-8-js-sdk/issues/21
- reported via [Slack](https://camunda.slack.com/archives/C06112NS4GJ/p1707169292640159?thread_ts=1706905378.492159&cid=C06112NS4GJ)

----

Depends on https://github.com/camunda-community-hub/zeebe-client-node-js/pull/363


Discussion: We'd need [zeebe-node](https://github.com/camunda-community-hub/zeebe-client-node-js) to support passing a custom `scope` within the token request and this [seems to be not implemented (yet)](https://github.com/search?q=repo%3Acamunda-community-hub%2Fzeebe-client-node-js+scope&type=code). So as it stands we're currently blocked by upstream here I believe? 

Shall I create an upstream issue @jwulf?
@wollefitz Thanks for opening this issue! I buy into both the enhancement + proposed renaming.
> Shall I create an upstream issue @jwulf?

@nikku There's already https://github.com/camunda-community-hub/camunda-8-js-sdk/issues/21 (linked below "Additional Contect") created by Josh.
Thanks @wollefitz. I'm a little bit confused as I don't care about camunda-8-js-sdk. We use the zeebe-node client and have not immediate intention to move to something else, unless explicitly required to do so.
@nikku - with the 8.5.0 release we will be supporting the JS SDK. 

The Zeebe Node client published as zeebe-node is going to be unmaintained from 8.5.0. 

The new package will be @camunda8/zeebe. It’s the same code base, but moved to the camunda-8-js-sdk monorepo.


Thanks for the heads-up @jwulf.
Created https://github.com/camunda/camunda-modeler/issues/4109 from our end. Would be great to get a heads-up when `@camunda8/zeebe` is ready to be migrated to.

It would help us if we'd get `scope` support in one way or the other, so we can integrate it into the modeler.
Waiting for upstream https://github.com/camunda-community-hub/zeebe-client-node-js/pull/363 to be merged and released.
Continuing to integrate this via https://github.com/camunda/camunda-modeler/pull/4169.



Fix (patch):
diff --git a/app/lib/zeebe-api/zeebe-api.js b/app/lib/zeebe-api/zeebe-api.js
index 52e2de9fd3..7dd06f57aa 100644
--- a/app/lib/zeebe-api/zeebe-api.js
+++ b/app/lib/zeebe-api/zeebe-api.js
@@ -66,6 +66,7 @@ const RESOURCE_TYPES = {
  * @property {'oauth'} type
  * @property {string} url
  * @property {string} audience
+ * @property {string} [scope]
  * @property {string} clientId
  * @property {string} clientSecret
  */
@@ -323,6 +324,7 @@ class ZeebeAPI {
         oAuth: {
           url: endpoint.oauthURL,
           audience: endpoint.audience,
+          scope: endpoint.scope,
           clientId: endpoint.clientId,
           clientSecret: endpoint.clientSecret,
           cacheOnDisk: false
diff --git a/app/package.json b/app/package.json
index 99e8c277bc..7933d5b206 100644
--- a/app/package.json
+++ b/app/package.json
@@ -20,7 +20,7 @@
     "min-dash": "^4.1.1",
     "mri": "^1.1.6",
     "parents": "^1.0.1",
-    "zeebe-node": "^8.3.1"
+    "zeebe-node": "^8.3.2"
   },
   "homepage": "https://github.com/camunda/camunda-modeler",
   "repository": {
diff --git a/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginConstants.js b/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginConstants.js
index a7b85d6423..2575a561a8 100644
--- a/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginConstants.js
+++ b/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginConstants.js
@@ -9,7 +9,7 @@
  */
 
 export const OVERLAY_TITLE = 'Deploy diagram';
-export const ENDPOINT_CONFIGURATION_TITLE = 'Endpoint Configuration';
+export const ENDPOINT_CONFIGURATION_TITLE = 'Endpoint configuration';
 export const CANCEL = 'Cancel';
 export const DEPLOY = 'Deploy';
 export const NEXT = 'Next';
@@ -22,8 +22,9 @@ export const CAMUNDA_CLOUD_TEXT = 'Camunda 8 SaaS';
 export const CONTACT_POINT = 'Cluster endpoint';
 export const DEPLOYMENT_NAME_HINT = 'Default value is the file name.';
 export const CONTACT_POINT_HINT = 'http://localhost:26500';
-export const OAUTH_URL = 'OAuth Token URL';
-export const AUDIENCE = 'Audience';
+export const OAUTH_URL = 'OAuth token URL';
+export const AUDIENCE = 'OAuth audience';
+export const SCOPE = 'OAuth scope';
 export const CLIENT_ID = 'Client ID';
 export const CLIENT_SECRET = 'Client secret';
 export const CLUSTER_URL = 'Cluster URL';
@@ -63,5 +64,5 @@ export const CONNECTION_ERROR_MESSAGES = {
   [ ERROR_REASONS.UNKNOWN ]: 'Unknown error. Please check Zeebe cluster status.',
   [ ERROR_REASONS.UNSUPPORTED_ENGINE ]: 'Unsupported Zeebe version.',
   [ ERROR_REASONS.INVALID_CLIENT_ID ]: 'Invalid Client ID.',
-  [ ERROR_REASONS.INVALID_CREDENTIALS ]: 'The client secret is not valid for the Client ID provided.'
+  [ ERROR_REASONS.INVALID_CREDENTIALS ]: 'The client secret is not valid for the client ID provided.'
 };
diff --git a/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginOverlay.js b/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginOverlay.js
index 6ff53b09c6..654c50b832 100644
--- a/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginOverlay.js
+++ b/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginOverlay.js
@@ -24,6 +24,7 @@ import {
   CONTACT_POINT_HINT,
   OAUTH_URL,
   AUDIENCE,
+  SCOPE,
   TENANT_ID,
   CLIENT_ID,
   CLIENT_SECRET,
@@ -80,6 +81,7 @@ export default class DeploymentPluginOverlay extends React.PureComponent {
       contactPoint: validator.validateZeebeContactPoint,
       oauthURL: validator.validateOAuthURL,
       audience: validator.validateAudience,
+      scope: validator.validateScope,
       clientId: validator.validateClientId,
       clientSecret: validator.validateClientSecret,
       camundaCloudClientId: validator.validateClientId,
@@ -138,7 +140,8 @@ export default class DeploymentPluginOverlay extends React.PureComponent {
         'clientSecret',
         'camundaCloudClientId',
         'camundaCloudClientSecret',
-        'audience'
+        'audience',
+        'scope'
       ].includes(fieldName) && CONNECTION_ERROR_MESSAGES[failureReason];
     case ERROR_REASONS.OAUTH_URL:
       return fieldName === 'oauthURL' && CONNECTION_ERROR_MESSAGES[failureReason];
@@ -159,16 +162,21 @@ export default class DeploymentPluginOverlay extends React.PureComponent {
   }
 
   scheduleConnectionCheck = formValues => {
-    this.connectionChecker.check(formValues.endpoint);
-
     const { endpoint } = formValues;
 
+    // empty scope shall not be passed
+    if (!endpoint.scope) {
+      delete endpoint.scope;
+    }
+
     // Extract clusterId and clusterRegion as required by zeebeAPI for camundaCloud
     if (endpoint.targetType === CAMUNDA_CLOUD && endpoint.camundaCloudClusterUrl) {
       endpoint.camundaCloudClusterId = extractClusterId(endpoint.camundaCloudClusterUrl);
       endpoint.camundaCloudClusterRegion = extractClusterRegion(endpoint.camundaCloudClusterUrl);
     }
 
+    this.connectionChecker.check(endpoint);
+
     this.setState({ configValues: formValues });
   };
 
@@ -205,6 +213,10 @@ export default class DeploymentPluginOverlay extends React.PureComponent {
       endpoint.camundaCloudClusterRegion = extractClusterRegion(endpoint.camundaCloudClusterUrl);
     }
 
+    if (!endpoint.scope) {
+      delete endpoint.scope;
+    }
+
     if (endpoint.authType === AUTH_TYPES.NONE) {
       delete deployment.tenantId;
     }
@@ -246,7 +258,7 @@ export default class DeploymentPluginOverlay extends React.PureComponent {
             form => (
               <form onSubmit={ form.handleSubmit }>
                 <Section>
-                  <Section.Header> { OVERLAY_TITLE } </Section.Header>
+                  <Section.Header>{ OVERLAY_TITLE }</Section.Header>
                   <Section.Body>
                     <fieldset className="fieldset">
                       <div className="fields">
@@ -339,6 +351,13 @@ export default class DeploymentPluginOverlay extends React.PureComponent {
                                 fieldError={ this.endpointConfigurationFieldError }
                                 validate={ validatorFunctionsByFieldNames.audience }
                               />
+                              <Field
+                                name="endpoint.scope"
+                                component={ TextInput }
+                                label={ SCOPE }
+                                fieldError={ this.endpointConfigurationFieldError }
+                                validate={ validatorFunctionsByFieldNames.scope }
+                              />
                             </React.Fragment>
                           )
                         }
diff --git a/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginValidator.js b/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginValidator.js
index a236db8fff..89d3dcc0df 100644
--- a/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginValidator.js
+++ b/client/src/plugins/zeebe-plugin/deployment-plugin/DeploymentPluginValidator.js
@@ -54,6 +54,10 @@ export default class DeploymentPluginValidator {
     return this.validateNonEmpty(value, AUDIENCE_MUST_NOT_BE_EMPTY);
   };
 
+  validateScope = (value) => {
+    return null;
+  };
+
   validateClientId = (value) => {
     return this.validateNonEmpty(value, CLIENT_ID_MUST_NOT_BE_EMPTY);
   };
diff --git a/client/src/remote/ZeebeAPI.js b/client/src/remote/ZeebeAPI.js
index 55816599db..2aba3c4fcb 100644
--- a/client/src/remote/ZeebeAPI.js
+++ b/client/src/remote/ZeebeAPI.js
@@ -75,6 +75,7 @@ function getEndpointConfiguration(endpoint) {
   const {
     authType,
     audience,
+    scope,
     targetType,
     clientId,
     clientSecret,
@@ -101,6 +102,7 @@ function getEndpointConfiguration(endpoint) {
         url: contactPoint,
         oauthURL,
         audience,
+        scope,
         clientId,
         clientSecret
       };
diff --git a/package-lock.json b/package-lock.json
index e92b18a9eb..3051726597 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -62,7 +62,7 @@
         "min-dash": "^4.1.1",
         "mri": "^1.1.6",
         "parents": "^1.0.1",
-        "zeebe-node": "^8.3.1"
+        "zeebe-node": "^8.3.2"
       },
       "optionalDependencies": {
         "vscode-windows-ca-certs": "^0.3.0"
@@ -31203,9 +31203,9 @@
       "integrity": "sha512-ES/UZFO0VmKvAzL4+cD3VcQpKvlmgLtnFKTyiv0DdDcxNrdQg1rI0OmUdrKMiybAbtAgPDkVXZCusE3kkXwEyQ=="
     },
     "node_modules/zeebe-node": {
-      "version": "8.3.1",
-      "resolved": "https://registry.npmjs.org/zeebe-node/-/zeebe-node-8.3.1.tgz",
-      "integrity": "sha512-68ascWO3g7g+9WwDzvfa3I9TkLKHku5auEgSINP+k5ktNfsfGW68ELDmEJA+XHZgzvGsdGILZqGRzVd5SC8aaQ==",
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/zeebe-node/-/zeebe-node-8.3.2.tgz",
+      "integrity": "sha512-3/xbiTvhaa668JHtMEwELv5dN6HR7Qw8gzmCdjp3Brj6ekdhROVx8x/0JWKSV3Mx64ac3+eEc+9nB5+ZXcO/bg==",
       "dependencies": {
         "@grpc/grpc-js": "1.9.7",
         "@grpc/proto-loader": "0.7.10",
@@ -37455,7 +37455,7 @@
         "mri": "^1.1.6",
         "parents": "^1.0.1",
         "vscode-windows-ca-certs": "^0.3.0",
-        "zeebe-node": "^8.3.1"
+        "zeebe-node": "^8.3.2"
       },
       "dependencies": {
         "form-data": {
@@ -53307,9 +53307,9 @@
       "integrity": "sha512-ES/UZFO0VmKvAzL4+cD3VcQpKvlmgLtnFKTyiv0DdDcxNrdQg1rI0OmUdrKMiybAbtAgPDkVXZCusE3kkXwEyQ=="
     },
     "zeebe-node": {
-      "version": "8.3.1",
-      "resolved": "https://registry.npmjs.org/zeebe-node/-/zeebe-node-8.3.1.tgz",
-      "integrity": "sha512-68ascWO3g7g+9WwDzvfa3I9TkLKHku5auEgSINP+k5ktNfsfGW68ELDmEJA+XHZgzvGsdGILZqGRzVd5SC8aaQ==",
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/zeebe-node/-/zeebe-node-8.3.2.tgz",
+      "integrity": "sha512-3/xbiTvhaa668JHtMEwELv5dN6HR7Qw8gzmCdjp3Brj6ekdhROVx8x/0JWKSV3Mx64ac3+eEc+9nB5+ZXcO/bg==",
       "requires": {
         "@grpc/grpc-js": "1.9.7",
         "@grpc/proto-loader": "0.7.10",