mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01:00
[Captcha] Failed login attempts (#698)
* [Captcha] Failed login attempts * Fix logIn.strategy test * Updated with the stark majority of requested changes * Fix typo * Unused import
This commit is contained in:
parent
adfc2f234d
commit
48a4c27fe7
@ -17,8 +17,10 @@ import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
|
||||
import { TwoFactorProviders } from "jslib-common/services/twoFactor.service";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captchaProtected.component";
|
||||
|
||||
@Directive()
|
||||
export class TwoFactorComponent implements OnInit, OnDestroy {
|
||||
export class TwoFactorComponent extends CaptchaProtectedComponent implements OnInit, OnDestroy {
|
||||
token = "";
|
||||
remember = false;
|
||||
webAuthnReady = false;
|
||||
@ -56,6 +58,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
||||
protected logService: LogService,
|
||||
protected twoFactorService: TwoFactorService
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||
}
|
||||
|
||||
@ -153,6 +156,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.setupCaptcha();
|
||||
|
||||
if (this.token == null || this.token === "") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
@ -185,14 +190,20 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async doSubmit() {
|
||||
this.formPromise = this.authService.logInTwoFactor({
|
||||
provider: this.selectedProviderType,
|
||||
token: this.token,
|
||||
remember: this.remember,
|
||||
});
|
||||
this.formPromise = this.authService.logInTwoFactor(
|
||||
{
|
||||
provider: this.selectedProviderType,
|
||||
token: this.token,
|
||||
remember: this.remember,
|
||||
},
|
||||
this.captchaToken
|
||||
);
|
||||
const response: AuthResult = await this.formPromise;
|
||||
const disableFavicon = await this.stateService.getDisableFavicon();
|
||||
await this.stateService.setDisableFavicon(!!disableFavicon);
|
||||
if (this.handleCaptchaRequired(response)) {
|
||||
return;
|
||||
}
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
|
@ -13,7 +13,10 @@ export abstract class AuthService {
|
||||
logIn: (
|
||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||
) => Promise<AuthResult>;
|
||||
logInTwoFactor: (twoFactor: TokenRequestTwoFactor) => Promise<AuthResult>;
|
||||
logInTwoFactor: (
|
||||
twoFactor: TokenRequestTwoFactor,
|
||||
captchaResponse: string
|
||||
) => Promise<AuthResult>;
|
||||
logOut: (callback: () => void) => void;
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
||||
authingWithApiKey: () => boolean;
|
||||
|
@ -27,6 +27,7 @@ import { IdentityTwoFactorResponse } from "../../models/response/identityTwoFact
|
||||
|
||||
export abstract class LogInStrategy {
|
||||
protected abstract tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
|
||||
protected captchaBypassToken: string = null;
|
||||
|
||||
constructor(
|
||||
protected cryptoService: CryptoService,
|
||||
@ -44,7 +45,10 @@ export abstract class LogInStrategy {
|
||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||
): Promise<AuthResult>;
|
||||
|
||||
async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise<AuthResult> {
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenRequestTwoFactor,
|
||||
captchaResponse: string = null
|
||||
): Promise<AuthResult> {
|
||||
this.tokenRequest.setTwoFactor(twoFactor);
|
||||
return this.startLogIn();
|
||||
}
|
||||
@ -152,6 +156,7 @@ export abstract class LogInStrategy {
|
||||
const result = new AuthResult();
|
||||
result.twoFactorProviders = response.twoFactorProviders2;
|
||||
this.twoFactorService.setProviders(response);
|
||||
this.captchaBypassToken = response.captchaToken ?? null;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -9,9 +9,11 @@ import { StateService } from "../../abstractions/state.service";
|
||||
import { TokenService } from "../../abstractions/token.service";
|
||||
import { TwoFactorService } from "../../abstractions/twoFactor.service";
|
||||
import { HashPurpose } from "../../enums/hashPurpose";
|
||||
import { AuthResult } from "../../models/domain/authResult";
|
||||
import { PasswordLogInCredentials } from "../../models/domain/logInCredentials";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
|
||||
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
|
||||
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequest";
|
||||
|
||||
import { LogInStrategy } from "./logIn.strategy";
|
||||
|
||||
@ -59,6 +61,14 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
await this.cryptoService.setKeyHash(this.localHashedPassword);
|
||||
}
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenRequestTwoFactor,
|
||||
captchaResponse: string
|
||||
): Promise<AuthResult> {
|
||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
||||
return super.logInTwoFactor(twoFactor);
|
||||
}
|
||||
|
||||
async logIn(credentials: PasswordLogInCredentials) {
|
||||
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
||||
|
||||
|
@ -115,16 +115,19 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
return result;
|
||||
}
|
||||
|
||||
async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise<AuthResult> {
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenRequestTwoFactor,
|
||||
captchaResponse: string
|
||||
): Promise<AuthResult> {
|
||||
if (this.logInStrategy == null) {
|
||||
throw new Error(this.i18nService.t("sessionTimeout"));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.logInStrategy.logInTwoFactor(twoFactor);
|
||||
const result = await this.logInStrategy.logInTwoFactor(twoFactor, captchaResponse);
|
||||
|
||||
// Only clear state if 2FA token has been accepted, otherwise we need to be able to try again
|
||||
if (!result.requiresTwoFactor) {
|
||||
if (!result.requiresTwoFactor && !result.requiresCaptcha) {
|
||||
this.clearState();
|
||||
}
|
||||
return result;
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
} from "jslib-common/models/domain/logInCredentials";
|
||||
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequest";
|
||||
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
|
||||
import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest";
|
||||
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
|
||||
@ -69,6 +70,8 @@ export class LoginCommand {
|
||||
let clientId: string = null;
|
||||
let clientSecret: string = null;
|
||||
|
||||
let selectedProvider: any = null;
|
||||
|
||||
if (options.apikey != null) {
|
||||
const apiIdentifiers = await this.apiIdentifiers();
|
||||
clientId = apiIdentifiers.clientId;
|
||||
@ -177,39 +180,17 @@ export class LoginCommand {
|
||||
);
|
||||
}
|
||||
if (response.captchaSiteKey) {
|
||||
const badCaptcha = Response.badRequest(
|
||||
"Your authentication request appears to be coming from a bot\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)"
|
||||
);
|
||||
const credentials = new PasswordLogInCredentials(email, password);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
|
||||
|
||||
try {
|
||||
const captchaClientSecret = await this.apiClientSecret(true);
|
||||
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
|
||||
return badCaptcha;
|
||||
}
|
||||
|
||||
const secondResponse = await this.authService.logIn(
|
||||
new PasswordLogInCredentials(email, password, captchaClientSecret, {
|
||||
provider: twoFactorMethod,
|
||||
token: twoFactorToken,
|
||||
remember: false,
|
||||
})
|
||||
);
|
||||
response = secondResponse;
|
||||
} catch (e) {
|
||||
if (
|
||||
(e instanceof ErrorResponse || e.constructor.name === "ErrorResponse") &&
|
||||
(e as ErrorResponse).message.includes("Captcha is invalid")
|
||||
) {
|
||||
return badCaptcha;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
// Error Response
|
||||
if (handledResponse instanceof Response) {
|
||||
return handledResponse;
|
||||
} else {
|
||||
response = handledResponse;
|
||||
}
|
||||
}
|
||||
if (response.requiresTwoFactor) {
|
||||
let selectedProvider: any = null;
|
||||
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
return Response.badRequest("No providers available for this client.");
|
||||
@ -276,11 +257,30 @@ export class LoginCommand {
|
||||
}
|
||||
}
|
||||
|
||||
response = await this.authService.logInTwoFactor({
|
||||
response = await this.authService.logInTwoFactor(
|
||||
{
|
||||
provider: selectedProvider.type,
|
||||
token: twoFactorToken,
|
||||
remember: false,
|
||||
},
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (response.captchaSiteKey) {
|
||||
const twoFactorRequest: TokenRequestTwoFactor = {
|
||||
provider: selectedProvider.type,
|
||||
token: twoFactorToken,
|
||||
remember: false,
|
||||
});
|
||||
};
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);
|
||||
|
||||
// Error Response
|
||||
if (handledResponse instanceof Response) {
|
||||
return handledResponse;
|
||||
} else {
|
||||
response = handledResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.requiresTwoFactor) {
|
||||
@ -435,6 +435,48 @@ export class LoginCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCaptchaRequired(
|
||||
twoFactorRequest: TokenRequestTwoFactor,
|
||||
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.authService.logIn(credentials);
|
||||
} else {
|
||||
authResultResponse = await this.authService.logInTwoFactor(
|
||||
twoFactorRequest,
|
||||
captchaClientSecret
|
||||
);
|
||||
}
|
||||
|
||||
return authResultResponse;
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof ErrorResponse ||
|
||||
(e.constructor.name === "ErrorResponse" &&
|
||||
(e as ErrorResponse).message.includes("Captcha is invalid"))
|
||||
) {
|
||||
return badCaptcha;
|
||||
} else {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPasswordStrengthUserInput() {
|
||||
let userInput: string[] = [];
|
||||
const atPosition = this.email.indexOf("@");
|
||||
|
@ -267,11 +267,14 @@ describe("LogInStrategy", () => {
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await passwordLogInStrategy.logInTwoFactor({
|
||||
provider: twoFactorProviderType,
|
||||
token: twoFactorToken,
|
||||
remember: twoFactorRemember,
|
||||
});
|
||||
await passwordLogInStrategy.logInTwoFactor(
|
||||
{
|
||||
provider: twoFactorProviderType,
|
||||
token: twoFactorToken,
|
||||
remember: twoFactorRemember,
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
|
Loading…
Reference in New Issue
Block a user