From d6acdfddcef8413226e3366932df5b6bda234e47 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 24 May 2024 13:28:11 +1200 Subject: [PATCH] feat(camunda8): support Basic Auth * feat(camunda8): support Basic Auth Implement Basic Auth provider for all clients fixes #165 * refactor: remove trace output of auth creds --- README.md | 39 ++++++++++------ package-lock.json | 23 ++++++++++ package.json | 2 + .../oauth/OAuthProvider.unit.spec.ts | 44 ++++++++++++++++++- src/admin/lib/AdminApiClient.ts | 1 - src/lib/Configuration.ts | 17 +++++++ src/lib/ConstructOAuthProvider.ts | 21 ++++++++- src/modeler/lib/ModelerAPIClient.ts | 5 +-- src/oauth/lib/BasicAuthProvider.ts | 39 ++++++++++++++++ 9 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 src/oauth/lib/BasicAuthProvider.ts diff --git a/README.md b/README.md index 416f941f..14ead87c 100644 --- a/README.md +++ b/README.md @@ -61,34 +61,42 @@ Some number values - for example: "_total returned results_ " - may be specified For `int64` values whose type is not known ahead of time, such as job variables, you can pass an annotated data transfer object (DTO) to decode them reliably. If no DTO is specified, the default behavior of the SDK is to serialise all numbers to JavaScript `number`, and if a number value is detected at a runtime that cannot be accurately stored as `number`, to throw an exception. -## OAuth +## Authorization -Calls to APIs are authorized using a token that is obtained via a client id/secret pair exchange, and then passes as an authorization header on API calls. The SDK handles this for you. +Calls to APIs can be authorized using basic auth or via OAuth - a token that is obtained via a client id/secret pair exchange. -If your Camunda 8 platform is secured using token exchange, provide the client id and secret to the SDK. +### Disable Auth -### Disable OAuth +To disable OAuth, set the environment variable `CAMUNDA_OAUTH_STRATEGY=NONE`. You can use this when running against a minimal Zeebe broker in a development environment, for example. -To disable OAuth, set the environment variable `CAMUNDA_OAUTH_DISABLED`. You can use this when running against a minimal Zeebe broker in a development environment, for example. +### Basic Auth -With this environment variable set, the SDK will inject a `NullAuthProvider` that does nothing. +To use basic auth, set the following values either via the environment or explicitly in code via the constructor: -### Configuring OAuth +```bash +CAMUNDA_AUTH_STRATEGY=BASIC +CAMUNDA_BASIC_AUTH_USERNAME=.... +CAMUNDA_BASIC_AUTH_PASSWORD=... +``` + +### OAuth -To get a token for use with the application APIs, provide the following configuration fields at a minimum, either via the `Camunda8` constructor or in environment variables: +If your platform is secured with OAuth token exchange (Camunda SaaS or Self-Managed with Identity), provide the following configuration fields at a minimum, either via the `Camunda8` constructor or in environment variables: ```bash -ZEEBE_GRPC_ADDRESS -ZEEBE_CLIENT_ID -ZEEBE_CLIENT_SECRET -CAMUNDA_OAUTH_URL +CAMUNDA_AUTH_STRATEGY=OAUTH +ZEEBE_GRPC_ADDRESS=... +ZEEBE_CLIENT_ID=... +ZEEBE_CLIENT_SECRET=... +CAMUNDA_OAUTH_URL=... ``` To get a token for the Camunda SaaS Administration API or the Camunda SaaS Modeler API, set the following: ```bash -CAMUNDA_CONSOLE_CLIENT_ID -CAMUNDA_CONSOLE_CLIENT_SECRET +CAMUNDA_AUTH_STRATEGY=OAUTH +CAMUNDA_CONSOLE_CLIENT_ID=... +CAMUNDA_CONSOLE_CLIENT_SECRET=... ``` ### Token caching @@ -123,6 +131,7 @@ export ZEEBE_GRPC_ADDRESS='localhost:26500' export ZEEBE_REST_ADDRESS='http://localhost:8080' export ZEEBE_CLIENT_ID='zeebe' export ZEEBE_CLIENT_SECRET='zecret' +export CAMUNDA_OAUTH_STRATEGY='OAUTH' export CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token' export CAMUNDA_TASKLIST_BASE_URL='http://localhost:8082' export CAMUNDA_OPERATE_BASE_URL='http://localhost:8081' @@ -153,6 +162,7 @@ const c8 = new Camunda8({ ZEEBE_REST_ADDRESS: 'http://localhost:8080', ZEEBE_CLIENT_ID: 'zeebe', ZEEBE_CLIENT_SECRET: 'zecret', + CAMUNDA_OAUTH_STRATEGY: 'OAUTH', CAMUNDA_OAUTH_URL: 'http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token', CAMUNDA_TASKLIST_BASE_URL: 'http://localhost:8082', @@ -178,6 +188,7 @@ export CAMUNDA_TASKLIST_BASE_URL='https://syd-1.tasklist.camunda.io/5c34c0a7-7f2 export CAMUNDA_OPTIMIZE_BASE_URL='https://syd-1.optimize.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9' export CAMUNDA_OPERATE_BASE_URL='https://syd-1.operate.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9' export CAMUNDA_OAUTH_URL='https://login.cloud.camunda.io/oauth/token' +export CAMUNDA_AUTH_STRATEGY='OAUTH' # This is on by default, but we include it in case it got turned off for local tests export CAMUNDA_SECURE_CONNECTION=true diff --git a/package-lock.json b/package-lock.json index 26400939..d2f16e93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@sitapati/testcontainers": "^2.8.1", + "@types/basic-auth": "^1.1.8", "@types/debug": "^4.1.12", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", @@ -45,6 +46,7 @@ "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", + "basic-auth": "^2.0.1", "commitizen": "^4.3.0", "cross-env": "^7.0.3", "cz-conventional-changelog": "^3.3.0", @@ -3589,6 +3591,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/basic-auth": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/basic-auth/-/basic-auth-1.1.8.tgz", + "integrity": "sha512-dKcUeixGuZn8pBjcUrf1N7x5K6lWuKuwHHitM2IZ4vwZUDWEhhNtwCWiba8jTA9zn0GQQ+fTFkWpKx8pOU/enw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -4701,6 +4712,18 @@ ], "license": "MIT" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/before-after-hook": { "version": "2.2.3", "dev": true, diff --git a/package.json b/package.json index 9abde404..38cd59f6 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@sitapati/testcontainers": "^2.8.1", + "@types/basic-auth": "^1.1.8", "@types/debug": "^4.1.12", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", @@ -109,6 +110,7 @@ "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", + "basic-auth": "^2.0.1", "commitizen": "^4.3.0", "cross-env": "^7.0.3", "cz-conventional-changelog": "^3.3.0", diff --git a/src/__tests__/oauth/OAuthProvider.unit.spec.ts b/src/__tests__/oauth/OAuthProvider.unit.spec.ts index 20625e3b..9d5cff29 100644 --- a/src/__tests__/oauth/OAuthProvider.unit.spec.ts +++ b/src/__tests__/oauth/OAuthProvider.unit.spec.ts @@ -4,9 +4,11 @@ import http from 'http' import os from 'os' import path from 'path' +import auth from 'basic-auth' +import got from 'got' import jwt from 'jsonwebtoken' -import { EnvironmentSetup } from '../../lib' +import { EnvironmentSetup, constructOAuthProvider } from '../../lib' import { OAuthProvider } from '../../oauth' jest.setTimeout(10000) @@ -573,4 +575,44 @@ describe('OAuthProvider', () => { expect(thrown).toBe(true) }) }) + + it('Can use Basic Auth as a strategy', async () => { + const server = http.createServer((req, res) => { + const credentials = auth(req) + + if ( + !credentials || + credentials.name !== 'admin' || + credentials.pass !== 'supersecret' + ) { + res.statusCode = 401 + res.setHeader('WWW-Authenticate', 'Basic realm="example"') + res.end('Access denied') + } else { + res.end('Access granted') + } + }) + + server.listen(3033, () => { + console.log('Server running on port 3033') + }) + + const oAuthProvider = constructOAuthProvider({ + CAMUNDA_AUTH_STRATEGY: 'BASIC', + CAMUNDA_BASIC_AUTH_PASSWORD: 'supersecret', + CAMUNDA_BASIC_AUTH_USERNAME: 'admin', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any) + const token = await oAuthProvider.getToken('ZEEBE') + await got + .get('http://localhost:3033', { + headers: { + Authorization: 'Basic ' + token, + }, + }) + .then((res) => { + server.close() + expect(res).toBeTruthy() + }) + }) }) diff --git a/src/admin/lib/AdminApiClient.ts b/src/admin/lib/AdminApiClient.ts index 675bbddf..b262c105 100644 --- a/src/admin/lib/AdminApiClient.ts +++ b/src/admin/lib/AdminApiClient.ts @@ -176,7 +176,6 @@ export class AdminApiClient { body: JSON.stringify(createClusterRequest), headers, } - debug(req) const rest = await this.rest return rest.post('', req).json() } diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index 14d42c19..73476b42 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -173,6 +173,21 @@ const getMainEnv = () => type: 'string', optional: true, }, + /** Username for Basic Auth */ + CAMUNDA_BASIC_AUTH_USERNAME: { + type: 'string', + optional: true, + }, + /** Username for Basic Auth */ + CAMUNDA_BASIC_AUTH_PASSWORD: { + type: 'string', + optional: true, + }, + CAMUNDA_AUTH_STRATEGY: { + type: 'string', + choices: ['BASIC', 'OAUTH', 'NONE'], + default: 'OAUTH', + }, }) const getZeebeEnv = () => @@ -377,6 +392,8 @@ export const CamundaEnvironmentVariableDictionary = 'GRPC_KEEPALIVE_TIME_MS', 'ZEEBE_REST_ADDRESS', 'ZEEBE_GRPC_ADDRESS', + 'CAMUNDA_BASIC_AUTH_USERNAME', + 'CAMUNDA_BASIC_AUTH_PASSWORD', 'ZEEBE_ADDRESS', 'ZEEBE_CLIENT_ID', 'ZEEBE_CLIENT_SECRET', diff --git a/src/lib/ConstructOAuthProvider.ts b/src/lib/ConstructOAuthProvider.ts index 32ef884b..23a23004 100644 --- a/src/lib/ConstructOAuthProvider.ts +++ b/src/lib/ConstructOAuthProvider.ts @@ -1,11 +1,28 @@ +import debug from 'debug' + import { NullAuthProvider, OAuthProvider } from '../oauth' +import { BasicAuthProvider } from '../oauth/lib/BasicAuthProvider' import { CamundaPlatform8Configuration } from './Configuration' +const trace = debug('camunda:oauth') + export function constructOAuthProvider(config: CamundaPlatform8Configuration) { - if (config.CAMUNDA_OAUTH_DISABLED) { + trace(`Auth strategy is ${config.CAMUNDA_AUTH_STRATEGY}`) + trace(`OAuth disabled is ${config.CAMUNDA_OAUTH_DISABLED}`) + if ( + config.CAMUNDA_OAUTH_DISABLED || + config.CAMUNDA_AUTH_STRATEGY === 'NONE' + ) { + trace(`Disabling Auth`) return new NullAuthProvider() } else { - return new OAuthProvider({ config }) + if (config.CAMUNDA_AUTH_STRATEGY === 'BASIC') { + trace(`Using Basic Auth`) + return new BasicAuthProvider({ config }) + } else { + trace(`Using OAuth`) + return new OAuthProvider({ config }) + } } } diff --git a/src/modeler/lib/ModelerAPIClient.ts b/src/modeler/lib/ModelerAPIClient.ts index c57dbfed..d052a22f 100644 --- a/src/modeler/lib/ModelerAPIClient.ts +++ b/src/modeler/lib/ModelerAPIClient.ts @@ -64,14 +64,13 @@ export class ModelerApiClient { private async getHeaders() { const token = await this.oAuthProvider.getToken('MODELER') - const auth = `Bearer ${token}` + const authorization = `Bearer ${token}` const headers = { 'content-type': 'application/json', - authorization: auth, + authorization, 'user-agent': this.userAgentString, accept: '*/*', } - debug(auth) return headers } diff --git a/src/oauth/lib/BasicAuthProvider.ts b/src/oauth/lib/BasicAuthProvider.ts new file mode 100644 index 00000000..925c90a2 --- /dev/null +++ b/src/oauth/lib/BasicAuthProvider.ts @@ -0,0 +1,39 @@ +import debug from 'debug' + +import { + CamundaEnvironmentConfigurator, + CamundaPlatform8Configuration, + DeepPartial, + RequireConfiguration, +} from '../../lib' + +import { IOAuthProvider, TokenGrantAudienceType } from './IOAuthProvider' + +const trace = debug('camunda:oauth') + +export class BasicAuthProvider implements IOAuthProvider { + private username: string | undefined + private password: string | undefined + constructor(options?: { + config?: DeepPartial + }) { + const config = CamundaEnvironmentConfigurator.mergeConfigWithEnvironment( + options?.config ?? {} + ) + this.username = RequireConfiguration( + config.CAMUNDA_BASIC_AUTH_USERNAME, + 'CAMUNDA_BASIC_AUTH_USERNAME' + ) + this.password = RequireConfiguration( + config.CAMUNDA_BASIC_AUTH_PASSWORD, + 'CAMUNDA_BASIC_AUTH_PASSWORD' + ) + } + getToken(audience: TokenGrantAudienceType): Promise { + trace(`Requesting token for audience ${audience}`) + const token = Buffer.from(`${this.username}:${this.password}`).toString( + 'base64' + ) + return Promise.resolve(token) + } +}