diff --git a/package-lock.json b/package-lock.json index ffc4c18f..fc29a91a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "long": "^4.0.0", "lossless-json": "^4.0.1", "neon-env": "^0.1.3", - "node-fetch": "^2.7.0", "promise-retry": "^1.1.1", "reflect-metadata": "^0.2.1", "stack-trace": "0.0.10", @@ -6121,27 +6120,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encoding": { - "version": "0.1.13", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "license": "MIT", @@ -11259,25 +11237,6 @@ "node": ">=18" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-int64": { "version": "0.4.0", "dev": true, @@ -15586,7 +15545,7 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/semantic-release": { @@ -16741,11 +16700,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/traverse": { "version": "0.6.7", "dev": true, @@ -17495,20 +17449,6 @@ "defaults": "^1.0.3" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "dev": true, diff --git a/package.json b/package.json index 7f712fbf..e31e531f 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,6 @@ "long": "^4.0.0", "lossless-json": "^4.0.1", "neon-env": "^0.1.3", - "node-fetch": "^2.7.0", "promise-retry": "^1.1.1", "reflect-metadata": "^0.2.1", "stack-trace": "0.0.10", diff --git a/src/__tests__/oauth/OAuthProvider.spec.ts b/src/__tests__/oauth/OAuthProvider.spec.ts index 38179b56..d181ca21 100644 --- a/src/__tests__/oauth/OAuthProvider.spec.ts +++ b/src/__tests__/oauth/OAuthProvider.spec.ts @@ -16,7 +16,6 @@ const jwt = new JSONWebToken(strategy) const payload = { id: 1 } const access_token = jwt.generate(payload) -const access_token2 = jwt.generate(payload) jest.setTimeout(10000) let server: http.Server @@ -194,6 +193,16 @@ test('In-memory cache is populated and evicted after timeout', (done) => { }, }) + const strategy = new HS256Strategy({ + ttl: 2000, + secret: 'YOUR_SECRET', + }) + + const jwt = new JSONWebToken(strategy) + const payload = { id: 1 } + + const access_token = jwt.generate(payload) + let requestCount = 0 server = http .createServer((req, res) => { @@ -206,7 +215,7 @@ test('In-memory cache is populated and evicted after timeout', (done) => { req.on('end', () => { res.writeHead(200, { 'Content-Type': 'application/json' }) const expiresIn = 2 // seconds - const token = requestCount % 2 === 0 ? access_token : access_token2 + const token = `${access_token}${requestCount}` res.end(`{"access_token": "${token}", "expires_in": ${expiresIn}}`) requestCount++ expect(body).toEqual( @@ -218,13 +227,13 @@ test('In-memory cache is populated and evicted after timeout', (done) => { .listen(serverPort3002) o.getToken('ZEEBE').then(async (token) => { - expect(token).toBe(access_token) + expect(token).toBe(`${access_token}0`) await delay(500) const token2 = await o.getToken('ZEEBE') - expect(token2).toBe(access_token) + expect(token2).toBe(`${access_token}0`) await delay(1600) const token3 = await o.getToken('ZEEBE') - expect(token3).toBe(access_token2) + expect(token3).toBe(`${access_token}1`) done() }) }) @@ -296,7 +305,7 @@ test('Uses a custom audience for an Operate token, if one is configured', (done) o.getToken('OPERATE') }) -test.only('Passes scope, if provided', () => { +test('Passes scope, if provided', () => { const serverPort3004 = 3004 const o = new OAuthProvider({ config: { @@ -510,7 +519,7 @@ test('Passes no audience for Modeler API when self-hosted', (done) => { req.on('end', () => { res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(`{"token": "${access_token}"}`) + res.end(`{"access_token": "${access_token}"}`) expect(body).toEqual( 'client_id=clientId17&client_secret=clientSecret&grant_type=client_credentials' ) diff --git a/src/__tests__/operate/operate-integration.spec.ts b/src/__tests__/operate/operate-integration.spec.ts index e2355765..d69b6784 100644 --- a/src/__tests__/operate/operate-integration.spec.ts +++ b/src/__tests__/operate/operate-integration.spec.ts @@ -16,7 +16,7 @@ afterAll(async () => { restoreZeebeLogging() }) -jest.setTimeout(15000) +jest.setTimeout(20000) describe('Operate Integration', () => { xtest('It can get the Incident', async () => { const c = new OperateApiClient() @@ -61,7 +61,7 @@ test('getJSONVariablesforProcess works', async () => { }) // Wait for Operate to catch up. - await new Promise((res) => setTimeout(() => res(null), 12000)) + await new Promise((res) => setTimeout(() => res(null), 15000)) // Make sure that the process instance exists in Operate. const process = await c.getProcessInstance(p.processInstanceKey) // If this fails, it is probably a timing issue. diff --git a/src/__tests__/zeebe/integration/Client-MigrateProcessInstance.spec.ts b/src/__tests__/zeebe/integration/Client-MigrateProcessInstance.spec.ts index e78053df..a13b077f 100644 --- a/src/__tests__/zeebe/integration/Client-MigrateProcessInstance.spec.ts +++ b/src/__tests__/zeebe/integration/Client-MigrateProcessInstance.spec.ts @@ -3,6 +3,8 @@ import { ZeebeGrpcClient } from '../../../zeebe/index' import { cancelProcesses } from '../../../zeebe/lib/cancelProcesses' import { DeployResourceResponse, ProcessDeployment } from '../../../zeebe/types' +jest.setTimeout(15000) + suppressZeebeLogging() let res: DeployResourceResponse | undefined @@ -21,15 +23,12 @@ afterAll(async () => { const zbc = new ZeebeGrpcClient() test('ZeebeGrpcClient can migrate a process instance', async () => { - expect(true).toBe(true) // Deploy a process model - res = await zbc.deployResource({ processFilename: './src/__tests__/testdata/MigrateProcess-Version-1.bpmn', }) // Create an instance of the process model - const processInstance = await zbc.createProcessInstance({ bpmnProcessId: 'migrant-work', variables: {}, @@ -62,6 +61,7 @@ test('ZeebeGrpcClient can migrate a process instance', async () => { }) // Migrate the process instance to the updated process model + await zbc.migrateProcessInstance({ processInstanceKey: processInstance.processInstanceKey, migrationPlan: { @@ -105,7 +105,7 @@ test('ZeebeGrpcClient can migrate a process instance', async () => { }, }) }) - + await zbc.close() expect(instanceKey).toBe(processInstance.processInstanceKey) expect(processVersion).toBe('2') }) diff --git a/src/oauth/lib/OAuthProvider.ts b/src/oauth/lib/OAuthProvider.ts index a67876f9..52cf8b83 100644 --- a/src/oauth/lib/OAuthProvider.ts +++ b/src/oauth/lib/OAuthProvider.ts @@ -1,18 +1,20 @@ import * as fs from 'fs' -import https from 'https' import * as os from 'os' import { debug } from 'debug' +import got from 'got' import { jwtDecode } from 'jwt-decode' -import fetch from 'node-fetch' import { CamundaEnvironmentConfigurator, CamundaPlatform8Configuration, DeepPartial, GetCertificateAuthority, + GotRetryConfig, RequireConfiguration, createUserAgentString, + gotBeforeErrorHook, + gotErrorHandler, } from '../../lib' import { IOAuthProvider, Token, TokenError } from '../index' @@ -29,7 +31,6 @@ export class OAuthProvider implements IOAuthProvider { private authServerUrl: string private clientId: string | undefined private clientSecret: string | undefined - private customRootCert?: Buffer private useFileCache: boolean public tokenCache: { [key: string]: Token } = {} private failed = false @@ -43,6 +44,7 @@ export class OAuthProvider implements IOAuthProvider { private isCamundaSaaS: boolean private camundaModelerOAuthAudience: string | undefined private refreshWindow: number + private rest: typeof got constructor(options?: { config?: DeepPartial @@ -83,11 +85,18 @@ export class OAuthProvider implements IOAuthProvider { ) { throw new Error('You need to supply both a client ID and a client secret') } + const certificateAuthority = GetCertificateAuthority(config) - const customRootCert = GetCertificateAuthority(config) - this.customRootCert = customRootCert - ? Buffer.from(customRootCert) - : undefined + this.rest = got.extend({ + retry: GotRetryConfig, + https: { + certificateAuthority, + }, + handlers: [gotErrorHandler], + hooks: { + beforeError: [gotBeforeErrorHook], + }, + }) this.scope = config.CAMUNDA_TOKEN_SCOPE this.useFileCache = !config.CAMUNDA_TOKEN_DISK_CACHE_DISABLE @@ -125,6 +134,20 @@ export class OAuthProvider implements IOAuthProvider { 'If you are running on AWS Lambda, set the HOME environment variable of your lambda function to /tmp' ) } + + const certificateAuthority = GetCertificateAuthority(config) + + this.rest = got.extend({ + // prefixUrl, + retry: GotRetryConfig, + https: { + certificateAuthority, + }, + handlers: [gotErrorHandler], + hooks: { + beforeError: [gotBeforeErrorHook], + }, + }) } this.isCamundaSaaS = this.authServerUrl.includes( @@ -134,7 +157,6 @@ export class OAuthProvider implements IOAuthProvider { public async getToken(audienceType: TokenGrantAudienceType): Promise { debug(`Token request for ${audienceType}`) - // tslint:disable-next-line: no-console // We use the Console credential set if it we are requesting from // the SaaS OAuth endpoint, and it is a Modeler or Admin Console token. // Otherwise we use the application credential set, unless a Console credential set exists. @@ -261,45 +283,26 @@ export class OAuthProvider implements IOAuthProvider { /* Add a scope to the token request, if one is set */ const bodyWithScope = this.scope ? `${body}&scope=${this.scope}` : body - if (this.customRootCert) { - trace('Using custom root certificate') - } - - const customAgent = this.customRootCert - ? new https.Agent({ ca: this.customRootCert }) - : undefined - const options = { - agent: customAgent, - method: 'POST', body: bodyWithScope, headers: { 'content-type': 'application/x-www-form-urlencoded', 'user-agent': this.userAgentString, + accept: '*/*', }, } - const optionsWithAgent = this.customRootCert - ? { ...options, agent: customAgent } - : options + trace(`Making token request to the token endpoint: `) trace(` ${this.authServerUrl}`) - trace(optionsWithAgent) - return fetch(this.authServerUrl, optionsWithAgent) + trace(options) + return this.rest + .post(this.authServerUrl, options) .catch((e) => { console.log(`Erroring requesting token for Client Id ${clientIdToUse}`) console.log(e) throw e }) - .then((res) => - res.json().catch(() => { - trace( - `Failed to parse response from token endpoint. Status ${res.status}: ${res.statusText}` - ) - throw new Error( - `Failed to parse response from token endpoint. Status ${res.status}: ${res.statusText}` - ) - }) - ) + .then((res) => JSON.parse(res.body)) .then((t) => { trace( `Got token for Client Id ${clientIdToUse}: ${JSON.stringify( @@ -315,6 +318,10 @@ export class OAuthProvider implements IOAuthProvider { `Failed to get token: ${t.error} - ${t.error_description}` ) } + if (t.access_token === undefined) { + console.error(audienceType, t) + throw new Error('Failed to get token: no access_token in response') + } const token = { ...(t as Token), audience: audienceType } if (this.useFileCache) { this.sendToFileCache({ @@ -336,11 +343,16 @@ export class OAuthProvider implements IOAuthProvider { token: Token }) { const key = this.getCacheKey(audience) - - const decoded = jwtDecode(token.access_token) - - token.expiry = decoded.exp ?? 0 - this.tokenCache[key] = token + try { + const decoded = jwtDecode(token.access_token) + + token.expiry = decoded.exp ?? 0 + this.tokenCache[key] = token + } catch (e) { + console.error('audience', audience) + console.error('token', token.access_token) + throw e + } } private retrieveFromFileCache(