@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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
304
|
+
const setupSuccessfulRefreshResponse = () => {
|
|
191
305
|
const refreshToken = 'test-refresh-token';
|
|
192
|
-
const accessToken = '
|
|
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';
|
package/oclif.manifest.json
CHANGED