[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:
Vincent Salucci 2022-03-02 19:47:57 -06:00 committed by GitHub
parent adfc2f234d
commit 48a4c27fe7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 124 additions and 47 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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