diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index ec59a1fef0..52ad849a23 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2501,6 +2501,9 @@ "importEncKeyError": { "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." }, + "invalidFilePassword": { + "message": "Invalid file password, please use the password you entered when you created the export file." + }, "importDestination": { "message": "Import destination" }, @@ -2611,8 +2614,65 @@ "useBrowserName": { "message": "Use browser" }, + "multifactorAuthenticationCancelled": { + "message": "Multifactor authentication cancelled" + }, + "noLastPassDataFound": { + "message": "No LastPass data found" + }, + "incorrectUsernameOrPassword": { + "message": "Incorrect username or password" + }, + "multifactorAuthenticationFailed": { + "message": "Multifactor authentication failed" + }, + "includeSharedFolders": { + "message": "Include shared folders" + }, + "lastPassEmail": { + "message": "LastPass Email" + }, + "importingYourAccount": { + "message": "Importing your account..." + }, + "lastPassMFARequired": { + "message": "LastPass multifactor authentication required" + }, + "lastPassMFADesc": { + "message": "Enter your one-time passcode from your authentication app" + }, + "lastPassOOBDesc": { + "message": "Approve the login request in your authentication app or enter a one-time passcode." + }, + "passcode": { + "message": "Passcode" + }, + "lastPassMasterPassword": { + "message": "LastPass master password" + }, + "lastPassAuthRequired": { + "message": "LastPass authentication required" + }, + "awaitingSSO": { + "message": "Awaiting SSO authentication" + }, + "awaitingSSODesc": { + "message": "Please continue to log in using your company credentials." + }, "seeDetailedInstructions": { "message": "See detailed instructions on our help site at", "description": "This is followed a by a hyperlink to the help website." + }, + "importDirectlyFromLastPass": { + "message": "Import directly from LastPass" + }, + "importFromCSV": { + "message": "Import from CSV" + }, + "lastPassTryAgainCheckEmail": { + "message": "Try again or look for an email from LastPass to verify it's you." + }, + "collection": { + "message": "Collection" } } diff --git a/apps/browser/src/autofill/content/message_handler.ts b/apps/browser/src/autofill/content/message_handler.ts index 5ef0abdb7c..3fdf0f2012 100644 --- a/apps/browser/src/autofill/content/message_handler.ts +++ b/apps/browser/src/autofill/content/message_handler.ts @@ -10,6 +10,7 @@ window.addEventListener( command: event.data.command, code: event.data.code, state: event.data.state, + lastpass: event.data.lastpass, referrer: event.source.location.hostname, }); } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 53711c1dc1..159efc7de7 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -259,15 +259,22 @@ export default class RuntimeBackground { return; } - try { - BrowserApi.createNewTab( - "popup/index.html?uilocation=popout#/sso?code=" + - encodeURIComponent(msg.code) + - "&state=" + - encodeURIComponent(msg.state) - ); - } catch { - this.logService.error("Unable to open sso popout tab"); + if (msg.lastpass) { + this.messagingService.send("importCallbackLastPass", { + code: msg.code, + state: msg.state, + }); + } else { + try { + BrowserApi.createNewTab( + "popup/index.html?uilocation=popout#/sso?code=" + + encodeURIComponent(msg.code) + + "&state=" + + encodeURIComponent(msg.state) + ); + } catch { + this.logService.error("Unable to open sso popout tab"); + } } break; } diff --git a/apps/web/src/connectors/sso.ts b/apps/web/src/connectors/sso.ts index 9195c61f9c..d51f14b33d 100644 --- a/apps/web/src/connectors/sso.ts +++ b/apps/web/src/connectors/sso.ts @@ -5,9 +5,12 @@ require("./sso.scss"); document.addEventListener("DOMContentLoaded", () => { const code = getQsParam("code"); const state = getQsParam("state"); + const lastpass = getQsParam("lp"); - if (state != null && state.includes(":clientId=browser")) { - initiateBrowserSso(code, state); + if (lastpass === "1") { + initiateBrowserSso(code, state, true); + } else if (state != null && state.includes(":clientId=browser")) { + initiateBrowserSso(code, state, false); } else { window.location.href = window.location.origin + "/#/sso?code=" + code + "&state=" + state; // Match any characters between "_returnUri='" and the next "'" @@ -20,8 +23,8 @@ document.addEventListener("DOMContentLoaded", () => { } }); -function initiateBrowserSso(code: string, state: string) { - window.postMessage({ command: "authResult", code: code, state: state }, "*"); +function initiateBrowserSso(code: string, state: string, lastpass: boolean) { + window.postMessage({ command: "authResult", code: code, state: state, lastpass: lastpass }, "*"); const handOffMessage = ("; " + document.cookie) .split("; ssoHandOffMessage=") .pop() diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index a72e3c347c..baf8718eed 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -188,7 +188,8 @@ export class ImportComponent implements OnInit, OnDestroy { protected get showLastPassToggle(): boolean { return ( this.format === "lastpasscsv" && - this.platformUtilsService.getClientType() === ClientType.Desktop + (this.platformUtilsService.getClientType() === ClientType.Desktop || + this.platformUtilsService.getClientType() === ClientType.Browser) ); } protected get showLastPassOptions(): boolean { diff --git a/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts b/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts index c3687cda30..4995e37913 100644 --- a/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts +++ b/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts @@ -3,9 +3,11 @@ import { OidcClient } from "oidc-client-ts"; import { Subject, firstValueFrom } from "rxjs"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; +import { ClientType } from "@bitwarden/common/enums"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @@ -32,13 +34,14 @@ export class LastPassDirectImportService { constructor( private tokenService: TokenService, private cryptoFunctionService: CryptoFunctionService, + private environmentService: EnvironmentService, private appIdService: AppIdService, private lastPassDirectImportUIService: LastPassDirectImportUIService, + private platformUtilsService: PlatformUtilsService, private passwordGenerationService: PasswordGenerationServiceAbstraction, private broadcasterService: BroadcasterService, private ngZone: NgZone, - private dialogService: DialogService, - private platformUtilsService: PlatformUtilsService + private dialogService: DialogService ) { this.vault = new Vault(this.cryptoFunctionService, this.tokenService); @@ -110,8 +113,7 @@ export class LastPassDirectImportService { this.oidcClient = new OidcClient({ authority: this.vault.userType.openIDConnectAuthorityBase, client_id: this.vault.userType.openIDConnectClientId, - // TODO: this is different per client - redirect_uri: "bitwarden://import-callback-lp", + redirect_uri: this.getOidcRedirectUrl(), response_type: "code", scope: this.vault.userType.oidcScope, response_mode: "query", @@ -131,6 +133,25 @@ export class LastPassDirectImportService { }); } + private getOidcRedirectUrlWithParams(oidcCode: string, oidcState: string) { + const redirectUri = this.oidcClient.settings.redirect_uri; + const params = "code=" + oidcCode + "&state=" + oidcState; + if (redirectUri.indexOf("bitwarden://") === 0) { + return redirectUri + "/?" + params; + } + + return redirectUri + "&" + params; + } + + private getOidcRedirectUrl() { + const clientType = this.platformUtilsService.getClientType(); + if (clientType === ClientType.Desktop) { + return "bitwarden://import-callback-lp"; + } + const webUrl = this.environmentService.getWebVaultUrl(); + return webUrl + "/sso-connector.html?lp=1"; + } + private async handleStandardImport( email: string, password: string, @@ -150,7 +171,7 @@ export class LastPassDirectImportService { includeSharedFolders: boolean ): Promise { const response = await this.oidcClient.processSigninResponse( - this.oidcClient.settings.redirect_uri + "/?code=" + oidcCode + "&state=" + oidcState + this.getOidcRedirectUrlWithParams(oidcCode, oidcState) ); const userState = response.userState as any;