mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-21 21:11:35 +01: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:
parent
790d666929
commit
13df63fbac
@ -1 +1,2 @@
|
||||
export { ClientInfo } from "./models";
|
||||
export { Vault } from "./vault";
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { Platform } from "../enums";
|
||||
|
||||
export class ClientInfo {
|
||||
platform: Platform;
|
||||
id: 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 { Chunk } from "./chunk";
|
||||
export { ClientInfo } from "./client-info";
|
||||
export { ExportedAccount } from "./exported-account";
|
||||
export { FederatedUserContext } from "./federated-user-context";
|
||||
export { OobResult } from "./oob-result";
|
||||
export { OtpResult } from "./otp-result";
|
||||
|
@ -2,24 +2,36 @@ import { IdpProvider, LastpassLoginType } from "../enums";
|
||||
|
||||
export class UserTypeContext {
|
||||
type: LastpassLoginType;
|
||||
IdentityProviderGUID: string;
|
||||
IdentityProviderURL: string;
|
||||
OpenIDConnectAuthority: string;
|
||||
OpenIDConnectClientId: string;
|
||||
CompanyId: number;
|
||||
Provider: IdpProvider;
|
||||
PkceEnabled: boolean;
|
||||
IsPasswordlessEnabled: boolean;
|
||||
identityProviderGUID: string;
|
||||
identityProviderURL: string;
|
||||
openIDConnectAuthority: string;
|
||||
openIDConnectClientId: string;
|
||||
companyId: number;
|
||||
provider: IdpProvider;
|
||||
pkceEnabled: boolean;
|
||||
isPasswordlessEnabled: boolean;
|
||||
|
||||
isFederated(): boolean {
|
||||
return (
|
||||
this.type === LastpassLoginType.Federated &&
|
||||
this.hasValue(this.IdentityProviderURL) &&
|
||||
this.hasValue(this.OpenIDConnectAuthority) &&
|
||||
this.hasValue(this.OpenIDConnectClientId)
|
||||
this.hasValue(this.identityProviderURL) &&
|
||||
this.hasValue(this.openIDConnectAuthority) &&
|
||||
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) {
|
||||
return str != null && str.trim() !== "";
|
||||
}
|
||||
|
@ -229,13 +229,13 @@ export class Client {
|
||||
let passcode: OtpResult = null;
|
||||
switch (method) {
|
||||
case OtpMethod.GoogleAuth:
|
||||
passcode = ui.provideGoogleAuthPasscode();
|
||||
passcode = await ui.provideGoogleAuthPasscode();
|
||||
break;
|
||||
case OtpMethod.MicrosoftAuth:
|
||||
passcode = ui.provideMicrosoftAuthPasscode();
|
||||
passcode = await ui.provideMicrosoftAuthPasscode();
|
||||
break;
|
||||
case OtpMethod.Yubikey:
|
||||
passcode = ui.provideYubikeyPasscode();
|
||||
passcode = await ui.provideYubikeyPasscode();
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid OTP method");
|
||||
@ -273,7 +273,7 @@ export class Client {
|
||||
ui: Ui,
|
||||
rest: RestClient
|
||||
): Promise<Session> {
|
||||
const answer = this.approveOob(username, parameters, ui, rest);
|
||||
const answer = await this.approveOob(username, parameters, ui, rest);
|
||||
if (answer == OobResult.cancel) {
|
||||
throw new Error("Out of band step is canceled by the user");
|
||||
}
|
||||
@ -318,7 +318,12 @@ export class Client {
|
||||
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");
|
||||
if (method == null) {
|
||||
throw new Error("Out of band method is not specified");
|
||||
@ -335,12 +340,12 @@ export class Client {
|
||||
}
|
||||
}
|
||||
|
||||
private approveDuo(
|
||||
private async approveDuo(
|
||||
username: string,
|
||||
parameters: Map<string, string>,
|
||||
ui: Ui,
|
||||
rest: RestClient
|
||||
): OobResult {
|
||||
): Promise<OobResult> {
|
||||
return parameters.get("preferduowebsdk") == "1"
|
||||
? this.approveDuoWebSdk(username, parameters, ui, rest)
|
||||
: ui.approveDuo();
|
||||
@ -525,6 +530,7 @@ export class Client {
|
||||
switch (cause.value) {
|
||||
case "unknownemail":
|
||||
return "Invalid username";
|
||||
case "password_invalid":
|
||||
case "unknownpassword":
|
||||
return "Invalid password";
|
||||
case "googleauthfailed":
|
||||
|
@ -43,9 +43,6 @@ export class RestClient {
|
||||
): Promise<Response> {
|
||||
const setBody = (requestInit: RequestInit, headerMap: Map<string, string>) => {
|
||||
if (body != null) {
|
||||
if (headerMap == null) {
|
||||
headerMap = new Map<string, string>();
|
||||
}
|
||||
headerMap.set("Content-Type", "application/json; charset=utf-8");
|
||||
requestInit.body = JSON.stringify(body);
|
||||
}
|
||||
@ -63,6 +60,9 @@ export class RestClient {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
};
|
||||
if (headers == null) {
|
||||
headers = new Map<string, string>();
|
||||
}
|
||||
setBody(requestInit, headers);
|
||||
this.setHeaders(requestInit, headers, cookies);
|
||||
const request = new Request(this.baseUrl + "/" + endpoint, requestInit);
|
||||
|
@ -4,9 +4,9 @@ import { DuoUi } from "./duo-ui";
|
||||
|
||||
export abstract class Ui extends DuoUi {
|
||||
// To cancel return OtpResult.Cancel, otherwise only valid data is expected.
|
||||
provideGoogleAuthPasscode: () => OtpResult;
|
||||
provideMicrosoftAuthPasscode: () => OtpResult;
|
||||
provideYubikeyPasscode: () => OtpResult;
|
||||
provideGoogleAuthPasscode: () => Promise<OtpResult>;
|
||||
provideMicrosoftAuthPasscode: () => Promise<OtpResult>;
|
||||
provideYubikeyPasscode: () => Promise<OtpResult>;
|
||||
|
||||
/*
|
||||
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
|
||||
OobResult.continueWithPasscode(passcode, rememberMe).
|
||||
*/
|
||||
approveLastPassAuth: () => OobResult;
|
||||
approveDuo: () => OobResult;
|
||||
approveSalesforceAuth: () => OobResult;
|
||||
approveLastPassAuth: () => Promise<OobResult>;
|
||||
approveDuo: () => Promise<OobResult>;
|
||||
approveSalesforceAuth: () => Promise<OobResult>;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import * as papa from "papaparse";
|
||||
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
@ -7,6 +9,7 @@ import { IdpProvider } from "./enums";
|
||||
import {
|
||||
Account,
|
||||
ClientInfo,
|
||||
ExportedAccount,
|
||||
FederatedUserContext,
|
||||
ParserOptions,
|
||||
UserTypeContext,
|
||||
@ -68,20 +71,35 @@ export class Vault {
|
||||
if (response.status === HttpStatusCode.Ok) {
|
||||
const json = await response.json();
|
||||
this.userType = new UserTypeContext();
|
||||
this.userType.CompanyId = json.CompanyId;
|
||||
this.userType.IdentityProviderGUID = json.IdentityProviderGUID;
|
||||
this.userType.IdentityProviderURL = json.IdentityProviderURL;
|
||||
this.userType.IsPasswordlessEnabled = json.IsPasswordlessEnabled;
|
||||
this.userType.OpenIDConnectAuthority = json.OpenIDConnectAuthority;
|
||||
this.userType.OpenIDConnectClientId = json.OpenIDConnectClientId;
|
||||
this.userType.PkceEnabled = json.PkceEnabled;
|
||||
this.userType.Provider = json.Provider;
|
||||
this.userType.companyId = json.CompanyId;
|
||||
this.userType.identityProviderGUID = json.IdentityProviderGUID;
|
||||
this.userType.identityProviderURL = json.IdentityProviderURL;
|
||||
this.userType.isPasswordlessEnabled = json.IsPasswordlessEnabled;
|
||||
this.userType.openIDConnectAuthority = json.OpenIDConnectAuthority;
|
||||
this.userType.openIDConnectClientId = json.OpenIDConnectClientId;
|
||||
this.userType.pkceEnabled = json.PkceEnabled;
|
||||
this.userType.provider = json.Provider;
|
||||
this.userType.type = json.type;
|
||||
return;
|
||||
}
|
||||
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> {
|
||||
if (this.userType == null) {
|
||||
throw new Error("User type is not set.");
|
||||
@ -96,18 +114,18 @@ export class Vault {
|
||||
}
|
||||
|
||||
let k1: Uint8Array = null;
|
||||
if (federatedUser.idpUserInfo?.LastPassK1 !== null) {
|
||||
if (federatedUser.idpUserInfo?.LastPassK1 != null) {
|
||||
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);
|
||||
} else if (this.userType.Provider === IdpProvider.Google) {
|
||||
} else if (this.userType.provider === IdpProvider.Google) {
|
||||
k1 = await this.getK1Google(federatedUser);
|
||||
} else {
|
||||
const b64Encoded = this.userType.Provider === IdpProvider.PingOne;
|
||||
k1 = this.getK1FromAccessToken(federatedUser, b64Encoded);
|
||||
const b64Encoded = this.userType.provider === IdpProvider.PingOne;
|
||||
k1 = await this.getK1FromAccessToken(federatedUser, b64Encoded);
|
||||
}
|
||||
|
||||
if (k1 !== null) {
|
||||
if (k1 != null) {
|
||||
return k1;
|
||||
}
|
||||
|
||||
@ -125,7 +143,7 @@ export class Vault {
|
||||
if (response.status === HttpStatusCode.Ok) {
|
||||
const json = await response.json();
|
||||
const k1 = json?.extensions?.LastPassK1 as string;
|
||||
if (k1 !== null) {
|
||||
if (k1 != null) {
|
||||
return Utils.fromB64ToArray(k1);
|
||||
}
|
||||
}
|
||||
@ -149,7 +167,7 @@ export class Vault {
|
||||
if (response.status === HttpStatusCode.Ok) {
|
||||
const json = await response.json();
|
||||
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
|
||||
rest.baseUrl = "https://www.googleapis.com";
|
||||
const response = await rest.get(
|
||||
@ -165,10 +183,10 @@ export class Vault {
|
||||
return null;
|
||||
}
|
||||
|
||||
private getK1FromAccessToken(federatedUser: FederatedUserContext, b64: boolean) {
|
||||
const decodedAccessToken = this.tokenService.decodeToken(federatedUser.accessToken);
|
||||
private async getK1FromAccessToken(federatedUser: FederatedUserContext, b64: boolean) {
|
||||
const decodedAccessToken = await this.tokenService.decodeToken(federatedUser.accessToken);
|
||||
const k1 = decodedAccessToken?.LastPassK1 as string;
|
||||
if (k1 !== null) {
|
||||
if (k1 != null) {
|
||||
return b64 ? Utils.fromB64ToArray(k1) : Utils.fromByteStringToArray(k1);
|
||||
}
|
||||
return null;
|
||||
@ -184,15 +202,15 @@ export class Vault {
|
||||
}
|
||||
|
||||
const rest = new RestClient();
|
||||
rest.baseUrl = this.userType.IdentityProviderURL;
|
||||
rest.baseUrl = this.userType.identityProviderURL;
|
||||
const response = await rest.postJson("federatedlogin/api/v1/getkey", {
|
||||
company_id: this.userType.CompanyId,
|
||||
company_id: this.userType.companyId,
|
||||
id_token: federatedUser.idToken,
|
||||
});
|
||||
if (response.status === HttpStatusCode.Ok) {
|
||||
const json = await response.json();
|
||||
const k2 = json?.k2 as string;
|
||||
if (k2 !== null) {
|
||||
if (k2 != null) {
|
||||
return Utils.fromB64ToArray(k2);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user