diff --git a/angular/src/components/login.component.ts b/angular/src/components/login.component.ts index 5cfd6a6d..f9039551 100644 --- a/angular/src/components/login.component.ts +++ b/angular/src/components/login.component.ts @@ -93,7 +93,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit } try { - this.formPromise = this.authService.logIn(this.email, this.masterPassword, this.captchaToken); + this.formPromise = this.authService.logIn(this.email, this.masterPassword, null, this.captchaToken); const response = await this.formPromise; if (this.rememberEmail) { await this.stateService.setRememberedEmail(this.email); diff --git a/angular/src/components/two-factor.component.ts b/angular/src/components/two-factor.component.ts index 2d15ca00..2e5c887f 100644 --- a/angular/src/components/two-factor.component.ts +++ b/angular/src/components/two-factor.component.ts @@ -193,9 +193,11 @@ export class TwoFactorComponent implements OnInit, OnDestroy { async doSubmit() { this.formPromise = this.authService.logInTwoFactor( - this.selectedProviderType, - this.token, - this.remember + { + provider: this.selectedProviderType, + token: this.token, + remember: this.remember + } ); const response: AuthResult = await this.formPromise; const disableFavicon = await this.stateService.getDisableFavicon(); diff --git a/common/src/abstractions/auth.service.ts b/common/src/abstractions/auth.service.ts index 97d95262..81b90793 100644 --- a/common/src/abstractions/auth.service.ts +++ b/common/src/abstractions/auth.service.ts @@ -11,15 +11,15 @@ export abstract class AuthService { clientId: string; clientSecret: string; - logIn: (email: string, masterPassword: string, twoFactor: TwoFactorData, captchaToken?: string) => Promise; + logIn: (email: string, masterPassword: string, twoFactor?: TwoFactorData, captchaToken?: string) => Promise; logInSso: ( code: string, codeVerifier: string, redirectUrl: string, - twoFactor: TwoFactorData, - orgId: string + orgId: string, + twoFactor?: TwoFactorData, ) => Promise; - logInApiKey: (clientId: string, clientSecret: string, twoFactor: TwoFactorData) => Promise; + logInApiKey: (clientId: string, clientSecret: string, twoFactor?: TwoFactorData) => Promise; logInTwoFactor: ( twoFactor: TwoFactorData ) => Promise; diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index 86d589c3..7f0d87ab 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -65,7 +65,7 @@ export class AuthService implements AuthServiceAbstraction { private setCryptoKeys = true ) {} - async logIn(email: string, masterPassword: string, twoFactor: TwoFactorData, captchaToken?: string): Promise { + async logIn(email: string, masterPassword: string, twoFactor?: TwoFactorData, captchaToken?: string): Promise { this.twoFactorService.clearSelectedProvider(); const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); @@ -94,8 +94,8 @@ export class AuthService implements AuthServiceAbstraction { code: string, codeVerifier: string, redirectUrl: string, - twoFactor: TwoFactorData, - orgId: string + orgId: string, + twoFactor?: TwoFactorData, ): Promise { this.twoFactorService.clearSelectedProvider(); return await this.logInHelper( @@ -114,7 +114,7 @@ export class AuthService implements AuthServiceAbstraction { ); } - async logInApiKey(clientId: string, clientSecret: string, twoFactor: TwoFactorData): Promise { + async logInApiKey(clientId: string, clientSecret: string, twoFactor?: TwoFactorData): Promise { this.twoFactorService.clearSelectedProvider(); return await this.logInHelper( null, diff --git a/node/src/cli/commands/login.command.ts b/node/src/cli/commands/login.command.ts index ff20fb1c..3fa9ca68 100644 --- a/node/src/cli/commands/login.command.ts +++ b/node/src/cli/commands/login.command.ts @@ -156,155 +156,148 @@ export class LoginCommand { } let response: AuthResult = null; - if (twoFactorToken != null && twoFactorMethod != null) { - if (clientId != null && clientSecret != null) { - response = await this.authService.logInApiKeyComplete( - clientId, - clientSecret, - twoFactorMethod, - twoFactorToken, - false - ); - } else if (ssoCode != null && ssoCodeVerifier != null) { - response = await this.authService.logInSsoComplete( - ssoCode, - ssoCodeVerifier, - this.ssoRedirectUri, - twoFactorMethod, - twoFactorToken, - false - ); - } else { - response = await this.authService.logInComplete( + if (clientId != null && clientSecret != null) { + response = await this.authService.logInApiKey( + clientId, + clientSecret, + { + provider: twoFactorMethod, + token: twoFactorToken, + remember: false + } + ); + } else if (ssoCode != null && ssoCodeVerifier != null) { + response = await this.authService.logInSso( + ssoCode, + ssoCodeVerifier, + this.ssoRedirectUri, + orgIdentifier, + { + provider: twoFactorMethod, + token: twoFactorToken, + remember: false + } + ); + } else { + response = await this.authService.logIn( + email, + password, + { + provider: twoFactorMethod, + token: twoFactorToken, + remember: false, + } + ); + } + 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/article/cli-auth-challenges)" + ); + + try { + const captchaClientSecret = await this.apiClientSecret(true); + if (Utils.isNullOrWhitespace(captchaClientSecret)) { + return badCaptcha; + } + + const secondResponse = await this.authService.logIn( email, password, - twoFactorMethod, - twoFactorToken, - false, - this.clientSecret + { + provider: twoFactorMethod, + token: twoFactorToken, + remember: false, + }, + captchaClientSecret ); - } - } else { - if (clientId != null && clientSecret != null) { - response = await this.authService.logInApiKey(clientId, clientSecret); - } else if (ssoCode != null && ssoCodeVerifier != null) { - response = await this.authService.logInSso( - ssoCode, - ssoCodeVerifier, - this.ssoRedirectUri, - orgIdentifier - ); - } else { - response = await this.authService.logIn(email, password); - } - 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/article/cli-auth-challenges)" - ); - - 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; - const twoFactorProviders = this.twoFactorService.getSupportedProviders(null); - if (twoFactorProviders.length === 0) { - return Response.badRequest("No providers available for this client."); - } - - if (twoFactorMethod != null) { - try { - selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0]; - } catch (e) { - return Response.error("Invalid two-step login method."); - } - } - - if (selectedProvider == null) { - if (twoFactorProviders.length === 1) { - selectedProvider = twoFactorProviders[0]; - } else if (this.canInteract) { - const twoFactorOptions = twoFactorProviders.map((p) => p.name); - twoFactorOptions.push(new inquirer.Separator()); - twoFactorOptions.push("Cancel"); - const answer: inquirer.Answers = await inquirer.createPromptModule({ - output: process.stderr, - })({ - type: "list", - name: "method", - message: "Two-step login method:", - choices: twoFactorOptions, - }); - const i = twoFactorOptions.indexOf(answer.method); - if (i === twoFactorOptions.length - 1) { - return Response.error("Login failed."); - } - selectedProvider = twoFactorProviders[i]; - } - if (selectedProvider == null) { - return Response.error("Login failed. No provider selected."); - } - } - + response = secondResponse; + } catch (e) { if ( - twoFactorToken == null && - response.twoFactorProviders.size > 1 && - selectedProvider.type === TwoFactorProviderType.Email + (e instanceof ErrorResponse || e.constructor.name === "ErrorResponse") && + (e as ErrorResponse).message.includes("Captcha is invalid") ) { - const emailReq = new TwoFactorEmailRequest(); - emailReq.email = this.authService.email; - emailReq.masterPasswordHash = this.authService.masterPasswordHash; - await this.apiService.postTwoFactorEmail(emailReq); + return badCaptcha; + } else { + throw e; } - - if (twoFactorToken == null) { - if (this.canInteract) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ - output: process.stderr, - })({ - type: "input", - name: "token", - message: "Two-step login code:", - }); - twoFactorToken = answer.token; - } - if (twoFactorToken == null || twoFactorToken === "") { - return Response.badRequest("Code is required."); - } - } - - response = await this.authService.logInTwoFactor( - selectedProvider.type, - twoFactorToken, - false - ); } } + if (response.twoFactor) { + let selectedProvider: any = null; + const twoFactorProviders = this.twoFactorService.getSupportedProviders(null); + if (twoFactorProviders.length === 0) { + return Response.badRequest("No providers available for this client."); + } + + if (twoFactorMethod != null) { + try { + selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0]; + } catch (e) { + return Response.error("Invalid two-step login method."); + } + } + + if (selectedProvider == null) { + if (twoFactorProviders.length === 1) { + selectedProvider = twoFactorProviders[0]; + } else if (this.canInteract) { + const twoFactorOptions = twoFactorProviders.map((p) => p.name); + twoFactorOptions.push(new inquirer.Separator()); + twoFactorOptions.push("Cancel"); + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "list", + name: "method", + message: "Two-step login method:", + choices: twoFactorOptions, + }); + const i = twoFactorOptions.indexOf(answer.method); + if (i === twoFactorOptions.length - 1) { + return Response.error("Login failed."); + } + selectedProvider = twoFactorProviders[i]; + } + if (selectedProvider == null) { + return Response.error("Login failed. No provider selected."); + } + } + + if ( + twoFactorToken == null && + response.twoFactorProviders.size > 1 && + selectedProvider.type === TwoFactorProviderType.Email + ) { + const emailReq = new TwoFactorEmailRequest(); + emailReq.email = this.authService.email; + emailReq.masterPasswordHash = this.authService.masterPasswordHash; + await this.apiService.postTwoFactorEmail(emailReq); + } + + if (twoFactorToken == null) { + if (this.canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "input", + name: "token", + message: "Two-step login code:", + }); + twoFactorToken = answer.token; + } + if (twoFactorToken == null || twoFactorToken === "") { + return Response.badRequest("Code is required."); + } + } + + response = await this.authService.logInTwoFactor({ + provider: selectedProvider.type, + token: twoFactorToken, + remember: false, + }); + } if (response.twoFactor) { return Response.error("Login failed."); diff --git a/spec/common/services/auth.service.spec.ts b/spec/common/services/auth.service.spec.ts index c583df95..e2bb749b 100644 --- a/spec/common/services/auth.service.spec.ts +++ b/spec/common/services/auth.service.spec.ts @@ -190,7 +190,7 @@ describe("Cipher Service", () => { const expected = newAuthResponse(); // Act - const result = await authService.logIn(email, masterPassword, null); + const result = await authService.logIn(email, masterPassword); // Assert // Api call: @@ -240,7 +240,7 @@ describe("Cipher Service", () => { expected.captchaSiteKey = siteKey; // Act - const result = await authService.logIn(email, masterPassword, null); + const result = await authService.logIn(email, masterPassword); // Assertions stateService.didNotReceive().addAccount(Arg.any()); @@ -274,7 +274,7 @@ describe("Cipher Service", () => { ); // Act - const result = await authService.logIn(email, masterPassword, null); + const result = await authService.logIn(email, masterPassword); // Assertions commonSuccessAssertions(); @@ -293,7 +293,7 @@ describe("Cipher Service", () => { tokenService.getTwoFactorToken(email).resolves(null); apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - const result = await authService.logIn(email, masterPassword, null); + const result = await authService.logIn(email, masterPassword); commonSuccessAssertions(); apiService.received(1).postAccountKeys(Arg.any()); @@ -317,7 +317,7 @@ describe("Cipher Service", () => { expected.twoFactorProviders = twoFactorProviders; expected.captchaSiteKey = undefined; - const result = await authService.logIn(email, masterPassword, null); + const result = await authService.logIn(email, masterPassword); stateService.didNotReceive().addAccount(Arg.any()); messagingService.didNotReceive().send(Arg.any()); @@ -331,7 +331,7 @@ describe("Cipher Service", () => { tokenService.getTwoFactorToken(email).resolves(twoFactorToken); - await authService.logIn(email, masterPassword, null); + await authService.logIn(email, masterPassword); apiService.received(1).postIdentityToken( Arg.is((actual) => { @@ -406,7 +406,7 @@ describe("Cipher Service", () => { tokenService.getTwoFactorToken(null).resolves(null); apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - const result = await authService.logInSso(ssoCode, ssoCodeVerifier, ssoRedirectUrl, null, ssoOrgId); + const result = await authService.logInSso(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); // Assert // Api call: @@ -452,7 +452,7 @@ describe("Cipher Service", () => { tokenService.getTwoFactorToken(null).resolves(null); apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - const result = await authService.logInSso(ssoCode, ssoCodeVerifier, ssoRedirectUrl, null, ssoOrgId); + const result = await authService.logInSso(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); // Assert cryptoService.didNotReceive().setEncPrivateKey(privateKey); @@ -466,7 +466,7 @@ describe("Cipher Service", () => { apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - const result = await authService.logInSso(ssoCode, ssoCodeVerifier, ssoRedirectUrl, null, ssoOrgId); + const result = await authService.logInSso(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); commonSuccessAssertions(); keyConnectorService.received(1).getAndSetKey(keyConnectorUrl); @@ -500,7 +500,7 @@ describe("Cipher Service", () => { apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - const result = await authService.logInSso(ssoCode, ssoCodeVerifier, ssoRedirectUrl, null, ssoOrgId); + const result = await authService.logInSso(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); commonSuccessAssertions(); cryptoService.received(1).setKey(preloginKey); @@ -529,7 +529,7 @@ describe("Cipher Service", () => { const tokenResponse = newTokenResponse(); apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - const result = await authService.logInApiKey(apiClientId, apiClientSecret, null); + const result = await authService.logInApiKey(apiClientId, apiClientSecret); apiService.received(1).postIdentityToken( Arg.is((actual) => {