mirror of
https://github.com/bitwarden/mobile
synced 2025-12-29 06:33:53 +00:00
Feature/use hcaptcha if bot (#1476)
* Add captcha to login models and methods * Add captcha web auth to login * Extract captcha to abstract base class * Add Captcha to register * Null out captcha token after each successful challenge * Cancel > close
This commit is contained in:
@@ -82,7 +82,7 @@
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
<Button Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" />
|
||||
<Button Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" IsEnabled="{Binding LoginEnabled}"/>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -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<IStorageService>("storageService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("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()
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" IsEnabled="{Binding SubmitEnabled}"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
|
||||
@@ -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<IApiService>("apiService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("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<string, object>
|
||||
{
|
||||
@@ -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(),
|
||||
|
||||
88
src/App/Pages/CaptchaProtectedViewModel.cs
Normal file
88
src/App/Pages/CaptchaProtectedViewModel.cs
Normal file
@@ -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<bool> 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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
12
src/App/Resources/AppResources.Designer.cs
generated
12
src/App/Resources/AppResources.Designer.cs
generated
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2010,4 +2010,10 @@
|
||||
<data name="PasswordConfirmationDesc" xml:space="preserve">
|
||||
<value>This action is protected, to continue please re-enter your master password to verify your identity.</value>
|
||||
</data>
|
||||
<data name="CaptchaRequired" xml:space="preserve">
|
||||
<value>Captcha Required</value>
|
||||
</data>
|
||||
<data name="CaptchaFailed" xml:space="preserve">
|
||||
<value>Captcha Failed. Please try again.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Reference in New Issue
Block a user