mirror of
https://github.com/bitwarden/android.git
synced 2025-01-11 18:57:39 +03: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:
parent
9042b1009e
commit
2f2fa8a25b
18 changed files with 322 additions and 42 deletions
|
@ -82,7 +82,7 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout Padding="10, 0">
|
<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>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
@ -8,10 +8,15 @@ using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Text;
|
||||||
|
using Xamarin.Essentials;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class LoginPageViewModel : BaseViewModel
|
public class LoginPageViewModel : CaptchaProtectedViewModel
|
||||||
{
|
{
|
||||||
private const string Keys_RememberedEmail = "rememberedEmail";
|
private const string Keys_RememberedEmail = "rememberedEmail";
|
||||||
private const string Keys_RememberEmail = "rememberEmail";
|
private const string Keys_RememberEmail = "rememberEmail";
|
||||||
|
@ -22,10 +27,13 @@ namespace Bit.App.Pages
|
||||||
private readonly IStorageService _storageService;
|
private readonly IStorageService _storageService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
|
private readonly IEnvironmentService _environmentService;
|
||||||
|
private readonly II18nService _i18nService;
|
||||||
|
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
private string _email;
|
private string _email;
|
||||||
private string _masterPassword;
|
private string _masterPassword;
|
||||||
|
private bool _loginEnabled = true;
|
||||||
|
|
||||||
public LoginPageViewModel()
|
public LoginPageViewModel()
|
||||||
{
|
{
|
||||||
|
@ -35,6 +43,8 @@ namespace Bit.App.Pages
|
||||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||||
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
|
|
||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
|
@ -63,6 +73,16 @@ namespace Bit.App.Pages
|
||||||
set => SetProperty(ref _masterPassword, value);
|
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 LogInCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||||
|
@ -70,7 +90,12 @@ namespace Bit.App.Pages
|
||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
public Action LogInSuccessAction { get; set; }
|
public Action LogInSuccessAction { get; set; }
|
||||||
public Action CloseAction { 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()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(Email))
|
if (string.IsNullOrWhiteSpace(Email))
|
||||||
|
@ -115,9 +140,13 @@ namespace Bit.App.Pages
|
||||||
ShowPassword = false;
|
ShowPassword = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
if (!Loading)
|
||||||
var response = await _authService.LogInAsync(Email, MasterPassword);
|
{
|
||||||
MasterPassword = string.Empty;
|
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||||
|
Loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
|
||||||
if (RememberEmail)
|
if (RememberEmail)
|
||||||
{
|
{
|
||||||
await _storageService.SaveAsync(Keys_RememberedEmail, Email);
|
await _storageService.SaveAsync(Keys_RememberedEmail, Email);
|
||||||
|
@ -128,6 +157,24 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
await _deviceActionService.HideLoadingAsync();
|
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)
|
if (response.TwoFactor)
|
||||||
{
|
{
|
||||||
StartTwoFactorAction?.Invoke();
|
StartTwoFactorAction?.Invoke();
|
||||||
|
@ -142,6 +189,8 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
catch (ApiException e)
|
||||||
{
|
{
|
||||||
|
_captchaToken = null;
|
||||||
|
MasterPassword = string.Empty;
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
if (e?.Error != null)
|
if (e?.Error != null)
|
||||||
{
|
{
|
||||||
|
@ -149,6 +198,7 @@ namespace Bit.App.Pages
|
||||||
AppResources.AnErrorHasOccurred);
|
AppResources.AnErrorHasOccurred);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TogglePassword()
|
public void TogglePassword()
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
<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>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
|
|
|
@ -12,14 +12,17 @@ using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class RegisterPageViewModel : BaseViewModel
|
public class RegisterPageViewModel : CaptchaProtectedViewModel
|
||||||
{
|
{
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly II18nService _i18nService;
|
||||||
|
private readonly IEnvironmentService _environmentService;
|
||||||
private readonly IApiService _apiService;
|
private readonly IApiService _apiService;
|
||||||
private readonly ICryptoService _cryptoService;
|
private readonly ICryptoService _cryptoService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
private bool _acceptPolicies;
|
private bool _acceptPolicies;
|
||||||
|
private bool _submitEnabled = true;
|
||||||
|
|
||||||
public RegisterPageViewModel()
|
public RegisterPageViewModel()
|
||||||
{
|
{
|
||||||
|
@ -27,6 +30,8 @@ namespace Bit.App.Pages
|
||||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||||
|
|
||||||
PageTitle = AppResources.CreateAccount;
|
PageTitle = AppResources.CreateAccount;
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
|
@ -55,6 +60,16 @@ namespace Bit.App.Pages
|
||||||
get => _acceptPolicies;
|
get => _acceptPolicies;
|
||||||
set => SetProperty(ref _acceptPolicies, value);
|
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
|
public Thickness SwitchMargin
|
||||||
{
|
{
|
||||||
|
@ -76,6 +91,11 @@ namespace Bit.App.Pages
|
||||||
public Action RegistrationSuccess { get; set; }
|
public Action RegistrationSuccess { get; set; }
|
||||||
public Action CloseAction { 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()
|
public async Task SubmitAsync()
|
||||||
{
|
{
|
||||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||||
|
@ -145,15 +165,21 @@ namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
PublicKey = keys.Item1,
|
PublicKey = keys.Item1,
|
||||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
EncryptedPrivateKey = keys.Item2.EncryptedString
|
||||||
}
|
},
|
||||||
|
CaptchaResponse = _captchaToken,
|
||||||
};
|
};
|
||||||
// TODO: org invite?
|
// TODO: org invite?
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
|
if (!Loading)
|
||||||
|
{
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
|
||||||
|
Loading = true;
|
||||||
|
}
|
||||||
await _apiService.PostRegisterAsync(request);
|
await _apiService.PostRegisterAsync(request);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
Loading = false;
|
||||||
_platformUtilsService.ShowToast("success", null, AppResources.AccountCreated,
|
_platformUtilsService.ShowToast("success", null, AppResources.AccountCreated,
|
||||||
new System.Collections.Generic.Dictionary<string, object>
|
new System.Collections.Generic.Dictionary<string, object>
|
||||||
{
|
{
|
||||||
|
@ -163,7 +189,23 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
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();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
Loading = false;
|
||||||
if (e?.Error != null)
|
if (e?.Error != null)
|
||||||
{
|
{
|
||||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
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);
|
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">
|
<data name="PasswordConfirmationDesc" xml:space="preserve">
|
||||||
<value>This action is protected, to continue please re-enter your master password to verify your identity.</value>
|
<value>This action is protected, to continue please re-enter your master password to verify your identity.</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|
|
@ -30,7 +30,7 @@ namespace Bit.Core.Abstractions
|
||||||
Task<CipherResponse> PostCipherAsync(CipherRequest request);
|
Task<CipherResponse> PostCipherAsync(CipherRequest request);
|
||||||
Task<CipherResponse> PostCipherCreateAsync(CipherCreateRequest request);
|
Task<CipherResponse> PostCipherCreateAsync(CipherCreateRequest request);
|
||||||
Task<FolderResponse> PostFolderAsync(FolderRequest request);
|
Task<FolderResponse> PostFolderAsync(FolderRequest request);
|
||||||
Task<Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>> PostIdentityTokenAsync(TokenRequest request);
|
Task<IdentityResponse> PostIdentityTokenAsync(TokenRequest request);
|
||||||
Task PostPasswordHintAsync(PasswordHintRequest request);
|
Task PostPasswordHintAsync(PasswordHintRequest request);
|
||||||
Task SetPasswordAsync(SetPasswordRequest request);
|
Task SetPasswordAsync(SetPasswordRequest request);
|
||||||
Task<PreloginResponse> PostPreloginAsync(PreloginRequest request);
|
Task<PreloginResponse> PostPreloginAsync(PreloginRequest request);
|
||||||
|
|
|
@ -21,7 +21,7 @@ namespace Bit.Core.Abstractions
|
||||||
bool AuthingWithSso();
|
bool AuthingWithSso();
|
||||||
bool AuthingWithPassword();
|
bool AuthingWithPassword();
|
||||||
List<TwoFactorProvider> GetSupportedTwoFactorProviders();
|
List<TwoFactorProvider> GetSupportedTwoFactorProviders();
|
||||||
Task<AuthResult> LogInAsync(string email, string masterPassword);
|
Task<AuthResult> LogInAsync(string email, string masterPassword, string captchaToken);
|
||||||
Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl);
|
Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl);
|
||||||
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
||||||
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
||||||
|
|
|
@ -6,6 +6,8 @@ namespace Bit.Core.Models.Domain
|
||||||
public class AuthResult
|
public class AuthResult
|
||||||
{
|
{
|
||||||
public bool TwoFactor { get; set; }
|
public bool TwoFactor { get; set; }
|
||||||
|
public bool CaptchaNeeded => !string.IsNullOrWhiteSpace(CaptchaSiteKey);
|
||||||
|
public string CaptchaSiteKey { get; set; }
|
||||||
public bool ResetMasterPassword { get; set; }
|
public bool ResetMasterPassword { get; set; }
|
||||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
|
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,5 +15,6 @@ namespace Bit.Core.Models.Request
|
||||||
public Guid? OrganizationUserId { get; set; }
|
public Guid? OrganizationUserId { get; set; }
|
||||||
public KdfType? Kdf { get; set; }
|
public KdfType? Kdf { get; set; }
|
||||||
public int? KdfIterations { get; set; }
|
public int? KdfIterations { get; set; }
|
||||||
|
public string CaptchaResponse { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,11 @@ namespace Bit.Core.Models.Request
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
public TwoFactorProviderType? Provider { get; set; }
|
public TwoFactorProviderType? Provider { get; set; }
|
||||||
public bool? Remember { get; set; }
|
public bool? Remember { get; set; }
|
||||||
|
public string CaptchaToken { get; set; }
|
||||||
public DeviceRequest Device { get; set; }
|
public DeviceRequest Device { get; set; }
|
||||||
|
|
||||||
public TokenRequest(string[] credentials, string[] codes, TwoFactorProviderType? provider, string token,
|
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)
|
if (credentials != null && credentials.Length > 1)
|
||||||
{
|
{
|
||||||
|
@ -36,6 +37,7 @@ namespace Bit.Core.Models.Request
|
||||||
Provider = provider;
|
Provider = provider;
|
||||||
Remember = remember;
|
Remember = remember;
|
||||||
Device = device;
|
Device = device;
|
||||||
|
CaptchaToken = captchaToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, string> ToIdentityToken(string clientId)
|
public Dictionary<string, string> ToIdentityToken(string clientId)
|
||||||
|
@ -77,6 +79,11 @@ namespace Bit.Core.Models.Request
|
||||||
obj.Add("twoFactorProvider", ((int)Provider.Value).ToString());
|
obj.Add("twoFactorProvider", ((int)Provider.Value).ToString());
|
||||||
obj.Add("twoFactorRemember", Remember.GetValueOrDefault() ? "1" : "0");
|
obj.Add("twoFactorRemember", Remember.GetValueOrDefault() ? "1" : "0");
|
||||||
}
|
}
|
||||||
|
if (CaptchaToken != null)
|
||||||
|
{
|
||||||
|
obj.Add("captchaResponse", CaptchaToken);
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,10 @@ namespace Bit.Core.Models.Response
|
||||||
var model = errorModel.ToObject<ErrorModel>();
|
var model = errorModel.ToObject<ErrorModel>();
|
||||||
Message = model.Message;
|
Message = model.Message;
|
||||||
ValidationErrors = model.ValidationErrors;
|
ValidationErrors = model.ValidationErrors;
|
||||||
|
CaptchaSiteKey = ValidationErrors.ContainsKey("HCaptcha_SiteKey") ?
|
||||||
|
ValidationErrors["HCaptcha_SiteKey"]?.FirstOrDefault() :
|
||||||
|
null;
|
||||||
|
CaptchaRequired = !string.IsNullOrWhiteSpace(CaptchaSiteKey);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -44,6 +48,8 @@ namespace Bit.Core.Models.Response
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
public Dictionary<string, List<string>> ValidationErrors { get; set; }
|
public Dictionary<string, List<string>> ValidationErrors { get; set; }
|
||||||
public HttpStatusCode StatusCode { get; set; }
|
public HttpStatusCode StatusCode { get; set; }
|
||||||
|
public string CaptchaSiteKey { get; set; }
|
||||||
|
public bool CaptchaRequired { get; set; } = false;
|
||||||
|
|
||||||
public string GetSingleMessage()
|
public string GetSingleMessage()
|
||||||
{
|
{
|
||||||
|
|
13
src/Core/Models/Response/IdentityCaptchaResponse.cs
Normal file
13
src/Core/Models/Response/IdentityCaptchaResponse.cs
Normal file
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
50
src/Core/Models/Response/IdentityResponse.cs
Normal file
50
src/Core/Models/Response/IdentityResponse.cs
Normal file
|
@ -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<IdentityTokenResponse>();
|
||||||
|
parsed = true;
|
||||||
|
}
|
||||||
|
else if (httpStatusCode == HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
if (JObjectHasProperty(responseJObject, "TwoFactorProviders2"))
|
||||||
|
{
|
||||||
|
TwoFactorResponse = responseJObject.ToObject<IdentityTwoFactorResponse>();
|
||||||
|
parsed = true;
|
||||||
|
}
|
||||||
|
else if (JObjectHasProperty(responseJObject, "HCaptcha_SiteKey"))
|
||||||
|
{
|
||||||
|
CaptchaResponse = responseJObject.ToObject<IdentityCaptchaResponse>();
|
||||||
|
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<string>() != null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Response
|
namespace Bit.Core.Models.Response
|
||||||
|
@ -7,5 +8,7 @@ namespace Bit.Core.Models.Response
|
||||||
{
|
{
|
||||||
public List<TwoFactorProviderType> TwoFactorProviders { get; set; }
|
public List<TwoFactorProviderType> TwoFactorProviders { get; set; }
|
||||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders2 { get; set; }
|
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders2 { get; set; }
|
||||||
|
[JsonProperty("CaptchaBypassToken")]
|
||||||
|
public string CaptchaToken { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,8 +80,7 @@ namespace Bit.Core.Services
|
||||||
|
|
||||||
#region Auth APIs
|
#region Auth APIs
|
||||||
|
|
||||||
public async Task<Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>> PostIdentityTokenAsync(
|
public async Task<IdentityResponse> PostIdentityTokenAsync(TokenRequest request)
|
||||||
TokenRequest request)
|
|
||||||
{
|
{
|
||||||
var requestMessage = new HttpRequestMessage
|
var requestMessage = new HttpRequestMessage
|
||||||
{
|
{
|
||||||
|
@ -109,23 +108,14 @@ namespace Bit.Core.Services
|
||||||
responseJObject = JObject.Parse(responseJsonString);
|
responseJObject = JObject.Parse(responseJsonString);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseJObject != null)
|
var identityResponse = new IdentityResponse(response.StatusCode, responseJObject);
|
||||||
|
|
||||||
|
if (identityResponse.FailedToParse)
|
||||||
{
|
{
|
||||||
if (response.IsSuccessStatusCode)
|
throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true));
|
||||||
{
|
|
||||||
return new Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>(
|
|
||||||
responseJObject.ToObject<IdentityTokenResponse>(), null);
|
|
||||||
}
|
|
||||||
else if (response.StatusCode == HttpStatusCode.BadRequest &&
|
|
||||||
responseJObject.ContainsKey("TwoFactorProviders2") &&
|
|
||||||
responseJObject["TwoFactorProviders2"] != null &&
|
|
||||||
responseJObject["TwoFactorProviders2"].HasValues)
|
|
||||||
{
|
|
||||||
return new Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>(
|
|
||||||
null, responseJObject.ToObject<IdentityTwoFactorResponse>());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true));
|
|
||||||
|
return identityResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshIdentityTokenAsync()
|
public async Task RefreshIdentityTokenAsync()
|
||||||
|
|
|
@ -92,6 +92,7 @@ namespace Bit.Core.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
public string CaptchaToken { get; set; }
|
||||||
public string MasterPasswordHash { get; set; }
|
public string MasterPasswordHash { get; set; }
|
||||||
public string LocalMasterPasswordHash { get; set; }
|
public string LocalMasterPasswordHash { get; set; }
|
||||||
public string Code { get; set; }
|
public string Code { get; set; }
|
||||||
|
@ -119,13 +120,14 @@ namespace Bit.Core.Services
|
||||||
TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc");
|
TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AuthResult> LogInAsync(string email, string masterPassword)
|
public async Task<AuthResult> LogInAsync(string email, string masterPassword, string captchaToken)
|
||||||
{
|
{
|
||||||
SelectedTwoFactorProviderType = null;
|
SelectedTwoFactorProviderType = null;
|
||||||
var key = await MakePreloginKeyAsync(masterPassword, email);
|
var key = await MakePreloginKeyAsync(masterPassword, email);
|
||||||
var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key);
|
var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key);
|
||||||
var localHashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization);
|
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<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl)
|
public async Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl)
|
||||||
|
@ -138,7 +140,7 @@ namespace Bit.Core.Services
|
||||||
bool? remember = null)
|
bool? remember = null)
|
||||||
{
|
{
|
||||||
return LogInHelperAsync(Email, MasterPasswordHash, LocalMasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key,
|
return LogInHelperAsync(Email, MasterPasswordHash, LocalMasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key,
|
||||||
twoFactorProvider, twoFactorToken, remember);
|
twoFactorProvider, twoFactorToken, remember, CaptchaToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AuthResult> LogInCompleteAsync(string email, string masterPassword,
|
public async Task<AuthResult> LogInCompleteAsync(string email, string masterPassword,
|
||||||
|
@ -271,7 +273,8 @@ namespace Bit.Core.Services
|
||||||
|
|
||||||
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string localHashedPassword,
|
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string localHashedPassword,
|
||||||
string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key,
|
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 storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email);
|
||||||
var appId = await _appIdService.GetAppIdAsync();
|
var appId = await _appIdService.GetAppIdAsync();
|
||||||
|
@ -300,25 +303,30 @@ namespace Bit.Core.Services
|
||||||
if (twoFactorToken != null && twoFactorProvider != null)
|
if (twoFactorToken != null && twoFactorProvider != null)
|
||||||
{
|
{
|
||||||
request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember,
|
request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember,
|
||||||
deviceRequest);
|
captchaToken, deviceRequest);
|
||||||
}
|
}
|
||||||
else if (storedTwoFactorToken != null)
|
else if (storedTwoFactorToken != null)
|
||||||
{
|
{
|
||||||
request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember,
|
request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember,
|
||||||
storedTwoFactorToken, false, deviceRequest);
|
storedTwoFactorToken, false, captchaToken, deviceRequest);
|
||||||
}
|
}
|
||||||
else
|
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);
|
var response = await _apiService.PostIdentityTokenAsync(request);
|
||||||
ClearState();
|
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)
|
if (result.TwoFactor)
|
||||||
{
|
{
|
||||||
// Two factor required.
|
// Two factor required.
|
||||||
var twoFactorResponse = response.Item2;
|
|
||||||
Email = email;
|
Email = email;
|
||||||
MasterPasswordHash = hashedPassword;
|
MasterPasswordHash = hashedPassword;
|
||||||
LocalMasterPasswordHash = localHashedPassword;
|
LocalMasterPasswordHash = localHashedPassword;
|
||||||
|
@ -326,12 +334,13 @@ namespace Bit.Core.Services
|
||||||
CodeVerifier = codeVerifier;
|
CodeVerifier = codeVerifier;
|
||||||
SsoRedirectUrl = redirectUrl;
|
SsoRedirectUrl = redirectUrl;
|
||||||
_key = _setCryptoKeys ? key : null;
|
_key = _setCryptoKeys ? key : null;
|
||||||
TwoFactorProvidersData = twoFactorResponse.TwoFactorProviders2;
|
TwoFactorProvidersData = response.TwoFactorResponse.TwoFactorProviders2;
|
||||||
result.TwoFactorProviders = twoFactorResponse.TwoFactorProviders2;
|
result.TwoFactorProviders = response.TwoFactorResponse.TwoFactorProviders2;
|
||||||
|
CaptchaToken = response.TwoFactorResponse.CaptchaToken;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenResponse = response.Item1;
|
var tokenResponse = response.TokenResponse;
|
||||||
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
|
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
|
||||||
if (tokenResponse.TwoFactorToken != null)
|
if (tokenResponse.TwoFactorToken != null)
|
||||||
{
|
{
|
||||||
|
@ -374,6 +383,7 @@ namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
_key = null;
|
_key = null;
|
||||||
Email = null;
|
Email = null;
|
||||||
|
CaptchaToken = null;
|
||||||
MasterPasswordHash = null;
|
MasterPasswordHash = null;
|
||||||
Code = null;
|
Code = null;
|
||||||
CodeVerifier = null;
|
CodeVerifier = null;
|
||||||
|
|
Loading…
Reference in a new issue