[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 { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
|
||||||
import { TwoFactorProviders } from "jslib-common/services/twoFactor.service";
|
import { TwoFactorProviders } from "jslib-common/services/twoFactor.service";
|
||||||
|
|
||||||
|
import { CaptchaProtectedComponent } from "./captchaProtected.component";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class TwoFactorComponent implements OnInit, OnDestroy {
|
export class TwoFactorComponent extends CaptchaProtectedComponent implements OnInit, OnDestroy {
|
||||||
token = "";
|
token = "";
|
||||||
remember = false;
|
remember = false;
|
||||||
webAuthnReady = false;
|
webAuthnReady = false;
|
||||||
|
@ -56,6 +58,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected twoFactorService: TwoFactorService
|
protected twoFactorService: TwoFactorService
|
||||||
) {
|
) {
|
||||||
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +156,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
|
await this.setupCaptcha();
|
||||||
|
|
||||||
if (this.token == null || this.token === "") {
|
if (this.token == null || this.token === "") {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
|
@ -185,14 +190,20 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async doSubmit() {
|
async doSubmit() {
|
||||||
this.formPromise = this.authService.logInTwoFactor({
|
this.formPromise = this.authService.logInTwoFactor(
|
||||||
|
{
|
||||||
provider: this.selectedProviderType,
|
provider: this.selectedProviderType,
|
||||||
token: this.token,
|
token: this.token,
|
||||||
remember: this.remember,
|
remember: this.remember,
|
||||||
});
|
},
|
||||||
|
this.captchaToken
|
||||||
|
);
|
||||||
const response: AuthResult = await this.formPromise;
|
const response: AuthResult = await this.formPromise;
|
||||||
const disableFavicon = await this.stateService.getDisableFavicon();
|
const disableFavicon = await this.stateService.getDisableFavicon();
|
||||||
await this.stateService.setDisableFavicon(!!disableFavicon);
|
await this.stateService.setDisableFavicon(!!disableFavicon);
|
||||||
|
if (this.handleCaptchaRequired(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.onSuccessfulLogin != null) {
|
if (this.onSuccessfulLogin != null) {
|
||||||
this.onSuccessfulLogin();
|
this.onSuccessfulLogin();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,10 @@ export abstract class AuthService {
|
||||||
logIn: (
|
logIn: (
|
||||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||||
) => Promise<AuthResult>;
|
) => Promise<AuthResult>;
|
||||||
logInTwoFactor: (twoFactor: TokenRequestTwoFactor) => Promise<AuthResult>;
|
logInTwoFactor: (
|
||||||
|
twoFactor: TokenRequestTwoFactor,
|
||||||
|
captchaResponse: string
|
||||||
|
) => Promise<AuthResult>;
|
||||||
logOut: (callback: () => void) => void;
|
logOut: (callback: () => void) => void;
|
||||||
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
||||||
authingWithApiKey: () => boolean;
|
authingWithApiKey: () => boolean;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { IdentityTwoFactorResponse } from "../../models/response/identityTwoFact
|
||||||
|
|
||||||
export abstract class LogInStrategy {
|
export abstract class LogInStrategy {
|
||||||
protected abstract tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
|
protected abstract tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
|
||||||
|
protected captchaBypassToken: string = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
|
@ -44,7 +45,10 @@ export abstract class LogInStrategy {
|
||||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||||
): Promise<AuthResult>;
|
): Promise<AuthResult>;
|
||||||
|
|
||||||
async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise<AuthResult> {
|
async logInTwoFactor(
|
||||||
|
twoFactor: TokenRequestTwoFactor,
|
||||||
|
captchaResponse: string = null
|
||||||
|
): Promise<AuthResult> {
|
||||||
this.tokenRequest.setTwoFactor(twoFactor);
|
this.tokenRequest.setTwoFactor(twoFactor);
|
||||||
return this.startLogIn();
|
return this.startLogIn();
|
||||||
}
|
}
|
||||||
|
@ -152,6 +156,7 @@ export abstract class LogInStrategy {
|
||||||
const result = new AuthResult();
|
const result = new AuthResult();
|
||||||
result.twoFactorProviders = response.twoFactorProviders2;
|
result.twoFactorProviders = response.twoFactorProviders2;
|
||||||
this.twoFactorService.setProviders(response);
|
this.twoFactorService.setProviders(response);
|
||||||
|
this.captchaBypassToken = response.captchaToken ?? null;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,11 @@ import { StateService } from "../../abstractions/state.service";
|
||||||
import { TokenService } from "../../abstractions/token.service";
|
import { TokenService } from "../../abstractions/token.service";
|
||||||
import { TwoFactorService } from "../../abstractions/twoFactor.service";
|
import { TwoFactorService } from "../../abstractions/twoFactor.service";
|
||||||
import { HashPurpose } from "../../enums/hashPurpose";
|
import { HashPurpose } from "../../enums/hashPurpose";
|
||||||
|
import { AuthResult } from "../../models/domain/authResult";
|
||||||
import { PasswordLogInCredentials } from "../../models/domain/logInCredentials";
|
import { PasswordLogInCredentials } from "../../models/domain/logInCredentials";
|
||||||
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
|
||||||
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
|
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
|
||||||
|
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequest";
|
||||||
|
|
||||||
import { LogInStrategy } from "./logIn.strategy";
|
import { LogInStrategy } from "./logIn.strategy";
|
||||||
|
|
||||||
|
@ -59,6 +61,14 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||||
await this.cryptoService.setKeyHash(this.localHashedPassword);
|
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) {
|
async logIn(credentials: PasswordLogInCredentials) {
|
||||||
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
||||||
|
|
||||||
|
|
|
@ -115,16 +115,19 @@ export class AuthService implements AuthServiceAbstraction {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise<AuthResult> {
|
async logInTwoFactor(
|
||||||
|
twoFactor: TokenRequestTwoFactor,
|
||||||
|
captchaResponse: string
|
||||||
|
): Promise<AuthResult> {
|
||||||
if (this.logInStrategy == null) {
|
if (this.logInStrategy == null) {
|
||||||
throw new Error(this.i18nService.t("sessionTimeout"));
|
throw new Error(this.i18nService.t("sessionTimeout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
// 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();
|
this.clearState();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
PasswordLogInCredentials,
|
PasswordLogInCredentials,
|
||||||
SsoLogInCredentials,
|
SsoLogInCredentials,
|
||||||
} from "jslib-common/models/domain/logInCredentials";
|
} from "jslib-common/models/domain/logInCredentials";
|
||||||
|
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequest";
|
||||||
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
|
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
|
||||||
import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest";
|
import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest";
|
||||||
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
|
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
|
||||||
|
@ -69,6 +70,8 @@ export class LoginCommand {
|
||||||
let clientId: string = null;
|
let clientId: string = null;
|
||||||
let clientSecret: string = null;
|
let clientSecret: string = null;
|
||||||
|
|
||||||
|
let selectedProvider: any = null;
|
||||||
|
|
||||||
if (options.apikey != null) {
|
if (options.apikey != null) {
|
||||||
const apiIdentifiers = await this.apiIdentifiers();
|
const apiIdentifiers = await this.apiIdentifiers();
|
||||||
clientId = apiIdentifiers.clientId;
|
clientId = apiIdentifiers.clientId;
|
||||||
|
@ -177,39 +180,17 @@ export class LoginCommand {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (response.captchaSiteKey) {
|
if (response.captchaSiteKey) {
|
||||||
const badCaptcha = Response.badRequest(
|
const credentials = new PasswordLogInCredentials(email, password);
|
||||||
"Your authentication request appears to be coming from a bot\n" +
|
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
|
||||||
"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 {
|
// Error Response
|
||||||
const captchaClientSecret = await this.apiClientSecret(true);
|
if (handledResponse instanceof Response) {
|
||||||
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
|
return handledResponse;
|
||||||
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 {
|
} else {
|
||||||
throw e;
|
response = handledResponse;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (response.requiresTwoFactor) {
|
if (response.requiresTwoFactor) {
|
||||||
let selectedProvider: any = null;
|
|
||||||
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
|
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
|
||||||
if (twoFactorProviders.length === 0) {
|
if (twoFactorProviders.length === 0) {
|
||||||
return Response.badRequest("No providers available for this client.");
|
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,
|
provider: selectedProvider.type,
|
||||||
token: twoFactorToken,
|
token: twoFactorToken,
|
||||||
remember: false,
|
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) {
|
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() {
|
private getPasswordStrengthUserInput() {
|
||||||
let userInput: string[] = [];
|
let userInput: string[] = [];
|
||||||
const atPosition = this.email.indexOf("@");
|
const atPosition = this.email.indexOf("@");
|
||||||
|
|
|
@ -267,11 +267,14 @@ describe("LogInStrategy", () => {
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||||
|
|
||||||
await passwordLogInStrategy.logInTwoFactor({
|
await passwordLogInStrategy.logInTwoFactor(
|
||||||
|
{
|
||||||
provider: twoFactorProviderType,
|
provider: twoFactorProviderType,
|
||||||
token: twoFactorToken,
|
token: twoFactorToken,
|
||||||
remember: twoFactorRemember,
|
remember: twoFactorRemember,
|
||||||
});
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
apiService.received(1).postIdentityToken(
|
apiService.received(1).postIdentityToken(
|
||||||
Arg.is((actual) => {
|
Arg.is((actual) => {
|
||||||
|
|
Loading…
Reference in New Issue