1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +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

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