Skip to content

Commit

Permalink
feat: support configuration of OAuth scope
Browse files Browse the repository at this point in the history
  • Loading branch information
nikku committed Feb 29, 2024
1 parent eaf03a3 commit 7fb01c9
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 45 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ const zbc = new ZBClient("my-secure-broker.io:443", {
oAuth: {
url: "https://your-auth-endpoint/oauth/token",
audience: "my-secure-broker.io",
scope: "myScope",
clientId: "myClientId",
clientSecret: "randomClientSecret",
customRootCert: fs.readFileSync('./my_CA.pem'),
Expand Down Expand Up @@ -599,6 +600,7 @@ Self-hosted or local broker with OAuth + TLS:
ZEEBE_CLIENT_ID
ZEEBE_CLIENT_SECRET
ZEEBE_TOKEN_AUDIENCE
ZEEBE_TOKEN_SCOPE
ZEEBE_AUTHORIZATION_SERVER_URL
ZEEBE_ADDRESS
```
Expand All @@ -613,6 +615,7 @@ ZEEBE_CLIENT_ID='zeebe'
ZEEBE_CLIENT_SECRET='zecret'
ZEEBE_AUTHORIZATION_SERVER_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token'
ZEEBE_TOKEN_AUDIENCE='zeebe.camunda.io'
ZEEBE_TOKEN_SCOPE='not needed'
CAMUNDA_CREDENTIALS_SCOPES='Zeebe'
CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token'
```
Expand Down
40 changes: 37 additions & 3 deletions src/__tests__/ConfigurationHydrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ const ENV_VARS_TO_STORE = [
'ZEEBE_GATEWAY_ADDRESS',
'ZEEBE_ADDRESS',
'ZEEBE_TOKEN_AUDIENCE',
'ZEEBE_TOKEN_SCOPE',
'ZEEBE_AUTHORIZATION_SERVER_URL',
'ZEEBE_CLIENT_MAX_RETRIES',
'ZEEBE_CLIENT_RETRY',
'ZEEBE_CLIENT_MAX_RETRY_TIMEOUT',
'ZEEBE_CLIENT_SSL_ROOT_CERTS_PATH',
'ZEEBE_CLIENT_SSL_PRIVATE_KEY_PATH',
'ZEEBE_CLIENT_SSL_CERT_CHAIN_PATH',
'ZEEBE_TENANT_ID'
'ZEEBE_TENANT_ID',
]

beforeAll(() => {
Expand Down Expand Up @@ -96,6 +97,29 @@ test('Takes an explicit Gateway address over the environment ZEEBE_GATEWAY_ADDRE
expect(conf.port).toBe('26600')
})

/**
* Self-managed
*/
test('Constructs the self-managed connection with oauth credentials', () => {
process.env.ZEEBE_CLIENT_SECRET = 'CLIENT_SECRET'
process.env.ZEEBE_CLIENT_ID = 'CLIENT_ID'
process.env.ZEEBE_GATEWAY_ADDRESS = 'zeebe://my-server:26600'
process.env.ZEEBE_TOKEN_AUDIENCE = 'TOKEN_AUDIENCE'
process.env.ZEEBE_TOKEN_SCOPE = 'TOKEN_SCOPE'
process.env.ZEEBE_AUTHORIZATION_SERVER_URL = 'https://auz'

const conf = ConfigurationHydrator.configure(undefined, undefined)

expect(conf.hostname).toBe('my-server')
expect(conf.port).toBe('26600')
expect(conf.oAuth!.audience).toBe('TOKEN_AUDIENCE')
expect(conf.oAuth!.scope).toBe('TOKEN_SCOPE')
expect(conf.oAuth!.clientId).toBe('CLIENT_ID')
expect(conf.oAuth!.audience).toBe('TOKEN_AUDIENCE')
expect(conf.oAuth!.clientSecret).toBe('CLIENT_SECRET')
expect(conf.oAuth!.url).toBe('https://auz')
})

/**
* Camunda Cloud
*/
Expand All @@ -105,7 +129,13 @@ test('Constructs the Camunda Cloud connection from the environment with clusterI
process.env.ZEEBE_CLIENT_SECRET =
'WZahIGHjyj0-oQ7DZ_aH2wwNuZt5O8Sq0ZJTz0OaxfO7D6jaDBZxM_Q-BHRsiGO_'
process.env.ZEEBE_CLIENT_ID = 'yStuGvJ6a1RQhy8DQpeXJ80yEpar3pXh'

delete process.env.ZEEBE_GATEWAY_ADDRESS
delete process.env.ZEEBE_TOKEN_AUDIENCE
delete process.env.ZEEBE_TOKEN_SCOPE
delete process.env.ZEEBE_AUTHORIZATION_SERVER_URL
delete process.env.ZEEBE_GATEWAY_ADDRESS

// process.env.ZEEBE_GATEWAY_ADDRESS = 'zeebe://localhost:26500'
const conf = ConfigurationHydrator.configure(undefined, undefined)
expect(conf.hostname).toBe(
Expand Down Expand Up @@ -526,13 +556,17 @@ test('Tenant ID is picked up from environment', () => {
})

test('Tenant ID is picked up from constructor options', () => {
const conf = ConfigurationHydrator.configure(undefined, {tenantId: 'thisOne'})
const conf = ConfigurationHydrator.configure(undefined, {
tenantId: 'thisOne',
})
expect(conf.tenantId).toBe('thisOne')
})

test('Tenant ID from constructor overrides environment', () => {
process.env.ZEEBE_TENANT_ID = 'someId'
const conf = ConfigurationHydrator.configure(undefined, {tenantId: 'thisOne'})
const conf = ConfigurationHydrator.configure(undefined, {
tenantId: 'thisOne',
})
expect(conf.tenantId).toBe('thisOne')
})

Expand Down
139 changes: 99 additions & 40 deletions src/__tests__/OAuthProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,79 @@ test('Throws in the constructor if the token cache is not writable', () => {
expect(fs.existsSync(tokenCache)).toBe(false)
})

test('Send form encoded request', () => {
const o = new OAuthProvider({
audience: 'token',
cacheOnDisk: false,
clientId: 'clientId',
clientSecret: 'clientSecret',
url: 'http://127.0.0.1:3001/foobar',
})
const server = http
.createServer((req, res) => {
expect(req.url).toBe('/foobar')
expect(req.method).toBe('POST')
expect(req.headers['user-agent']).toContain('zeebe-client-nodejs/')

let body = ''
req.on('data', chunk => {
body += chunk
})

req.on('end', () => {
expect(body).toEqual(
'audience=token&client_id=clientId&client_secret=clientSecret&grant_type=client_credentials'
)

res.writeHead(200, { 'Content-Type': 'application/json' })
res.end('{"token": "something"}')
})
})
.listen(3001)
return o.getToken().finally(() => {
o.stopExpiryTimer()
return server.close()
})
})

test('Can set a custom user agent', () => {
process.env.ZEEBE_CLIENT_CUSTOM_AGENT_STRING = 'modeler'
const o = new OAuthProvider({
audience: 'token',
cacheOnDisk: true,
clientId: 'clientId',
clientSecret: 'clientSecret',
url: 'url',
url: 'http://127.0.0.1:3002',
})
const server = http
.createServer((req, res) => {
expect(req.method).toBe('POST')
expect(req.headers['user-agent']).toContain('modeler')

req.on('data', () => {
// ignoring
})

req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end('{"token": "something"}')
})
})
.listen(3002)

return o.getToken().finally(() => {
o.stopExpiryTimer()

delete process.env.ZEEBE_CLIENT_CUSTOM_AGENT_STRING

return server.close()
})
expect(o.userAgentString.includes(' modeler')).toBe(true)
o.stopExpiryTimer()
})

test('Uses form encoding for request', done => {
test('Passes scope, if provided', () => {
const o = new OAuthProvider({
audience: 'token',
scope: 'scope',
cacheOnDisk: false,
clientId: 'clientId',
clientSecret: 'clientSecret',
Expand All @@ -162,21 +219,23 @@ test('Uses form encoding for request', done => {
req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end('{"token": "something"}')
server.close()

expect(body).toEqual(
'audience=token&client_id=clientId&client_secret=clientSecret&grant_type=client_credentials'
'audience=token&client_id=clientId&client_secret=clientSecret&grant_type=client_credentials&scope=scope'
)
done()
})
}
})
.listen(3001)
o.getToken().then(() => o.stopExpiryTimer())

expect(o.userAgentString.includes(' modeler')).toBe(true)
return o.getToken().finally(() => {
o.stopExpiryTimer()

return server.close()
})
})

test('In-memory cache is populated and evicted after timeout', done => {
test('In-memory cache is populated and evicted after timeout', () => {
const delay = timeout =>
new Promise(res => setTimeout(() => res(null), timeout))

Expand All @@ -185,40 +244,40 @@ test('In-memory cache is populated and evicted after timeout', done => {
cacheOnDisk: false,
clientId: 'clientId',
clientSecret: 'clientSecret',
url: 'http://127.0.0.1:3002',
url: 'http://127.0.0.1:3001',
})
const server = http
.createServer((req, res) => {
if (req.method === 'POST') {
let body = ''
req.on('data', chunk => {
body += chunk
})
expect(req.method).toBe('POST')

req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
let expires_in = 2 // seconds
res.end(
'{"access_token": "something", "expires_in": ' +
expires_in +
'}'
)
server.close()
expect(body).toEqual(
'audience=token&client_id=clientId&client_secret=clientSecret&grant_type=client_credentials'
)
})
}
req.on('data', () => {
// ignoring
})

req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'application/json' })
let expires_in = 2 // seconds
res.end(
'{"access_token": "something", "expires_in": ' +
expires_in +
'}'
)
})
})
.listen(3002)
.listen(3001)

o.getToken().then(async _ => {
expect(o.tokenCache['clientId']).toBeDefined()
await delay(500)
expect(o.tokenCache['clientId']).toBeDefined()
await delay(1600)
expect(o.tokenCache['clientId']).not.toBeDefined()
o.stopExpiryTimer()
done()
})
return o
.getToken()
.then(async () => {
expect(o.tokenCache['clientId']).toBeDefined()
await delay(500)
expect(o.tokenCache['clientId']).toBeDefined()
await delay(1600)
expect(o.tokenCache['clientId']).not.toBeDefined()
})
.finally(() => {
o.stopExpiryTimer()

return server.close()
})
})
7 changes: 5 additions & 2 deletions src/lib/ConfigurationHydrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class ConfigurationHydrator {
'ZEEBE_CLIENT_SECRET',
'ZEEBE_SECURE_CONNECTION',
'ZEEBE_TOKEN_AUDIENCE',
'ZEEBE_TOKEN_SCOPE',
'ZEEBE_AUTHORIZATION_SERVER_URL',
'ZEEBE_CAMUNDA_CLOUD_CLUSTER_ID',
'ZEEBE_BASIC_AUTH_PASSWORD',
Expand All @@ -32,7 +33,7 @@ export class ConfigurationHydrator {
'ZEEBE_CLIENT_SSL_ROOT_CERTS_PATH',
'ZEEBE_CLIENT_SSL_PRIVATE_KEY_PATH',
'ZEEBE_CLIENT_SSL_CERT_CHAIN_PATH',
'ZEEBE_TENANT_ID'
'ZEEBE_TENANT_ID',
])

public static configure(
Expand All @@ -52,7 +53,7 @@ export class ConfigurationHydrator {
...ConfigurationHydrator.readTLSFromEnvironment(options),
...ConfigurationHydrator.getEagerStatus(options),
...ConfigurationHydrator.getRetryConfiguration(options),
...ConfigurationHydrator.getTenantId(options)
...ConfigurationHydrator.getTenantId(options),
}

// inherit oAuth custom root certificates, unless
Expand Down Expand Up @@ -128,6 +129,7 @@ export class ConfigurationHydrator {
const clientId = ConfigurationHydrator.getClientIdFromEnv()
const clientSecret = ConfigurationHydrator.getClientSecretFromEnv()
const audience = ConfigurationHydrator.ENV().ZEEBE_TOKEN_AUDIENCE
const scope = ConfigurationHydrator.ENV().ZEEBE_TOKEN_SCOPE
const authServerUrl = ConfigurationHydrator.ENV()
.ZEEBE_AUTHORIZATION_SERVER_URL
const clusterId = ConfigurationHydrator.ENV()
Expand All @@ -144,6 +146,7 @@ export class ConfigurationHydrator {
? {
oAuth: {
audience,
scope,
cacheOnDisk: true,
clientId: clientId!,
clientSecret,
Expand Down
10 changes: 10 additions & 0 deletions src/lib/OAuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface OAuthProviderConfig {
url: string
/** OAuth Audience */
audience: string
/** OAuth Scope */
scope?: string
clientId: string
clientSecret: string
/** Custom TLS certificate for OAuth */
Expand All @@ -39,6 +41,7 @@ export class OAuthProvider {
process.env.ZEEBE_TOKEN_CACHE_DIR || OAuthProvider.defaultTokenCache
public cacheDir: string
public audience: string
public scope?: string
public url: string
public clientId: string
public clientSecret: string
Expand All @@ -56,6 +59,8 @@ export class OAuthProvider {
url,
/** OAuth Audience */
audience,
/** OAuth Scope */
scope,
cacheDir,
clientId,
clientSecret,
Expand All @@ -66,6 +71,7 @@ export class OAuthProvider {
}: {
url: string
audience: string
scope?: string
cacheDir?: string
clientId: string
clientSecret: string
Expand All @@ -74,6 +80,7 @@ export class OAuthProvider {
}) {
this.url = url
this.audience = audience
this.scope = scope
this.clientId = clientId
this.clientSecret = clientSecret
this.customRootCert = customRootCert
Expand Down Expand Up @@ -153,6 +160,9 @@ export class OAuthProvider {
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'client_credentials',
...(
this.scope && { scope: this.scope } || {}
)
}

debug(`Requesting token from token endpoint...`)
Expand Down

0 comments on commit 7fb01c9

Please sign in to comment.