@unito/integration-cli 0.62.4 → 0.62.5
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.
|
@@ -2,7 +2,7 @@ import * as openUrl from 'openurl';
|
|
|
2
2
|
import { Oauth2 } from '../configurationTypes';
|
|
3
3
|
import { Environment } from '../resources/globalConfiguration';
|
|
4
4
|
export declare const open: typeof openUrl;
|
|
5
|
-
export declare const HTML_ERROR_MSG
|
|
5
|
+
export declare const HTML_ERROR_MSG: (errorMsg?: string) => string;
|
|
6
6
|
export declare const HTML_SUCCESS_MSG = "<!doctype html><head><title>Unito</title></head><body style=\"text-align:center\"> Redirected to the CLI successfully </body>";
|
|
7
7
|
export interface Oauth2Credentials {
|
|
8
8
|
clientId: string;
|
|
@@ -59,6 +59,7 @@ declare class OAuth2Service {
|
|
|
59
59
|
* @param res The express Response object.
|
|
60
60
|
*/
|
|
61
61
|
private handleCallback;
|
|
62
|
+
private parseOAuth2Response;
|
|
62
63
|
/**
|
|
63
64
|
* Waits for the authorization code to be set.
|
|
64
65
|
* @returns A promise that resolves when the code is set.
|
|
@@ -13,7 +13,8 @@ const errors_1 = require("../errors");
|
|
|
13
13
|
const globalConfiguration_1 = require("../resources/globalConfiguration");
|
|
14
14
|
// It allows to stub openUrl library in the test
|
|
15
15
|
exports.open = openUrl;
|
|
16
|
-
|
|
16
|
+
const HTML_ERROR_MSG = (errorMsg = '') => `<!doctype html><head><title>Unito</title></head><body style="text-align:center"> An unknown error occured. ${errorMsg}</body>`;
|
|
17
|
+
exports.HTML_ERROR_MSG = HTML_ERROR_MSG;
|
|
17
18
|
exports.HTML_SUCCESS_MSG = `<!doctype html><head><title>Unito</title></head><body style="text-align:center"> Redirected to the CLI successfully </body>`;
|
|
18
19
|
const AUTHORIZATION_RESPONSE_QUERY_PARAMS = ['code', 'state', 'error', 'error_description', 'error_uri'];
|
|
19
20
|
class OAuth2Service {
|
|
@@ -97,7 +98,7 @@ class OAuth2Service {
|
|
|
97
98
|
const error = req.query.error;
|
|
98
99
|
if (error) {
|
|
99
100
|
res.setHeader('Content-Type', 'text/html');
|
|
100
|
-
res.send(exports.HTML_ERROR_MSG);
|
|
101
|
+
res.send((0, exports.HTML_ERROR_MSG)(error));
|
|
101
102
|
return;
|
|
102
103
|
}
|
|
103
104
|
// We keep all the non-standard query parameters of the authorization response
|
|
@@ -138,10 +139,7 @@ class OAuth2Service {
|
|
|
138
139
|
body: this.encodeBody(tokenRequestPayload, this.requestContentType),
|
|
139
140
|
method: 'POST',
|
|
140
141
|
});
|
|
141
|
-
|
|
142
|
-
throw new errors_1.FailedToRetrieveAccessTokenError(await tokenResponse.text());
|
|
143
|
-
}
|
|
144
|
-
const response = await tokenResponse.json();
|
|
142
|
+
const response = await this.parseOAuth2Response(tokenResponse);
|
|
145
143
|
this.oauth2Response = {
|
|
146
144
|
accessToken: response.access_token,
|
|
147
145
|
refreshToken: response.refresh_token,
|
|
@@ -150,13 +148,45 @@ class OAuth2Service {
|
|
|
150
148
|
res.send(exports.HTML_SUCCESS_MSG);
|
|
151
149
|
}
|
|
152
150
|
catch (error) {
|
|
151
|
+
const err = error;
|
|
153
152
|
if (!res.headersSent) {
|
|
154
153
|
res.setHeader('Content-Type', 'text/html');
|
|
155
|
-
res.send(exports.HTML_ERROR_MSG);
|
|
154
|
+
res.send((0, exports.HTML_ERROR_MSG)(err?.message));
|
|
156
155
|
}
|
|
157
|
-
throw new errors_1.FailedToRetrieveAccessTokenError(
|
|
156
|
+
throw new errors_1.FailedToRetrieveAccessTokenError(err?.message, err);
|
|
158
157
|
}
|
|
159
158
|
}
|
|
159
|
+
async parseOAuth2Response(response) {
|
|
160
|
+
const contentType = response.headers.get('content-type');
|
|
161
|
+
let responseObj = {};
|
|
162
|
+
// Try JSON first if content-type indicates JSON
|
|
163
|
+
if (contentType?.includes('application/json')) {
|
|
164
|
+
try {
|
|
165
|
+
responseObj = await response.json();
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
// Fallback to text if JSON parse fails
|
|
169
|
+
console.warn('Failed to parse JSON response, falling back to form-urlencoded');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// Handle form-urlencoded
|
|
174
|
+
const text = await response.text();
|
|
175
|
+
try {
|
|
176
|
+
// Try to parse as JSON anyway (some servers send wrong content-type)
|
|
177
|
+
responseObj = JSON.parse(text);
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
// Parse as form-urlencoded
|
|
181
|
+
responseObj = Object.fromEntries(new URLSearchParams(text));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (responseObj.error || response.status !== 200) {
|
|
185
|
+
const errorMsg = responseObj.error_description || responseObj.error;
|
|
186
|
+
throw new errors_1.FailedToRetrieveAccessTokenError(errorMsg, responseObj);
|
|
187
|
+
}
|
|
188
|
+
return responseObj;
|
|
189
|
+
}
|
|
160
190
|
/**
|
|
161
191
|
* Waits for the authorization code to be set.
|
|
162
192
|
* @returns A promise that resolves when the code is set.
|
|
@@ -214,17 +244,15 @@ class OAuth2Service {
|
|
|
214
244
|
body: this.encodeBody(bodyData, this.requestContentType),
|
|
215
245
|
method: 'POST',
|
|
216
246
|
});
|
|
217
|
-
|
|
218
|
-
throw new errors_1.FailedToRetrieveAccessTokenError(await tokenResponse.text());
|
|
219
|
-
}
|
|
220
|
-
const response = await tokenResponse.json();
|
|
247
|
+
const response = await this.parseOAuth2Response(tokenResponse);
|
|
221
248
|
return {
|
|
222
249
|
accessToken: response.access_token,
|
|
223
250
|
refreshToken: response.refresh_token,
|
|
224
251
|
};
|
|
225
252
|
}
|
|
226
253
|
catch (error) {
|
|
227
|
-
|
|
254
|
+
const err = error;
|
|
255
|
+
throw new errors_1.FailedToRetrieveAccessTokenError(err?.message, err);
|
|
228
256
|
}
|
|
229
257
|
}
|
|
230
258
|
/**
|
|
@@ -40,7 +40,12 @@ describe('OAuth2Helper', () => {
|
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
oauth2Helper = new oauth2_1.default(authorizationInfo, globalConfiguration_1.Environment.Production, { foo: 'fooValue', bar: 'barValue' });
|
|
43
|
-
fetchStub = sinon_1.default.stub().resolves({
|
|
43
|
+
fetchStub = sinon_1.default.stub().resolves({
|
|
44
|
+
json: sinon_1.default.stub().resolves({}),
|
|
45
|
+
status: 200,
|
|
46
|
+
text: sinon_1.default.stub().resolves(''),
|
|
47
|
+
headers: new Headers(),
|
|
48
|
+
});
|
|
44
49
|
sinon_1.default.stub(oauth2Helper, 'startServer').resolves('http://localhost:5050');
|
|
45
50
|
sinon_1.default.stub(oauth2Helper, 'stopServer');
|
|
46
51
|
sinon_1.default.replace(global, 'fetch', fetchStub);
|
|
@@ -117,21 +122,68 @@ describe('OAuth2Helper', () => {
|
|
|
117
122
|
(0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
|
|
118
123
|
}
|
|
119
124
|
sinon_1.default.assert.calledOnce(res.send);
|
|
120
|
-
sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG);
|
|
125
|
+
sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG('Failed to retrieve access token'));
|
|
121
126
|
});
|
|
122
127
|
it('handles errors - response', async () => {
|
|
123
128
|
const code = 'test-code';
|
|
124
129
|
const req = { query: { code } };
|
|
125
130
|
const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
|
|
126
|
-
fetchStub.resolves({
|
|
131
|
+
fetchStub.resolves({
|
|
132
|
+
status: 500,
|
|
133
|
+
headers: new Headers({ 'content-type': 'application/x-www-form-urlencoded' }),
|
|
134
|
+
text: sinon_1.default.stub().resolves(''),
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
await oauth2Helper['handleCallback'](req, res);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
(0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
|
|
141
|
+
}
|
|
142
|
+
sinon_1.default.assert.calledOnce(res.send);
|
|
143
|
+
sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG());
|
|
144
|
+
});
|
|
145
|
+
it('handles form-urlencoded error responses', async () => {
|
|
146
|
+
const code = 'test-code';
|
|
147
|
+
const req = { query: { code } };
|
|
148
|
+
const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub(), headersSent: false };
|
|
149
|
+
const errorText = 'error=incorrect_client_credentials&error_description=Invalid+credentials';
|
|
150
|
+
fetchStub.resolves({
|
|
151
|
+
status: 400,
|
|
152
|
+
headers: new Headers({ 'content-type': 'application/x-www-form-urlencoded' }),
|
|
153
|
+
text: sinon_1.default.stub().resolves(errorText),
|
|
154
|
+
});
|
|
127
155
|
try {
|
|
128
156
|
await oauth2Helper['handleCallback'](req, res);
|
|
157
|
+
strict_1.default.fail('Should have thrown an error');
|
|
129
158
|
}
|
|
130
159
|
catch (err) {
|
|
131
160
|
(0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
|
|
161
|
+
(0, strict_1.default)(err.message.includes('Invalid credentials'));
|
|
132
162
|
}
|
|
133
163
|
sinon_1.default.assert.calledOnce(res.send);
|
|
134
|
-
sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG);
|
|
164
|
+
sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG('Invalid credentials'));
|
|
165
|
+
});
|
|
166
|
+
it('handles JSON error responses', async () => {
|
|
167
|
+
const code = 'test-code';
|
|
168
|
+
const req = { query: { code } };
|
|
169
|
+
const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub(), headersSent: false };
|
|
170
|
+
const errorJson = { error: 'invalid_request', error_description: 'Bad request' };
|
|
171
|
+
fetchStub.resolves({
|
|
172
|
+
status: 400,
|
|
173
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
174
|
+
json: sinon_1.default.stub().resolves(errorJson),
|
|
175
|
+
text: sinon_1.default.stub().resolves(JSON.stringify(errorJson)),
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
await oauth2Helper['handleCallback'](req, res);
|
|
179
|
+
strict_1.default.fail('Should have thrown an error');
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
(0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
|
|
183
|
+
(0, strict_1.default)(err.message.includes('Bad request'));
|
|
184
|
+
}
|
|
185
|
+
sinon_1.default.assert.calledOnce(res.send);
|
|
186
|
+
sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG('Bad request'));
|
|
135
187
|
});
|
|
136
188
|
});
|
|
137
189
|
describe('updateToken', () => {
|
|
@@ -139,7 +191,12 @@ describe('OAuth2Helper', () => {
|
|
|
139
191
|
const refreshToken = 'test-refresh-token';
|
|
140
192
|
const accessToken = 'test-access-token';
|
|
141
193
|
const fetchResponse = { access_token: accessToken, refresh_token: refreshToken };
|
|
142
|
-
fetchStub.resolves({
|
|
194
|
+
fetchStub.resolves({
|
|
195
|
+
status: 200,
|
|
196
|
+
json: sinon_1.default.stub().resolves(fetchResponse),
|
|
197
|
+
text: sinon_1.default.stub().resolves(''),
|
|
198
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
199
|
+
});
|
|
143
200
|
const result = await oauth2Helper.updateToken(refreshToken);
|
|
144
201
|
strict_1.default.deepEqual(result, { accessToken, refreshToken });
|
|
145
202
|
sinon_1.default.assert.calledOnce(fetchStub);
|
package/oclif.manifest.json
CHANGED