diff --git a/angular/src/components/two-factor.component.ts b/angular/src/components/two-factor.component.ts index d6b8812c9b..a40dc3c594 100644 --- a/angular/src/components/two-factor.component.ts +++ b/angular/src/components/two-factor.component.ts @@ -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(); } diff --git a/common/src/abstractions/auth.service.ts b/common/src/abstractions/auth.service.ts index fddf565115..447ade6c21 100644 --- a/common/src/abstractions/auth.service.ts +++ b/common/src/abstractions/auth.service.ts @@ -13,7 +13,10 @@ export abstract class AuthService { logIn: ( credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials ) => Promise; - logInTwoFactor: (twoFactor: TokenRequestTwoFactor) => Promise; + logInTwoFactor: ( + twoFactor: TokenRequestTwoFactor, + captchaResponse: string + ) => Promise; logOut: (callback: () => void) => void; makePreloginKey: (masterPassword: string, email: string) => Promise; authingWithApiKey: () => boolean; diff --git a/common/src/misc/logInStrategies/logIn.strategy.ts b/common/src/misc/logInStrategies/logIn.strategy.ts index 4bd25ac165..86bfa30bdb 100644 --- a/common/src/misc/logInStrategies/logIn.strategy.ts +++ b/common/src/misc/logInStrategies/logIn.strategy.ts @@ -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; - async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise { + async logInTwoFactor( + twoFactor: TokenRequestTwoFactor, + captchaResponse: string = null + ): Promise { 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; } diff --git a/common/src/misc/logInStrategies/passwordLogin.strategy.ts b/common/src/misc/logInStrategies/passwordLogin.strategy.ts index 78e1a317c3..6db930e693 100644 --- a/common/src/misc/logInStrategies/passwordLogin.strategy.ts +++ b/common/src/misc/logInStrategies/passwordLogin.strategy.ts @@ -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 { + this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken; + return super.logInTwoFactor(twoFactor); + } + async logIn(credentials: PasswordLogInCredentials) { const { email, masterPassword, captchaToken, twoFactor } = credentials; diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index 06ad640ceb..66f5340af1 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -115,16 +115,19 @@ export class AuthService implements AuthServiceAbstraction { return result; } - async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise { + async logInTwoFactor( + twoFactor: TokenRequestTwoFactor, + captchaResponse: string + ): Promise { 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; diff --git a/node/src/cli/commands/login.command.ts b/node/src/cli/commands/login.command.ts index 01df0ceae1..db6d0f615e 100644 --- a/node/src/cli/commands/login.command.ts +++ b/node/src/cli/commands/login.command.ts @@ -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 { + 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("@"); diff --git a/spec/common/misc/logInStrategies/logIn.strategy.spec.ts b/spec/common/misc/logInStrategies/logIn.strategy.spec.ts index 308091e991..49e36bb66d 100644 --- a/spec/common/misc/logInStrategies/logIn.strategy.spec.ts +++ b/spec/common/misc/logInStrategies/logIn.strategy.spec.ts @@ -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) => {