bitwarden-mobile/src/App/Pages/Accounts/TwoFactorPageViewModel.cs

409 lines
16 KiB
C#
Raw Normal View History

2022-04-26 17:21:17 +02:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Bit.App.Abstractions;
2019-05-24 03:19:45 +02:00
using Bit.App.Resources;
2022-04-26 17:21:17 +02:00
using Bit.App.Utilities;
2019-05-24 03:19:45 +02:00
using Bit.Core.Abstractions;
2019-05-27 16:28:38 +02:00
using Bit.Core.Enums;
2019-05-24 03:19:45 +02:00
using Bit.Core.Exceptions;
2019-05-27 16:28:38 +02:00
using Bit.Core.Models.Request;
2019-05-24 03:19:45 +02:00
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Xamarin.Essentials;
2019-05-24 03:19:45 +02:00
using Xamarin.Forms;
namespace Bit.App.Pages
{
2022-03-07 19:39:38 +01:00
public class TwoFactorPageViewModel : CaptchaProtectedViewModel
2019-05-24 03:19:45 +02:00
{
private readonly IDeviceActionService _deviceActionService;
private readonly IAuthService _authService;
private readonly ISyncService _syncService;
2019-05-27 16:28:38 +02:00
private readonly IApiService _apiService;
private readonly IPlatformUtilsService _platformUtilsService;
2019-05-27 17:57:10 +02:00
private readonly IEnvironmentService _environmentService;
2019-05-28 15:54:08 +02:00
private readonly IMessagingService _messagingService;
private readonly IBroadcasterService _broadcasterService;
private readonly IStateService _stateService;
2022-03-07 19:39:38 +01:00
private readonly II18nService _i18nService;
2019-05-24 03:19:45 +02:00
2019-05-27 16:28:38 +02:00
private TwoFactorProviderType? _selectedProviderType;
2019-05-28 16:12:51 +02:00
private string _totpInstruction;
2019-05-27 17:57:10 +02:00
private string _webVaultUrl = "https://vault.bitwarden.com";
private bool _authingWithSso = false;
private bool _enableContinue = false;
private bool _showContinue = true;
2019-05-24 03:19:45 +02:00
public TwoFactorPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_authService = ServiceContainer.Resolve<IAuthService>("authService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
2019-05-27 16:28:38 +02:00
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
2019-05-27 17:57:10 +02:00
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
2019-05-28 15:54:08 +02:00
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
2022-03-07 19:39:38 +01:00
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
2019-05-28 15:04:20 +02:00
PageTitle = AppResources.TwoStepLogin;
2019-05-31 18:13:14 +02:00
SubmitCommand = new Command(async () => await SubmitAsync());
2019-05-24 03:19:45 +02:00
}
2019-05-28 16:12:51 +02:00
public string TotpInstruction
2019-05-24 03:19:45 +02:00
{
2019-05-28 16:12:51 +02:00
get => _totpInstruction;
set => SetProperty(ref _totpInstruction, value);
2019-05-24 03:19:45 +02:00
}
2019-05-27 16:28:38 +02:00
public bool Remember { get; set; }
public string Token { get; set; }
2019-05-28 15:04:20 +02:00
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
SelectedProviderType == TwoFactorProviderType.OrganizationDuo;
2019-05-27 16:28:38 +02:00
public bool Fido2Method => SelectedProviderType == TwoFactorProviderType.Fido2WebAuthn;
2019-05-27 16:28:38 +02:00
public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey;
public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator;
public bool EmailMethod => SelectedProviderType == TwoFactorProviderType.Email;
2019-05-27 17:57:10 +02:00
public bool TotpMethod => AuthenticatorMethod || EmailMethod;
public bool ShowTryAgain => (YubikeyMethod && Device.RuntimePlatform == Device.iOS) || Fido2Method;
2019-07-07 03:59:13 +02:00
public bool ShowContinue
{
get => _showContinue;
set => SetProperty(ref _showContinue, value);
}
public bool EnableContinue
{
get => _enableContinue;
set => SetProperty(ref _enableContinue, value);
}
2019-05-27 17:57:10 +02:00
public string YubikeyInstruction => Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos :
AppResources.YubiKeyInstruction;
2019-05-27 16:28:38 +02:00
public TwoFactorProviderType? SelectedProviderType
2019-05-24 03:19:45 +02:00
{
2019-05-27 16:28:38 +02:00
get => _selectedProviderType;
set => SetProperty(ref _selectedProviderType, value, additionalPropertyNames: new string[]
{
nameof(EmailMethod),
nameof(DuoMethod),
nameof(Fido2Method),
2019-05-27 16:28:38 +02:00
nameof(YubikeyMethod),
2019-05-27 17:57:10 +02:00
nameof(AuthenticatorMethod),
nameof(TotpMethod),
2019-07-07 03:59:13 +02:00
nameof(ShowTryAgain),
2019-05-27 16:28:38 +02:00
});
}
2019-05-31 18:13:14 +02:00
public Command SubmitCommand { get; }
public Action TwoFactorAuthSuccessAction { get; set; }
public Action StartSetPasswordAction { get; set; }
[Auto Logout] Final review of feature (#932) * Initial commit of LockService name refactor (#831) * [Auto-Logout] Update Service layer logic (#835) * Initial commit of service logic update * Added default value for action * Updated ToggleTokensAsync conditional * Removed unused variables, updated action conditional * Initial commit: lockOption/lock refactor app layer (#840) * [Auto-Logout] Settings Refactor - Application Layer Part 2 (#844) * Initial commit of app layer part 2 * Updated biometrics position * Reverted resource name refactor * LockOptions refactor revert * Updated method casing :: Removed VaultTimeout prefix for timeouts * Fixed dupe string resource (#854) * Updated dependency to use VaultTimeoutService (#896) * [Auto Logout] Xamarin Forms in AutoFill flow (iOS) (#902) * fix typo in PINRequireMasterPasswordRestart (#900) * initial commit for xf usage in autofill * Fixed databinding for hint button * Updated Two Factor page launch - removed unused imports * First pass at broadcast/messenger implentation for autofill * setting theme in extension using theme manager * extension app resources * App resources from main app * fix ref to twoFactorPage * apply resources to page * load empty app for sytling in extension * move ios renderers to ios core * static ref to resources and GetResourceColor helper * fix method ref * move application.current.resources refs to helper * switch login page alerts to device action dialogs * run on main thread * showDialog with device action service * abstract action sheet to device action service * add support for yubikey * add yubikey iimages to extension * support close button action * add support to action extension * remove empty lines Co-authored-by: Jonas Kittner <54631600+theendlessriver13@users.noreply.github.com> Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com> * [Auto Logout] Update lock option to be default value (#929) * Initial commit - make lock action default * Removed extra whitespace Co-authored-by: Jonas Kittner <54631600+theendlessriver13@users.noreply.github.com> Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com> Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
2020-05-29 18:26:36 +02:00
public Action CloseAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
2022-04-26 17:21:17 +02:00
2022-03-07 19:39:38 +01:00
protected override II18nService i18nService => _i18nService;
protected override IEnvironmentService environmentService => _environmentService;
protected override IDeviceActionService deviceActionService => _deviceActionService;
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
2019-05-27 16:28:38 +02:00
public void Init()
{
if ((!_authService.AuthingWithSso() && !_authService.AuthingWithPassword()) ||
2019-05-27 16:28:38 +02:00
_authService.TwoFactorProvidersData == null)
{
// TODO: dismiss modal?
return;
}
_authingWithSso = _authService.AuthingWithSso();
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
2019-05-27 17:57:10 +02:00
{
_webVaultUrl = _environmentService.BaseUrl;
}
else if (!string.IsNullOrWhiteSpace(_environmentService.WebVaultUrl))
2019-05-27 17:57:10 +02:00
{
_webVaultUrl = _environmentService.WebVaultUrl;
}
SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_platformUtilsService.SupportsFido2());
2019-05-27 16:28:38 +02:00
Load();
}
public void Load()
{
if (SelectedProviderType == null)
2019-05-27 16:28:38 +02:00
{
PageTitle = AppResources.LoginUnavailable;
return;
}
2019-05-28 15:12:05 +02:00
var page = Page as TwoFactorPage;
2019-05-27 16:28:38 +02:00
PageTitle = _authService.TwoFactorProviders[SelectedProviderType.Value].Name;
var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value];
switch (SelectedProviderType.Value)
2019-05-27 16:28:38 +02:00
{
case TwoFactorProviderType.Fido2WebAuthn:
Fido2AuthenticateAsync(providerData);
2019-05-27 16:28:38 +02:00
break;
2019-05-28 15:54:08 +02:00
case TwoFactorProviderType.YubiKey:
_messagingService.Send("listenYubiKeyOTP", true);
break;
2019-05-27 16:28:38 +02:00
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.OrganizationDuo:
2019-05-27 17:57:10 +02:00
var host = WebUtility.UrlEncode(providerData["Host"] as string);
var req = WebUtility.UrlEncode(providerData["Signature"] as string);
page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}";
page.DuoWebView.RegisterAction(sig =>
2019-05-27 17:57:10 +02:00
{
Token = sig;
2020-02-24 14:58:15 +01:00
Device.BeginInvokeOnMainThread(async () => await SubmitAsync());
2019-05-27 17:57:10 +02:00
});
2019-05-27 16:28:38 +02:00
break;
case TwoFactorProviderType.Email:
2019-05-28 16:12:51 +02:00
TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail,
providerData["Email"] as string);
if (_authService.TwoFactorProvidersData.Count > 1)
2019-05-27 16:28:38 +02:00
{
var emailTask = Task.Run(() => SendEmailAsync(false, false));
}
break;
2019-05-28 16:12:51 +02:00
case TwoFactorProviderType.Authenticator:
TotpInstruction = AppResources.EnterVerificationCodeApp;
break;
2019-05-27 16:28:38 +02:00
default:
break;
}
2019-05-28 15:54:08 +02:00
if (!YubikeyMethod)
2019-05-28 15:54:08 +02:00
{
_messagingService.Send("listenYubiKeyOTP", false);
}
ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
}
public async Task Fido2AuthenticateAsync(Dictionary<string, object> providerData = null)
{
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
if (providerData == null)
{
providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn];
}
var callbackUri = "bitwarden://webauthn-callback";
var data = AppHelpers.EncodeDataParameter(new
{
callbackUri = callbackUri,
data = JsonConvert.SerializeObject(providerData),
headerText = AppResources.Fido2Title,
btnText = AppResources.Fido2AuthenticateWebAuthn,
btnReturnText = AppResources.Fido2ReturnToApp,
});
var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data +
"&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2";
WebAuthenticatorResult authResult = null;
try
{
var options = new WebAuthenticatorOptions
{
Url = new Uri(url),
CallbackUrl = new Uri(callbackUri),
PrefersEphemeralWebBrowserSession = true,
};
authResult = await WebAuthenticator.AuthenticateAsync(options);
}
catch (TaskCanceledException)
{
// user canceled
await _deviceActionService.HideLoadingAsync();
return;
}
string response = null;
if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData))
{
response = Uri.UnescapeDataString(resultData);
}
if (!string.IsNullOrWhiteSpace(response))
{
Token = response;
await SubmitAsync(false);
}
else
{
await _deviceActionService.HideLoadingAsync();
if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError))
{
var message = AppResources.Fido2CheckBrowser + "\n\n" + resultError;
await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred,
AppResources.Ok);
}
else
{
await _platformUtilsService.ShowDialogAsync(AppResources.Fido2CheckBrowser,
AppResources.AnErrorHasOccurred, AppResources.Ok);
}
}
2019-05-24 03:19:45 +02:00
}
public async Task SubmitAsync(bool showLoading = true)
2019-05-24 03:19:45 +02:00
{
if (SelectedProviderType == null)
2019-05-27 16:28:38 +02:00
{
return;
}
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
return;
}
if (string.IsNullOrWhiteSpace(Token))
2019-05-27 16:28:38 +02:00
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.VerificationCode),
AppResources.AnErrorHasOccurred, AppResources.Ok);
return;
2019-05-27 16:28:38 +02:00
}
if (SelectedProviderType == TwoFactorProviderType.Email ||
2019-05-27 16:28:38 +02:00
SelectedProviderType == TwoFactorProviderType.Authenticator)
{
Token = Token.Replace(" ", string.Empty).Trim();
}
try
{
if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
}
2022-03-07 19:39:38 +01:00
var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, _captchaToken, Remember);
2022-04-26 17:21:17 +02:00
2022-03-07 19:39:38 +01:00
if (result.CaptchaNeeded)
{
if (await HandleCaptchaAsync(result.CaptchaSiteKey))
{
await SubmitAsync(false);
_captchaToken = null;
}
return;
}
_captchaToken = null;
2022-04-26 17:21:17 +02:00
2019-05-27 16:28:38 +02:00
var task = Task.Run(() => _syncService.FullSyncAsync(true));
await _deviceActionService.HideLoadingAsync();
2019-05-28 15:54:08 +02:00
_messagingService.Send("listenYubiKeyOTP", false);
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
2022-04-26 17:21:17 +02:00
if (_authingWithSso && result.ResetMasterPassword)
{
StartSetPasswordAction?.Invoke();
}
else if (result.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
2022-04-26 17:21:17 +02:00
}
else
{
TwoFactorAuthSuccessAction?.Invoke();
}
2019-05-27 16:28:38 +02:00
}
catch (ApiException e)
2019-05-27 16:28:38 +02:00
{
2022-03-07 19:39:38 +01:00
_captchaToken = null;
2019-05-27 16:28:38 +02:00
await _deviceActionService.HideLoadingAsync();
if (e?.Error != null)
2019-10-22 22:37:40 +02:00
{
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred, AppResources.Ok);
2019-10-22 22:37:40 +02:00
}
2019-05-27 16:28:38 +02:00
}
}
2019-05-24 03:19:45 +02:00
2019-05-27 16:28:38 +02:00
public async Task AnotherMethodAsync()
{
var supportedProviders = _authService.GetSupportedTwoFactorProviders();
var options = supportedProviders.Select(p => p.Name).ToList();
options.Add(AppResources.RecoveryCodeTitle);
[Auto Logout] Final review of feature (#932) * Initial commit of LockService name refactor (#831) * [Auto-Logout] Update Service layer logic (#835) * Initial commit of service logic update * Added default value for action * Updated ToggleTokensAsync conditional * Removed unused variables, updated action conditional * Initial commit: lockOption/lock refactor app layer (#840) * [Auto-Logout] Settings Refactor - Application Layer Part 2 (#844) * Initial commit of app layer part 2 * Updated biometrics position * Reverted resource name refactor * LockOptions refactor revert * Updated method casing :: Removed VaultTimeout prefix for timeouts * Fixed dupe string resource (#854) * Updated dependency to use VaultTimeoutService (#896) * [Auto Logout] Xamarin Forms in AutoFill flow (iOS) (#902) * fix typo in PINRequireMasterPasswordRestart (#900) * initial commit for xf usage in autofill * Fixed databinding for hint button * Updated Two Factor page launch - removed unused imports * First pass at broadcast/messenger implentation for autofill * setting theme in extension using theme manager * extension app resources * App resources from main app * fix ref to twoFactorPage * apply resources to page * load empty app for sytling in extension * move ios renderers to ios core * static ref to resources and GetResourceColor helper * fix method ref * move application.current.resources refs to helper * switch login page alerts to device action dialogs * run on main thread * showDialog with device action service * abstract action sheet to device action service * add support for yubikey * add yubikey iimages to extension * support close button action * add support to action extension * remove empty lines Co-authored-by: Jonas Kittner <54631600+theendlessriver13@users.noreply.github.com> Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com> * [Auto Logout] Update lock option to be default value (#929) * Initial commit - make lock action default * Removed extra whitespace Co-authored-by: Jonas Kittner <54631600+theendlessriver13@users.noreply.github.com> Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com> Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
2020-05-29 18:26:36 +02:00
var method = await _deviceActionService.DisplayActionSheetAsync(AppResources.TwoStepLoginOptions,
AppResources.Cancel, null, options.ToArray());
if (method == AppResources.RecoveryCodeTitle)
2019-05-27 16:28:38 +02:00
{
_platformUtilsService.LaunchUri("https://bitwarden.com/help/lost-two-step-device/");
2019-05-27 16:28:38 +02:00
}
else if (method != AppResources.Cancel && method != null)
2019-05-27 16:28:38 +02:00
{
2019-05-28 16:12:51 +02:00
var selected = supportedProviders.FirstOrDefault(p => p.Name == method)?.Type;
if (selected == SelectedProviderType)
2019-05-28 16:12:51 +02:00
{
// Nothing changed
return;
}
SelectedProviderType = selected;
2019-05-27 16:28:38 +02:00
Load();
}
}
public async Task<bool> SendEmailAsync(bool showLoading, bool doToast)
{
if (!EmailMethod)
2019-05-27 16:28:38 +02:00
{
return false;
}
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
return false;
}
2019-05-27 16:28:38 +02:00
try
{
if (showLoading)
2019-05-27 16:28:38 +02:00
{
await _deviceActionService.ShowLoadingAsync(AppResources.Submitting);
}
var request = new TwoFactorEmailRequest
{
Email = _authService.Email,
MasterPasswordHash = _authService.MasterPasswordHash
};
await _apiService.PostTwoFactorEmailAsync(request);
if (showLoading)
2019-05-27 16:28:38 +02:00
{
await _deviceActionService.HideLoadingAsync();
}
if (doToast)
2019-05-27 16:28:38 +02:00
{
_platformUtilsService.ShowToast("success", null, AppResources.VerificationEmailSent);
}
return true;
}
catch (ApiException)
2019-05-27 16:28:38 +02:00
{
if (showLoading)
2019-05-27 16:28:38 +02:00
{
await _deviceActionService.HideLoadingAsync();
}
await _platformUtilsService.ShowDialogAsync(AppResources.VerificationEmailNotSent,
AppResources.AnErrorHasOccurred, AppResources.Ok);
2019-05-27 16:28:38 +02:00
return false;
}
2019-05-24 03:19:45 +02:00
}
}
}