mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
[PM-4195] Lastpass lib cleanup (#6636)
* Casing fixes from the original port of the code * Add static createClientInfo and export * Add way to transform retrieve accounts into csv format Create ExportAccount model accountsToExportedCsvString can transform and export csv * Make calls needed for UI class async/awaitable * Add helpers for SSO on the UserTypeContext * Add additional error handling case * Fixes for SSO login --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
790d666929
commit
13df63fbac
@@ -1 +1,2 @@
|
|||||||
|
export { ClientInfo } from "./models";
|
||||||
export { Vault } from "./vault";
|
export { Vault } from "./vault";
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
import { Platform } from "../enums";
|
import { Platform } from "../enums";
|
||||||
|
|
||||||
export class ClientInfo {
|
export class ClientInfo {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
id: string;
|
id: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
static createClientInfo(): ClientInfo {
|
||||||
|
return { platform: Platform.Desktop, id: Utils.newGuid(), description: "Importer" };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Account } from "./account";
|
||||||
|
|
||||||
|
export class ExportedAccount {
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
totp: string;
|
||||||
|
extra: string;
|
||||||
|
name: string;
|
||||||
|
grouping: string;
|
||||||
|
fav: number;
|
||||||
|
|
||||||
|
constructor(account: Account) {
|
||||||
|
this.url = account.url;
|
||||||
|
this.username = account.username;
|
||||||
|
this.password = account.password;
|
||||||
|
this.totp = account.totp;
|
||||||
|
this.extra = account.notes;
|
||||||
|
this.name = account.name;
|
||||||
|
this.grouping = account.path === "(none)" ? null : account.path;
|
||||||
|
this.fav = account.isFavorite ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export { Account } from "./account";
|
export { Account } from "./account";
|
||||||
export { Chunk } from "./chunk";
|
export { Chunk } from "./chunk";
|
||||||
export { ClientInfo } from "./client-info";
|
export { ClientInfo } from "./client-info";
|
||||||
|
export { ExportedAccount } from "./exported-account";
|
||||||
export { FederatedUserContext } from "./federated-user-context";
|
export { FederatedUserContext } from "./federated-user-context";
|
||||||
export { OobResult } from "./oob-result";
|
export { OobResult } from "./oob-result";
|
||||||
export { OtpResult } from "./otp-result";
|
export { OtpResult } from "./otp-result";
|
||||||
|
|||||||
@@ -2,24 +2,36 @@ import { IdpProvider, LastpassLoginType } from "../enums";
|
|||||||
|
|
||||||
export class UserTypeContext {
|
export class UserTypeContext {
|
||||||
type: LastpassLoginType;
|
type: LastpassLoginType;
|
||||||
IdentityProviderGUID: string;
|
identityProviderGUID: string;
|
||||||
IdentityProviderURL: string;
|
identityProviderURL: string;
|
||||||
OpenIDConnectAuthority: string;
|
openIDConnectAuthority: string;
|
||||||
OpenIDConnectClientId: string;
|
openIDConnectClientId: string;
|
||||||
CompanyId: number;
|
companyId: number;
|
||||||
Provider: IdpProvider;
|
provider: IdpProvider;
|
||||||
PkceEnabled: boolean;
|
pkceEnabled: boolean;
|
||||||
IsPasswordlessEnabled: boolean;
|
isPasswordlessEnabled: boolean;
|
||||||
|
|
||||||
isFederated(): boolean {
|
isFederated(): boolean {
|
||||||
return (
|
return (
|
||||||
this.type === LastpassLoginType.Federated &&
|
this.type === LastpassLoginType.Federated &&
|
||||||
this.hasValue(this.IdentityProviderURL) &&
|
this.hasValue(this.identityProviderURL) &&
|
||||||
this.hasValue(this.OpenIDConnectAuthority) &&
|
this.hasValue(this.openIDConnectAuthority) &&
|
||||||
this.hasValue(this.OpenIDConnectClientId)
|
this.hasValue(this.openIDConnectClientId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get oidcScope(): string {
|
||||||
|
let scope = "openid profile email";
|
||||||
|
if (this.provider === IdpProvider.PingOne) {
|
||||||
|
scope += " lastpass";
|
||||||
|
}
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
get openIDConnectAuthorityBase(): string {
|
||||||
|
return this.openIDConnectAuthority.replace("/.well-known/openid-configuration", "");
|
||||||
|
}
|
||||||
|
|
||||||
private hasValue(str: string) {
|
private hasValue(str: string) {
|
||||||
return str != null && str.trim() !== "";
|
return str != null && str.trim() !== "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,13 +229,13 @@ export class Client {
|
|||||||
let passcode: OtpResult = null;
|
let passcode: OtpResult = null;
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case OtpMethod.GoogleAuth:
|
case OtpMethod.GoogleAuth:
|
||||||
passcode = ui.provideGoogleAuthPasscode();
|
passcode = await ui.provideGoogleAuthPasscode();
|
||||||
break;
|
break;
|
||||||
case OtpMethod.MicrosoftAuth:
|
case OtpMethod.MicrosoftAuth:
|
||||||
passcode = ui.provideMicrosoftAuthPasscode();
|
passcode = await ui.provideMicrosoftAuthPasscode();
|
||||||
break;
|
break;
|
||||||
case OtpMethod.Yubikey:
|
case OtpMethod.Yubikey:
|
||||||
passcode = ui.provideYubikeyPasscode();
|
passcode = await ui.provideYubikeyPasscode();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid OTP method");
|
throw new Error("Invalid OTP method");
|
||||||
@@ -273,7 +273,7 @@ export class Client {
|
|||||||
ui: Ui,
|
ui: Ui,
|
||||||
rest: RestClient
|
rest: RestClient
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const answer = this.approveOob(username, parameters, ui, rest);
|
const answer = await this.approveOob(username, parameters, ui, rest);
|
||||||
if (answer == OobResult.cancel) {
|
if (answer == OobResult.cancel) {
|
||||||
throw new Error("Out of band step is canceled by the user");
|
throw new Error("Out of band step is canceled by the user");
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,12 @@ export class Client {
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
private approveOob(username: string, parameters: Map<string, string>, ui: Ui, rest: RestClient) {
|
private async approveOob(
|
||||||
|
username: string,
|
||||||
|
parameters: Map<string, string>,
|
||||||
|
ui: Ui,
|
||||||
|
rest: RestClient
|
||||||
|
): Promise<OobResult> {
|
||||||
const method = parameters.get("outofbandtype");
|
const method = parameters.get("outofbandtype");
|
||||||
if (method == null) {
|
if (method == null) {
|
||||||
throw new Error("Out of band method is not specified");
|
throw new Error("Out of band method is not specified");
|
||||||
@@ -335,12 +340,12 @@ export class Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private approveDuo(
|
private async approveDuo(
|
||||||
username: string,
|
username: string,
|
||||||
parameters: Map<string, string>,
|
parameters: Map<string, string>,
|
||||||
ui: Ui,
|
ui: Ui,
|
||||||
rest: RestClient
|
rest: RestClient
|
||||||
): OobResult {
|
): Promise<OobResult> {
|
||||||
return parameters.get("preferduowebsdk") == "1"
|
return parameters.get("preferduowebsdk") == "1"
|
||||||
? this.approveDuoWebSdk(username, parameters, ui, rest)
|
? this.approveDuoWebSdk(username, parameters, ui, rest)
|
||||||
: ui.approveDuo();
|
: ui.approveDuo();
|
||||||
@@ -525,6 +530,7 @@ export class Client {
|
|||||||
switch (cause.value) {
|
switch (cause.value) {
|
||||||
case "unknownemail":
|
case "unknownemail":
|
||||||
return "Invalid username";
|
return "Invalid username";
|
||||||
|
case "password_invalid":
|
||||||
case "unknownpassword":
|
case "unknownpassword":
|
||||||
return "Invalid password";
|
return "Invalid password";
|
||||||
case "googleauthfailed":
|
case "googleauthfailed":
|
||||||
|
|||||||
@@ -43,9 +43,6 @@ export class RestClient {
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const setBody = (requestInit: RequestInit, headerMap: Map<string, string>) => {
|
const setBody = (requestInit: RequestInit, headerMap: Map<string, string>) => {
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
if (headerMap == null) {
|
|
||||||
headerMap = new Map<string, string>();
|
|
||||||
}
|
|
||||||
headerMap.set("Content-Type", "application/json; charset=utf-8");
|
headerMap.set("Content-Type", "application/json; charset=utf-8");
|
||||||
requestInit.body = JSON.stringify(body);
|
requestInit.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
@@ -63,6 +60,9 @@ export class RestClient {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
};
|
};
|
||||||
|
if (headers == null) {
|
||||||
|
headers = new Map<string, string>();
|
||||||
|
}
|
||||||
setBody(requestInit, headers);
|
setBody(requestInit, headers);
|
||||||
this.setHeaders(requestInit, headers, cookies);
|
this.setHeaders(requestInit, headers, cookies);
|
||||||
const request = new Request(this.baseUrl + "/" + endpoint, requestInit);
|
const request = new Request(this.baseUrl + "/" + endpoint, requestInit);
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { DuoUi } from "./duo-ui";
|
|||||||
|
|
||||||
export abstract class Ui extends DuoUi {
|
export abstract class Ui extends DuoUi {
|
||||||
// To cancel return OtpResult.Cancel, otherwise only valid data is expected.
|
// To cancel return OtpResult.Cancel, otherwise only valid data is expected.
|
||||||
provideGoogleAuthPasscode: () => OtpResult;
|
provideGoogleAuthPasscode: () => Promise<OtpResult>;
|
||||||
provideMicrosoftAuthPasscode: () => OtpResult;
|
provideMicrosoftAuthPasscode: () => Promise<OtpResult>;
|
||||||
provideYubikeyPasscode: () => OtpResult;
|
provideYubikeyPasscode: () => Promise<OtpResult>;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The UI implementations should provide the following possibilities for the user:
|
The UI implementations should provide the following possibilities for the user:
|
||||||
@@ -23,7 +23,7 @@ export abstract class Ui extends DuoUi {
|
|||||||
passcode instead of performing an action in the app. In this case the UI should return
|
passcode instead of performing an action in the app. In this case the UI should return
|
||||||
OobResult.continueWithPasscode(passcode, rememberMe).
|
OobResult.continueWithPasscode(passcode, rememberMe).
|
||||||
*/
|
*/
|
||||||
approveLastPassAuth: () => OobResult;
|
approveLastPassAuth: () => Promise<OobResult>;
|
||||||
approveDuo: () => OobResult;
|
approveDuo: () => Promise<OobResult>;
|
||||||
approveSalesforceAuth: () => OobResult;
|
approveSalesforceAuth: () => Promise<OobResult>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import * as papa from "papaparse";
|
||||||
|
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
@@ -7,6 +9,7 @@ import { IdpProvider } from "./enums";
|
|||||||
import {
|
import {
|
||||||
Account,
|
Account,
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
|
ExportedAccount,
|
||||||
FederatedUserContext,
|
FederatedUserContext,
|
||||||
ParserOptions,
|
ParserOptions,
|
||||||
UserTypeContext,
|
UserTypeContext,
|
||||||
@@ -68,20 +71,35 @@ export class Vault {
|
|||||||
if (response.status === HttpStatusCode.Ok) {
|
if (response.status === HttpStatusCode.Ok) {
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
this.userType = new UserTypeContext();
|
this.userType = new UserTypeContext();
|
||||||
this.userType.CompanyId = json.CompanyId;
|
this.userType.companyId = json.CompanyId;
|
||||||
this.userType.IdentityProviderGUID = json.IdentityProviderGUID;
|
this.userType.identityProviderGUID = json.IdentityProviderGUID;
|
||||||
this.userType.IdentityProviderURL = json.IdentityProviderURL;
|
this.userType.identityProviderURL = json.IdentityProviderURL;
|
||||||
this.userType.IsPasswordlessEnabled = json.IsPasswordlessEnabled;
|
this.userType.isPasswordlessEnabled = json.IsPasswordlessEnabled;
|
||||||
this.userType.OpenIDConnectAuthority = json.OpenIDConnectAuthority;
|
this.userType.openIDConnectAuthority = json.OpenIDConnectAuthority;
|
||||||
this.userType.OpenIDConnectClientId = json.OpenIDConnectClientId;
|
this.userType.openIDConnectClientId = json.OpenIDConnectClientId;
|
||||||
this.userType.PkceEnabled = json.PkceEnabled;
|
this.userType.pkceEnabled = json.PkceEnabled;
|
||||||
this.userType.Provider = json.Provider;
|
this.userType.provider = json.Provider;
|
||||||
this.userType.type = json.type;
|
this.userType.type = json.type;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error("Cannot determine LastPass user type.");
|
throw new Error("Cannot determine LastPass user type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountsToExportedCsvString(skipShared = false): string {
|
||||||
|
if (this.accounts == null) {
|
||||||
|
throw new Error("Vault has not opened any accounts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportedAccounts = this.accounts
|
||||||
|
.filter((a) => !a.isShared || (a.isShared && !skipShared))
|
||||||
|
.map((a) => new ExportedAccount(a));
|
||||||
|
|
||||||
|
if (exportedAccounts.length === 0) {
|
||||||
|
throw new Error("No accounts to transform");
|
||||||
|
}
|
||||||
|
return papa.unparse(exportedAccounts);
|
||||||
|
}
|
||||||
|
|
||||||
private async getK1(federatedUser: FederatedUserContext): Promise<Uint8Array> {
|
private async getK1(federatedUser: FederatedUserContext): Promise<Uint8Array> {
|
||||||
if (this.userType == null) {
|
if (this.userType == null) {
|
||||||
throw new Error("User type is not set.");
|
throw new Error("User type is not set.");
|
||||||
@@ -96,18 +114,18 @@ export class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let k1: Uint8Array = null;
|
let k1: Uint8Array = null;
|
||||||
if (federatedUser.idpUserInfo?.LastPassK1 !== null) {
|
if (federatedUser.idpUserInfo?.LastPassK1 != null) {
|
||||||
return Utils.fromByteStringToArray(federatedUser.idpUserInfo.LastPassK1);
|
return Utils.fromByteStringToArray(federatedUser.idpUserInfo.LastPassK1);
|
||||||
} else if (this.userType.Provider === IdpProvider.Azure) {
|
} else if (this.userType.provider === IdpProvider.Azure) {
|
||||||
k1 = await this.getK1Azure(federatedUser);
|
k1 = await this.getK1Azure(federatedUser);
|
||||||
} else if (this.userType.Provider === IdpProvider.Google) {
|
} else if (this.userType.provider === IdpProvider.Google) {
|
||||||
k1 = await this.getK1Google(federatedUser);
|
k1 = await this.getK1Google(federatedUser);
|
||||||
} else {
|
} else {
|
||||||
const b64Encoded = this.userType.Provider === IdpProvider.PingOne;
|
const b64Encoded = this.userType.provider === IdpProvider.PingOne;
|
||||||
k1 = this.getK1FromAccessToken(federatedUser, b64Encoded);
|
k1 = await this.getK1FromAccessToken(federatedUser, b64Encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (k1 !== null) {
|
if (k1 != null) {
|
||||||
return k1;
|
return k1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +143,7 @@ export class Vault {
|
|||||||
if (response.status === HttpStatusCode.Ok) {
|
if (response.status === HttpStatusCode.Ok) {
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
const k1 = json?.extensions?.LastPassK1 as string;
|
const k1 = json?.extensions?.LastPassK1 as string;
|
||||||
if (k1 !== null) {
|
if (k1 != null) {
|
||||||
return Utils.fromB64ToArray(k1);
|
return Utils.fromB64ToArray(k1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +167,7 @@ export class Vault {
|
|||||||
if (response.status === HttpStatusCode.Ok) {
|
if (response.status === HttpStatusCode.Ok) {
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
const files = json?.files as any[];
|
const files = json?.files as any[];
|
||||||
if (files !== null && files.length > 0 && files[0].id != null && files[0].name === "k1.lp") {
|
if (files != null && files.length > 0 && files[0].id != null && files[0].name === "k1.lp") {
|
||||||
// Open the k1.lp file
|
// Open the k1.lp file
|
||||||
rest.baseUrl = "https://www.googleapis.com";
|
rest.baseUrl = "https://www.googleapis.com";
|
||||||
const response = await rest.get(
|
const response = await rest.get(
|
||||||
@@ -165,10 +183,10 @@ export class Vault {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getK1FromAccessToken(federatedUser: FederatedUserContext, b64: boolean) {
|
private async getK1FromAccessToken(federatedUser: FederatedUserContext, b64: boolean) {
|
||||||
const decodedAccessToken = this.tokenService.decodeToken(federatedUser.accessToken);
|
const decodedAccessToken = await this.tokenService.decodeToken(federatedUser.accessToken);
|
||||||
const k1 = decodedAccessToken?.LastPassK1 as string;
|
const k1 = decodedAccessToken?.LastPassK1 as string;
|
||||||
if (k1 !== null) {
|
if (k1 != null) {
|
||||||
return b64 ? Utils.fromB64ToArray(k1) : Utils.fromByteStringToArray(k1);
|
return b64 ? Utils.fromB64ToArray(k1) : Utils.fromByteStringToArray(k1);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -184,15 +202,15 @@ export class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rest = new RestClient();
|
const rest = new RestClient();
|
||||||
rest.baseUrl = this.userType.IdentityProviderURL;
|
rest.baseUrl = this.userType.identityProviderURL;
|
||||||
const response = await rest.postJson("federatedlogin/api/v1/getkey", {
|
const response = await rest.postJson("federatedlogin/api/v1/getkey", {
|
||||||
company_id: this.userType.CompanyId,
|
company_id: this.userType.companyId,
|
||||||
id_token: federatedUser.idToken,
|
id_token: federatedUser.idToken,
|
||||||
});
|
});
|
||||||
if (response.status === HttpStatusCode.Ok) {
|
if (response.status === HttpStatusCode.Ok) {
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
const k2 = json?.k2 as string;
|
const k2 = json?.k2 as string;
|
||||||
if (k2 !== null) {
|
if (k2 != null) {
|
||||||
return Utils.fromB64ToArray(k2);
|
return Utils.fromB64ToArray(k2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user