@unito/integration-cli 0.63.2 → 0.63.3

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.
@@ -110,6 +110,8 @@ class OAuth2Service {
110
110
  }
111
111
  }
112
112
  const templateVariables = { ...(this.credentialPayload ?? {}), ...authorizationResponseVariables };
113
+ templateVariables.clientId ??= this.clientId;
114
+ templateVariables.clientSecret ??= this.clientSecret;
113
115
  const tokenRequestPayload = {
114
116
  ...Object.fromEntries(Object.entries(this.tokenRequestParameters?.body ?? {}).map(([key, value]) => [
115
117
  key,
@@ -235,11 +237,23 @@ class OAuth2Service {
235
237
  bodyData.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
236
238
  bodyData.redirect_uri = `${IntegrationsPlatformClient.Servers[this.environment]}/credentials/new/oauth2/callback-cli`;
237
239
  }
240
+ const templateVariables = structuredClone(this.credentialPayload ?? {});
241
+ templateVariables.clientId ??= this.clientId;
242
+ templateVariables.clientSecret ??= this.clientSecret;
243
+ const tokenRequestHeaders = {
244
+ 'Content-Type': this.requestContentType,
245
+ ...(this.refreshRequestParameters?.header ?? {}),
246
+ };
247
+ for (const [headerKey, headerValue] of Object.entries(this.tokenRequestParameters?.header ?? {})) {
248
+ tokenRequestHeaders[headerKey] = (0, template_1.expandTemplate)(String(headerValue), templateVariables, {
249
+ urlEncodeVariables: false,
250
+ });
251
+ }
238
252
  try {
239
253
  const tokenResponse = await fetch(this.tokenUrl, {
240
254
  headers: {
241
255
  'Content-Type': this.requestContentType,
242
- ...(this.refreshRequestParameters?.header ?? {}),
256
+ ...(tokenRequestHeaders ?? {}),
243
257
  },
244
258
  body: this.encodeBody(bodyData, this.requestContentType),
245
259
  method: 'POST',
@@ -142,7 +142,121 @@ describe('OAuth2Helper', () => {
142
142
  sinon_1.default.assert.calledOnce(res.send);
143
143
  sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG());
144
144
  });
145
- it('handles form-urlencoded error responses', async () => {
145
+ describe('template variables for clientId and clientSecret', () => {
146
+ const setupHelperWithStubs = (authorizationInfo, credentialPayload) => {
147
+ oauth2Helper = new oauth2_1.default(authorizationInfo, globalConfiguration_1.Environment.Production, credentialPayload);
148
+ sinon_1.default.stub(oauth2Helper, 'startServer').resolves('http://localhost:5050');
149
+ sinon_1.default.stub(oauth2Helper, 'stopServer');
150
+ };
151
+ afterEach(() => {
152
+ sinon_1.default.restore();
153
+ });
154
+ const performCallback = async () => {
155
+ const req = { query: { code: 'test-code' } };
156
+ const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
157
+ await oauth2Helper['handleCallback'](req, res);
158
+ return { req, res };
159
+ };
160
+ it('expands clientId and clientSecret in headers', async () => {
161
+ // What matters: Template variables {+clientId} and {+clientSecret} in headers
162
+ // should be replaced with actual service-level client credentials
163
+ setupHelperWithStubs({
164
+ clientId: 'my-client-id',
165
+ clientSecret: 'my-client-secret',
166
+ authorizationUrl: 'https://provider.com/oauth/authorize',
167
+ scopes: [{ name: 'scope1' }],
168
+ tokenUrl: 'https://provider.com/oauth/token',
169
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
170
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
171
+ tokenRequestParameters: {
172
+ header: { 'X-Client-ID': '{+clientId}', 'X-Client-Secret': '{+clientSecret}' },
173
+ },
174
+ });
175
+ await performCallback();
176
+ // What matters: The fetch call should have expanded the template variables in headers
177
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
178
+ headers: {
179
+ 'Content-Type': 'application/x-www-form-urlencoded',
180
+ 'X-Client-ID': 'my-client-id', // Template {+clientId} expanded
181
+ 'X-Client-Secret': 'my-client-secret', // Template {+clientSecret} expanded
182
+ },
183
+ body: sinon_1.default.match.string,
184
+ method: 'POST',
185
+ });
186
+ });
187
+ it('expands clientId and clientSecret in body', async () => {
188
+ // What matters: Template variables in request body should be expanded
189
+ // while preserving other static parameters
190
+ setupHelperWithStubs({
191
+ clientId: 'my-client-id',
192
+ clientSecret: 'my-client-secret',
193
+ authorizationUrl: 'https://provider.com/oauth/authorize',
194
+ scopes: [{ name: 'scope1' }],
195
+ tokenUrl: 'https://provider.com/oauth/token',
196
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
197
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
198
+ tokenRequestParameters: {
199
+ body: {
200
+ custom_client_id: '{+clientId}',
201
+ custom_client_secret: '{+clientSecret}',
202
+ additional_param: 'static_value',
203
+ },
204
+ },
205
+ });
206
+ await performCallback();
207
+ const requestBody = fetchStub.getCall(0).args[1].body;
208
+ const bodyParams = new URLSearchParams(requestBody);
209
+ // What matters: Template variables in body are expanded to actual values
210
+ strict_1.default.equal(bodyParams.get('custom_client_id'), 'my-client-id');
211
+ strict_1.default.equal(bodyParams.get('custom_client_secret'), 'my-client-secret');
212
+ // What matters: Static parameters are preserved unchanged
213
+ strict_1.default.equal(bodyParams.get('additional_param'), 'static_value');
214
+ });
215
+ it('expands clientId and clientSecret in URL', async () => {
216
+ // What matters: Template variables in token URL should be expanded
217
+ // and properly URL-encoded as query parameters
218
+ setupHelperWithStubs({
219
+ clientId: 'my-client-id',
220
+ clientSecret: 'my-client-secret',
221
+ authorizationUrl: 'https://provider.com/oauth/authorize',
222
+ scopes: [{ name: 'scope1' }],
223
+ tokenUrl: 'https://provider.com/oauth/token?client_id={+clientId}&secret={+clientSecret}',
224
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
225
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
226
+ });
227
+ await performCallback();
228
+ // What matters: The URL template variables are expanded in the fetch call
229
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token?client_id=my-client-id&secret=my-client-secret', sinon_1.default.match.object);
230
+ });
231
+ it('allows credential payload to override service values', async () => {
232
+ // What matters: Credential payload values should take precedence over
233
+ // service-level clientId/clientSecret when expanding templates
234
+ setupHelperWithStubs({
235
+ clientId: 'default-client-id', // Service-level values
236
+ clientSecret: 'default-client-secret',
237
+ authorizationUrl: 'https://provider.com/oauth/authorize',
238
+ scopes: [{ name: 'scope1' }],
239
+ tokenUrl: 'https://provider.com/oauth/token',
240
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
241
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
242
+ tokenRequestParameters: {
243
+ header: { 'X-Client-ID': '{+clientId}', 'X-Client-Secret': '{+clientSecret}' },
244
+ },
245
+ }, { clientId: 'override-client-id', clientSecret: 'override-client-secret' });
246
+ await performCallback();
247
+ // What matters: Template expansion uses the credential payload override values
248
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
249
+ headers: {
250
+ 'Content-Type': 'application/x-www-form-urlencoded',
251
+ 'X-Client-ID': 'override-client-id', // Uses credential payload value
252
+ 'X-Client-Secret': 'override-client-secret', // Uses credential payload value
253
+ },
254
+ body: sinon_1.default.match.string,
255
+ method: 'POST',
256
+ });
257
+ });
258
+ });
259
+ it('form-urlencoded error responses', async () => {
146
260
  const code = 'test-code';
147
261
  const req = { query: { code } };
148
262
  const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub(), headersSent: false };
@@ -187,9 +301,9 @@ describe('OAuth2Helper', () => {
187
301
  });
188
302
  });
189
303
  describe('updateToken', () => {
190
- it('retrieves the access token when a refresh token is available', async () => {
304
+ const setupSuccessfulRefreshResponse = () => {
191
305
  const refreshToken = 'test-refresh-token';
192
- const accessToken = 'test-access-token';
306
+ const accessToken = 'new-access-token';
193
307
  const fetchResponse = { access_token: accessToken, refresh_token: refreshToken };
194
308
  fetchStub.resolves({
195
309
  status: 200,
@@ -197,9 +311,103 @@ describe('OAuth2Helper', () => {
197
311
  text: sinon_1.default.stub().resolves(''),
198
312
  headers: new Headers({ 'content-type': 'application/json' }),
199
313
  });
314
+ return { refreshToken, accessToken };
315
+ };
316
+ it('retrieves the access token when a refresh token is available', async () => {
317
+ const { refreshToken, accessToken } = setupSuccessfulRefreshResponse();
200
318
  const result = await oauth2Helper.updateToken(refreshToken);
201
319
  strict_1.default.deepEqual(result, { accessToken, refreshToken });
202
320
  sinon_1.default.assert.calledOnce(fetchStub);
321
+ // Assert that the refresh token is actually sent in the request body
322
+ const fetchCall = fetchStub.getCall(0);
323
+ const requestBody = fetchCall.args[1].body;
324
+ const bodyParams = new URLSearchParams(requestBody);
325
+ strict_1.default.equal(bodyParams.get('refresh_token'), refreshToken);
326
+ strict_1.default.equal(bodyParams.get('grant_type'), 'refresh_token');
327
+ });
328
+ describe('template variables for clientId and clientSecret', () => {
329
+ const createHelperWithTemplateHeaders = (clientId, clientSecret, credentialPayload) => {
330
+ return new oauth2_1.default({
331
+ clientId,
332
+ clientSecret,
333
+ authorizationUrl: 'https://provider.com/oauth/authorize',
334
+ scopes: [{ name: 'scope1' }],
335
+ tokenUrl: 'https://provider.com/oauth/token',
336
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
337
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
338
+ tokenRequestParameters: {
339
+ header: { 'X-Client-ID': '{+clientId}', 'X-Client-Secret': '{+clientSecret}' },
340
+ },
341
+ }, globalConfiguration_1.Environment.Production, credentialPayload);
342
+ };
343
+ it('expands template variables in headers', async () => {
344
+ // What matters: Template variables in tokenRequestParameters headers
345
+ // should be expanded during token refresh operations
346
+ oauth2Helper = createHelperWithTemplateHeaders('refresh-client-id', 'refresh-client-secret');
347
+ const { refreshToken } = setupSuccessfulRefreshResponse();
348
+ await oauth2Helper.updateToken(refreshToken);
349
+ // What matters: Fetch call for refresh token includes expanded template variables
350
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
351
+ headers: {
352
+ 'Content-Type': 'application/x-www-form-urlencoded',
353
+ 'X-Client-ID': 'refresh-client-id', // Template expanded
354
+ 'X-Client-Secret': 'refresh-client-secret', // Template expanded
355
+ },
356
+ body: sinon_1.default.match.string,
357
+ method: 'POST',
358
+ });
359
+ });
360
+ it('allows credential payload override', async () => {
361
+ // What matters: During token refresh, credential payload values should
362
+ // override service-level values for template expansion
363
+ oauth2Helper = createHelperWithTemplateHeaders('default-id', 'default-secret', {
364
+ clientId: 'override-refresh-client-id', // Override values
365
+ clientSecret: 'override-refresh-client-secret',
366
+ });
367
+ const { refreshToken } = setupSuccessfulRefreshResponse();
368
+ await oauth2Helper.updateToken(refreshToken);
369
+ // What matters: Template expansion uses credential payload override values for refresh
370
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
371
+ headers: {
372
+ 'Content-Type': 'application/x-www-form-urlencoded',
373
+ 'X-Client-ID': 'override-refresh-client-id', // Uses override value
374
+ 'X-Client-Secret': 'override-refresh-client-secret', // Uses override value
375
+ },
376
+ body: sinon_1.default.match.string,
377
+ method: 'POST',
378
+ });
379
+ });
380
+ });
381
+ it('includes refreshRequestParameters headers in token request', async () => {
382
+ oauth2Helper = new oauth2_1.default({
383
+ clientId: 'test-client-id',
384
+ clientSecret: 'test-client-secret',
385
+ authorizationUrl: 'https://provider.com/oauth/authorize',
386
+ scopes: [{ name: 'scope1' }],
387
+ tokenUrl: 'https://provider.com/oauth/token',
388
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
389
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
390
+ tokenRequestParameters: {
391
+ header: { 'X-Token-Header': 'token-value' },
392
+ },
393
+ refreshRequestParameters: {
394
+ header: { 'X-Refresh-Header': 'refresh-value', 'X-Special-Auth': 'special-token' },
395
+ },
396
+ }, globalConfiguration_1.Environment.Production);
397
+ const { refreshToken } = setupSuccessfulRefreshResponse();
398
+ await oauth2Helper.updateToken(refreshToken);
399
+ // What matters: Both tokenRequestParameters and refreshRequestParameters headers
400
+ // should be present in the token refresh request
401
+ sinon_1.default.assert.calledWith(fetchStub, 'https://provider.com/oauth/token', {
402
+ headers: {
403
+ 'Content-Type': 'application/x-www-form-urlencoded',
404
+ 'X-Token-Header': 'token-value', // From tokenRequestParameters.header
405
+ 'X-Refresh-Header': 'refresh-value', // From refreshRequestParameters.header
406
+ 'X-Special-Auth': 'special-token', // From refreshRequestParameters.header
407
+ },
408
+ body: sinon_1.default.match.string,
409
+ method: 'POST',
410
+ });
203
411
  });
204
412
  it('throws an error when failing to refresh the access token', async () => {
205
413
  const refreshToken = 'test-refresh-token';
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.63.2",
2
+ "version": "0.63.3",
3
3
  "commands": {
4
4
  "activity": {
5
5
  "id": "activity",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-cli",
3
- "version": "0.63.2",
3
+ "version": "0.63.3",
4
4
  "description": "Integration CLI",
5
5
  "bin": {
6
6
  "integration-cli": "./bin/run"