@unito/integration-cli 0.63.2 → 0.63.4
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.
- package/dist/src/commands/oauth2.js +1 -1
- package/dist/src/resources/oauth2.d.ts +1 -1
- package/dist/src/resources/oauth2.js +2 -2
- package/dist/src/services/oauth2.js +16 -2
- package/dist/test/commands/oauth2.test.js +66 -0
- package/dist/test/resources/oauth2.test.js +28 -0
- package/dist/test/services/oauth2.test.js +248 -3
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -102,7 +102,7 @@ class Oauth2 extends core_1.Command {
|
|
|
102
102
|
}
|
|
103
103
|
const refreshToken = decryptionResult?.successful.refreshToken;
|
|
104
104
|
core_1.ux.action.start(`Refreshing test account ${testAccount}`, undefined, { stdout: true });
|
|
105
|
-
credentials = await Oauth2Resource.updateToken(oauth2, refreshToken);
|
|
105
|
+
credentials = await Oauth2Resource.updateToken(oauth2, refreshToken, decryptionResult?.successful, environment);
|
|
106
106
|
// If provider response doesn't contain a new refresh token, use the existing one
|
|
107
107
|
credentials.refreshToken = credentials.refreshToken ?? refreshToken;
|
|
108
108
|
core_1.ux.action.stop();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Oauth2Response, Oauth2Payload } from '../services/oauth2';
|
|
2
2
|
import { Environment } from './globalConfiguration';
|
|
3
3
|
export declare function performOAuth2Flow(applicationCredentials: Oauth2Payload, environment?: Environment, credentialPayload?: Record<string, unknown>): Promise<Oauth2Response>;
|
|
4
|
-
export declare function updateToken(applicationCredentials: Oauth2Payload, refreshToken: string): Promise<Oauth2Response>;
|
|
4
|
+
export declare function updateToken(applicationCredentials: Oauth2Payload, refreshToken: string, credentialPayload?: Record<string, unknown>, environment?: Environment): Promise<Oauth2Response>;
|
|
@@ -22,7 +22,7 @@ async function performOAuth2Flow(applicationCredentials, environment = globalCon
|
|
|
22
22
|
}
|
|
23
23
|
return oauth2Response;
|
|
24
24
|
}
|
|
25
|
-
async function updateToken(applicationCredentials, refreshToken) {
|
|
26
|
-
const oauthHelper = new oauth2_1.default(applicationCredentials);
|
|
25
|
+
async function updateToken(applicationCredentials, refreshToken, credentialPayload, environment = globalConfiguration_1.Environment.Production) {
|
|
26
|
+
const oauthHelper = new oauth2_1.default(applicationCredentials, environment, credentialPayload);
|
|
27
27
|
return oauthHelper.updateToken(refreshToken);
|
|
28
28
|
}
|
|
@@ -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
|
-
const tokenResponse = await fetch(this.tokenUrl, {
|
|
253
|
+
const tokenResponse = await fetch((0, template_1.expandTemplate)(this.tokenUrl, templateVariables, { urlEncodeVariables: true }), {
|
|
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',
|
|
@@ -303,4 +303,70 @@ describe('oauth2', () => {
|
|
|
303
303
|
.it('must successfully decrypt all secrets', ctx => {
|
|
304
304
|
(0, test_1.expect)(ctx.stderr).to.contain((0, styles_1.uncolorize)('The secret boom! could not be decrypted.'));
|
|
305
305
|
});
|
|
306
|
+
test_1.test
|
|
307
|
+
.stdout()
|
|
308
|
+
.do(() => {
|
|
309
|
+
getConfigurationsStub.returns({
|
|
310
|
+
...baseConfiguration,
|
|
311
|
+
testAccounts: {
|
|
312
|
+
development: credentials,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
sinon
|
|
316
|
+
.stub(IntegrationsPlatform, 'encryptData')
|
|
317
|
+
.onFirstCall()
|
|
318
|
+
.resolves({ encryptedData: `${Configuration.ENCRYPTION_PREFIX}encryptedAccessToken` })
|
|
319
|
+
.onSecondCall()
|
|
320
|
+
.resolves({ encryptedData: `${Configuration.ENCRYPTION_PREFIX}encryptedRefreshToken` });
|
|
321
|
+
})
|
|
322
|
+
.stub(DecryptionResource, 'decryptEntries', stub => stub
|
|
323
|
+
.onFirstCall()
|
|
324
|
+
.resolves({ successful: { ...oauth2Information }, failed: [] })
|
|
325
|
+
.onSecondCall()
|
|
326
|
+
.resolves({ successful: { refreshToken: 'test-refresh-token' }, failed: [] }))
|
|
327
|
+
.command(['oauth2', '--test-account', 'development', '--environment', 'staging'])
|
|
328
|
+
.it('passes the staging environment to updateToken when specified', () => {
|
|
329
|
+
(0, test_1.expect)(performOAuth2FlowStub.getCalls().length).to.equal(0);
|
|
330
|
+
(0, test_1.expect)(updateTokenStub.getCalls().length).to.equal(1);
|
|
331
|
+
// Verify that updateToken was called with the correct environment parameter
|
|
332
|
+
const updateTokenCall = updateTokenStub.getCall(0);
|
|
333
|
+
(0, test_1.expect)(updateTokenCall.args).to.have.lengthOf(4);
|
|
334
|
+
(0, test_1.expect)(updateTokenCall.args[0]).to.deep.equal(oauth2Information); // oauth2 config
|
|
335
|
+
(0, test_1.expect)(updateTokenCall.args[1]).to.equal('test-refresh-token'); // refresh token
|
|
336
|
+
(0, test_1.expect)(updateTokenCall.args[2]).to.deep.equal({ refreshToken: 'test-refresh-token' }); // credential payload
|
|
337
|
+
(0, test_1.expect)(updateTokenCall.args[3]).to.equal('staging'); // environment
|
|
338
|
+
});
|
|
339
|
+
test_1.test
|
|
340
|
+
.stdout()
|
|
341
|
+
.do(() => {
|
|
342
|
+
getConfigurationsStub.returns({
|
|
343
|
+
...baseConfiguration,
|
|
344
|
+
testAccounts: {
|
|
345
|
+
development: credentials,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
sinon
|
|
349
|
+
.stub(IntegrationsPlatform, 'encryptData')
|
|
350
|
+
.onFirstCall()
|
|
351
|
+
.resolves({ encryptedData: `${Configuration.ENCRYPTION_PREFIX}encryptedAccessToken` })
|
|
352
|
+
.onSecondCall()
|
|
353
|
+
.resolves({ encryptedData: `${Configuration.ENCRYPTION_PREFIX}encryptedRefreshToken` });
|
|
354
|
+
})
|
|
355
|
+
.stub(DecryptionResource, 'decryptEntries', stub => stub
|
|
356
|
+
.onFirstCall()
|
|
357
|
+
.resolves({ successful: { ...oauth2Information }, failed: [] })
|
|
358
|
+
.onSecondCall()
|
|
359
|
+
.resolves({ successful: { refreshToken: 'test-refresh-token' }, failed: [] }))
|
|
360
|
+
.command(['oauth2', '--test-account', 'development'])
|
|
361
|
+
.it('defaults to production environment when no environment flag is specified', () => {
|
|
362
|
+
(0, test_1.expect)(performOAuth2FlowStub.getCalls().length).to.equal(0);
|
|
363
|
+
(0, test_1.expect)(updateTokenStub.getCalls().length).to.equal(1);
|
|
364
|
+
// Verify that updateToken was called with the default production environment
|
|
365
|
+
const updateTokenCall = updateTokenStub.getCall(0);
|
|
366
|
+
(0, test_1.expect)(updateTokenCall.args).to.have.lengthOf(4);
|
|
367
|
+
(0, test_1.expect)(updateTokenCall.args[0]).to.deep.equal(oauth2Information); // oauth2 config
|
|
368
|
+
(0, test_1.expect)(updateTokenCall.args[1]).to.equal('test-refresh-token'); // refresh token
|
|
369
|
+
(0, test_1.expect)(updateTokenCall.args[2]).to.deep.equal({ refreshToken: 'test-refresh-token' }); // credential payload
|
|
370
|
+
(0, test_1.expect)(updateTokenCall.args[3]).to.equal('production'); // environment (default)
|
|
371
|
+
});
|
|
306
372
|
});
|
|
@@ -52,4 +52,32 @@ describe('OAuth2Helper', () => {
|
|
|
52
52
|
await strict_1.default.rejects(response, errors_1.FailedToRetrieveAccessTokenError);
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
+
describe('updateToken', () => {
|
|
56
|
+
let updateTokenStub;
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
updateTokenStub = sinon_1.default.stub(oauth2_1.default.prototype, 'updateToken');
|
|
59
|
+
});
|
|
60
|
+
it('should create OAuth2Service with credential payload and call updateToken', async () => {
|
|
61
|
+
const mockResponse = { accessToken: 'new-token', refreshToken: 'new-refresh-token' };
|
|
62
|
+
updateTokenStub.resolves(mockResponse);
|
|
63
|
+
const credentialPayload = { domain: 'test-domain', customVar: 'test-value' };
|
|
64
|
+
const refreshToken = 'test-refresh-token';
|
|
65
|
+
const result = await oauth2HelperResource.updateToken(authorizationInfo, refreshToken, credentialPayload);
|
|
66
|
+
// Assert that the OAuth2Service was instantiated with the credential payload
|
|
67
|
+
// and that updateToken was called with the refresh token
|
|
68
|
+
sinon_1.default.assert.calledOnce(updateTokenStub);
|
|
69
|
+
sinon_1.default.assert.calledWith(updateTokenStub, refreshToken);
|
|
70
|
+
strict_1.default.deepEqual(result, mockResponse);
|
|
71
|
+
});
|
|
72
|
+
it('should work without credential payload', async () => {
|
|
73
|
+
const mockResponse = { accessToken: 'new-token', refreshToken: 'new-refresh-token' };
|
|
74
|
+
updateTokenStub.resolves(mockResponse);
|
|
75
|
+
const refreshToken = 'test-refresh-token';
|
|
76
|
+
const result = await oauth2HelperResource.updateToken(authorizationInfo, refreshToken);
|
|
77
|
+
// Should still work when no credential payload is provided
|
|
78
|
+
sinon_1.default.assert.calledOnce(updateTokenStub);
|
|
79
|
+
sinon_1.default.assert.calledWith(updateTokenStub, refreshToken);
|
|
80
|
+
strict_1.default.deepEqual(result, mockResponse);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
55
83
|
});
|
|
@@ -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';
|
|
@@ -214,6 +422,43 @@ describe('OAuth2Helper', () => {
|
|
|
214
422
|
const response = oauth2Helper.updateToken(refreshToken);
|
|
215
423
|
await strict_1.default.rejects(response, errors_1.InvalidRequestContentTypeError);
|
|
216
424
|
});
|
|
425
|
+
describe('token URL template expansion', () => {
|
|
426
|
+
it('expands template variables in token URL during refresh', async () => {
|
|
427
|
+
const credentialPayload = { domain: 'test-domain' };
|
|
428
|
+
oauth2Helper = new oauth2_1.default({
|
|
429
|
+
clientId: 'test-client-id',
|
|
430
|
+
clientSecret: 'test-client-secret',
|
|
431
|
+
authorizationUrl: 'https://provider.com/oauth/authorize',
|
|
432
|
+
scopes: [{ name: 'scope1' }],
|
|
433
|
+
tokenUrl: 'https://{+domain}.provider.com/oauth/token', // Template URL
|
|
434
|
+
grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
|
|
435
|
+
requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
|
|
436
|
+
}, globalConfiguration_1.Environment.Production, credentialPayload);
|
|
437
|
+
const { refreshToken } = setupSuccessfulRefreshResponse();
|
|
438
|
+
await oauth2Helper.updateToken(refreshToken);
|
|
439
|
+
// Assert that the token URL was expanded with the domain
|
|
440
|
+
sinon_1.default.assert.calledWith(fetchStub, 'https://test-domain.provider.com/oauth/token', sinon_1.default.match.object);
|
|
441
|
+
});
|
|
442
|
+
it('expands multiple template variables in token URL', async () => {
|
|
443
|
+
const credentialPayload = {
|
|
444
|
+
domain: 'my-company',
|
|
445
|
+
environment: 'staging',
|
|
446
|
+
};
|
|
447
|
+
oauth2Helper = new oauth2_1.default({
|
|
448
|
+
clientId: 'test-client-id',
|
|
449
|
+
clientSecret: 'test-client-secret',
|
|
450
|
+
authorizationUrl: 'https://provider.com/oauth/authorize',
|
|
451
|
+
scopes: [{ name: 'scope1' }],
|
|
452
|
+
tokenUrl: 'https://{+domain}.{+environment}.provider.com/oauth/token',
|
|
453
|
+
grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
|
|
454
|
+
requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
|
|
455
|
+
}, globalConfiguration_1.Environment.Production, credentialPayload);
|
|
456
|
+
const { refreshToken } = setupSuccessfulRefreshResponse();
|
|
457
|
+
await oauth2Helper.updateToken(refreshToken);
|
|
458
|
+
// Assert that both template variables were expanded
|
|
459
|
+
sinon_1.default.assert.calledWith(fetchStub, 'https://my-company.staging.provider.com/oauth/token', sinon_1.default.match.object);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
217
462
|
});
|
|
218
463
|
describe('encodeBody', () => {
|
|
219
464
|
it('encodes body data as URL-encoded when content type is URL_ENCODED', () => {
|
package/oclif.manifest.json
CHANGED