Use apikey client secret as captcha validation (#454)

* Use apikey client secret as captcha validation

* Linter fixes
This commit is contained in:
Matt Gibson 2021-08-12 16:11:26 -04:00 committed by GitHub
parent 26e8b48deb
commit c5f236c2e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 79 additions and 32 deletions

View File

@ -20,7 +20,7 @@ export abstract class AuthService {
logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string,
remember?: boolean) => Promise<AuthResult>; remember?: boolean) => Promise<AuthResult>;
logInComplete: (email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, logInComplete: (email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType,
twoFactorToken: string, remember?: boolean) => Promise<AuthResult>; twoFactorToken: string, remember?: boolean, captchaToken?: string) => Promise<AuthResult>;
logInSsoComplete: (code: string, codeVerifier: string, redirectUrl: string, logInSsoComplete: (code: string, codeVerifier: string, redirectUrl: string,
twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise<AuthResult>; twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise<AuthResult>;
logInApiKeyComplete: (clientId: string, clientSecret: string, twoFactorProvider: TwoFactorProviderType, logInApiKeyComplete: (clientId: string, clientSecret: string, twoFactorProvider: TwoFactorProviderType,

View File

@ -151,14 +151,14 @@ export class AuthService implements AuthServiceAbstraction {
} }
async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType,
twoFactorToken: string, remember?: boolean): Promise<AuthResult> { twoFactorToken: string, remember?: boolean, captchaToken?: string): Promise<AuthResult> {
this.selectedTwoFactorProviderType = null; this.selectedTwoFactorProviderType = null;
const key = await this.makePreloginKey(masterPassword, email); const key = await this.makePreloginKey(masterPassword, email);
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
const localHashedPassword = await this.cryptoService.hashPassword(masterPassword, key, const localHashedPassword = await this.cryptoService.hashPassword(masterPassword, key,
HashPurpose.LocalAuthorization); HashPurpose.LocalAuthorization);
return await this.logInHelper(email, hashedPassword, localHashedPassword, null, null, null, null, null, key, 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, async logInSsoComplete(code: string, codeVerifier: string, redirectUrl: string,

View File

@ -6,6 +6,7 @@ import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType'
import { AuthResult } from 'jslib-common/models/domain/authResult'; 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 { ErrorResponse } from 'jslib-common/models/response/errorResponse';
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from 'jslib-common/abstractions/auth.service';
@ -30,6 +31,7 @@ export class LoginCommand {
protected success: () => Promise<MessageResponse>; protected success: () => Promise<MessageResponse>;
protected canInteract: boolean; protected canInteract: boolean;
protected clientId: string; protected clientId: string;
protected clientSecret: string;
private ssoRedirectUri: string = null; private ssoRedirectUri: string = null;
@ -51,32 +53,9 @@ export class LoginCommand {
let clientSecret: string = null; let clientSecret: string = null;
if (options.apikey != null) { if (options.apikey != null) {
const storedClientId: string = process.env.BW_CLIENTID; const apiIdentifiers = await this.apiIdentifiers();
const storedClientSecret: string = process.env.BW_CLIENTSECRET; clientId = apiIdentifiers.clientId;
if (storedClientId == null) { clientSecret = apiIdentifiers.clientSecret;
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;
}
} else if (options.sso != null && this.canInteract) { } else if (options.sso != null && this.canInteract) {
const passwordOptions: any = { const passwordOptions: any = {
type: 'password', type: 'password',
@ -156,7 +135,7 @@ export class LoginCommand {
twoFactorMethod, twoFactorToken, false); twoFactorMethod, twoFactorToken, false);
} else { } else {
response = await this.authService.logInComplete(email, password, twoFactorMethod, response = await this.authService.logInComplete(email, password, twoFactorMethod,
twoFactorToken, false); twoFactorToken, false, this.clientSecret);
} }
} else { } else {
if (clientId != null && clientSecret != null) { if (clientId != null && clientSecret != null) {
@ -167,8 +146,27 @@ export class LoginCommand {
response = await this.authService.logIn(email, password); response = await this.authService.logIn(email, password);
} }
if (response.captchaSiteKey) { if (response.captchaSiteKey) {
return Response.badRequest('Your authentication request appears to be coming from a bot\n' + const badCaptcha = 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)'); '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) { if (response.twoFactor) {
let selectedProvider: any = null; let selectedProvider: any = null;
@ -258,6 +256,55 @@ export class LoginCommand {
} }
} }
private async apiClientId(): Promise<string> {
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<string> {
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<string> { private async getSsoCode(codeChallenge: string, state: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const callbackServer = http.createServer((req, res) => { const callbackServer = http.createServer((req, res) => {