diff --git a/src/App/Pages/Accounts/LoginPage.xaml b/src/App/Pages/Accounts/LoginPage.xaml
index 1363785ce..e43026992 100644
--- a/src/App/Pages/Accounts/LoginPage.xaml
+++ b/src/App/Pages/Accounts/LoginPage.xaml
@@ -82,7 +82,7 @@
-
+
diff --git a/src/App/Pages/Accounts/LoginPageViewModel.cs b/src/App/Pages/Accounts/LoginPageViewModel.cs
index 29c99304e..0f989217c 100644
--- a/src/App/Pages/Accounts/LoginPageViewModel.cs
+++ b/src/App/Pages/Accounts/LoginPageViewModel.cs
@@ -8,10 +8,15 @@ using System;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Xamarin.Forms;
+using Newtonsoft.Json;
+using System.Text;
+using Xamarin.Essentials;
+using System.Text.RegularExpressions;
+using Bit.Core.Services;
namespace Bit.App.Pages
{
- public class LoginPageViewModel : BaseViewModel
+ public class LoginPageViewModel : CaptchaProtectedViewModel
{
private const string Keys_RememberedEmail = "rememberedEmail";
private const string Keys_RememberEmail = "rememberEmail";
@@ -22,10 +27,13 @@ namespace Bit.App.Pages
private readonly IStorageService _storageService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IStateService _stateService;
+ private readonly IEnvironmentService _environmentService;
+ private readonly II18nService _i18nService;
private bool _showPassword;
private string _email;
private string _masterPassword;
+ private bool _loginEnabled = true;
public LoginPageViewModel()
{
@@ -35,6 +43,8 @@ namespace Bit.App.Pages
_storageService = ServiceContainer.Resolve("storageService");
_platformUtilsService = ServiceContainer.Resolve("platformUtilsService");
_stateService = ServiceContainer.Resolve("stateService");
+ _environmentService = ServiceContainer.Resolve("environmentService");
+ _i18nService = ServiceContainer.Resolve("i18nService");
PageTitle = AppResources.Bitwarden;
TogglePasswordCommand = new Command(TogglePassword);
@@ -63,6 +73,16 @@ namespace Bit.App.Pages
set => SetProperty(ref _masterPassword, value);
}
+ public bool LoginEnabled {
+ get => _loginEnabled;
+ set => SetProperty(ref _loginEnabled, value);
+ }
+ public bool Loading
+ {
+ get => !LoginEnabled;
+ set => LoginEnabled = !value;
+ }
+
public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? "" : "";
@@ -70,7 +90,12 @@ namespace Bit.App.Pages
public Action StartTwoFactorAction { get; set; }
public Action LogInSuccessAction { get; set; }
public Action CloseAction { get; set; }
-
+
+ protected override II18nService i18nService => _i18nService;
+ protected override IEnvironmentService environmentService => _environmentService;
+ protected override IDeviceActionService deviceActionService => _deviceActionService;
+ protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
+
public async Task InitAsync()
{
if (string.IsNullOrWhiteSpace(Email))
@@ -115,9 +140,13 @@ namespace Bit.App.Pages
ShowPassword = false;
try
{
- await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
- var response = await _authService.LogInAsync(Email, MasterPassword);
- MasterPassword = string.Empty;
+ if (!Loading)
+ {
+ await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
+ Loading = true;
+ }
+
+ var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
if (RememberEmail)
{
await _storageService.SaveAsync(Keys_RememberedEmail, Email);
@@ -128,6 +157,24 @@ namespace Bit.App.Pages
}
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await _deviceActionService.HideLoadingAsync();
+
+ if (response.CaptchaNeeded)
+ {
+ if (await HandleCaptchaAsync(response.CaptchaSiteKey))
+ {
+ await LogInAsync();
+ _captchaToken = null;
+ return;
+ }
+ else
+ {
+ Loading = false;
+ return;
+ }
+ }
+ MasterPassword = string.Empty;
+ _captchaToken = null;
+
if (response.TwoFactor)
{
StartTwoFactorAction?.Invoke();
@@ -142,6 +189,8 @@ namespace Bit.App.Pages
}
catch (ApiException e)
{
+ _captchaToken = null;
+ MasterPassword = string.Empty;
await _deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
@@ -149,6 +198,7 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred);
}
}
+ Loading = false;
}
public void TogglePassword()
diff --git a/src/App/Pages/Accounts/RegisterPage.xaml b/src/App/Pages/Accounts/RegisterPage.xaml
index 19d2a30fc..342f0dd29 100644
--- a/src/App/Pages/Accounts/RegisterPage.xaml
+++ b/src/App/Pages/Accounts/RegisterPage.xaml
@@ -21,7 +21,7 @@
-
+
diff --git a/src/App/Pages/Accounts/RegisterPageViewModel.cs b/src/App/Pages/Accounts/RegisterPageViewModel.cs
index c383fa1e5..37a67c5fb 100644
--- a/src/App/Pages/Accounts/RegisterPageViewModel.cs
+++ b/src/App/Pages/Accounts/RegisterPageViewModel.cs
@@ -12,14 +12,17 @@ using Xamarin.Forms;
namespace Bit.App.Pages
{
- public class RegisterPageViewModel : BaseViewModel
+ public class RegisterPageViewModel : CaptchaProtectedViewModel
{
private readonly IDeviceActionService _deviceActionService;
+ private readonly II18nService _i18nService;
+ private readonly IEnvironmentService _environmentService;
private readonly IApiService _apiService;
private readonly ICryptoService _cryptoService;
private readonly IPlatformUtilsService _platformUtilsService;
private bool _showPassword;
private bool _acceptPolicies;
+ private bool _submitEnabled = true;
public RegisterPageViewModel()
{
@@ -27,6 +30,8 @@ namespace Bit.App.Pages
_apiService = ServiceContainer.Resolve("apiService");
_cryptoService = ServiceContainer.Resolve("cryptoService");
_platformUtilsService = ServiceContainer.Resolve("platformUtilsService");
+ _i18nService = ServiceContainer.Resolve("i18nService");
+ _environmentService = ServiceContainer.Resolve("environmentService");
PageTitle = AppResources.CreateAccount;
TogglePasswordCommand = new Command(TogglePassword);
@@ -55,6 +60,16 @@ namespace Bit.App.Pages
get => _acceptPolicies;
set => SetProperty(ref _acceptPolicies, value);
}
+ public bool SubmitEnabled
+ {
+ get => _submitEnabled;
+ set => SetProperty(ref _submitEnabled, value);
+ }
+ public bool Loading
+ {
+ get => !SubmitEnabled;
+ set => SubmitEnabled = !value;
+ }
public Thickness SwitchMargin
{
@@ -76,6 +91,11 @@ namespace Bit.App.Pages
public Action RegistrationSuccess { get; set; }
public Action CloseAction { get; set; }
+ protected override II18nService i18nService => _i18nService;
+ protected override IEnvironmentService environmentService => _environmentService;
+ protected override IDeviceActionService deviceActionService => _deviceActionService;
+ protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
+
public async Task SubmitAsync()
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
@@ -145,15 +165,21 @@ namespace Bit.App.Pages
{
PublicKey = keys.Item1,
EncryptedPrivateKey = keys.Item2.EncryptedString
- }
+ },
+ CaptchaResponse = _captchaToken,
};
// TODO: org invite?
try
{
- await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
+ if (!Loading)
+ {
+ await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
+ Loading = true;
+ }
await _apiService.PostRegisterAsync(request);
await _deviceActionService.HideLoadingAsync();
+ Loading = false;
_platformUtilsService.ShowToast("success", null, AppResources.AccountCreated,
new System.Collections.Generic.Dictionary
{
@@ -163,7 +189,23 @@ namespace Bit.App.Pages
}
catch (ApiException e)
{
+ if (e?.Error != null && e.Error.CaptchaRequired)
+ {
+ if (await HandleCaptchaAsync(e.Error.CaptchaSiteKey))
+ {
+ await SubmitAsync();
+ _captchaToken = null;
+ return;
+ }
+ else
+ {
+ await _deviceActionService.HideLoadingAsync();
+ Loading = false;
+ return;
+ };
+ }
await _deviceActionService.HideLoadingAsync();
+ Loading = false;
if (e?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
diff --git a/src/App/Pages/CaptchaProtectedViewModel.cs b/src/App/Pages/CaptchaProtectedViewModel.cs
new file mode 100644
index 000000000..a39700d1b
--- /dev/null
+++ b/src/App/Pages/CaptchaProtectedViewModel.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Bit.App.Abstractions;
+using Bit.App.Resources;
+using Bit.Core.Abstractions;
+using Newtonsoft.Json;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Bit.App.Pages
+{
+ public abstract class CaptchaProtectedViewModel : BaseViewModel
+ {
+ protected abstract II18nService i18nService { get; }
+ protected abstract IEnvironmentService environmentService { get; }
+ protected abstract IDeviceActionService deviceActionService { get; }
+ protected abstract IPlatformUtilsService platformUtilsService { get; }
+ protected string _captchaToken = null;
+
+ protected async Task HandleCaptchaAsync(string CaptchaSiteKey)
+ {
+ var callbackUri = "bitwarden://captcha-callback";
+ var data = EncodeDataParameter(new
+ {
+ siteKey = CaptchaSiteKey,
+ locale = i18nService.Culture.TwoLetterISOLanguageName,
+ callbackUri = callbackUri,
+ captchaRequiredText = AppResources.CaptchaRequired,
+ });
+
+ var url = environmentService.WebVaultUrl + "/captcha-mobile-connector.html?" + "data=" + data +
+ "&parent=" + Uri.EscapeDataString(callbackUri) + "&v=1";
+
+ WebAuthenticatorResult authResult = null;
+ bool cancelled = false;
+ try
+ {
+ authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
+ new Uri(callbackUri));
+ }
+ catch (TaskCanceledException)
+ {
+ await deviceActionService.HideLoadingAsync();
+ cancelled = true;
+ }
+ catch (Exception e)
+ {
+ // WebAuthenticator throws NSErrorException if iOS flow is cancelled - by setting cancelled to true
+ // here we maintain the appearance of a clean cancellation (we don't want to do this across the board
+ // because we still want to present legitimate errors). If/when this is fixed, we can remove this
+ // particular catch block (catching taskCanceledException above must remain)
+ // https://github.com/xamarin/Essentials/issues/1240
+ if (Device.RuntimePlatform == Device.iOS)
+ {
+ await deviceActionService.HideLoadingAsync();
+ cancelled = true;
+ }
+ }
+
+ if (cancelled == false && authResult != null &&
+ authResult.Properties.TryGetValue("token", out _captchaToken))
+ {
+ return true;
+ }
+ else
+ {
+ await platformUtilsService.ShowDialogAsync(AppResources.CaptchaFailed,
+ AppResources.CaptchaRequired);
+ return false;
+ }
+ }
+
+ private string EncodeDataParameter(object obj)
+ {
+ string EncodeMultibyte(Match match)
+ {
+ return Convert.ToChar(Convert.ToUInt32($"0x{match.Groups[1].Value}", 16)).ToString();
+ }
+
+ var escaped = Uri.EscapeDataString(JsonConvert.SerializeObject(obj));
+ var multiByteEscaped = Regex.Replace(escaped, "%([0-9A-F]{2})", EncodeMultibyte);
+ return Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped));
+ }
+
+ }
+}
diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs
index 3b174ae04..9b761f3d4 100644
--- a/src/App/Resources/AppResources.Designer.cs
+++ b/src/App/Resources/AppResources.Designer.cs
@@ -3550,5 +3550,17 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("PasswordConfirmationDesc", resourceCulture);
}
}
+
+ public static string CaptchaRequired {
+ get {
+ return ResourceManager.GetString("CaptchaRequired", resourceCulture);
+ }
+ }
+
+ public static string CaptchaFailed {
+ get {
+ return ResourceManager.GetString("CaptchaFailed", resourceCulture);
+ }
+ }
}
}
diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx
index 99030fb49..21b4af715 100644
--- a/src/App/Resources/AppResources.resx
+++ b/src/App/Resources/AppResources.resx
@@ -2010,4 +2010,10 @@
This action is protected, to continue please re-enter your master password to verify your identity.
+
+ Captcha Required
+
+
+ Captcha Failed. Please try again.
+
diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs
index 87214347b..01394172a 100644
--- a/src/Core/Abstractions/IApiService.cs
+++ b/src/Core/Abstractions/IApiService.cs
@@ -30,7 +30,7 @@ namespace Bit.Core.Abstractions
Task PostCipherAsync(CipherRequest request);
Task PostCipherCreateAsync(CipherCreateRequest request);
Task PostFolderAsync(FolderRequest request);
- Task> PostIdentityTokenAsync(TokenRequest request);
+ Task PostIdentityTokenAsync(TokenRequest request);
Task PostPasswordHintAsync(PasswordHintRequest request);
Task SetPasswordAsync(SetPasswordRequest request);
Task PostPreloginAsync(PreloginRequest request);
diff --git a/src/Core/Abstractions/IAuthService.cs b/src/Core/Abstractions/IAuthService.cs
index 3e495edf1..d2c88f678 100644
--- a/src/Core/Abstractions/IAuthService.cs
+++ b/src/Core/Abstractions/IAuthService.cs
@@ -21,7 +21,7 @@ namespace Bit.Core.Abstractions
bool AuthingWithSso();
bool AuthingWithPassword();
List GetSupportedTwoFactorProviders();
- Task LogInAsync(string email, string masterPassword);
+ Task LogInAsync(string email, string masterPassword, string captchaToken);
Task LogInSsoAsync(string code, string codeVerifier, string redirectUrl);
Task LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
diff --git a/src/Core/Models/Domain/AuthResult.cs b/src/Core/Models/Domain/AuthResult.cs
index 495b273ab..258589240 100644
--- a/src/Core/Models/Domain/AuthResult.cs
+++ b/src/Core/Models/Domain/AuthResult.cs
@@ -6,6 +6,8 @@ namespace Bit.Core.Models.Domain
public class AuthResult
{
public bool TwoFactor { get; set; }
+ public bool CaptchaNeeded => !string.IsNullOrWhiteSpace(CaptchaSiteKey);
+ public string CaptchaSiteKey { get; set; }
public bool ResetMasterPassword { get; set; }
public Dictionary> TwoFactorProviders { get; set; }
}
diff --git a/src/Core/Models/Request/RegisterRequest.cs b/src/Core/Models/Request/RegisterRequest.cs
index 959b1d988..ee38ff026 100644
--- a/src/Core/Models/Request/RegisterRequest.cs
+++ b/src/Core/Models/Request/RegisterRequest.cs
@@ -15,5 +15,6 @@ namespace Bit.Core.Models.Request
public Guid? OrganizationUserId { get; set; }
public KdfType? Kdf { get; set; }
public int? KdfIterations { get; set; }
+ public string CaptchaResponse { get; set; }
}
}
diff --git a/src/Core/Models/Request/TokenRequest.cs b/src/Core/Models/Request/TokenRequest.cs
index a188fe2c3..d492405ac 100644
--- a/src/Core/Models/Request/TokenRequest.cs
+++ b/src/Core/Models/Request/TokenRequest.cs
@@ -16,10 +16,11 @@ namespace Bit.Core.Models.Request
public string Token { get; set; }
public TwoFactorProviderType? Provider { get; set; }
public bool? Remember { get; set; }
+ public string CaptchaToken { get; set; }
public DeviceRequest Device { get; set; }
public TokenRequest(string[] credentials, string[] codes, TwoFactorProviderType? provider, string token,
- bool? remember, DeviceRequest device = null)
+ bool? remember, string captchaToken, DeviceRequest device = null)
{
if (credentials != null && credentials.Length > 1)
{
@@ -36,6 +37,7 @@ namespace Bit.Core.Models.Request
Provider = provider;
Remember = remember;
Device = device;
+ CaptchaToken = captchaToken;
}
public Dictionary ToIdentityToken(string clientId)
@@ -77,6 +79,11 @@ namespace Bit.Core.Models.Request
obj.Add("twoFactorProvider", ((int)Provider.Value).ToString());
obj.Add("twoFactorRemember", Remember.GetValueOrDefault() ? "1" : "0");
}
+ if (CaptchaToken != null)
+ {
+ obj.Add("captchaResponse", CaptchaToken);
+ }
+
return obj;
}
diff --git a/src/Core/Models/Response/ErrorResponse.cs b/src/Core/Models/Response/ErrorResponse.cs
index 56316dcbc..ca91df845 100644
--- a/src/Core/Models/Response/ErrorResponse.cs
+++ b/src/Core/Models/Response/ErrorResponse.cs
@@ -30,6 +30,10 @@ namespace Bit.Core.Models.Response
var model = errorModel.ToObject();
Message = model.Message;
ValidationErrors = model.ValidationErrors;
+ CaptchaSiteKey = ValidationErrors.ContainsKey("HCaptcha_SiteKey") ?
+ ValidationErrors["HCaptcha_SiteKey"]?.FirstOrDefault() :
+ null;
+ CaptchaRequired = !string.IsNullOrWhiteSpace(CaptchaSiteKey);
}
else
{
@@ -44,6 +48,8 @@ namespace Bit.Core.Models.Response
public string Message { get; set; }
public Dictionary> ValidationErrors { get; set; }
public HttpStatusCode StatusCode { get; set; }
+ public string CaptchaSiteKey { get; set; }
+ public bool CaptchaRequired { get; set; } = false;
public string GetSingleMessage()
{
diff --git a/src/Core/Models/Response/IdentityCaptchaResponse.cs b/src/Core/Models/Response/IdentityCaptchaResponse.cs
new file mode 100644
index 000000000..018248b94
--- /dev/null
+++ b/src/Core/Models/Response/IdentityCaptchaResponse.cs
@@ -0,0 +1,13 @@
+using Bit.Core.Enums;
+using Newtonsoft.Json;
+using System.Collections.Generic;
+
+namespace Bit.Core.Models.Response
+{
+ public class IdentityCaptchaResponse
+ {
+ [JsonProperty("HCaptcha_SiteKey")]
+ public string SiteKey { get; set; }
+ }
+
+}
diff --git a/src/Core/Models/Response/IdentityResponse.cs b/src/Core/Models/Response/IdentityResponse.cs
new file mode 100644
index 000000000..6f693e571
--- /dev/null
+++ b/src/Core/Models/Response/IdentityResponse.cs
@@ -0,0 +1,50 @@
+using System.Net;
+using Newtonsoft.Json.Linq;
+
+namespace Bit.Core.Models.Response
+{
+ public class IdentityResponse
+ {
+ public IdentityTokenResponse TokenResponse { get; }
+ public IdentityTwoFactorResponse TwoFactorResponse { get; }
+ public IdentityCaptchaResponse CaptchaResponse { get; }
+
+ public bool TwoFactorNeeded => TwoFactorResponse != null;
+ public bool FailedToParse { get; }
+
+ public IdentityResponse(HttpStatusCode httpStatusCode, JObject responseJObject)
+ {
+ var parsed = false;
+ if (responseJObject != null)
+ {
+ if (IsSuccessStatusCode(httpStatusCode))
+ {
+ TokenResponse = responseJObject.ToObject();
+ parsed = true;
+ }
+ else if (httpStatusCode == HttpStatusCode.BadRequest)
+ {
+ if (JObjectHasProperty(responseJObject, "TwoFactorProviders2"))
+ {
+ TwoFactorResponse = responseJObject.ToObject();
+ parsed = true;
+ }
+ else if (JObjectHasProperty(responseJObject, "HCaptcha_SiteKey"))
+ {
+ CaptchaResponse = responseJObject.ToObject();
+ parsed = true;
+ }
+ }
+ }
+ FailedToParse = !parsed;
+ }
+
+ private bool IsSuccessStatusCode(HttpStatusCode httpStatusCode) =>
+ (int)httpStatusCode >= 200 && (int)httpStatusCode < 300;
+
+ private bool JObjectHasProperty(JObject jObject, string propertyName) =>
+ jObject.ContainsKey(propertyName) &&
+ jObject[propertyName] != null &&
+ (jObject[propertyName].HasValues || jObject[propertyName].Value() != null);
+ }
+}
diff --git a/src/Core/Models/Response/IdentityTwoFactorResponse.cs b/src/Core/Models/Response/IdentityTwoFactorResponse.cs
index 82f082e9e..65b1f94b8 100644
--- a/src/Core/Models/Response/IdentityTwoFactorResponse.cs
+++ b/src/Core/Models/Response/IdentityTwoFactorResponse.cs
@@ -1,4 +1,5 @@
using Bit.Core.Enums;
+using Newtonsoft.Json;
using System.Collections.Generic;
namespace Bit.Core.Models.Response
@@ -7,5 +8,7 @@ namespace Bit.Core.Models.Response
{
public List TwoFactorProviders { get; set; }
public Dictionary> TwoFactorProviders2 { get; set; }
+ [JsonProperty("CaptchaBypassToken")]
+ public string CaptchaToken { get; set; }
}
}
diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs
index 5d86c031e..78a06e80d 100644
--- a/src/Core/Services/ApiService.cs
+++ b/src/Core/Services/ApiService.cs
@@ -80,8 +80,7 @@ namespace Bit.Core.Services
#region Auth APIs
- public async Task> PostIdentityTokenAsync(
- TokenRequest request)
+ public async Task PostIdentityTokenAsync(TokenRequest request)
{
var requestMessage = new HttpRequestMessage
{
@@ -109,23 +108,14 @@ namespace Bit.Core.Services
responseJObject = JObject.Parse(responseJsonString);
}
- if (responseJObject != null)
+ var identityResponse = new IdentityResponse(response.StatusCode, responseJObject);
+
+ if (identityResponse.FailedToParse)
{
- if (response.IsSuccessStatusCode)
- {
- return new Tuple(
- responseJObject.ToObject(), null);
- }
- else if (response.StatusCode == HttpStatusCode.BadRequest &&
- responseJObject.ContainsKey("TwoFactorProviders2") &&
- responseJObject["TwoFactorProviders2"] != null &&
- responseJObject["TwoFactorProviders2"].HasValues)
- {
- return new Tuple(
- null, responseJObject.ToObject());
- }
+ throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true));
}
- throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true));
+
+ return identityResponse;
}
public async Task RefreshIdentityTokenAsync()
diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs
index 95ba58e6c..e8c9e9675 100644
--- a/src/Core/Services/AuthService.cs
+++ b/src/Core/Services/AuthService.cs
@@ -92,6 +92,7 @@ namespace Bit.Core.Services
}
public string Email { get; set; }
+ public string CaptchaToken { get; set; }
public string MasterPasswordHash { get; set; }
public string LocalMasterPasswordHash { get; set; }
public string Code { get; set; }
@@ -119,13 +120,14 @@ namespace Bit.Core.Services
TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc");
}
- public async Task LogInAsync(string email, string masterPassword)
+ public async Task LogInAsync(string email, string masterPassword, string captchaToken)
{
SelectedTwoFactorProviderType = null;
var key = await MakePreloginKeyAsync(masterPassword, email);
var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key);
var localHashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization);
- return await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, null, null, null);
+ return await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, null, null,
+ null, captchaToken);
}
public async Task LogInSsoAsync(string code, string codeVerifier, string redirectUrl)
@@ -138,7 +140,7 @@ namespace Bit.Core.Services
bool? remember = null)
{
return LogInHelperAsync(Email, MasterPasswordHash, LocalMasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key,
- twoFactorProvider, twoFactorToken, remember);
+ twoFactorProvider, twoFactorToken, remember, CaptchaToken);
}
public async Task LogInCompleteAsync(string email, string masterPassword,
@@ -271,7 +273,8 @@ namespace Bit.Core.Services
private async Task LogInHelperAsync(string email, string hashedPassword, string localHashedPassword,
string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key,
- TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null)
+ TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null,
+ string captchaToken = null)
{
var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email);
var appId = await _appIdService.GetAppIdAsync();
@@ -300,25 +303,30 @@ namespace Bit.Core.Services
if (twoFactorToken != null && twoFactorProvider != null)
{
request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember,
- deviceRequest);
+ captchaToken, deviceRequest);
}
else if (storedTwoFactorToken != null)
{
request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember,
- storedTwoFactorToken, false, deviceRequest);
+ storedTwoFactorToken, false, captchaToken, deviceRequest);
}
else
{
- request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, deviceRequest);
+ request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, captchaToken, deviceRequest);
}
var response = await _apiService.PostIdentityTokenAsync(request);
ClearState();
- var result = new AuthResult { TwoFactor = response.Item2 != null };
+ var result = new AuthResult { TwoFactor = response.TwoFactorNeeded, CaptchaSiteKey = response.CaptchaResponse?.SiteKey };
+
+ if (result.CaptchaNeeded)
+ {
+ return result;
+ }
+
if (result.TwoFactor)
{
// Two factor required.
- var twoFactorResponse = response.Item2;
Email = email;
MasterPasswordHash = hashedPassword;
LocalMasterPasswordHash = localHashedPassword;
@@ -326,12 +334,13 @@ namespace Bit.Core.Services
CodeVerifier = codeVerifier;
SsoRedirectUrl = redirectUrl;
_key = _setCryptoKeys ? key : null;
- TwoFactorProvidersData = twoFactorResponse.TwoFactorProviders2;
- result.TwoFactorProviders = twoFactorResponse.TwoFactorProviders2;
+ TwoFactorProvidersData = response.TwoFactorResponse.TwoFactorProviders2;
+ result.TwoFactorProviders = response.TwoFactorResponse.TwoFactorProviders2;
+ CaptchaToken = response.TwoFactorResponse.CaptchaToken;
return result;
}
- var tokenResponse = response.Item1;
+ var tokenResponse = response.TokenResponse;
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
if (tokenResponse.TwoFactorToken != null)
{
@@ -374,6 +383,7 @@ namespace Bit.Core.Services
{
_key = null;
Email = null;
+ CaptchaToken = null;
MasterPasswordHash = null;
Code = null;
CodeVerifier = null;