mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
chore(captcha): [PM-15162] Remove handling of captcha enforcement and bypass token
* Removed captcha references. * Removed connectors from webpack * Fixed extra parameter. * Resolve merge conflicts. * Fixed extra argument. * Fixed failing tests. * Fixed failing test. * Accessibility cookie cleanup * Cleaned up accessibility component. * Deleted old registration endpoint * Remove unused register request object. * Fixed merge error that changed font family. * Fixed formatting from merge. * Linting
This commit is contained in:
@@ -211,12 +211,6 @@ p.lead {
|
||||
}
|
||||
}
|
||||
|
||||
#hcaptcha_iframe {
|
||||
width: 100%;
|
||||
border: none;
|
||||
transition: height 0.25s linear;
|
||||
}
|
||||
|
||||
body.linux-webauthn {
|
||||
width: 485px !important;
|
||||
#web-authn-frame {
|
||||
|
||||
@@ -33,7 +33,6 @@ import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
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";
|
||||
@@ -222,7 +221,7 @@ export class LoginCommand {
|
||||
);
|
||||
} else {
|
||||
response = await this.loginStrategyService.logIn(
|
||||
new PasswordLoginCredentials(email, password, null, twoFactor),
|
||||
new PasswordLoginCredentials(email, password, twoFactor),
|
||||
);
|
||||
}
|
||||
if (response.requiresEncryptionKeyMigration) {
|
||||
@@ -230,17 +229,6 @@ export class LoginCommand {
|
||||
"Encryption key migration required. Please login through the web vault to update your encryption key.",
|
||||
);
|
||||
}
|
||||
if (response.captchaSiteKey) {
|
||||
const credentials = new PasswordLoginCredentials(email, password);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
|
||||
|
||||
// Error Response
|
||||
if (handledResponse instanceof Response) {
|
||||
return handledResponse;
|
||||
} else {
|
||||
response = handledResponse;
|
||||
}
|
||||
}
|
||||
if (response.requiresTwoFactor) {
|
||||
const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
@@ -312,7 +300,6 @@ export class LoginCommand {
|
||||
|
||||
response = await this.loginStrategyService.logInTwoFactor(
|
||||
new TokenTwoFactorRequest(selectedProvider.type, twoFactorToken),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,18 +323,6 @@ export class LoginCommand {
|
||||
response = await this.loginStrategyService.logInNewDeviceVerification(newDeviceToken);
|
||||
}
|
||||
|
||||
if (response.captchaSiteKey) {
|
||||
const twoFactorRequest = new TokenTwoFactorRequest(selectedProvider.type, twoFactorToken);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);
|
||||
|
||||
// Error Response
|
||||
if (handledResponse instanceof Response) {
|
||||
return handledResponse;
|
||||
} else {
|
||||
response = handledResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.requiresTwoFactor) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
@@ -629,48 +604,6 @@ export class LoginCommand {
|
||||
return { newPasswordHash, newUserKey: newUserKey, hint: masterPasswordHint };
|
||||
}
|
||||
|
||||
private async handleCaptchaRequired(
|
||||
twoFactorRequest: TokenTwoFactorRequest,
|
||||
credentials: PasswordLoginCredentials = null,
|
||||
): Promise<AuthResult | Response> {
|
||||
const badCaptcha = Response.badRequest(
|
||||
"Your authentication request has been flagged and will require user interaction to proceed.\n" +
|
||||
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
|
||||
"(https://bitwarden.com/help/cli-auth-challenges)",
|
||||
);
|
||||
|
||||
try {
|
||||
const captchaClientSecret = await this.apiClientSecret(true);
|
||||
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
|
||||
return badCaptcha;
|
||||
}
|
||||
|
||||
let authResultResponse: AuthResult = null;
|
||||
if (credentials != null) {
|
||||
credentials.captchaToken = captchaClientSecret;
|
||||
credentials.twoFactor = twoFactorRequest;
|
||||
authResultResponse = await this.loginStrategyService.logIn(credentials);
|
||||
} else {
|
||||
authResultResponse = await this.loginStrategyService.logInTwoFactor(
|
||||
twoFactorRequest,
|
||||
captchaClientSecret,
|
||||
);
|
||||
}
|
||||
|
||||
return authResultResponse;
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof ErrorResponse ||
|
||||
(e.constructor.name === ErrorResponse.name &&
|
||||
(e as ErrorResponse).message.includes("Captcha is invalid"))
|
||||
) {
|
||||
return badCaptcha;
|
||||
} else {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async apiClientId(): Promise<string> {
|
||||
let clientId: string = null;
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
@@ -111,7 +110,6 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
}),
|
||||
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
|
||||
{ path: "set-password", component: SetPasswordComponent },
|
||||
{
|
||||
path: "send",
|
||||
|
||||
@@ -10,7 +10,6 @@ import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-
|
||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||
import { CalloutModule, DialogModule } from "@bitwarden/components";
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { LoginModule } from "../auth/login/login.module";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
@@ -59,7 +58,6 @@ import { SharedModule } from "./shared/shared.module";
|
||||
VaultV2Component,
|
||||
],
|
||||
declarations: [
|
||||
AccessibilityCookieComponent,
|
||||
AccountSwitcherComponent,
|
||||
AddEditComponent,
|
||||
AddEditCustomFieldsComponent,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<form id="accessibility-cookie-page" #form [formGroup]="accessibilityForm" (ngSubmit)="submit()">
|
||||
<div class="content">
|
||||
<h1>{{ "loadAccessibilityCookie" | i18n }}</h1>
|
||||
<p>
|
||||
{{ "registerAccessibilityUser" | i18n }}
|
||||
<a (click)="registerhCaptcha()">hcaptcha.com</a>.
|
||||
{{ "copyPasteLink" | i18n }}
|
||||
</p>
|
||||
<div class="box last">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="link">{{ "hCaptchaUrl" | i18n }}</label>
|
||||
<input
|
||||
id="link"
|
||||
type="text"
|
||||
name="Link"
|
||||
aria-describedby="linkHelp"
|
||||
formControlName="link"
|
||||
placeholder="{{ 'ex' | i18n }} https://accounts.hcaptcha.com/verify_email"
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="linkHelp" class="box-footer">{{ "enterhCaptchaUrl" | i18n }}</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button type="submit" class="btn primary block" [disabled]="!accessibilityForm.valid">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button type="button" (click)="close()" class="btn block">{{ "done" | i18n }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,76 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { UntypedFormControl, UntypedFormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-accessibility-cookie",
|
||||
templateUrl: "accessibility-cookie.component.html",
|
||||
})
|
||||
export class AccessibilityCookieComponent {
|
||||
listenForCookie = false;
|
||||
hCaptchaWindow: Window;
|
||||
|
||||
accessibilityForm = new UntypedFormGroup({
|
||||
link: new UntypedFormControl("", Validators.required),
|
||||
});
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected i18nService: I18nService,
|
||||
protected ngZone: NgZone,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
registerhCaptcha() {
|
||||
this.platformUtilsService.launchUri("https://www.hcaptcha.com/accessibility");
|
||||
}
|
||||
|
||||
async close() {
|
||||
const [cookie] = await ipc.auth.getHcaptchaAccessibilityCookie();
|
||||
if (cookie) {
|
||||
this.onCookieSavedSuccess();
|
||||
} else {
|
||||
this.onCookieSavedFailure();
|
||||
}
|
||||
await this.router.navigate(["/login"]);
|
||||
}
|
||||
|
||||
onCookieSavedSuccess() {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("accessibilityCookieSaved"),
|
||||
});
|
||||
}
|
||||
|
||||
onCookieSavedFailure() {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("noAccessibilityCookieSaved"),
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (Utils.getHostname(this.accessibilityForm.value.link) !== "accounts.hcaptcha.com") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidUrl"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.listenForCookie = true;
|
||||
window.open(this.accessibilityForm.value.link, "_blank", "noopener noreferrer");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
export default {
|
||||
getHcaptchaAccessibilityCookie: (): Promise<[string]> =>
|
||||
ipcRenderer.invoke("getCookie", { url: "https://www.hcaptcha.com/", name: "hc_accessibility" }),
|
||||
|
||||
loginRequest: (alertTitle: string, alertBody: string, buttonText: string): Promise<void> =>
|
||||
ipcRenderer.invoke("loginRequest", {
|
||||
alertTitle,
|
||||
|
||||
@@ -1725,40 +1725,9 @@
|
||||
"filePasswordAndConfirmFilePasswordDoNotMatch": {
|
||||
"message": "“File password” and “Confirm file password“ do not match."
|
||||
},
|
||||
"hCaptchaUrl": {
|
||||
"message": "hCaptcha Url",
|
||||
"description": "hCaptcha is the name of a website, should not be translated"
|
||||
},
|
||||
"loadAccessibilityCookie": {
|
||||
"message": "Load accessibility cookie"
|
||||
},
|
||||
"registerAccessibilityUser": {
|
||||
"message": "Register as an accessibility user at",
|
||||
"description": "ex. Register as an accessibility user at hcaptcha.com"
|
||||
},
|
||||
"copyPasteLink": {
|
||||
"message": "Copy and paste the link sent to your email below"
|
||||
},
|
||||
"enterhCaptchaUrl": {
|
||||
"message": "Enter URL to load accessibility cookie for hCaptcha",
|
||||
"description": "hCaptcha is the name of a website, should not be translated"
|
||||
},
|
||||
"hCaptchaUrlRequired": {
|
||||
"message": "hCaptcha Url is required",
|
||||
"description": "hCaptcha is the name of a website, should not be translated"
|
||||
},
|
||||
"invalidUrl": {
|
||||
"message": "Invalid Url"
|
||||
},
|
||||
"done": {
|
||||
"message": "Done"
|
||||
},
|
||||
"accessibilityCookieSaved": {
|
||||
"message": "Accessibility cookie saved!"
|
||||
},
|
||||
"noAccessibilityCookieSaved": {
|
||||
"message": "No accessibility cookie saved"
|
||||
},
|
||||
"warning": {
|
||||
"message": "WARNING",
|
||||
"description": "WARNING (should stay in capitalized letters if the language permits)"
|
||||
|
||||
@@ -252,12 +252,6 @@ p.lead {
|
||||
}
|
||||
}
|
||||
|
||||
#hcaptcha_iframe {
|
||||
width: 100%;
|
||||
border: none;
|
||||
transition: height 0.25s linear;
|
||||
}
|
||||
|
||||
form,
|
||||
.form {
|
||||
.form-group {
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#accessibility-cookie-page,
|
||||
#register-page,
|
||||
#hint-page,
|
||||
#update-temp-password-page,
|
||||
@@ -43,7 +42,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#accessibility-cookie-page,
|
||||
#register-page,
|
||||
#hint-page,
|
||||
#lock-page,
|
||||
|
||||
@@ -52,10 +52,6 @@ export class ElectronMainMessagingService implements MessageSender {
|
||||
return windowMain.win?.isVisible();
|
||||
});
|
||||
|
||||
ipcMain.handle("getCookie", async (event, options) => {
|
||||
return await this.windowMain.session.cookies.get(options);
|
||||
});
|
||||
|
||||
ipcMain.handle("loginRequest", async (event, options) => {
|
||||
const alert = new Notification({
|
||||
title: options.alertTitle,
|
||||
|
||||
@@ -172,7 +172,6 @@ describe("WebRegistrationFinishService", () => {
|
||||
let userKey: UserKey;
|
||||
let userKeyEncString: EncString;
|
||||
let userKeyPair: [string, EncString];
|
||||
let capchaBypassToken: string;
|
||||
|
||||
let orgInvite: OrganizationInvite;
|
||||
let orgSponsoredFreeFamilyPlanToken: string;
|
||||
@@ -198,7 +197,6 @@ describe("WebRegistrationFinishService", () => {
|
||||
userKeyEncString = new EncString("userKeyEncrypted");
|
||||
|
||||
userKeyPair = ["publicKey", new EncString("privateKey")];
|
||||
capchaBypassToken = "capchaBypassToken";
|
||||
|
||||
orgInvite = new OrganizationInvite();
|
||||
orgInvite.organizationUserId = "organizationUserId";
|
||||
@@ -219,19 +217,13 @@ describe("WebRegistrationFinishService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("registers the user and returns a captcha bypass token when given valid email verification input", async () => {
|
||||
it("registers the user when given valid email verification input", async () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
const result = await service.finishRegistration(
|
||||
email,
|
||||
passwordInputResult,
|
||||
emailVerificationToken,
|
||||
);
|
||||
|
||||
expect(result).toEqual(capchaBypassToken);
|
||||
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
|
||||
|
||||
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
|
||||
@@ -261,15 +253,13 @@ describe("WebRegistrationFinishService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("it registers the user and returns a captcha bypass token when given an org invite", async () => {
|
||||
it("it registers the user when given an org invite", async () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
|
||||
const result = await service.finishRegistration(email, passwordInputResult);
|
||||
|
||||
expect(result).toEqual(capchaBypassToken);
|
||||
await service.finishRegistration(email, passwordInputResult);
|
||||
|
||||
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
|
||||
@@ -299,21 +289,19 @@ describe("WebRegistrationFinishService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("registers the user and returns a captcha bypass token when given an org sponsored free family plan token", async () => {
|
||||
it("registers the user when given an org sponsored free family plan token", async () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
const result = await service.finishRegistration(
|
||||
await service.finishRegistration(
|
||||
email,
|
||||
passwordInputResult,
|
||||
undefined,
|
||||
orgSponsoredFreeFamilyPlanToken,
|
||||
);
|
||||
|
||||
expect(result).toEqual(capchaBypassToken);
|
||||
|
||||
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
|
||||
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
|
||||
@@ -342,13 +330,13 @@ describe("WebRegistrationFinishService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("registers the user and returns a captcha bypass token when given an emergency access invite token", async () => {
|
||||
it("registers the user when given an emergency access invite token", async () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
const result = await service.finishRegistration(
|
||||
await service.finishRegistration(
|
||||
email,
|
||||
passwordInputResult,
|
||||
undefined,
|
||||
@@ -357,8 +345,6 @@ describe("WebRegistrationFinishService", () => {
|
||||
emergencyAccessId,
|
||||
);
|
||||
|
||||
expect(result).toEqual(capchaBypassToken);
|
||||
|
||||
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
|
||||
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
|
||||
@@ -387,13 +373,13 @@ describe("WebRegistrationFinishService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("registers the user and returns a captcha bypass token when given a provider invite token", async () => {
|
||||
it("registers the user when given a provider invite token", async () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
const result = await service.finishRegistration(
|
||||
await service.finishRegistration(
|
||||
email,
|
||||
passwordInputResult,
|
||||
undefined,
|
||||
@@ -404,8 +390,6 @@ describe("WebRegistrationFinishService", () => {
|
||||
providerUserId,
|
||||
);
|
||||
|
||||
expect(result).toEqual(capchaBypassToken);
|
||||
|
||||
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
|
||||
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
|
||||
|
||||
@@ -80,12 +80,7 @@ export class RecoverTwoFactorComponent implements OnInit {
|
||||
remember: false,
|
||||
};
|
||||
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
email,
|
||||
this.masterPassword,
|
||||
"",
|
||||
twoFactorRequest,
|
||||
);
|
||||
const credentials = new PasswordLoginCredentials(email, this.masterPassword, twoFactorRequest);
|
||||
|
||||
try {
|
||||
const authResult = await this.loginStrategyService.logIn(credentials);
|
||||
|
||||
@@ -366,14 +366,9 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const captchaToken = await this.finishRegistration(passwordInputResult);
|
||||
await this.finishRegistration(passwordInputResult);
|
||||
|
||||
if (captchaToken == null) {
|
||||
this.submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.logIn(passwordInputResult.newPassword, captchaToken);
|
||||
await this.logIn(passwordInputResult.newPassword);
|
||||
|
||||
this.submitting = false;
|
||||
|
||||
@@ -389,14 +384,9 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/** Logs the user in based using the token received by the `finishRegistration` method */
|
||||
private async logIn(masterPassword: string, captchaBypassToken: string): Promise<void> {
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
this.email,
|
||||
masterPassword,
|
||||
captchaBypassToken,
|
||||
null,
|
||||
);
|
||||
/** Logs the user in */
|
||||
private async logIn(masterPassword: string): Promise<void> {
|
||||
const credentials = new PasswordLoginCredentials(this.email, masterPassword);
|
||||
|
||||
await this.loginStrategyService.logIn(credentials);
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<!doctype html>
|
||||
<html class="theme_light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
|
||||
/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<title>Bitwarden Captcha Connector</title>
|
||||
</head>
|
||||
|
||||
<body class="layout_frontend">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div>
|
||||
<img src="..//images/logo-dark@2x.png" class="logo mb-2" alt="Bitwarden" />
|
||||
<p id="captchaRequired" class="lead text-center mx-4 mb-4">Captcha Required</p>
|
||||
<div id="captcha"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
@import "../scss/styles.scss";
|
||||
@@ -1,17 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
|
||||
/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<title>Bitwarden Captcha Connector</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="captcha"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +0,0 @@
|
||||
body {
|
||||
min-width: 0px !important;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { b64Decode, getQsParam } from "./common";
|
||||
|
||||
declare let hcaptcha: any;
|
||||
|
||||
if (window.location.pathname.includes("mobile")) {
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("./captcha-mobile.scss");
|
||||
} else {
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("./captcha.scss");
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// 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
|
||||
init();
|
||||
});
|
||||
|
||||
(window as any).captchaSuccess = captchaSuccess;
|
||||
(window as any).captchaError = captchaError;
|
||||
|
||||
let parentUrl: string = null;
|
||||
let parentOrigin: string = null;
|
||||
let mobileResponse: boolean = null;
|
||||
let sentSuccess = false;
|
||||
|
||||
async function init() {
|
||||
await start();
|
||||
onMessage();
|
||||
}
|
||||
|
||||
async function start() {
|
||||
sentSuccess = false;
|
||||
|
||||
const data = getQsParam("data");
|
||||
if (!data) {
|
||||
error("No data.");
|
||||
return;
|
||||
}
|
||||
|
||||
parentUrl = getQsParam("parent");
|
||||
if (!parentUrl) {
|
||||
error("No parent.");
|
||||
return;
|
||||
} else {
|
||||
parentUrl = decodeURIComponent(parentUrl);
|
||||
parentOrigin = new URL(parentUrl).origin;
|
||||
}
|
||||
|
||||
let decodedData: any;
|
||||
try {
|
||||
decodedData = JSON.parse(b64Decode(data, true));
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
error("Cannot parse data.");
|
||||
return;
|
||||
}
|
||||
mobileResponse = decodedData.callbackUri != null || decodedData.mobile === true;
|
||||
|
||||
let src = "https://hcaptcha.com/1/api.js?render=explicit";
|
||||
|
||||
// Set language code
|
||||
if (decodedData.locale) {
|
||||
src += `&hl=${encodeURIComponent(decodedData.locale) ?? "en"}`;
|
||||
}
|
||||
|
||||
// Set captchaRequired subtitle for mobile
|
||||
const subtitleEl = document.getElementById("captchaRequired");
|
||||
if (decodedData.captchaRequiredText && subtitleEl) {
|
||||
subtitleEl.textContent = decodedData.captchaRequiredText;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.addEventListener("load", () => {
|
||||
hcaptcha.render("captcha", {
|
||||
sitekey: encodeURIComponent(decodedData.siteKey),
|
||||
callback: "captchaSuccess",
|
||||
"error-callback": "captchaError",
|
||||
});
|
||||
// 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
|
||||
watchHeight();
|
||||
});
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function captchaSuccess(response: string) {
|
||||
if (mobileResponse) {
|
||||
document.location.replace("bitwarden://captcha-callback?token=" + encodeURIComponent(response));
|
||||
} else {
|
||||
success(response);
|
||||
}
|
||||
}
|
||||
|
||||
function captchaError() {
|
||||
error("An error occurred with the captcha. Try again.");
|
||||
}
|
||||
|
||||
function onMessage() {
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (!event.origin || event.origin === "" || event.origin !== parentOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data === "start") {
|
||||
// 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
|
||||
start();
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
function error(message: string) {
|
||||
parent.postMessage("error|" + message, parentUrl);
|
||||
}
|
||||
|
||||
function success(data: string) {
|
||||
if (sentSuccess) {
|
||||
return;
|
||||
}
|
||||
parent.postMessage("success|" + data, parentUrl);
|
||||
sentSuccess = true;
|
||||
}
|
||||
|
||||
function info(message: string | object) {
|
||||
parent.postMessage("info|" + JSON.stringify(message), parentUrl);
|
||||
}
|
||||
|
||||
async function watchHeight() {
|
||||
const imagesDiv = document.body.lastChild as HTMLElement;
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
info({
|
||||
height:
|
||||
imagesDiv.style.visibility === "hidden"
|
||||
? document.documentElement.offsetHeight
|
||||
: document.documentElement.scrollHeight,
|
||||
width: document.documentElement.scrollWidth,
|
||||
});
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
async function sleep(ms: number) {
|
||||
await new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
@@ -24,12 +24,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#hcaptcha_iframe {
|
||||
border: none;
|
||||
transition: height 0.25s linear;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@each $mfaType in $mfaTypes {
|
||||
.mfaType#{$mfaType} {
|
||||
content: url("../images/two-factor/" + $mfaType + ".png");
|
||||
|
||||
@@ -129,16 +129,6 @@ const plugins = [
|
||||
filename: "redirect-connector.html",
|
||||
chunks: ["connectors/redirect", "styles"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/connectors/captcha.html",
|
||||
filename: "captcha-connector.html",
|
||||
chunks: ["connectors/captcha"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/connectors/captcha-mobile.html",
|
||||
filename: "captcha-mobile-connector.html",
|
||||
chunks: ["connectors/captcha"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/connectors/duo-redirect.html",
|
||||
filename: "duo-redirect-connector.html",
|
||||
@@ -344,7 +334,6 @@ const webpackConfig = {
|
||||
"connectors/webauthn": "./src/connectors/webauthn.ts",
|
||||
"connectors/webauthn-fallback": "./src/connectors/webauthn-fallback.ts",
|
||||
"connectors/sso": "./src/connectors/sso.ts",
|
||||
"connectors/captcha": "./src/connectors/captcha.ts",
|
||||
"connectors/duo-redirect": "./src/connectors/duo-redirect.ts",
|
||||
"connectors/redirect": "./src/connectors/redirect.ts",
|
||||
styles: ["./src/scss/styles.scss", "./src/scss/tailwind.css"],
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, Input } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export abstract class CaptchaProtectedComponent {
|
||||
@Input() captchaSiteKey: string = null;
|
||||
captchaToken: string = null;
|
||||
captcha: CaptchaIFrame;
|
||||
|
||||
constructor(
|
||||
protected environmentService: EnvironmentService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async setupCaptcha() {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
|
||||
this.captcha = new CaptchaIFrame(
|
||||
window,
|
||||
webVaultUrl,
|
||||
this.i18nService,
|
||||
(token: string) => {
|
||||
this.captchaToken = token;
|
||||
},
|
||||
(error: string) => {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: error,
|
||||
});
|
||||
},
|
||||
(info: string) => {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: this.i18nService.t("info"),
|
||||
message: info,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
showCaptcha() {
|
||||
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||
}
|
||||
|
||||
protected handleCaptchaRequired(response: { captchaSiteKey: string }): boolean {
|
||||
if (Utils.isNullOrWhitespace(response.captchaSiteKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.captchaSiteKey = response.captchaSiteKey;
|
||||
this.captcha.init(response.captchaSiteKey);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
|
||||
@@ -39,11 +38,6 @@ export class ApiActionDirective implements OnChanges {
|
||||
},
|
||||
(e: any) => {
|
||||
this.el.nativeElement.loading = false;
|
||||
|
||||
if ((e as ErrorResponse).captchaRequired) {
|
||||
this.logService.error("Captcha required error response: " + e.getSingleMessage());
|
||||
return;
|
||||
}
|
||||
this.logService?.error(`Received API exception:`, e);
|
||||
this.validationService.showError(e);
|
||||
},
|
||||
|
||||
@@ -52,7 +52,6 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
let userKey: UserKey;
|
||||
let userKeyEncString: EncString;
|
||||
let userKeyPair: [string, EncString];
|
||||
let capchaBypassToken: string;
|
||||
|
||||
beforeEach(() => {
|
||||
email = "test@email.com";
|
||||
@@ -71,7 +70,6 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
userKeyEncString = new EncString("userKeyEncrypted");
|
||||
|
||||
userKeyPair = ["publicKey", new EncString("privateKey")];
|
||||
capchaBypassToken = "capchaBypassToken";
|
||||
});
|
||||
|
||||
it("throws an error if the user key cannot be created", async () => {
|
||||
@@ -82,18 +80,12 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("registers the user and returns a captcha bypass token when given valid email verification input", async () => {
|
||||
it("registers the user when given valid email verification input", async () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
|
||||
const result = await service.finishRegistration(
|
||||
email,
|
||||
passwordInputResult,
|
||||
emailVerificationToken,
|
||||
);
|
||||
|
||||
expect(result).toEqual(capchaBypassToken);
|
||||
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
|
||||
|
||||
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
|
||||
|
||||
@@ -34,7 +34,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
emergencyAccessId?: string,
|
||||
providerInviteToken?: string,
|
||||
providerUserId?: string,
|
||||
): Promise<string> {
|
||||
): Promise<void> {
|
||||
const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(
|
||||
passwordInputResult.masterKey,
|
||||
);
|
||||
@@ -57,9 +57,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
providerUserId,
|
||||
);
|
||||
|
||||
const capchaBypassToken = await this.accountApiService.registerFinish(registerRequest);
|
||||
|
||||
return capchaBypassToken;
|
||||
return await this.accountApiService.registerFinish(registerRequest);
|
||||
}
|
||||
|
||||
protected async buildRegisterRequest(
|
||||
|
||||
@@ -152,9 +152,8 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
|
||||
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
let captchaBypassToken: string = null;
|
||||
try {
|
||||
captchaBypassToken = await this.registrationFinishService.finishRegistration(
|
||||
await this.registrationFinishService.finishRegistration(
|
||||
this.email,
|
||||
passwordInputResult,
|
||||
this.emailVerificationToken,
|
||||
@@ -179,12 +178,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
|
||||
// login with the new account
|
||||
try {
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
this.email,
|
||||
passwordInputResult.newPassword,
|
||||
captchaBypassToken,
|
||||
null,
|
||||
);
|
||||
const credentials = new PasswordLoginCredentials(this.email, passwordInputResult.newPassword);
|
||||
|
||||
const authenticationResult = await this.loginStrategyService.logIn(credentials);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export abstract class RegistrationFinishService {
|
||||
* @param emergencyAccessId The optional emergency access id which is required to validate the emergency access invite token.
|
||||
* @param providerInviteToken The optional provider invite token.
|
||||
* @param providerUserId The optional provider user id which is required to validate the provider invite token.
|
||||
* @returns a promise which resolves to the captcha bypass token string upon a successful account creation.
|
||||
* @returns a promise which resolves upon a successful account creation.
|
||||
*/
|
||||
abstract finishRegistration(
|
||||
email: string,
|
||||
@@ -38,5 +38,5 @@ export abstract class RegistrationFinishService {
|
||||
emergencyAccessId?: string,
|
||||
providerInviteToken?: string,
|
||||
providerUserId?: string,
|
||||
): Promise<string>;
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -260,7 +260,6 @@ describe("TwoFactorAuthComponent", () => {
|
||||
// Assert
|
||||
expect(mockLoginStrategyService.logInTwoFactor).toHaveBeenCalledWith(
|
||||
new TokenTwoFactorRequest(component.selectedProviderType, token, remember),
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -335,7 +335,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
try {
|
||||
this.formPromise = this.loginStrategyService.logInTwoFactor(
|
||||
new TokenTwoFactorRequest(this.selectedProviderType, tokenValue, rememberValue),
|
||||
"", // TODO: PM-15162 - deprecate captchaResponse
|
||||
);
|
||||
const authResult: AuthResult = await this.formPromise;
|
||||
this.logService.info("Successfully submitted two factor token");
|
||||
|
||||
@@ -59,16 +59,11 @@ export abstract class LoginStrategyServiceAbstraction {
|
||||
| WebAuthnLoginCredentials,
|
||||
) => Promise<AuthResult>;
|
||||
/**
|
||||
* Sends a token request to the server with the provided two factor token
|
||||
* and captcha response. This uses data stored from {@link LoginStrategyServiceAbstraction.logIn},
|
||||
* so that must be called first.
|
||||
* Sends a token request to the server with the provided two factor token.
|
||||
* This uses data stored from {@link LoginStrategyServiceAbstraction.logIn}, so that must be called first.
|
||||
* Returns an error if no session data is found.
|
||||
*/
|
||||
logInTwoFactor: (
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
// TODO: PM-15162 - deprecate captchaResponse
|
||||
captchaResponse: string,
|
||||
) => Promise<AuthResult>;
|
||||
logInTwoFactor: (twoFactor: TokenTwoFactorRequest) => Promise<AuthResult>;
|
||||
/**
|
||||
* Creates a master key from the provided master password and email.
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,6 @@ import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||
|
||||
export class AuthRequestLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
captchaBypassToken: string;
|
||||
authRequestCredentials: AuthRequestLoginCredentials;
|
||||
|
||||
static fromJSON(obj: Jsonify<AuthRequestLoginStrategyData>): AuthRequestLoginStrategyData {
|
||||
@@ -54,7 +53,6 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
data.tokenRequest = new PasswordTokenRequest(
|
||||
credentials.email,
|
||||
credentials.accessCode,
|
||||
null,
|
||||
await this.buildTwoFactor(credentials.twoFactor, credentials.email),
|
||||
await this.buildDeviceRequest(),
|
||||
);
|
||||
@@ -66,12 +64,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
override async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string,
|
||||
): Promise<AuthResult> {
|
||||
override async logInTwoFactor(twoFactor: TokenTwoFactorRequest): Promise<AuthResult> {
|
||||
const data = this.cache.value;
|
||||
data.tokenRequest.captchaResponse = captchaResponse ?? data.captchaBypassToken;
|
||||
this.cache.next(data);
|
||||
|
||||
return super.logInTwoFactor(twoFactor);
|
||||
|
||||
@@ -11,7 +11,6 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response";
|
||||
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
@@ -59,7 +58,6 @@ const accessToken = "ACCESS_TOKEN";
|
||||
const refreshToken = "REFRESH_TOKEN";
|
||||
const userKey = "USER_KEY";
|
||||
const privateKey = "PRIVATE_KEY";
|
||||
const captchaSiteKey = "CAPTCHA_SITE_KEY";
|
||||
const kdf = 0;
|
||||
const kdfIterations = 10000;
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
@@ -298,7 +296,6 @@ describe("LoginStrategy", () => {
|
||||
expected.userId = userId;
|
||||
expected.resetMasterPassword = true;
|
||||
expected.twoFactorProviders = null;
|
||||
expected.captchaSiteKey = "";
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -314,7 +311,6 @@ describe("LoginStrategy", () => {
|
||||
expected.userId = userId;
|
||||
expected.resetMasterPassword = false;
|
||||
expected.twoFactorProviders = null;
|
||||
expected.captchaSiteKey = "";
|
||||
expect(result).toEqual(expected);
|
||||
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
@@ -323,28 +319,6 @@ describe("LoginStrategy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects login if CAPTCHA is required", async () => {
|
||||
// Sample CAPTCHA response
|
||||
const tokenResponse = new IdentityCaptchaResponse({
|
||||
error: "invalid_grant",
|
||||
error_description: "Captcha required.",
|
||||
HCaptcha_SiteKey: captchaSiteKey,
|
||||
});
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(stateService.addAccount).not.toHaveBeenCalled();
|
||||
expect(messagingService.send).not.toHaveBeenCalled();
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.captchaSiteKey = captchaSiteKey;
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("makes a new public and private key for an old account", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.privateKey = null;
|
||||
@@ -492,7 +466,6 @@ describe("LoginStrategy", () => {
|
||||
cache.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
masterPasswordHash,
|
||||
"",
|
||||
new TokenTwoFactorRequest(),
|
||||
);
|
||||
|
||||
@@ -524,7 +497,6 @@ describe("LoginStrategy", () => {
|
||||
|
||||
await passwordLoginStrategy.logInTwoFactor(
|
||||
new TokenTwoFactorRequest(twoFactorProviderType, twoFactorToken, twoFactorRemember),
|
||||
"",
|
||||
);
|
||||
|
||||
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
|
||||
@@ -541,13 +513,11 @@ describe("LoginStrategy", () => {
|
||||
|
||||
describe("Device verification", () => {
|
||||
it("processes device verification response", async () => {
|
||||
const captchaToken = "test-captcha-token";
|
||||
const deviceVerificationResponse = new IdentityDeviceVerificationResponse({
|
||||
error: "invalid_grant",
|
||||
error_description: "Device verification required.",
|
||||
email: "test@bitwarden.com",
|
||||
deviceVerificationRequest: true,
|
||||
captchaToken: captchaToken,
|
||||
});
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(deviceVerificationResponse);
|
||||
@@ -556,7 +526,6 @@ describe("LoginStrategy", () => {
|
||||
cache.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
masterPasswordHash,
|
||||
"",
|
||||
new TokenTwoFactorRequest(),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
|
||||
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
||||
import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response";
|
||||
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
@@ -56,7 +55,6 @@ import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
type IdentityResponse =
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityCaptchaResponse
|
||||
| IdentityDeviceVerificationResponse;
|
||||
|
||||
export abstract class LoginStrategyData {
|
||||
@@ -66,7 +64,6 @@ export abstract class LoginStrategyData {
|
||||
| SsoTokenRequest
|
||||
| WebAuthnLoginTokenRequest
|
||||
| undefined;
|
||||
captchaBypassToken?: string;
|
||||
|
||||
/** User's entered email obtained pre-login. */
|
||||
abstract userEnteredEmail?: string;
|
||||
@@ -108,10 +105,7 @@ export abstract class LoginStrategy {
|
||||
| WebAuthnLoginCredentials,
|
||||
): Promise<AuthResult>;
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string | null = null,
|
||||
): Promise<AuthResult> {
|
||||
async logInTwoFactor(twoFactor: TokenTwoFactorRequest): Promise<AuthResult> {
|
||||
const data = this.cache.value;
|
||||
if (!data.tokenRequest) {
|
||||
throw new Error("Token request is undefined");
|
||||
@@ -133,8 +127,6 @@ export abstract class LoginStrategy {
|
||||
|
||||
if (response instanceof IdentityTwoFactorResponse) {
|
||||
return [await this.processTwoFactorResponse(response), response];
|
||||
} else if (response instanceof IdentityCaptchaResponse) {
|
||||
return [await this.processCaptchaResponse(response), response];
|
||||
} else if (response instanceof IdentityTokenResponse) {
|
||||
return [await this.processTokenResponse(response), response];
|
||||
} else if (response instanceof IdentityDeviceVerificationResponse) {
|
||||
@@ -362,7 +354,6 @@ export abstract class LoginStrategy {
|
||||
result.twoFactorProviders = response.twoFactorProviders2;
|
||||
|
||||
await this.twoFactorService.setProviders(response);
|
||||
this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null });
|
||||
result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken;
|
||||
|
||||
result.email = response.email ?? "";
|
||||
@@ -379,12 +370,6 @@ export abstract class LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
private async processCaptchaResponse(response: IdentityCaptchaResponse): Promise<AuthResult> {
|
||||
const result = new AuthResult();
|
||||
result.captchaSiteKey = response.siteKey;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the active account is set after initialization.
|
||||
* Note: In browser there is a slight delay between when active account emits in background,
|
||||
@@ -407,7 +392,7 @@ export abstract class LoginStrategy {
|
||||
|
||||
/**
|
||||
* Handles the response from the server when a device verification is required.
|
||||
* It sets the requiresDeviceVerification flag to true and caches the captcha token if it came back.
|
||||
* It sets the requiresDeviceVerification flag to true.
|
||||
*
|
||||
* @param {IdentityDeviceVerificationResponse} response - The response from the server indicating that device verification is required.
|
||||
* @returns {Promise<AuthResult>} - A promise that resolves to an AuthResult object
|
||||
@@ -417,9 +402,6 @@ export abstract class LoginStrategy {
|
||||
): Promise<AuthResult> {
|
||||
const result = new AuthResult();
|
||||
result.requiresDeviceVerification = true;
|
||||
|
||||
// Extend cached data with captcha bypass token if it came back.
|
||||
this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,6 @@ describe("PasswordLoginStrategy", () => {
|
||||
provider: null,
|
||||
token: null,
|
||||
}),
|
||||
captchaResponse: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -260,14 +259,11 @@ describe("PasswordLoginStrategy", () => {
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(
|
||||
identityTokenResponseFactory(masterPasswordPolicy),
|
||||
);
|
||||
await passwordLoginStrategy.logInTwoFactor(
|
||||
{
|
||||
provider: TwoFactorProviderType.Authenticator,
|
||||
token: "123456",
|
||||
remember: false,
|
||||
},
|
||||
"",
|
||||
);
|
||||
await passwordLoginStrategy.logInTwoFactor({
|
||||
provider: TwoFactorProviderType.Authenticator,
|
||||
token: "123456",
|
||||
remember: false,
|
||||
});
|
||||
|
||||
// Second login attempt should save the force password reset options
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
|
||||
@@ -9,7 +9,6 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response";
|
||||
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
@@ -30,8 +29,6 @@ export class PasswordLoginStrategyData implements LoginStrategyData {
|
||||
|
||||
/** User's entered email obtained pre-login. Always present in MP login. */
|
||||
userEnteredEmail: string;
|
||||
/** If 2fa is required, token is returned to bypass captcha */
|
||||
captchaBypassToken?: string;
|
||||
/** The local version of the user's master key hash */
|
||||
localMasterKeyHash: string;
|
||||
/** The user's master key */
|
||||
@@ -79,7 +76,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
|
||||
override async logIn(credentials: PasswordLoginCredentials) {
|
||||
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
||||
const { email, masterPassword, twoFactor } = credentials;
|
||||
|
||||
const data = new PasswordLoginStrategyData();
|
||||
data.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email);
|
||||
@@ -96,7 +93,6 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
data.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
serverMasterKeyHash,
|
||||
captchaToken,
|
||||
await this.buildTwoFactor(twoFactor, email),
|
||||
await this.buildDeviceRequest(),
|
||||
);
|
||||
@@ -105,23 +101,12 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
|
||||
const [authResult, identityResponse] = await this.startLogIn();
|
||||
|
||||
if (identityResponse instanceof IdentityCaptchaResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
await this.evaluateMasterPasswordIfRequired(identityResponse, credentials, authResult);
|
||||
|
||||
return authResult;
|
||||
}
|
||||
|
||||
override async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string,
|
||||
): Promise<AuthResult> {
|
||||
const data = this.cache.value;
|
||||
data.tokenRequest.captchaResponse = captchaResponse ?? data.captchaBypassToken;
|
||||
this.cache.next(data);
|
||||
|
||||
override async logInTwoFactor(twoFactor: TokenTwoFactorRequest): Promise<AuthResult> {
|
||||
const result = await super.logInTwoFactor(twoFactor);
|
||||
|
||||
return result;
|
||||
|
||||
@@ -22,7 +22,6 @@ import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
import { LoginStrategyData, LoginStrategy } from "./login.strategy";
|
||||
|
||||
export class SsoLoginStrategyData implements LoginStrategyData {
|
||||
captchaBypassToken: string;
|
||||
tokenRequest: SsoTokenRequest;
|
||||
/**
|
||||
* User's entered email obtained pre-login. Present in most SSO flows, but not CLI + SSO Flow.
|
||||
|
||||
@@ -16,7 +16,6 @@ import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||
|
||||
export class UserApiLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: UserApiTokenRequest;
|
||||
captchaBypassToken: string;
|
||||
|
||||
static fromJSON(obj: Jsonify<UserApiLoginStrategyData>): UserApiLoginStrategyData {
|
||||
return Object.assign(new UserApiLoginStrategyData(), obj, {
|
||||
|
||||
@@ -208,11 +208,9 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
|
||||
expect(authResult).toBeInstanceOf(AuthResult);
|
||||
expect(authResult).toMatchObject({
|
||||
captchaSiteKey: "",
|
||||
resetMasterPassword: false,
|
||||
twoFactorProviders: null,
|
||||
requiresTwoFactor: false,
|
||||
requiresCaptcha: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||
|
||||
export class WebAuthnLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: WebAuthnLoginTokenRequest;
|
||||
captchaBypassToken?: string;
|
||||
credentials: WebAuthnLoginCredentials;
|
||||
|
||||
static fromJSON(obj: Jsonify<WebAuthnLoginStrategyData>): WebAuthnLoginStrategyData {
|
||||
|
||||
@@ -14,8 +14,6 @@ export class PasswordLoginCredentials {
|
||||
constructor(
|
||||
public email: string,
|
||||
public masterPassword: string,
|
||||
// TODO: PM-15162 - captcha is deprecated as part of UI refresh work
|
||||
public captchaToken?: string,
|
||||
public twoFactor?: TokenTwoFactorRequest,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ describe("LoginStrategyService", () => {
|
||||
premium: false,
|
||||
});
|
||||
|
||||
const result = await sut.logInTwoFactor(twoFactorToken, "CAPTCHA");
|
||||
const result = await sut.logInTwoFactor(twoFactorToken);
|
||||
|
||||
expect(result).toBeInstanceOf(AuthResult);
|
||||
});
|
||||
@@ -285,7 +285,7 @@ describe("LoginStrategyService", () => {
|
||||
true,
|
||||
);
|
||||
|
||||
await expect(sut.logInTwoFactor(twoFactorToken, "CAPTCHA")).rejects.toThrow();
|
||||
await expect(sut.logInTwoFactor(twoFactorToken)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("throw error on too low kdf config", async () => {
|
||||
|
||||
@@ -242,10 +242,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
return result;
|
||||
}
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string,
|
||||
): Promise<AuthResult> {
|
||||
async logInTwoFactor(twoFactor: TokenTwoFactorRequest): Promise<AuthResult> {
|
||||
if (!(await this.isSessionValid())) {
|
||||
throw new Error(this.i18nService.t("sessionTimeout"));
|
||||
}
|
||||
@@ -256,10 +253,10 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await strategy.logInTwoFactor(twoFactor, captchaResponse);
|
||||
const result = await strategy.logInTwoFactor(twoFactor);
|
||||
|
||||
// Only clear cache if 2FA token has been accepted, otherwise we need to be able to try again
|
||||
if (result != null && !result.requiresTwoFactor && !result.requiresCaptcha) {
|
||||
if (result != null && !result.requiresTwoFactor) {
|
||||
await this.clearCache();
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -47,7 +47,6 @@ describe("LOGIN_STRATEGY_CACHE_KEY", () => {
|
||||
actual.password.tokenRequest = new PasswordTokenRequest(
|
||||
"EMAIL",
|
||||
"LOCAL_PASSWORD_HASH",
|
||||
"CAPTCHA_TOKEN",
|
||||
twoFactorRequest,
|
||||
deviceRequest,
|
||||
);
|
||||
@@ -116,7 +115,7 @@ describe("LOGIN_STRATEGY_CACHE_KEY", () => {
|
||||
deviceResponse,
|
||||
deviceRequest,
|
||||
);
|
||||
actual.webAuthn.captchaBypassToken = "CAPTCHA_BYPASS_TOKEN";
|
||||
|
||||
actual.webAuthn.tokenRequest.setTwoFactor(
|
||||
new TokenTwoFactorRequest(TwoFactorProviderType.Email, "TOKEN", false),
|
||||
);
|
||||
|
||||
@@ -61,13 +61,11 @@ import { UpdateTwoFactorYubikeyOtpRequest } from "../auth/models/request/update-
|
||||
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
|
||||
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
|
||||
import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response";
|
||||
import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response";
|
||||
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
import { PreloginResponse } from "../auth/models/response/prelogin.response";
|
||||
import { RegisterResponse } from "../auth/models/response/register.response";
|
||||
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
|
||||
import { TwoFactorAuthenticatorResponse } from "../auth/models/response/two-factor-authenticator.response";
|
||||
import { TwoFactorDuoResponse } from "../auth/models/response/two-factor-duo.response";
|
||||
@@ -95,7 +93,6 @@ import { EventRequest } from "../models/request/event.request";
|
||||
import { KdfRequest } from "../models/request/kdf.request";
|
||||
import { KeysRequest } from "../models/request/keys.request";
|
||||
import { PreloginRequest } from "../models/request/prelogin.request";
|
||||
import { RegisterRequest } from "../models/request/register.request";
|
||||
import { StorageRequest } from "../models/request/storage.request";
|
||||
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
||||
import { UpdateDomainsRequest } from "../models/request/update-domains.request";
|
||||
@@ -147,10 +144,7 @@ export abstract class ApiService {
|
||||
| UserApiTokenRequest
|
||||
| WebAuthnLoginTokenRequest,
|
||||
) => Promise<
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityCaptchaResponse
|
||||
| IdentityDeviceVerificationResponse
|
||||
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
|
||||
>;
|
||||
refreshIdentityToken: () => Promise<any>;
|
||||
|
||||
@@ -167,7 +161,6 @@ export abstract class ApiService {
|
||||
postSecurityStamp: (request: SecretVerificationRequest) => Promise<any>;
|
||||
getAccountRevisionDate: () => Promise<number>;
|
||||
postPasswordHint: (request: PasswordHintRequest) => Promise<any>;
|
||||
postRegister: (request: RegisterRequest) => Promise<RegisterResponse>;
|
||||
postPremium: (data: FormData) => Promise<PaymentResponse>;
|
||||
postReinstatePremium: () => Promise<any>;
|
||||
postAccountStorage: (request: StorageRequest) => Promise<PaymentResponse>;
|
||||
|
||||
@@ -47,10 +47,9 @@ export abstract class AccountApiService {
|
||||
* @param request - The request object containing the user's email verification token,
|
||||
* the email, hashed MP, newly created user key, and new asymmetric user key pair along
|
||||
* with the KDF information used during the process.
|
||||
* @returns A promise that resolves to a string captcha bypass token when the
|
||||
* registration process is successfully completed.
|
||||
* @returns A promise that resolves when the registration process is successfully completed.
|
||||
*/
|
||||
abstract registerFinish(request: RegisterFinishRequest): Promise<string>;
|
||||
abstract registerFinish(request: RegisterFinishRequest): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets the [dbo].[User].[VerifyDevices] flag to true or false.
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { I18nService } from "../platform/abstractions/i18n.service";
|
||||
|
||||
import { IFrameComponent } from "./iframe-component";
|
||||
|
||||
// TODO: PM-15162 - captcha is deprecated as part of UI refresh work
|
||||
export class CaptchaIFrame extends IFrameComponent {
|
||||
constructor(
|
||||
win: Window,
|
||||
webVaultUrl: string,
|
||||
private i18nService: I18nService,
|
||||
successCallback: (message: string) => any,
|
||||
errorCallback: (message: string) => any,
|
||||
infoCallback: (message: string) => any,
|
||||
) {
|
||||
super(
|
||||
win,
|
||||
webVaultUrl,
|
||||
"captcha-connector.html",
|
||||
"hcaptcha_iframe",
|
||||
successCallback,
|
||||
errorCallback,
|
||||
(message: string) => {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
if (typeof parsedMessage !== "string") {
|
||||
this.iframe.height = parsedMessage.height.toString();
|
||||
this.iframe.width = parsedMessage.width.toString();
|
||||
} else {
|
||||
infoCallback(parsedMessage);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
init(siteKey: string): void {
|
||||
super.initComponent(
|
||||
this.createParams({ siteKey: siteKey, locale: this.i18nService.translationLocale }, 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
export abstract class IFrameComponent {
|
||||
iframe: HTMLIFrameElement;
|
||||
private connectorLink: HTMLAnchorElement;
|
||||
private parseFunction = this.parseMessage.bind(this);
|
||||
|
||||
constructor(
|
||||
private win: Window,
|
||||
protected webVaultUrl: string,
|
||||
private path: string,
|
||||
private iframeId: string,
|
||||
public successCallback?: (message: string) => any,
|
||||
public errorCallback?: (message: string) => any,
|
||||
public infoCallback?: (message: string) => any,
|
||||
) {
|
||||
this.connectorLink = win.document.createElement("a");
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.sendMessage("stop");
|
||||
}
|
||||
|
||||
start() {
|
||||
this.sendMessage("start");
|
||||
}
|
||||
|
||||
sendMessage(message: any) {
|
||||
if (!this.iframe || !this.iframe.src || !this.iframe.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iframe.contentWindow.postMessage(message, this.iframe.src);
|
||||
}
|
||||
|
||||
base64Encode(str: string): string {
|
||||
return btoa(
|
||||
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => {
|
||||
return String.fromCharCode(("0x" + p1) as any);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.win.removeEventListener("message", this.parseFunction, false);
|
||||
}
|
||||
|
||||
protected createParams(data: any, version: number) {
|
||||
return new URLSearchParams({
|
||||
data: this.base64Encode(JSON.stringify(data)),
|
||||
parent: encodeURIComponent(this.win.document.location.href),
|
||||
v: version.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
protected initComponent(params: URLSearchParams): void {
|
||||
this.connectorLink.href = `${this.webVaultUrl}/${this.path}?${params}`;
|
||||
this.iframe = this.win.document.getElementById(this.iframeId) as HTMLIFrameElement;
|
||||
this.iframe.src = this.connectorLink.href;
|
||||
|
||||
this.win.addEventListener("message", this.parseFunction, false);
|
||||
}
|
||||
|
||||
private parseMessage(event: MessageEvent) {
|
||||
if (!this.validMessage(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parts: string[] = event.data.split("|");
|
||||
if (parts[0] === "success" && this.successCallback) {
|
||||
this.successCallback(parts[1]);
|
||||
} else if (parts[0] === "error" && this.errorCallback) {
|
||||
this.errorCallback(parts[1]);
|
||||
} else if (parts[0] === "info" && this.infoCallback) {
|
||||
this.infoCallback(parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private validMessage(event: MessageEvent) {
|
||||
if (
|
||||
event.origin == null ||
|
||||
event.origin === "" ||
|
||||
event.origin !== (this.connectorLink as any).origin ||
|
||||
event.data == null ||
|
||||
typeof event.data !== "string"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
event.data.indexOf("success|") === 0 ||
|
||||
event.data.indexOf("error|") === 0 ||
|
||||
event.data.indexOf("info|") === 0
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
|
||||
|
||||
export class AuthResult {
|
||||
userId: UserId;
|
||||
captchaSiteKey = "";
|
||||
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
|
||||
/**
|
||||
* @deprecated
|
||||
@@ -21,10 +19,6 @@ export class AuthResult {
|
||||
requiresEncryptionKeyMigration: boolean;
|
||||
requiresDeviceVerification: boolean;
|
||||
|
||||
get requiresCaptcha() {
|
||||
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||
}
|
||||
|
||||
get requiresTwoFactor() {
|
||||
return this.twoFactorProviders != null;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
export abstract class CaptchaProtectedRequest {
|
||||
captchaResponse: string = null;
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { ClientType } from "../../../../enums";
|
||||
import { Utils } from "../../../../platform/misc/utils";
|
||||
import { CaptchaProtectedRequest } from "../captcha-protected.request";
|
||||
|
||||
import { DeviceRequest } from "./device.request";
|
||||
import { TokenTwoFactorRequest } from "./token-two-factor.request";
|
||||
import { TokenRequest } from "./token.request";
|
||||
|
||||
export class PasswordTokenRequest extends TokenRequest implements CaptchaProtectedRequest {
|
||||
export class PasswordTokenRequest extends TokenRequest {
|
||||
constructor(
|
||||
public email: string,
|
||||
public masterPasswordHash: string,
|
||||
public captchaResponse: string,
|
||||
protected twoFactor: TokenTwoFactorRequest,
|
||||
device?: DeviceRequest,
|
||||
public newDeviceOtp?: string,
|
||||
@@ -25,10 +23,6 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
|
||||
obj.username = this.email;
|
||||
obj.password = this.masterPasswordHash;
|
||||
|
||||
if (this.captchaResponse != null) {
|
||||
obj.captchaResponse = this.captchaResponse;
|
||||
}
|
||||
|
||||
if (this.newDeviceOtp) {
|
||||
obj.newDeviceOtp = this.newDeviceOtp;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface ICaptchaProtectedResponse {
|
||||
captchaBypassToken: string;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class IdentityCaptchaResponse extends BaseResponse {
|
||||
siteKey: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.siteKey = this.getResponseProperty("HCaptcha_SiteKey");
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,9 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class IdentityDeviceVerificationResponse extends BaseResponse {
|
||||
deviceVerified: boolean;
|
||||
captchaToken: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.deviceVerified = this.getResponseProperty("DeviceVerified") ?? false;
|
||||
|
||||
this.captchaToken = this.getResponseProperty("CaptchaBypassToken");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,12 @@ export class IdentityTwoFactorResponse extends BaseResponse {
|
||||
twoFactorProviders: TwoFactorProviderType[];
|
||||
// a map of two-factor providers to necessary data for completion
|
||||
twoFactorProviders2: Record<TwoFactorProviderType, Record<string, string>>;
|
||||
captchaToken: string;
|
||||
ssoEmail2faSessionToken: string;
|
||||
email?: string;
|
||||
masterPasswordPolicy?: MasterPasswordPolicyResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.captchaToken = this.getResponseProperty("CaptchaBypassToken");
|
||||
this.twoFactorProviders = this.getResponseProperty("TwoFactorProviders");
|
||||
this.twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2");
|
||||
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { ICaptchaProtectedResponse } from "./captcha-protected.response";
|
||||
|
||||
export class RegisterResponse extends BaseResponse implements ICaptchaProtectedResponse {
|
||||
captchaBypassToken: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.captchaBypassToken = this.getResponseProperty("CaptchaBypassToken");
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export class AccountApiServiceImplementation implements AccountApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async registerFinish(request: RegisterFinishRequest): Promise<string> {
|
||||
async registerFinish(request: RegisterFinishRequest): Promise<void> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { KdfType } from "@bitwarden/key-management";
|
||||
|
||||
import { CaptchaProtectedRequest } from "../../auth/models/request/captcha-protected.request";
|
||||
|
||||
import { KeysRequest } from "./keys.request";
|
||||
import { ReferenceEventRequest } from "./reference-event.request";
|
||||
|
||||
export class RegisterRequest implements CaptchaProtectedRequest {
|
||||
masterPasswordHint: string;
|
||||
keys: KeysRequest;
|
||||
token: string;
|
||||
organizationUserId: string;
|
||||
|
||||
constructor(
|
||||
public email: string,
|
||||
public name: string,
|
||||
public masterPasswordHash: string,
|
||||
masterPasswordHint: string,
|
||||
public key: string,
|
||||
public referenceData: ReferenceEventRequest,
|
||||
public captchaResponse: string,
|
||||
public kdf: KdfType,
|
||||
public kdfIterations: number,
|
||||
public kdfMemory?: number,
|
||||
public kdfParallelism?: number,
|
||||
) {
|
||||
this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
|
||||
import { BaseResponse } from "./base.response";
|
||||
|
||||
export class ErrorResponse extends BaseResponse {
|
||||
message: string;
|
||||
validationErrors: { [key: string]: string[] };
|
||||
statusCode: number;
|
||||
captchaRequired: boolean;
|
||||
captchaSiteKey: string;
|
||||
|
||||
constructor(response: any, status: number, identityResponse?: boolean) {
|
||||
super(response);
|
||||
@@ -28,8 +24,6 @@ export class ErrorResponse extends BaseResponse {
|
||||
} else if (errorModel) {
|
||||
this.message = this.getResponseProperty("Message", errorModel);
|
||||
this.validationErrors = this.getResponseProperty("ValidationErrors", errorModel);
|
||||
this.captchaSiteKey = this.validationErrors?.HCaptcha_SiteKey?.[0];
|
||||
this.captchaRequired = !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||
}
|
||||
this.statusCode = status;
|
||||
}
|
||||
|
||||
@@ -69,13 +69,11 @@ import { UpdateTwoFactorYubikeyOtpRequest } from "../auth/models/request/update-
|
||||
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
|
||||
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
|
||||
import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response";
|
||||
import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response";
|
||||
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
import { PreloginResponse } from "../auth/models/response/prelogin.response";
|
||||
import { RegisterResponse } from "../auth/models/response/register.response";
|
||||
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
|
||||
import { TwoFactorAuthenticatorResponse } from "../auth/models/response/two-factor-authenticator.response";
|
||||
import { TwoFactorDuoResponse } from "../auth/models/response/two-factor-duo.response";
|
||||
@@ -106,7 +104,6 @@ import { EventRequest } from "../models/request/event.request";
|
||||
import { KdfRequest } from "../models/request/kdf.request";
|
||||
import { KeysRequest } from "../models/request/keys.request";
|
||||
import { PreloginRequest } from "../models/request/prelogin.request";
|
||||
import { RegisterRequest } from "../models/request/register.request";
|
||||
import { StorageRequest } from "../models/request/storage.request";
|
||||
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
||||
import { UpdateDomainsRequest } from "../models/request/update-domains.request";
|
||||
@@ -200,10 +197,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
| SsoTokenRequest
|
||||
| WebAuthnLoginTokenRequest,
|
||||
): Promise<
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityCaptchaResponse
|
||||
| IdentityDeviceVerificationResponse
|
||||
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
|
||||
> {
|
||||
const headers = new Headers({
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
@@ -246,12 +240,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
Object.keys(responseJson.TwoFactorProviders2).length
|
||||
) {
|
||||
return new IdentityTwoFactorResponse(responseJson);
|
||||
} else if (
|
||||
response.status === 400 &&
|
||||
responseJson.HCaptcha_SiteKey &&
|
||||
Object.keys(responseJson.HCaptcha_SiteKey).length
|
||||
) {
|
||||
return new IdentityCaptchaResponse(responseJson);
|
||||
} else if (
|
||||
response.status === 400 &&
|
||||
responseJson?.ErrorModel?.Message === ApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE
|
||||
@@ -369,19 +357,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return this.send("POST", "/accounts/password-hint", request, false, false);
|
||||
}
|
||||
|
||||
async postRegister(request: RegisterRequest): Promise<RegisterResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const r = await this.send(
|
||||
"POST",
|
||||
"/accounts/register",
|
||||
request,
|
||||
false,
|
||||
true,
|
||||
env.getIdentityUrl(),
|
||||
);
|
||||
return new RegisterResponse(r);
|
||||
}
|
||||
|
||||
async postPremium(data: FormData): Promise<PaymentResponse> {
|
||||
const r = await this.send("POST", "/accounts/premium", data, true, true);
|
||||
return new PaymentResponse(r);
|
||||
|
||||
Reference in New Issue
Block a user