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:
@@ -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 {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user