@unito/integration-cli 0.62.3 → 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,12 +139,7 @@ class OAuth2Service {
|
|
|
138
139
|
body: this.encodeBody(tokenRequestPayload, this.requestContentType),
|
|
139
140
|
method: 'POST',
|
|
140
141
|
});
|
|
141
|
-
|
|
142
|
-
res.setHeader('Content-Type', 'text/html');
|
|
143
|
-
res.send(exports.HTML_ERROR_MSG);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const response = await tokenResponse.json();
|
|
142
|
+
const response = await this.parseOAuth2Response(tokenResponse);
|
|
147
143
|
this.oauth2Response = {
|
|
148
144
|
accessToken: response.access_token,
|
|
149
145
|
refreshToken: response.refresh_token,
|
|
@@ -152,11 +148,44 @@ class OAuth2Service {
|
|
|
152
148
|
res.send(exports.HTML_SUCCESS_MSG);
|
|
153
149
|
}
|
|
154
150
|
catch (error) {
|
|
151
|
+
const err = error;
|
|
155
152
|
if (!res.headersSent) {
|
|
156
153
|
res.setHeader('Content-Type', 'text/html');
|
|
157
|
-
res.send(exports.HTML_ERROR_MSG);
|
|
154
|
+
res.send((0, exports.HTML_ERROR_MSG)(err?.message));
|
|
155
|
+
}
|
|
156
|
+
throw new errors_1.FailedToRetrieveAccessTokenError(err?.message, err);
|
|
157
|
+
}
|
|
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');
|
|
158
170
|
}
|
|
159
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;
|
|
160
189
|
}
|
|
161
190
|
/**
|
|
162
191
|
* Waits for the authorization code to be set.
|
|
@@ -215,17 +244,15 @@ class OAuth2Service {
|
|
|
215
244
|
body: this.encodeBody(bodyData, this.requestContentType),
|
|
216
245
|
method: 'POST',
|
|
217
246
|
});
|
|
218
|
-
|
|
219
|
-
throw new errors_1.FailedToRetrieveAccessTokenError(await tokenResponse.text());
|
|
220
|
-
}
|
|
221
|
-
const response = await tokenResponse.json();
|
|
247
|
+
const response = await this.parseOAuth2Response(tokenResponse);
|
|
222
248
|
return {
|
|
223
249
|
accessToken: response.access_token,
|
|
224
250
|
refreshToken: response.refresh_token,
|
|
225
251
|
};
|
|
226
252
|
}
|
|
227
253
|
catch (error) {
|
|
228
|
-
|
|
254
|
+
const err = error;
|
|
255
|
+
throw new errors_1.FailedToRetrieveAccessTokenError(err?.message, err);
|
|
229
256
|
}
|
|
230
257
|
}
|
|
231
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);
|
|
@@ -110,18 +115,75 @@ describe('OAuth2Helper', () => {
|
|
|
110
115
|
const req = { query: { code } };
|
|
111
116
|
const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
|
|
112
117
|
fetchStub.rejects(new Error('Failed to retrieve access token'));
|
|
113
|
-
|
|
118
|
+
try {
|
|
119
|
+
await oauth2Helper['handleCallback'](req, res);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
(0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
|
|
123
|
+
}
|
|
114
124
|
sinon_1.default.assert.calledOnce(res.send);
|
|
115
|
-
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'));
|
|
116
126
|
});
|
|
117
127
|
it('handles errors - response', async () => {
|
|
118
128
|
const code = 'test-code';
|
|
119
129
|
const req = { query: { code } };
|
|
120
130
|
const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
|
|
121
|
-
fetchStub.resolves({
|
|
122
|
-
|
|
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
|
+
}
|
|
123
142
|
sinon_1.default.assert.calledOnce(res.send);
|
|
124
|
-
sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG);
|
|
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
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
await oauth2Helper['handleCallback'](req, res);
|
|
157
|
+
strict_1.default.fail('Should have thrown an error');
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
(0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
|
|
161
|
+
(0, strict_1.default)(err.message.includes('Invalid credentials'));
|
|
162
|
+
}
|
|
163
|
+
sinon_1.default.assert.calledOnce(res.send);
|
|
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'));
|
|
125
187
|
});
|
|
126
188
|
});
|
|
127
189
|
describe('updateToken', () => {
|
|
@@ -129,7 +191,12 @@ describe('OAuth2Helper', () => {
|
|
|
129
191
|
const refreshToken = 'test-refresh-token';
|
|
130
192
|
const accessToken = 'test-access-token';
|
|
131
193
|
const fetchResponse = { access_token: accessToken, refresh_token: refreshToken };
|
|
132
|
-
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
|
+
});
|
|
133
200
|
const result = await oauth2Helper.updateToken(refreshToken);
|
|
134
201
|
strict_1.default.deepEqual(result, { accessToken, refreshToken });
|
|
135
202
|
sinon_1.default.assert.calledOnce(fetchStub);
|
package/oclif.manifest.json
CHANGED