1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-22 04:14:04 +00:00
Files
browser/apps/desktop/src/platform/services/sso-localhost-callback.service.ts
Todd Martin 077e0f89cc [PM-17751] Store SSO email in state on web client (#13295)
* Moved saving of SSO email outside of browser/desktop code

* Clarified comments.

* Tests

* Refactored login component services to manage state

* Fixed input on login component

* Fixed tests

* Linting

* Moved web setting in state into web override

* updated tests

* Fixed typing.

* Fixed type safety issues.

* Added comments and renamed for clarity.

* Removed method parameters that weren't used

* Added clarifying comments

* Added more comments.

* Removed test that is not necessary on base

* Test cleanup

* More comments.

* Linting

* Fixed test.

* Fixed base URL

* Fixed typechecking.

* Type checking

* Moved setting of email state to default service

* Added comments.

* Consolidated SSO URL formatting

* Updated comment

* Fixed reference.

* Fixed missing parameter.

* Initialized service.

* Added comments

* Added initialization of new service

* Made email optional due to CLI.

* Fixed comment on handleSsoClick.

* Added SSO email persistence to v1 component.

---------

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
2025-02-21 17:09:50 -05:00

131 lines
4.3 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as http from "http";
import { ipcMain } from "electron";
import { firstValueFrom } from "rxjs";
import { SsoUrlService } from "@bitwarden/auth/common";
import { ClientType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
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.
*/
export class SSOLocalhostCallbackService {
private ssoRedirectUri = "";
constructor(
private environmentService: EnvironmentService,
private messagingService: MessageSender,
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,
});
});
}
private async openSsoPrompt(
codeChallenge: string,
state: string,
email: string,
): Promise<{ ssoCode: string; recvState: string }> {
const env = await firstValueFrom(this.environmentService.environment$);
return new Promise((resolve, reject) => {
const callbackServer = http.createServer((req, res) => {
const urlString = "http://localhost" + req.url;
const url = new URL(urlString);
const code = url.searchParams.get("code");
if (code == null) {
res.writeHead(404);
res.end("not found");
return;
}
const receivedState = url.searchParams.get("state");
res.setHeader("Content-Type", "text/html");
if (code != null && receivedState != null && this.checkState(receivedState, state)) {
res.writeHead(200);
res.end(
"<html><head><title>Success | Bitwarden Desktop</title></head><body>" +
"<h1>Successfully authenticated with the Bitwarden desktop app</h1>" +
"<p>You may now close this tab and return to the app.</p>" +
"</body></html>",
);
callbackServer.close(() =>
resolve({
ssoCode: code,
recvState: receivedState,
}),
);
} else {
res.writeHead(400);
res.end(
"<html><head><title>Failed | Bitwarden Desktop</title></head><body>" +
"<h1>Something went wrong logging into the Bitwarden desktop app</h1>" +
"<p>You may now close this tab and return to the app.</p>" +
"</body></html>",
);
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();
}
// after 5 minutes, close the server
setTimeout(
() => {
callbackServer.close(() => reject());
},
5 * 60 * 1000,
);
});
}
private checkState(state: string, checkState: string): boolean {
if (state === null || state === undefined) {
return false;
}
if (checkState === null || checkState === undefined) {
return false;
}
const stateSplit = state.split("_identifier=");
const checkStateSplit = checkState.split("_identifier=");
return stateSplit[0] === checkStateSplit[0];
}
}