1
0
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:
Todd Martin
2025-05-09 10:44:11 -04:00
committed by GitHub
parent 625256b08e
commit 4191bb9533
59 changed files with 56 additions and 977 deletions

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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",

View File

@@ -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,

View File

@@ -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>

View File

@@ -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");
}
}

View File

@@ -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,

View File

@@ -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)"

View File

@@ -252,12 +252,6 @@ p.lead {
}
}
#hcaptcha_iframe {
width: 100%;
border: none;
transition: height 0.25s linear;
}
form,
.form {
.form-group {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -1 +0,0 @@
@import "../scss/styles.scss";

View File

@@ -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>

View File

@@ -1,8 +0,0 @@
body {
min-width: 0px !important;
padding: 0;
margin: 0;
background: transparent;
display: flex;
justify-content: center;
}

View File

@@ -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));
}

View File

@@ -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");

View File

@@ -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"],

View File

@@ -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;
}
}

View File

@@ -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);
},

View File

@@ -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);

View File

@@ -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(

View File

@@ -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);

View File

@@ -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>;
}

View File

@@ -260,7 +260,6 @@ describe("TwoFactorAuthComponent", () => {
// Assert
expect(mockLoginStrategyService.logInTwoFactor).toHaveBeenCalledWith(
new TokenTwoFactorRequest(component.selectedProviderType, token, remember),
"",
);
});

View File

@@ -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");

View File

@@ -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.
*/

View File

@@ -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);

View File

@@ -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(),
);

View File

@@ -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;
}
}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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.

View File

@@ -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, {

View File

@@ -208,11 +208,9 @@ describe("WebAuthnLoginStrategy", () => {
expect(authResult).toBeInstanceOf(AuthResult);
expect(authResult).toMatchObject({
captchaSiteKey: "",
resetMasterPassword: false,
twoFactorProviders: null,
requiresTwoFactor: false,
requiresCaptcha: false,
});
});

View File

@@ -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 {

View File

@@ -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,
) {}
}

View File

@@ -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 () => {

View File

@@ -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;

View File

@@ -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),
);

View File

@@ -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>;

View File

@@ -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.

View File

@@ -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),
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,3 +0,0 @@
export interface ICaptchaProtectedResponse {
captchaBypassToken: string;
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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(

View File

@@ -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");
}
}

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);