Skip to content

Commit 17de177

Browse files
committed
fixed auth login bug that failed to return to cli
1 parent c3e4245 commit 17de177

1 file changed

Lines changed: 19 additions & 6 deletions

File tree

src/services/auth/loginFlow.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import crypto from 'node:crypto';
1010
import http from 'node:http';
1111
import https from 'node:https';
12-
import { execFile } from 'node:child_process';
12+
import { spawn } from 'node:child_process';
1313
import { URL } from 'node:url';
1414

1515
// All three ports must be pre-registered in the Cognito App Client.
@@ -77,18 +77,24 @@ function isPortFree(port: number): Promise<boolean> {
7777
* interpolated into a shell string — to avoid command injection.
7878
*/
7979
export function openBrowser(url: string): void {
80+
// detached:true + stdio:'ignore' + unref() is the standard Node.js pattern for
81+
// fire-and-forget child processes — the event loop will not wait for them to exit.
82+
const spawnOpts = { detached: true, stdio: 'ignore' as const };
83+
let child;
8084
switch (process.platform) {
8185
case 'darwin':
82-
execFile('open', [url]);
86+
child = spawn('open', [url], spawnOpts);
8387
break;
8488
case 'win32':
8589
// Pass the URL via $args[0] so it is never interpolated into the -Command
8690
// string — avoids quote-breaking and injection risk from special characters.
87-
execFile('powershell.exe', ['-NoProfile', '-Command', 'Start-Process $args[0]', '-args', url]);
91+
child = spawn('powershell.exe', ['-NoProfile', '-Command', 'Start-Process $args[0]', '-args', url], spawnOpts);
8892
break;
8993
default:
90-
execFile('xdg-open', [url]);
94+
child = spawn('xdg-open', [url], spawnOpts);
95+
break;
9196
}
97+
child.unref();
9298
}
9399

94100
// ── Localhost callback server ─────────────────────────────────────────────────
@@ -107,26 +113,33 @@ export function listenForCallback(port: number, expectedState?: string): Promise
107113
const callbackState = parsed.searchParams.get('state');
108114

109115
if (expectedState && callbackState !== expectedState) {
110-
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
116+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
111117
res.end(
112118
'<html><body style="font-family:sans-serif;padding:2rem;max-width:480px">' +
113119
'<h2 style="color:#c23934">Authentication failed</h2>' +
114120
'<p>Invalid state parameter — possible CSRF attack. Please try again.</p>' +
115121
'</body></html>'
116122
);
117123
server.close();
124+
server.closeAllConnections?.();
118125
reject(new Error('OAuth callback state mismatch — possible CSRF. Try again.'));
119126
return;
120127
}
121128

122-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
129+
// 'Connection: close' tells the browser to close the TCP connection after
130+
// this response so server.close() has no lingering keep-alive sockets to
131+
// wait for, allowing the Node.js event loop to exit promptly.
132+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
123133
res.end(
124134
'<html><body style="font-family:sans-serif;padding:2rem;max-width:480px">' +
125135
'<h2 style="color:#0070d2">Authentication complete</h2>' +
126136
'<p>You can close this tab and return to the terminal.</p>' +
127137
'</body></html>'
128138
);
129139
server.close();
140+
// Destroy any sockets that are still open (e.g. a browser that ignores
141+
// the Connection:close header). Requires Node 18.2+.
142+
server.closeAllConnections?.();
130143

131144
if (code) {
132145
resolve(code);

0 commit comments

Comments
 (0)