mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
fix(sso-localhost-callback) [PM-18678]: Update http server error handling to be asynchronous and solve for EADDRINUSE uncaught exceptions. (#16585)
This commit is contained in:
@@ -12,10 +12,13 @@ import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
|
||||
/**
|
||||
* The SSO Localhost login service uses a local host listener as fallback in case scheme handling deeplinks does not work.
|
||||
* This way it is possible to log in with SSO on appimage, snap, and electron dev using the same methods that the cli uses.
|
||||
* This way it is possible to log in with SSO on appimage and electron dev using the same methods that the cli uses.
|
||||
*/
|
||||
export class SSOLocalhostCallbackService {
|
||||
private ssoRedirectUri = "";
|
||||
// We will only track one server at a time for use-case and performance considerations.
|
||||
// This will result in a last-one-wins behavior if multiple SSO flows are started simultaneously.
|
||||
private currentServer: http.Server | null = null;
|
||||
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
@@ -23,11 +26,30 @@ export class SSOLocalhostCallbackService {
|
||||
private ssoUrlService: SsoUrlService,
|
||||
) {
|
||||
ipcMain.handle("openSsoPrompt", async (event, { codeChallenge, state, email }) => {
|
||||
const { ssoCode, recvState } = await this.openSsoPrompt(codeChallenge, state, email);
|
||||
this.messagingService.send("ssoCallback", {
|
||||
code: ssoCode,
|
||||
state: recvState,
|
||||
redirectUri: this.ssoRedirectUri,
|
||||
// Close any existing server before starting new one
|
||||
if (this.currentServer) {
|
||||
await this.closeCurrentServer();
|
||||
}
|
||||
|
||||
return this.openSsoPrompt(codeChallenge, state, email).then(({ ssoCode, recvState }) => {
|
||||
this.messagingService.send("ssoCallback", {
|
||||
code: ssoCode,
|
||||
state: recvState,
|
||||
redirectUri: this.ssoRedirectUri,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async closeCurrentServer(): Promise<void> {
|
||||
if (!this.currentServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
this.currentServer!.close(() => {
|
||||
this.currentServer = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -59,6 +81,7 @@ export class SSOLocalhostCallbackService {
|
||||
"<p>You may now close this tab and return to the app.</p>" +
|
||||
"</body></html>",
|
||||
);
|
||||
this.currentServer = null;
|
||||
callbackServer.close(() =>
|
||||
resolve({
|
||||
ssoCode: code,
|
||||
@@ -73,41 +96,68 @@ export class SSOLocalhostCallbackService {
|
||||
"<p>You may now close this tab and return to the app.</p>" +
|
||||
"</body></html>",
|
||||
);
|
||||
this.currentServer = null;
|
||||
callbackServer.close(() => reject());
|
||||
}
|
||||
});
|
||||
|
||||
let foundPort = false;
|
||||
const webUrl = env.getWebVaultUrl();
|
||||
for (let port = 8065; port <= 8070; port++) {
|
||||
try {
|
||||
this.ssoRedirectUri = "http://localhost:" + port;
|
||||
const ssoUrl = this.ssoUrlService.buildSsoUrl(
|
||||
webUrl,
|
||||
ClientType.Desktop,
|
||||
this.ssoRedirectUri,
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
);
|
||||
callbackServer.listen(port, () => {
|
||||
this.messagingService.send("launchUri", {
|
||||
url: ssoUrl,
|
||||
});
|
||||
});
|
||||
foundPort = true;
|
||||
break;
|
||||
} catch {
|
||||
// Ignore error since we run the same command up to 5 times.
|
||||
}
|
||||
}
|
||||
if (!foundPort) {
|
||||
reject();
|
||||
}
|
||||
// Store reference to current server
|
||||
this.currentServer = callbackServer;
|
||||
|
||||
// after 5 minutes, close the server
|
||||
const webUrl = env.getWebVaultUrl();
|
||||
|
||||
const tryNextPort = (port: number) => {
|
||||
if (port > 8070) {
|
||||
this.currentServer = null;
|
||||
reject("All available SSO ports in use");
|
||||
return;
|
||||
}
|
||||
|
||||
this.ssoRedirectUri = "http://localhost:" + port;
|
||||
const ssoUrl = this.ssoUrlService.buildSsoUrl(
|
||||
webUrl,
|
||||
ClientType.Desktop,
|
||||
this.ssoRedirectUri,
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
);
|
||||
|
||||
// Set up error handler before attempting to listen
|
||||
callbackServer.once("error", (err: any) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
// Port is in use, try next port
|
||||
tryNextPort(port + 1);
|
||||
} else {
|
||||
// Another error - reject and set the current server to null
|
||||
// (one server alive at a time)
|
||||
this.currentServer = null;
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
||||
// Attempt to listen on the port
|
||||
callbackServer.listen(port, () => {
|
||||
// Success - remove error listener and launch SSO
|
||||
callbackServer.removeAllListeners("error");
|
||||
|
||||
this.messagingService.send("launchUri", {
|
||||
url: ssoUrl,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Start trying from port 8065
|
||||
tryNextPort(8065);
|
||||
|
||||
// Don't allow any server to stay up for more than 5 minutes;
|
||||
// this gives plenty of time to complete SSO but ensures we don't
|
||||
// have a server running indefinitely.
|
||||
setTimeout(
|
||||
() => {
|
||||
if (this.currentServer === callbackServer) {
|
||||
this.currentServer = null;
|
||||
}
|
||||
callbackServer.close(() => reject());
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
|
||||
Reference in New Issue
Block a user