mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-11 10:10:25 +01: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:
parent
722c4737fc
commit
86f3a679ae
1
apps/desktop/desktop_native/napi/index.d.ts
vendored
1
apps/desktop/desktop_native/napi/index.d.ts
vendored
@ -47,7 +47,6 @@ export namespace processisolations {
|
|||||||
export function isCoreDumpingDisabled(): Promise<boolean>
|
export function isCoreDumpingDisabled(): Promise<boolean>
|
||||||
export function disableMemoryAccess(): Promise<void>
|
export function disableMemoryAccess(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace powermonitors {
|
export namespace powermonitors {
|
||||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||||
export function isLockMonitorAvailable(): Promise<boolean>
|
export function isLockMonitorAvailable(): Promise<boolean>
|
||||||
|
@ -234,7 +234,7 @@
|
|||||||
"autoStart": true,
|
"autoStart": true,
|
||||||
"base": "core22",
|
"base": "core22",
|
||||||
"confinement": "strict",
|
"confinement": "strict",
|
||||||
"plugs": ["default", "password-manager-service"],
|
"plugs": ["default", "network", "network-bind", "password-manager-service"],
|
||||||
"stagePackages": ["default"]
|
"stagePackages": ["default"]
|
||||||
},
|
},
|
||||||
"protocols": [
|
"protocols": [
|
||||||
|
@ -300,13 +300,19 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
this.systemService.clearClipboard(message.clipboardValue, message.clearMs);
|
this.systemService.clearClipboard(message.clipboardValue, message.clearMs);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ssoCallback":
|
case "ssoCallback": {
|
||||||
|
const queryParams = {
|
||||||
|
code: message.code,
|
||||||
|
state: message.state,
|
||||||
|
redirectUri: message.redirectUri ?? "bitwarden://sso-callback",
|
||||||
|
};
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.router.navigate(["sso"], {
|
this.router.navigate(["sso"], {
|
||||||
queryParams: { code: message.code, state: message.state },
|
queryParams: queryParams,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "premiumRequired": {
|
case "premiumRequired": {
|
||||||
const premiumConfirmed = await this.dialogService.openSimpleDialog({
|
const premiumConfirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "premiumRequired" },
|
title: { key: "premiumRequired" },
|
||||||
@ -455,6 +461,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
case "deepLink":
|
case "deepLink":
|
||||||
this.processDeepLink(message.urlString);
|
this.processDeepLink(message.urlString);
|
||||||
break;
|
break;
|
||||||
|
case "launchUri":
|
||||||
|
this.platformUtilsService.launchUri(message.url);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
|
|
||||||
@ -187,4 +188,40 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
|
|||||||
const email = this.loggedEmail;
|
const email = this.loggedEmail;
|
||||||
document.getElementById(email == null || email === "" ? "email" : "masterPassword")?.focus();
|
document.getElementById(email == null || email === "" ? "email" : "masterPassword")?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||||
|
if (!ipc.platform.isAppImage && !ipc.platform.isSnapStore && !ipc.platform.isDev) {
|
||||||
|
return super.launchSsoBrowser(clientId, ssoRedirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save off email for SSO
|
||||||
|
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||||
|
|
||||||
|
// Generate necessary sso params
|
||||||
|
const passwordOptions: any = {
|
||||||
|
type: "password",
|
||||||
|
length: 64,
|
||||||
|
uppercase: true,
|
||||||
|
lowercase: true,
|
||||||
|
numbers: true,
|
||||||
|
special: false,
|
||||||
|
};
|
||||||
|
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||||
|
const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||||
|
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
||||||
|
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||||
|
|
||||||
|
// Save sso params
|
||||||
|
await this.ssoLoginService.setSsoState(state);
|
||||||
|
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
|
||||||
|
try {
|
||||||
|
await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state);
|
||||||
|
} catch (err) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccured"),
|
||||||
|
this.i18nService.t("ssoError"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -738,10 +738,10 @@
|
|||||||
"selfHostedBaseUrlHint": {
|
"selfHostedBaseUrlHint": {
|
||||||
"message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
|
"message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
|
||||||
},
|
},
|
||||||
"selfHostedCustomEnvHeader" :{
|
"selfHostedCustomEnvHeader": {
|
||||||
"message": "For advanced configuration, you can specify the base URL of each service independently."
|
"message": "For advanced configuration, you can specify the base URL of each service independently."
|
||||||
},
|
},
|
||||||
"selfHostedEnvFormInvalid" :{
|
"selfHostedEnvFormInvalid": {
|
||||||
"message": "You must add either the base Server URL or at least one custom environment."
|
"message": "You must add either the base Server URL or at least one custom environment."
|
||||||
},
|
},
|
||||||
"customEnvironment": {
|
"customEnvironment": {
|
||||||
@ -1279,10 +1279,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errorRefreshingAccessToken":{
|
"errorRefreshingAccessToken": {
|
||||||
"message": "Access Token Refresh Error"
|
"message": "Access Token Refresh Error"
|
||||||
},
|
},
|
||||||
"errorRefreshingAccessTokenDesc":{
|
"errorRefreshingAccessTokenDesc": {
|
||||||
"message": "No refresh token or API keys found. Please try logging out and logging back in."
|
"message": "No refresh token or API keys found. Please try logging out and logging back in."
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
@ -1668,7 +1668,7 @@
|
|||||||
"message": "Your organization requires you to set a master password.",
|
"message": "Your organization requires you to set a master password.",
|
||||||
"description": "Used as a card title description on the set password page to explain why the user is there"
|
"description": "Used as a card title description on the set password page to explain why the user is there"
|
||||||
},
|
},
|
||||||
"verificationRequired" : {
|
"verificationRequired": {
|
||||||
"message": "Verification required",
|
"message": "Verification required",
|
||||||
"description": "Default title for the user verification dialog."
|
"description": "Default title for the user verification dialog."
|
||||||
},
|
},
|
||||||
@ -3052,5 +3052,8 @@
|
|||||||
},
|
},
|
||||||
"textSends": {
|
"textSends": {
|
||||||
"message": "Text Sends"
|
"message": "Text Sends"
|
||||||
|
},
|
||||||
|
"ssoError": {
|
||||||
|
"message": "No free ports could be found for the sso login."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ import { ElectronLogMainService } from "./platform/services/electron-log.main.se
|
|||||||
import { ElectronStorageService } from "./platform/services/electron-storage.service";
|
import { ElectronStorageService } from "./platform/services/electron-storage.service";
|
||||||
import { EphemeralValueStorageService } from "./platform/services/ephemeral-value-storage.main.service";
|
import { EphemeralValueStorageService } from "./platform/services/ephemeral-value-storage.main.service";
|
||||||
import { I18nMainService } from "./platform/services/i18n.main.service";
|
import { I18nMainService } from "./platform/services/i18n.main.service";
|
||||||
|
import { SSOLocalhostCallbackService } from "./platform/services/sso-localhost-callback.service";
|
||||||
import { ElectronMainMessagingService } from "./services/electron-main-messaging.service";
|
import { ElectronMainMessagingService } from "./services/electron-main-messaging.service";
|
||||||
import { isMacAppStore } from "./utils";
|
import { isMacAppStore } from "./utils";
|
||||||
|
|
||||||
@ -227,6 +228,7 @@ export class Main {
|
|||||||
this.clipboardMain.init();
|
this.clipboardMain.init();
|
||||||
|
|
||||||
new EphemeralValueStorageService();
|
new EphemeralValueStorageService();
|
||||||
|
new SSOLocalhostCallbackService(this.environmentService, this.messagingService);
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap() {
|
bootstrap() {
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
UnencryptedMessageResponse,
|
UnencryptedMessageResponse,
|
||||||
} from "../models/native-messaging";
|
} from "../models/native-messaging";
|
||||||
import { BiometricMessage, BiometricAction } from "../types/biometric-message";
|
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";
|
import { ClipboardWriteMessage } from "./types/clipboard";
|
||||||
|
|
||||||
@ -119,6 +119,12 @@ const ephemeralStore = {
|
|||||||
ipcRenderer.invoke("deleteEphemeralValue", key),
|
ipcRenderer.invoke("deleteEphemeralValue", key),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const localhostCallbackService = {
|
||||||
|
openSsoPrompt: (codeChallenge: string, state: string): Promise<void> => {
|
||||||
|
return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
versions: {
|
versions: {
|
||||||
app: (): Promise<string> => ipcRenderer.invoke("appVersion"),
|
app: (): Promise<string> => ipcRenderer.invoke("appVersion"),
|
||||||
@ -129,6 +135,7 @@ export default {
|
|||||||
isWindowsStore: isWindowsStore(),
|
isWindowsStore: isWindowsStore(),
|
||||||
isFlatpak: isFlatpak(),
|
isFlatpak: isFlatpak(),
|
||||||
isSnapStore: isSnapStore(),
|
isSnapStore: isSnapStore(),
|
||||||
|
isAppImage: isAppImage(),
|
||||||
reloadProcess: () => ipcRenderer.send("reload-process"),
|
reloadProcess: () => ipcRenderer.send("reload-process"),
|
||||||
log: (level: LogLevelType, message?: any, ...optionalParams: any[]) =>
|
log: (level: LogLevelType, message?: any, ...optionalParams: any[]) =>
|
||||||
ipcRenderer.invoke("ipc.log", { level, message, optionalParams }),
|
ipcRenderer.invoke("ipc.log", { level, message, optionalParams }),
|
||||||
@ -179,6 +186,7 @@ export default {
|
|||||||
nativeMessaging,
|
nativeMessaging,
|
||||||
crypto,
|
crypto,
|
||||||
ephemeralStore,
|
ephemeralStore,
|
||||||
|
localhostCallbackService,
|
||||||
};
|
};
|
||||||
|
|
||||||
function deviceType(): DeviceType {
|
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];
|
||||||
|
}
|
||||||
|
}
|
@ -81,6 +81,11 @@ export class SsoComponent implements OnInit {
|
|||||||
const state = await this.ssoLoginService.getSsoState();
|
const state = await this.ssoLoginService.getSsoState();
|
||||||
await this.ssoLoginService.setCodeVerifier(null);
|
await this.ssoLoginService.setCodeVerifier(null);
|
||||||
await this.ssoLoginService.setSsoState(null);
|
await this.ssoLoginService.setSsoState(null);
|
||||||
|
|
||||||
|
if (qParams.redirectUri != null) {
|
||||||
|
this.redirectUri = qParams.redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
qParams.code != null &&
|
qParams.code != null &&
|
||||||
codeVerifier != null &&
|
codeVerifier != null &&
|
||||||
|
Loading…
Reference in New Issue
Block a user