1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-01 08:03:20 +00:00

[PM-4530] Fix sso in snap desktop (#10548)

* Add localhost callback service for sso

* Fix redirect behaviour

* Update apps/desktop/src/app/app.component.ts

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>

* Fix incorrect http response for sso callback

* Add sso error

* Update error message

---------

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann
2024-08-26 15:13:45 +02:00
committed by GitHub
parent 722c4737fc
commit 86f3a679ae
9 changed files with 202 additions and 10 deletions

View File

@@ -11,7 +11,7 @@ import {
UnencryptedMessageResponse,
} from "../models/native-messaging";
import { BiometricMessage, BiometricAction } from "../types/biometric-message";
import { isDev, isFlatpak, isMacAppStore, isSnapStore, isWindowsStore } from "../utils";
import { isAppImage, isDev, isFlatpak, isMacAppStore, isSnapStore, isWindowsStore } from "../utils";
import { ClipboardWriteMessage } from "./types/clipboard";
@@ -119,6 +119,12 @@ const ephemeralStore = {
ipcRenderer.invoke("deleteEphemeralValue", key),
};
const localhostCallbackService = {
openSsoPrompt: (codeChallenge: string, state: string): Promise<void> => {
return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state });
},
};
export default {
versions: {
app: (): Promise<string> => ipcRenderer.invoke("appVersion"),
@@ -129,6 +135,7 @@ export default {
isWindowsStore: isWindowsStore(),
isFlatpak: isFlatpak(),
isSnapStore: isSnapStore(),
isAppImage: isAppImage(),
reloadProcess: () => ipcRenderer.send("reload-process"),
log: (level: LogLevelType, message?: any, ...optionalParams: any[]) =>
ipcRenderer.invoke("ipc.log", { level, message, optionalParams }),
@@ -179,6 +186,7 @@ export default {
nativeMessaging,
crypto,
ephemeralStore,
localhostCallbackService,
};
function deviceType(): DeviceType {

View File

@@ -0,0 +1,129 @@
import * as http from "http";
import { ipcMain } from "electron";
import { firstValueFrom } from "rxjs";
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,
) {
ipcMain.handle("openSsoPrompt", async (event, { codeChallenge, state }) => {
const { ssoCode } = await this.openSsoPrompt(codeChallenge, state);
this.messagingService.send("ssoCallback", {
code: ssoCode,
state: state,
redirectUri: this.ssoRedirectUri,
});
});
}
private async openSsoPrompt(
codeChallenge: string,
state: string,
): Promise<{ ssoCode: string; orgIdentifier: string }> {
const env = await firstValueFrom(this.environmentService.environment$);
return new Promise((resolve, reject) => {
const callbackServer = http.createServer((req, res) => {
// after 5 minutes, close the server
setTimeout(
() => {
callbackServer.close(() => reject());
},
5 * 60 * 1000,
);
const urlString = "http://localhost" + req.url;
const url = new URL(urlString);
const code = url.searchParams.get("code");
const receivedState = url.searchParams.get("state");
const orgIdentifier = this.getOrgIdentifierFromState(receivedState);
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,
orgIdentifier: orgIdentifier,
}),
);
} 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;
callbackServer.listen(port, () => {
this.messagingService.send("launchUri", {
url:
webUrl +
"/#/sso?clientId=" +
"desktop" +
"&redirectUri=" +
encodeURIComponent(this.ssoRedirectUri) +
"&state=" +
state +
"&codeChallenge=" +
codeChallenge,
});
});
foundPort = true;
break;
} catch {
// Ignore error since we run the same command up to 5 times.
}
}
if (!foundPort) {
reject();
}
});
}
private getOrgIdentifierFromState(state: string): string {
if (state === null || state === undefined) {
return null;
}
const stateSplit = state.split("_identifier=");
return stateSplit.length > 1 ? stateSplit[1] : null;
}
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];
}
}