From c5f236c2e443af703d822608dc89bc4b7be866db Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 12 Aug 2021 16:11:26 -0400 Subject: [PATCH] Use apikey client secret as captcha validation (#454) * Use apikey client secret as captcha validation * Linter fixes --- common/src/abstractions/auth.service.ts | 2 +- common/src/services/auth.service.ts | 4 +- node/src/cli/commands/login.command.ts | 105 +++++++++++++++++------- 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/common/src/abstractions/auth.service.ts b/common/src/abstractions/auth.service.ts index 00a2b65e08..7307bbdf67 100644 --- a/common/src/abstractions/auth.service.ts +++ b/common/src/abstractions/auth.service.ts @@ -20,7 +20,7 @@ export abstract class AuthService { logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; logInComplete: (email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, - twoFactorToken: string, remember?: boolean) => Promise; + twoFactorToken: string, remember?: boolean, captchaToken?: string) => Promise; logInSsoComplete: (code: string, codeVerifier: string, redirectUrl: string, twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; logInApiKeyComplete: (clientId: string, clientSecret: string, twoFactorProvider: TwoFactorProviderType, diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index 59771ec4e9..06594ffa93 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -151,14 +151,14 @@ export class AuthService implements AuthServiceAbstraction { } async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, - twoFactorToken: string, remember?: boolean): Promise { + twoFactorToken: string, remember?: boolean, captchaToken?: string): Promise { this.selectedTwoFactorProviderType = null; const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); const localHashedPassword = await this.cryptoService.hashPassword(masterPassword, key, HashPurpose.LocalAuthorization); return await this.logInHelper(email, hashedPassword, localHashedPassword, null, null, null, null, null, key, - twoFactorProvider, twoFactorToken, remember); + twoFactorProvider, twoFactorToken, remember, captchaToken); } async logInSsoComplete(code: string, codeVerifier: string, redirectUrl: string, diff --git a/node/src/cli/commands/login.command.ts b/node/src/cli/commands/login.command.ts index 2ab8aa6177..d77f7c4b48 100644 --- a/node/src/cli/commands/login.command.ts +++ b/node/src/cli/commands/login.command.ts @@ -6,6 +6,7 @@ import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType' import { AuthResult } from 'jslib-common/models/domain/authResult'; import { TwoFactorEmailRequest } from 'jslib-common/models/request/twoFactorEmailRequest'; +import { ErrorResponse } from 'jslib-common/models/response/errorResponse'; import { ApiService } from 'jslib-common/abstractions/api.service'; import { AuthService } from 'jslib-common/abstractions/auth.service'; @@ -30,6 +31,7 @@ export class LoginCommand { protected success: () => Promise; protected canInteract: boolean; protected clientId: string; + protected clientSecret: string; private ssoRedirectUri: string = null; @@ -51,32 +53,9 @@ export class LoginCommand { let clientSecret: string = null; if (options.apikey != null) { - const storedClientId: string = process.env.BW_CLIENTID; - const storedClientSecret: string = process.env.BW_CLIENTSECRET; - if (storedClientId == null) { - if (this.canInteract) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'input', - name: 'clientId', - message: 'client_id:', - }); - clientId = answer.clientId; - } else { - clientId = null; - } - } else { - clientId = storedClientId; - } - if (this.canInteract && storedClientSecret == null) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'input', - name: 'clientSecret', - message: 'client_secret:', - }); - clientSecret = answer.clientSecret; - } else { - clientSecret = storedClientSecret; - } + const apiIdentifiers = await this.apiIdentifiers(); + clientId = apiIdentifiers.clientId; + clientSecret = apiIdentifiers.clientSecret; } else if (options.sso != null && this.canInteract) { const passwordOptions: any = { type: 'password', @@ -156,7 +135,7 @@ export class LoginCommand { twoFactorMethod, twoFactorToken, false); } else { response = await this.authService.logInComplete(email, password, twoFactorMethod, - twoFactorToken, false); + twoFactorToken, false, this.clientSecret); } } else { if (clientId != null && clientSecret != null) { @@ -167,8 +146,27 @@ export class LoginCommand { response = await this.authService.logIn(email, password); } if (response.captchaSiteKey) { - return Response.badRequest('Your authentication request appears to be coming from a bot\n' + - 'Please log in using your API key (https://bitwarden.com/help/article/cli/#using-an-api-key)'); + 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/article/cli/#using-an-api-key)'); + + try { + const captchaClientSecret = await this.apiClientSecret(true); + if (Utils.isNullOrWhitespace(captchaClientSecret)) { + return badCaptcha; + } + + const secondResponse = await this.authService.logInComplete(email, password, twoFactorMethod, + twoFactorToken, false, captchaClientSecret); + 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; + } + } } if (response.twoFactor) { let selectedProvider: any = null; @@ -258,6 +256,55 @@ export class LoginCommand { } } + private async apiClientId(): Promise { + let clientId: string = null; + + const storedClientId: string = process.env.BW_CLIENTID; + if (storedClientId == null) { + if (this.canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'input', + name: 'clientId', + message: 'client_id:', + }); + clientId = answer.clientId; + } else { + clientId = null; + } + } else { + clientId = storedClientId; + } + + return clientId; + } + + private async apiClientSecret(isAdditionalAuthentication: boolean = false): Promise { + const additionalAuthenticationMessage = 'Additional authentication required.\nAPI key '; + let clientSecret: string = null; + + const storedClientSecret: string = this.clientSecret || process.env.BW_CLIENTSECRET; + if (this.canInteract && storedClientSecret == null) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'input', + name: 'clientSecret', + message: (isAdditionalAuthentication ? additionalAuthenticationMessage : '') + 'client_secret:', + + }); + clientSecret = answer.clientSecret; + } else { + clientSecret = storedClientSecret; + } + + return clientSecret; + } + + private async apiIdentifiers(): Promise<{ clientId: string, clientSecret: string; }> { + return { + clientId: await this.apiClientId(), + clientSecret: await this.apiClientSecret(), + }; + } + private async getSsoCode(codeChallenge: string, state: string): Promise { return new Promise((resolve, reject) => { const callbackServer = http.createServer((req, res) => {