@mcp-abap-adt/auth-providers 1.0.5 → 1.2.0
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/CHANGELOG.md +44 -0
- package/README.md +38 -3
- package/dist/auth/browserAuth.d.ts +19 -8
- package/dist/auth/browserAuth.d.ts.map +1 -1
- package/dist/auth/browserAuth.js +190 -466
- package/dist/auth/callbackServer.d.ts +40 -0
- package/dist/auth/callbackServer.d.ts.map +1 -0
- package/dist/auth/callbackServer.js +322 -0
- package/dist/auth/oidcBrowserAuth.d.ts +10 -2
- package/dist/auth/oidcBrowserAuth.d.ts.map +1 -1
- package/dist/auth/oidcBrowserAuth.js +37 -37
- package/dist/auth/saml2Auth.d.ts +8 -1
- package/dist/auth/saml2Auth.d.ts.map +1 -1
- package/dist/auth/saml2Auth.js +42 -42
- package/dist/providers/AuthorizationCodeProvider.d.ts.map +1 -1
- package/dist/providers/AuthorizationCodeProvider.js +11 -12
- package/package.json +4 -4
package/dist/auth/browserAuth.js
CHANGED
|
@@ -39,13 +39,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
39
39
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
40
|
};
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.extractCode = extractCode;
|
|
42
43
|
exports.exchangeCodeForToken = exchangeCodeForToken;
|
|
43
44
|
exports.startBrowserAuth = startBrowserAuth;
|
|
44
45
|
const child_process = __importStar(require("node:child_process"));
|
|
45
46
|
const http = __importStar(require("node:http"));
|
|
46
47
|
const net = __importStar(require("node:net"));
|
|
48
|
+
const readline = __importStar(require("node:readline"));
|
|
47
49
|
const axios_1 = __importDefault(require("axios"));
|
|
48
|
-
const
|
|
50
|
+
const callbackServer_1 = require("./callbackServer");
|
|
49
51
|
const BROWSER_MAP = {
|
|
50
52
|
chrome: 'chrome',
|
|
51
53
|
edge: 'msedge',
|
|
@@ -55,6 +57,37 @@ const BROWSER_MAP = {
|
|
|
55
57
|
headless: null, // no browser, log URL and wait for callback (SSH/remote)
|
|
56
58
|
none: null, // no browser, log URL and wait for callback (same as headless)
|
|
57
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* Extract an OAuth2 authorization code from arbitrary pasted input.
|
|
62
|
+
*
|
|
63
|
+
* Accepts:
|
|
64
|
+
* - a bare code: `abc123`
|
|
65
|
+
* - `code=abc123`
|
|
66
|
+
* - a full redirected URL: `http://localhost:7779/callback?code=abc123&state=...`
|
|
67
|
+
*
|
|
68
|
+
* Returns the decoded code, or null if nothing usable was found.
|
|
69
|
+
* @internal - Exported for testing and for manual-paste flows.
|
|
70
|
+
*/
|
|
71
|
+
function extractCode(input) {
|
|
72
|
+
if (!input)
|
|
73
|
+
return null;
|
|
74
|
+
const trimmed = input.trim();
|
|
75
|
+
if (!trimmed)
|
|
76
|
+
return null;
|
|
77
|
+
// Anywhere a `code=` query parameter appears (full URL or query string)
|
|
78
|
+
const fromQuery = trimmed.match(/[?&]code=([^&\s]+)/);
|
|
79
|
+
if (fromQuery)
|
|
80
|
+
return decodeURIComponent(fromQuery[1]);
|
|
81
|
+
// Bare `code=XYZ`
|
|
82
|
+
const bareKv = trimmed.match(/^code=([^&\s]+)$/);
|
|
83
|
+
if (bareKv)
|
|
84
|
+
return decodeURIComponent(bareKv[1]);
|
|
85
|
+
// Otherwise treat the whole token as the code, but reject anything with
|
|
86
|
+
// whitespace (clearly not a single code).
|
|
87
|
+
if (/\s/.test(trimmed))
|
|
88
|
+
return null;
|
|
89
|
+
return trimmed;
|
|
90
|
+
}
|
|
58
91
|
/**
|
|
59
92
|
* Get OAuth2 authorization URL
|
|
60
93
|
*/
|
|
@@ -135,482 +168,173 @@ function isPortAvailable(port) {
|
|
|
135
168
|
* @param port Port for OAuth callback server (default: 3001)
|
|
136
169
|
* @returns Promise that resolves to tokens
|
|
137
170
|
* @internal - Internal function, not exported from package
|
|
138
|
-
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
171
|
+
*/ /**
|
|
172
|
+
* Open the authorization URL, or tell the user how to do it.
|
|
173
|
+
*
|
|
174
|
+
* Never awaited on the critical path by the caller: a launcher that hangs must
|
|
175
|
+
* not delay the login timeout or the release of the port. A launcher that fails
|
|
176
|
+
* is reported through the scope's `fail`, which is just another way for the
|
|
177
|
+
* scope to end.
|
|
178
|
+
*/
|
|
179
|
+
async function launchBrowser(authorizationUrl, browser, port, announce, log) {
|
|
180
|
+
const browserApp = BROWSER_MAP[browser];
|
|
181
|
+
// 'none' / 'headless': show the URL and wait. For SSH and remote sessions.
|
|
182
|
+
if (browser === 'none' || browser === 'headless') {
|
|
183
|
+
announce('🔗 Open this URL in your browser to authenticate:');
|
|
184
|
+
announce(` ${authorizationUrl}`);
|
|
185
|
+
announce(` Waiting for callback on http://localhost:${port}/callback ...`);
|
|
186
|
+
announce(' If your browser is on another machine, after login copy the ' +
|
|
187
|
+
'`code` from the address bar and paste it at ' +
|
|
188
|
+
`http://<this-host>:${port}/ — or paste it here and press Enter.`);
|
|
189
|
+
return;
|
|
146
190
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const server = http.createServer(app);
|
|
154
|
-
// Disable keep-alive to ensure connections close immediately
|
|
155
|
-
server.keepAliveTimeout = 0;
|
|
156
|
-
server.headersTimeout = 0;
|
|
157
|
-
const PORT = port;
|
|
158
|
-
let serverInstance = null;
|
|
159
|
-
// Cleanup function to ensure server is closed on process termination
|
|
160
|
-
const cleanup = () => {
|
|
161
|
-
if (cleanupDone)
|
|
162
|
-
return;
|
|
163
|
-
cleanupDone = true;
|
|
164
|
-
log?.debug(`Cleaning up OAuth callback server on port ${PORT}`);
|
|
165
|
-
if (timeoutId) {
|
|
166
|
-
clearTimeout(timeoutId);
|
|
167
|
-
timeoutId = null;
|
|
168
|
-
}
|
|
169
|
-
if (finishTimeoutId) {
|
|
170
|
-
clearTimeout(finishTimeoutId);
|
|
171
|
-
finishTimeoutId = null;
|
|
172
|
-
}
|
|
173
|
-
if (server) {
|
|
174
|
-
try {
|
|
175
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
176
|
-
server.closeAllConnections();
|
|
177
|
-
}
|
|
178
|
-
server.close(() => {
|
|
179
|
-
log?.debug(`OAuth server closed during cleanup, port ${PORT} freed`);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
catch (_e) {
|
|
183
|
-
// Ignore errors during cleanup
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
// Remove cleanup listeners to prevent memory leaks
|
|
188
|
-
const removeCleanupListeners = () => {
|
|
189
|
-
process.removeListener('exit', cleanup);
|
|
190
|
-
process.removeListener('SIGTERM', cleanup);
|
|
191
|
-
process.removeListener('SIGINT', cleanup);
|
|
192
|
-
process.removeListener('SIGHUP', cleanup);
|
|
193
|
-
if (process.platform === 'win32') {
|
|
194
|
-
process.removeListener('SIGBREAK', cleanup);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
const resolve = (value) => {
|
|
198
|
-
if (resolved)
|
|
199
|
-
return; // Prevent double resolution
|
|
200
|
-
resolved = true;
|
|
201
|
-
if (timeoutId) {
|
|
202
|
-
clearTimeout(timeoutId);
|
|
203
|
-
timeoutId = null;
|
|
204
|
-
}
|
|
205
|
-
if (finishTimeoutId) {
|
|
206
|
-
clearTimeout(finishTimeoutId);
|
|
207
|
-
finishTimeoutId = null;
|
|
208
|
-
}
|
|
209
|
-
removeCleanupListeners();
|
|
210
|
-
originalResolve(value);
|
|
211
|
-
};
|
|
212
|
-
const reject = (reason) => {
|
|
213
|
-
if (timeoutId)
|
|
214
|
-
clearTimeout(timeoutId);
|
|
215
|
-
removeCleanupListeners();
|
|
216
|
-
originalReject(reason);
|
|
217
|
-
};
|
|
218
|
-
// Register cleanup handlers for process termination
|
|
219
|
-
// This ensures port is freed when Cline or other clients kill the process
|
|
220
|
-
process.once('exit', cleanup);
|
|
221
|
-
process.once('SIGTERM', cleanup);
|
|
222
|
-
process.once('SIGINT', cleanup);
|
|
223
|
-
process.once('SIGHUP', cleanup);
|
|
224
|
-
// SIGBREAK is Windows-specific (Ctrl+Break)
|
|
225
|
-
if (process.platform === 'win32') {
|
|
226
|
-
process.once('SIGBREAK', cleanup);
|
|
227
|
-
}
|
|
228
|
-
// Use provided authorization URL or build from authConfig
|
|
229
|
-
const authorizationUrl = authConfig.authorizationUrl ?? getJwtAuthorizationUrl(authConfig, PORT);
|
|
230
|
-
log?.info(`[browserAuth] Authorization URL: ${authorizationUrl}`);
|
|
231
|
-
log?.info(`[browserAuth] Server listening on port: ${PORT}`);
|
|
232
|
-
// Verify port in redirect_uri matches server port
|
|
233
|
-
const redirectUriMatch = authorizationUrl.match(/redirect_uri=([^&]+)/);
|
|
234
|
-
if (redirectUriMatch) {
|
|
235
|
-
const redirectUri = decodeURIComponent(redirectUriMatch[1]);
|
|
236
|
-
const urlPortMatch = redirectUri.match(/localhost:(\d+)/);
|
|
237
|
-
if (urlPortMatch) {
|
|
238
|
-
const urlPort = parseInt(urlPortMatch[1], 10);
|
|
239
|
-
if (urlPort !== PORT) {
|
|
240
|
-
log?.warn(`[browserAuth] WARNING: Port mismatch! URL has port ${urlPort}, but server listens on ${PORT}`);
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
log?.info(`[browserAuth] Port match: URL and server both use port ${PORT}`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
191
|
+
if (browser === 'auto') {
|
|
192
|
+
log?.info('🌐 Attempting to open browser for authentication...');
|
|
193
|
+
try {
|
|
194
|
+
const openModule = await Promise.resolve().then(() => __importStar(require('open')));
|
|
195
|
+
await openModule.default(authorizationUrl);
|
|
196
|
+
log?.info('✅ Browser opened successfully. Waiting for authentication...');
|
|
246
197
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const { error, error_description, error_uri } = req.query;
|
|
254
|
-
if (error) {
|
|
255
|
-
log?.error(`Callback error: ${error}${error_description ? ` - ${error_description}` : ''}`);
|
|
256
|
-
const errorMsg = error_description
|
|
257
|
-
? `${error}: ${error_description}`
|
|
258
|
-
: String(error);
|
|
259
|
-
const errorHtml = `<!DOCTYPE html>
|
|
260
|
-
<html lang="en">
|
|
261
|
-
<head>
|
|
262
|
-
<meta charset="UTF-8">
|
|
263
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
264
|
-
<title>Authentication Error</title>
|
|
265
|
-
<style>
|
|
266
|
-
body {
|
|
267
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
268
|
-
text-align: center;
|
|
269
|
-
margin: 0;
|
|
270
|
-
padding: 50px 20px;
|
|
271
|
-
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
|
|
272
|
-
color: white;
|
|
273
|
-
min-height: 100vh;
|
|
274
|
-
display: flex;
|
|
275
|
-
flex-direction: column;
|
|
276
|
-
justify-content: center;
|
|
277
|
-
align-items: center;
|
|
198
|
+
catch (error) {
|
|
199
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
200
|
+
log?.warn(`⚠️ Could not open browser automatically: ${message}`);
|
|
201
|
+
announce('🔗 Please open this URL in your browser to authenticate:');
|
|
202
|
+
announce(` ${authorizationUrl}`);
|
|
203
|
+
announce(` Waiting for callback on http://localhost:${port}/callback ...`);
|
|
278
204
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
<div class="error-icon">✗</div>
|
|
310
|
-
<h1>Authentication Failed</h1>
|
|
311
|
-
<p>${errorMsg}</p>
|
|
312
|
-
<p>Please check your service key configuration and try again.</p>
|
|
313
|
-
</div>
|
|
314
|
-
</body>
|
|
315
|
-
</html>`;
|
|
316
|
-
res.status(400).send(errorHtml);
|
|
317
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
318
|
-
server.closeAllConnections();
|
|
319
|
-
}
|
|
320
|
-
server.close(() => {
|
|
321
|
-
// Server closed on error
|
|
322
|
-
});
|
|
323
|
-
return reject(new Error(`OAuth2 authentication failed: ${errorMsg}${error_uri ? ` (${error_uri})` : ''}`));
|
|
324
|
-
}
|
|
325
|
-
const { code } = req.query;
|
|
326
|
-
log?.info(`[browserAuth] Callback code received: ${code ? 'yes' : 'no'}`);
|
|
327
|
-
log?.debug(`Callback code received: ${code ? 'yes' : 'no'}`);
|
|
328
|
-
if (!code || typeof code !== 'string') {
|
|
329
|
-
log?.error(`[browserAuth] Callback code missing`);
|
|
330
|
-
res.status(400).send('Error: Authorization code missing');
|
|
331
|
-
return reject(new Error('Authorization code missing'));
|
|
332
|
-
}
|
|
333
|
-
log?.info(`[browserAuth] Exchanging code for token...`);
|
|
334
|
-
// Send success page
|
|
335
|
-
const html = `<!DOCTYPE html>
|
|
336
|
-
<html lang="en">
|
|
337
|
-
<head>
|
|
338
|
-
<meta charset="UTF-8">
|
|
339
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
340
|
-
<title>SAP BTP Authentication</title>
|
|
341
|
-
<style>
|
|
342
|
-
body {
|
|
343
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
344
|
-
text-align: center;
|
|
345
|
-
margin: 0;
|
|
346
|
-
padding: 50px 20px;
|
|
347
|
-
background: linear-gradient(135deg, #0070f3 0%, #00d4ff 100%);
|
|
348
|
-
color: white;
|
|
349
|
-
min-height: 100vh;
|
|
350
|
-
display: flex;
|
|
351
|
-
flex-direction: column;
|
|
352
|
-
justify-content: center;
|
|
353
|
-
align-items: center;
|
|
354
|
-
}
|
|
355
|
-
.container {
|
|
356
|
-
background: rgba(255, 255, 255, 0.1);
|
|
357
|
-
border-radius: 20px;
|
|
358
|
-
padding: 40px;
|
|
359
|
-
backdrop-filter: blur(10px);
|
|
360
|
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
361
|
-
max-width: 500px;
|
|
362
|
-
width: 100%;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (browserApp === null)
|
|
208
|
+
return;
|
|
209
|
+
// On Linux, ensure DISPLAY is set for X11 applications.
|
|
210
|
+
if (process.platform === 'linux' &&
|
|
211
|
+
!process.env.DISPLAY &&
|
|
212
|
+
!process.env.WAYLAND_DISPLAY) {
|
|
213
|
+
process.env.DISPLAY = ':0';
|
|
214
|
+
log?.debug('DISPLAY not set, using fallback DISPLAY=:0');
|
|
215
|
+
}
|
|
216
|
+
let open = null;
|
|
217
|
+
try {
|
|
218
|
+
const openModule = await Promise.resolve().then(() => __importStar(require('open')));
|
|
219
|
+
open = openModule.default;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
open = null;
|
|
223
|
+
}
|
|
224
|
+
if (!open) {
|
|
225
|
+
// Fallback: shell out. Non-blocking by design.
|
|
226
|
+
const platform = process.platform;
|
|
227
|
+
let command;
|
|
228
|
+
if (browserApp === 'chrome') {
|
|
229
|
+
command =
|
|
230
|
+
platform === 'win32'
|
|
231
|
+
? 'cmd /c start "" "chrome"'
|
|
232
|
+
: platform === 'darwin'
|
|
233
|
+
? 'open -a "Google Chrome"'
|
|
234
|
+
: 'google-chrome || chromium || chromium-browser';
|
|
363
235
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
236
|
+
else if (browserApp === 'msedge') {
|
|
237
|
+
command =
|
|
238
|
+
platform === 'win32'
|
|
239
|
+
? 'cmd /c start "" "msedge"'
|
|
240
|
+
: platform === 'darwin'
|
|
241
|
+
? 'open -a "Microsoft Edge"'
|
|
242
|
+
: 'microsoft-edge || microsoft-edge-stable';
|
|
369
243
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
244
|
+
else if (browserApp === 'firefox') {
|
|
245
|
+
command =
|
|
246
|
+
platform === 'win32'
|
|
247
|
+
? 'cmd /c start "" "firefox"'
|
|
248
|
+
: platform === 'darwin'
|
|
249
|
+
? 'open -a Firefox'
|
|
250
|
+
: 'firefox || firefox-esr';
|
|
374
251
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
252
|
+
else {
|
|
253
|
+
command =
|
|
254
|
+
platform === 'win32'
|
|
255
|
+
? 'cmd /c start ""'
|
|
256
|
+
: platform === 'darwin'
|
|
257
|
+
? 'open'
|
|
258
|
+
: 'xdg-open';
|
|
380
259
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
<div class="container">
|
|
385
|
-
<div class="success-icon">✓</div>
|
|
386
|
-
<h1>Authentication Successful!</h1>
|
|
387
|
-
<p>You have successfully authenticated with SAP BTP.</p>
|
|
388
|
-
<p>You can now close this browser window.</p>
|
|
389
|
-
</div>
|
|
390
|
-
</body>
|
|
391
|
-
</html>`;
|
|
392
|
-
// Exchange code for tokens first
|
|
393
|
-
try {
|
|
394
|
-
log?.info(`[browserAuth] Starting token exchange...`);
|
|
395
|
-
const tokens = await exchangeCodeForToken(authConfig, code, PORT, log);
|
|
396
|
-
log?.info(`[browserAuth] Tokens received: accessToken(${tokens.accessToken?.length || 0} chars), refreshToken(${tokens.refreshToken?.length || 0} chars)`);
|
|
397
|
-
// Send success page (non-blocking, doesn't affect promise)
|
|
398
|
-
res.send(html);
|
|
399
|
-
log?.info(`[browserAuth] Response sent, waiting for finish...`);
|
|
400
|
-
// Close all connections and server after response is sent
|
|
401
|
-
// Resolve promise AFTER server is closed to prevent Jest from hanging
|
|
402
|
-
let serverClosing = false;
|
|
403
|
-
const closeServerAndResolve = () => {
|
|
404
|
-
if (serverClosing)
|
|
405
|
-
return; // Prevent double execution
|
|
406
|
-
serverClosing = true;
|
|
407
|
-
if (finishTimeoutId) {
|
|
408
|
-
clearTimeout(finishTimeoutId);
|
|
409
|
-
finishTimeoutId = null;
|
|
410
|
-
}
|
|
411
|
-
log?.info(`[browserAuth] Response finished, closing server...`);
|
|
412
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
413
|
-
server.closeAllConnections();
|
|
414
|
-
}
|
|
415
|
-
// Wait for server to close before resolving to prevent Jest from hanging
|
|
416
|
-
server.close(() => {
|
|
417
|
-
// Server closed - port should be freed
|
|
418
|
-
log?.info(`[browserAuth] Server closed, port ${PORT} should be freed`);
|
|
419
|
-
// Resolve after server is fully closed
|
|
420
|
-
log?.info(`[browserAuth] Resolving promise with tokens...`);
|
|
421
|
-
resolve({
|
|
422
|
-
accessToken: tokens.accessToken,
|
|
423
|
-
refreshToken: tokens.refreshToken,
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
};
|
|
427
|
-
// Wait for response to finish, but add timeout to prevent hanging
|
|
428
|
-
res.once('finish', closeServerAndResolve);
|
|
429
|
-
// Fallback: if finish event doesn't fire within 1 second, close anyway
|
|
430
|
-
finishTimeoutId = setTimeout(closeServerAndResolve, 1000);
|
|
431
|
-
}
|
|
432
|
-
catch (error) {
|
|
433
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
434
|
-
server.closeAllConnections();
|
|
435
|
-
}
|
|
436
|
-
// Use setTimeout to ensure connections are closed before server.close()
|
|
437
|
-
setTimeout(() => {
|
|
438
|
-
server.close(() => {
|
|
439
|
-
// Server closed on error - port should be freed
|
|
440
|
-
log?.debug(`Server closed on error, port ${PORT} should be freed`);
|
|
441
|
-
});
|
|
442
|
-
}, 100);
|
|
443
|
-
reject(error);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
catch (error) {
|
|
447
|
-
res.status(500).send('Error processing authentication');
|
|
448
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
449
|
-
server.closeAllConnections();
|
|
450
|
-
}
|
|
451
|
-
// Use setTimeout to ensure connections are closed before server.close()
|
|
452
|
-
setTimeout(() => {
|
|
453
|
-
server.close(() => {
|
|
454
|
-
// Server closed on error - port should be freed
|
|
455
|
-
log?.debug(`Server closed on error, port ${PORT} should be freed`);
|
|
456
|
-
});
|
|
457
|
-
}, 100);
|
|
458
|
-
reject(error);
|
|
260
|
+
child_process.exec(`${command} "${authorizationUrl}"`, (error) => {
|
|
261
|
+
if (error) {
|
|
262
|
+
log?.error(`❌ Failed to open browser: ${error.message}. Please open manually: ${authorizationUrl}`, { error: error.message, url: authorizationUrl });
|
|
459
263
|
}
|
|
460
264
|
});
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (browserApp)
|
|
268
|
+
await open(authorizationUrl, { app: { name: browserApp } });
|
|
269
|
+
else
|
|
270
|
+
await open(authorizationUrl);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Interactive browser login for the UAA authorization-code flow.
|
|
274
|
+
*
|
|
275
|
+
* The callback socket is owned by `withBrowserCallbackServer`: it is released
|
|
276
|
+
* when the scope ends, whatever ends it, and before the code is exchanged — so
|
|
277
|
+
* a slow UAA cannot hold the port, and a settled promise always means the port
|
|
278
|
+
* is free.
|
|
279
|
+
*/
|
|
280
|
+
async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3001, timeoutMs = 30 * 1000) {
|
|
281
|
+
const log = logger || null;
|
|
282
|
+
// Essential, user-facing prompts (the auth URL, paste instructions) must be
|
|
283
|
+
// visible even when no logger is supplied. Fall back to stderr — never stdout,
|
|
284
|
+
// so stdio-based RPC transports (MCP/LSP) are not corrupted.
|
|
285
|
+
const announce = (msg) => {
|
|
286
|
+
if (log)
|
|
287
|
+
log.info(msg);
|
|
288
|
+
else
|
|
289
|
+
process.stderr.write(`${msg}\n`);
|
|
290
|
+
};
|
|
291
|
+
// Pre-check kept for its message: AuthBroker matches /already in use/i to
|
|
292
|
+
// distinguish a busy port from other failures.
|
|
293
|
+
const portAvailable = await isPortAvailable(port);
|
|
294
|
+
if (!portAvailable) {
|
|
295
|
+
throw new Error(`Port ${port} is already in use. Please specify a different port or free the port.`);
|
|
296
|
+
}
|
|
297
|
+
let stdinReader = null;
|
|
298
|
+
const code = await (0, callbackServer_1.withBrowserCallbackServer)({ port, timeoutMs }, async (server) => {
|
|
299
|
+
const authorizationUrl = authConfig.authorizationUrl ??
|
|
300
|
+
getJwtAuthorizationUrl(authConfig, server.port);
|
|
301
|
+
log?.info(`[browserAuth] Authorization URL: ${authorizationUrl}`);
|
|
302
|
+
log?.info(`[browserAuth] Server listening on port: ${server.port}`);
|
|
303
|
+
const waiting = server.waitForResult();
|
|
304
|
+
// Not awaited: a launcher that hangs must not delay the timeout or the
|
|
305
|
+
// release, and one that fails ends the scope through `fail`.
|
|
306
|
+
void launchBrowser(authorizationUrl, browser, server.port, announce, log).catch((error) => {
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
log?.error(`❌ Failed to open browser: ${message}. Please open manually: ${authorizationUrl}`, { error: message, url: authorizationUrl });
|
|
309
|
+
server.fail(new Error(`Browser opening failed for destination authentication. Please open manually: ${authorizationUrl}`));
|
|
471
310
|
});
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
// Handle 'auto' mode - try to open browser, fallback to showing URL
|
|
485
|
-
if (browser === 'auto') {
|
|
486
|
-
log?.info('🌐 Attempting to open browser for authentication...');
|
|
487
|
-
try {
|
|
488
|
-
const openModule = await Promise.resolve().then(() => __importStar(require('open')));
|
|
489
|
-
const open = openModule.default;
|
|
490
|
-
await open(authorizationUrl);
|
|
491
|
-
log?.info('✅ Browser opened successfully. Waiting for authentication...');
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
catch (error) {
|
|
495
|
-
// If browser cannot be opened, show URL and wait
|
|
496
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
497
|
-
log?.warn(`⚠️ Could not open browser automatically: ${errorMessage}`);
|
|
498
|
-
log?.info(`🔗 Please open this URL in your browser to authenticate:`);
|
|
499
|
-
log?.info(` ${authorizationUrl}`);
|
|
500
|
-
log?.info(` Waiting for callback on http://localhost:${PORT}/callback ...`);
|
|
501
|
-
// Don't reject - wait for callback
|
|
311
|
+
// Manual stdin paste — only when attached to an interactive terminal.
|
|
312
|
+
// Under a stdio RPC transport stdin carries the protocol, so we must never
|
|
313
|
+
// consume it; isTTY guards that. A pasted line is handed to the same
|
|
314
|
+
// `/submit` route the paste form uses, so there is one way in, not two.
|
|
315
|
+
if ((browser === 'none' || browser === 'headless') &&
|
|
316
|
+
process.stdin.isTTY) {
|
|
317
|
+
stdinReader = readline.createInterface({ input: process.stdin });
|
|
318
|
+
stdinReader.on('line', (line) => {
|
|
319
|
+
const pasted = extractCode(line);
|
|
320
|
+
if (!pasted) {
|
|
321
|
+
process.stderr.write('Could not read an authorization code from that input. Try again.\n');
|
|
502
322
|
return;
|
|
503
323
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
// Try dynamic import first (for ES modules)
|
|
518
|
-
let open;
|
|
519
|
-
try {
|
|
520
|
-
const openModule = await Promise.resolve().then(() => __importStar(require('open')));
|
|
521
|
-
open = openModule.default;
|
|
522
|
-
}
|
|
523
|
-
catch (_importError) {
|
|
524
|
-
// Fallback: use child_process to open browser if import fails
|
|
525
|
-
// This works in both CommonJS and ES module environments (like Jest)
|
|
526
|
-
const platform = process.platform;
|
|
527
|
-
let command;
|
|
528
|
-
if (browserApp === 'chrome') {
|
|
529
|
-
command =
|
|
530
|
-
platform === 'win32'
|
|
531
|
-
? 'cmd /c start "" "chrome"'
|
|
532
|
-
: platform === 'darwin'
|
|
533
|
-
? 'open -a "Google Chrome"'
|
|
534
|
-
: 'google-chrome || google-chrome-stable || chromium || chromium-browser';
|
|
535
|
-
}
|
|
536
|
-
else if (browserApp === 'edge') {
|
|
537
|
-
command =
|
|
538
|
-
platform === 'win32'
|
|
539
|
-
? 'cmd /c start "" "msedge"'
|
|
540
|
-
: platform === 'darwin'
|
|
541
|
-
? 'open -a "Microsoft Edge"'
|
|
542
|
-
: 'microsoft-edge || microsoft-edge-stable';
|
|
543
|
-
}
|
|
544
|
-
else if (browserApp === 'firefox') {
|
|
545
|
-
command =
|
|
546
|
-
platform === 'win32'
|
|
547
|
-
? 'cmd /c start "" "firefox"'
|
|
548
|
-
: platform === 'darwin'
|
|
549
|
-
? 'open -a Firefox'
|
|
550
|
-
: 'firefox || firefox-esr';
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
// System default
|
|
554
|
-
command =
|
|
555
|
-
platform === 'win32'
|
|
556
|
-
? 'cmd /c start ""'
|
|
557
|
-
: platform === 'darwin'
|
|
558
|
-
? 'open'
|
|
559
|
-
: 'xdg-open';
|
|
560
|
-
}
|
|
561
|
-
// Use child_process as fallback (non-blocking)
|
|
562
|
-
child_process.exec(`${command} "${authorizationUrl}"`, (error) => {
|
|
563
|
-
if (error) {
|
|
564
|
-
log?.error(`❌ Failed to open browser: ${error.message}. Please open manually: ${authorizationUrl}`, { error: error.message, url: authorizationUrl });
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
return; // Exit early since we're using child_process (non-blocking)
|
|
568
|
-
}
|
|
569
|
-
// Use open module if import succeeded
|
|
570
|
-
if (browserApp) {
|
|
571
|
-
await open(authorizationUrl, { app: { name: browserApp } });
|
|
572
|
-
}
|
|
573
|
-
else {
|
|
574
|
-
await open(authorizationUrl);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
catch (error) {
|
|
578
|
-
// If browser cannot be opened, close server and show URL
|
|
579
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
580
|
-
server.closeAllConnections();
|
|
581
|
-
}
|
|
582
|
-
// Use setTimeout to ensure connections are closed before server.close()
|
|
583
|
-
setTimeout(() => {
|
|
584
|
-
server.close(() => {
|
|
585
|
-
// Server closed on browser open error - port should be freed
|
|
586
|
-
log?.debug(`Server closed on browser open error, port ${PORT} should be freed`);
|
|
587
|
-
});
|
|
588
|
-
}, 100);
|
|
589
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
590
|
-
log?.error(`❌ Failed to open browser: ${errorMessage}. Please open manually: ${authorizationUrl}`, { error: errorMessage, url: authorizationUrl });
|
|
591
|
-
log?.info(`🔗 Open in browser: ${authorizationUrl}`, {
|
|
592
|
-
url: authorizationUrl,
|
|
593
|
-
});
|
|
594
|
-
// Throw error so consumer can distinguish this from "service key missing" error
|
|
595
|
-
reject(new Error(`Browser opening failed for destination authentication. Please open manually: ${authorizationUrl}`));
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
// Timeout after 30 seconds to prevent blocking consumer
|
|
600
|
-
timeoutId = setTimeout(() => {
|
|
601
|
-
if (serverInstance) {
|
|
602
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
603
|
-
server.closeAllConnections();
|
|
604
|
-
}
|
|
605
|
-
// Use setTimeout to ensure connections are closed before server.close()
|
|
606
|
-
setTimeout(() => {
|
|
607
|
-
server.close(() => {
|
|
608
|
-
// Server closed on timeout - port should be freed
|
|
609
|
-
log?.debug(`Server closed on timeout, port ${PORT} should be freed`);
|
|
610
|
-
});
|
|
611
|
-
}, 100);
|
|
612
|
-
reject(new Error('Authentication timeout after 30 seconds. Please try again.'));
|
|
613
|
-
}
|
|
614
|
-
}, 30 * 1000);
|
|
324
|
+
const req = http.get({
|
|
325
|
+
host: '127.0.0.1',
|
|
326
|
+
port: server.port,
|
|
327
|
+
path: `/submit?input=${encodeURIComponent(pasted)}`,
|
|
328
|
+
agent: false,
|
|
329
|
+
});
|
|
330
|
+
req.on('error', () => undefined);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return await waiting;
|
|
334
|
+
}).finally(() => {
|
|
335
|
+
stdinReader?.close();
|
|
336
|
+
stdinReader = null;
|
|
615
337
|
});
|
|
338
|
+
log?.info('[browserAuth] Exchanging code for token...');
|
|
339
|
+
return await exchangeCodeForToken(authConfig, code, port, log);
|
|
616
340
|
}
|