mirror of
https://github.com/bitwarden/mobile.git
synced 2024-12-26 16:57:59 +01:00
Added SSO flows and functionality (#1047)
* SSO login flow for pre-existing user and no 2FA * 2FA progress * 2FA support * Added SSO flows and functionality * Handle webauthenticator cancellation gracefully * updates & bugfixes * Added state validation to web auth response handling * SSO auth, account registration, and environment settings support for iOS extensions * Added SSO prevalidation to auth process * prevalidation now hitting identity service base url * additional error handling * Requested changes * fixed case
This commit is contained in:
parent
3af08a4727
commit
f1419a75f6
@ -78,7 +78,7 @@
|
||||
<Version>1.8.6.7</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Essentials">
|
||||
<Version>1.5.3.1</Version>
|
||||
<Version>1.5.3.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||
<Version>71.1740.0</Version>
|
||||
@ -142,6 +142,7 @@
|
||||
<Compile Include="Tiles\MyVaultTileService.cs" />
|
||||
<Compile Include="Utilities\AndroidHelpers.cs" />
|
||||
<Compile Include="Utilities\AppCenterHelper.cs" />
|
||||
<Compile Include="WebAuthCallbackActivity.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidAsset Include="Assets\FontAwesome.ttf" />
|
||||
|
@ -144,6 +144,7 @@ namespace Bit.Droid
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
Xamarin.Essentials.Platform.OnResume();
|
||||
if (_deviceActionService.SupportsNfc())
|
||||
{
|
||||
try
|
||||
|
11
src/Android/WebAuthCallbackActivity.cs
Normal file
11
src/Android/WebAuthCallbackActivity.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
|
||||
namespace Bit.Droid
|
||||
{
|
||||
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
|
||||
[IntentFilter(new[] { Android.Content.Intent.ActionView },
|
||||
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
|
||||
DataScheme = "bitwarden")]
|
||||
public class WebAuthCallbackActivity : Xamarin.Essentials.WebAuthenticatorCallbackActivity { }
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="3.2.1" />
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.1" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.1" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.2" />
|
||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="4.5.0.725" />
|
||||
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
||||
@ -111,6 +111,14 @@
|
||||
<Compile Update="Pages\Vault\GroupingsPage\GroupingsPage.xaml.cs">
|
||||
<DependentUpon>GroupingsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Accounts\LoginSsoPage.xaml.cs">
|
||||
<DependentUpon>LoginSsoPage.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Accounts\SetPasswordPage.xaml.cs">
|
||||
<DependentUpon>ResetMasterPasswordPage.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -19,5 +19,28 @@ namespace Bit.App.Models
|
||||
public string SaveCardExpYear { get; set; }
|
||||
public string SaveCardCode { get; set; }
|
||||
public bool IosExtension { get; set; }
|
||||
|
||||
public void SetAllFrom(AppOptions o)
|
||||
{
|
||||
if (o == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
MyVaultTile = o.MyVaultTile;
|
||||
GeneratorTile = o.GeneratorTile;
|
||||
FromAutofillFramework = o.FromAutofillFramework;
|
||||
FillType = o.FillType;
|
||||
Uri = o.Uri;
|
||||
SaveType = o.SaveType;
|
||||
SaveName = o.SaveName;
|
||||
SaveUsername = o.SaveUsername;
|
||||
SavePassword = o.SavePassword;
|
||||
SaveCardName = o.SaveCardName;
|
||||
SaveCardNumber = o.SaveCardNumber;
|
||||
SaveCardExpMonth = o.SaveCardExpMonth;
|
||||
SaveCardExpYear = o.SaveCardExpYear;
|
||||
SaveCardCode = o.SaveCardCode;
|
||||
IosExtension = o.IosExtension;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,21 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class EnvironmentPage : BaseContentPage
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly EnvironmentPageViewModel _vm;
|
||||
|
||||
public EnvironmentPage()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
InitializeComponent();
|
||||
@ -28,6 +32,12 @@ namespace Bit.App.Pages
|
||||
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
|
||||
_identityEntry.ReturnType = ReturnType.Next;
|
||||
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
|
||||
_vm.SubmitSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
@ -38,12 +48,17 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
private async Task SubmitSuccessAsync()
|
||||
{
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@ -8,12 +9,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public class EnvironmentPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
|
||||
public EnvironmentPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
|
||||
PageTitle = AppResources.Settings;
|
||||
@ -33,6 +32,8 @@ namespace Bit.App.Pages
|
||||
public string WebVaultUrl { get; set; }
|
||||
public string IconsUrl { get; set; }
|
||||
public string NotificationsUrls { get; set; }
|
||||
public Action SubmitSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
@ -54,8 +55,7 @@ namespace Bit.App.Pages
|
||||
IconsUrl = resUrls.Icons;
|
||||
NotificationsUrls = resUrls.Notifications;
|
||||
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
SubmitSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,12 +11,16 @@
|
||||
<ContentPage.BindingContext>
|
||||
<pages:HomeViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<StackLayout Spacing="0" Padding="10, 5">
|
||||
<controls:FaButton Text=""
|
||||
StyleClass="btn-muted, btn-icon, btn-icon-platform"
|
||||
HorizontalOptions="Start"
|
||||
Clicked="Settings_Clicked"
|
||||
Clicked="Environment_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}">
|
||||
<controls:FaButton.Margin>
|
||||
@ -39,6 +43,8 @@
|
||||
Clicked="LogIn_Clicked"></Button>
|
||||
<Button Text="{u:I18n CreateAccount}"
|
||||
Clicked="Register_Clicked"></Button>
|
||||
<Button Text="{u:I18n LogInSso}"
|
||||
Clicked="LogInSso_Clicked"></Button>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
@ -10,6 +10,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public partial class HomePage : BaseContentPage
|
||||
{
|
||||
private readonly HomeViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
private IMessagingService _messagingService;
|
||||
|
||||
@ -19,6 +20,12 @@ namespace Bit.App.Pages
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as HomeViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.StartLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartLoginAsync());
|
||||
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
|
||||
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
||||
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
|
||||
_logo.Source = !ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png";
|
||||
}
|
||||
|
||||
@ -33,29 +40,69 @@ namespace Bit.App.Pages
|
||||
base.OnAppearing();
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private void LogIn_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PushModalAsync(new NavigationPage(new LoginPage(null, _appOptions)));
|
||||
_vm.StartLoginAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartLoginAsync()
|
||||
{
|
||||
var page = new LoginPage(null, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void Register_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PushModalAsync(new NavigationPage(new RegisterPage(this)));
|
||||
_vm.StartRegisterAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartRegisterAsync()
|
||||
{
|
||||
var page = new RegisterPage(this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void Settings_Clicked(object sender, EventArgs e)
|
||||
private void LogInSso_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PushModalAsync(new NavigationPage(new EnvironmentPage()));
|
||||
_vm.StartSsoLoginAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartSsoLoginAsync()
|
||||
{
|
||||
var page = new LoginSsoPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void Environment_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.StartEnvironmentAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartEnvironmentAsync()
|
||||
{
|
||||
var page = new EnvironmentPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@ -6,7 +7,13 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public HomeViewModel()
|
||||
{
|
||||
PageTitle = "Home Page";
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
}
|
||||
|
||||
public Action StartLoginAction { get; set; }
|
||||
public Action StartRegisterAction { get; set; }
|
||||
public Action StartSsoLoginAction { get; set; }
|
||||
public Action StartEnvironmentAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@ -99,24 +99,11 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task UnlockedAsync()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
if (_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
if (_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
}
|
||||
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
|
||||
if (previousPage != null)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PreviousPageKey);
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,14 @@ using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Request;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LockPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
@ -39,6 +41,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public LockPageViewModel()
|
||||
{
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
@ -224,18 +227,33 @@ namespace Bit.App.Pages
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdf, kdfIterations);
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
if (storedKeyHash == null)
|
||||
var passwordValid = false;
|
||||
if (keyHash != null)
|
||||
{
|
||||
var oldKey = await _secureStorageService.GetAsync<string>("oldKey");
|
||||
if (key.KeyB64 == oldKey)
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
if (storedKeyHash != null)
|
||||
{
|
||||
await _secureStorageService.RemoveAsync("oldKey");
|
||||
await _cryptoService.SetKeyHashAsync(keyHash);
|
||||
storedKeyHash = keyHash;
|
||||
passwordValid = storedKeyHash == keyHash;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
try
|
||||
{
|
||||
await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
passwordValid = true;
|
||||
await _cryptoService.SetKeyHashAsync(keyHash);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
}
|
||||
if (storedKeyHash != null && keyHash != null && storedKeyHash == keyHash)
|
||||
if (passwordValid)
|
||||
{
|
||||
if (_pinSet.Item1)
|
||||
{
|
||||
|
@ -1,9 +1,9 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@ -25,7 +25,7 @@ namespace Bit.App.Pages
|
||||
_vm = BindingContext as LoginPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.LoggedInAction = () => Device.BeginInvokeOnMainThread(async () => await LoggedInAsync());
|
||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
@ -74,7 +74,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
@ -84,30 +84,17 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task StartTwoFactorAsync()
|
||||
{
|
||||
var page = new TwoFactorPage();
|
||||
var page = new TwoFactorPage(false, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task LoggedInAsync()
|
||||
private async Task LogInSuccessAsync()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
if (_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
if (_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
}
|
||||
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
|
||||
if (previousPage != null)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PreviousPageKey);
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ namespace Bit.App.Pages
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public bool RememberEmail { get; set; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action LoggedInAction { get; set; }
|
||||
public Action LogInSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public bool HideHintButton
|
||||
@ -142,7 +142,7 @@ namespace Bit.App.Pages
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
LoggedInAction?.Invoke();
|
||||
LogInSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
|
42
src/App/Pages/Accounts/LoginSsoPage.xaml
Normal file
42
src/App/Pages/Accounts/LoginSsoPage.xaml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.LoginSsoPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginSsoPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginSsoPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20" Margin="0,10,0,0">
|
||||
<StackLayout StyleClass="box">
|
||||
<Label Text="{u:I18n LogInSsoSummary}"
|
||||
StyleClass="text-md"
|
||||
HorizontalTextAlignment="Start"></Label>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n OrgIdentifier}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_orgIdentifier"
|
||||
Text="{Binding OrgIdentifier}"
|
||||
Keyboard="Default"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding LogInCommand}" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
114
src/App/Pages/Accounts/LoginSsoPage.xaml.cs
Normal file
114
src/App/Pages/Accounts/LoginSsoPage.xaml.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginSsoPage : BaseContentPage
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly LoginSsoPageViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
private AppOptions _appOptionsCopy;
|
||||
|
||||
public LoginSsoPage(AppOptions appOptions = null)
|
||||
{
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginSsoPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
if (string.IsNullOrWhiteSpace(_vm.OrgIdentifier))
|
||||
{
|
||||
RequestFocus(_orgIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyAppOptions()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
{
|
||||
// create an object copy of _appOptions to persist values when app is exited during web auth flow
|
||||
_appOptionsCopy = new AppOptions();
|
||||
_appOptionsCopy.SetAllFrom(_appOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreAppOptionsFromCopy()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
{
|
||||
// restore values to original readonly _appOptions object from copy
|
||||
_appOptions.SetAllFrom(_appOptionsCopy);
|
||||
_appOptionsCopy = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void LogIn_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
CopyAppOptions();
|
||||
await _vm.LogInAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartTwoFactorAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
var page = new TwoFactorPage(true, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartSetPasswordAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
var page = new SetPasswordPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task SsoAuthSuccessAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
}
|
||||
}
|
||||
}
|
218
src/App/Pages/Accounts/LoginSsoPageViewModel.cs
Normal file
218
src/App/Pages/Accounts/LoginSsoPageViewModel.cs
Normal file
@ -0,0 +1,218 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginSsoPageViewModel : BaseViewModel
|
||||
{
|
||||
private const string Keys_RememberedOrgIdentifier = "rememberedOrgIdentifier";
|
||||
private const string Keys_RememberOrgIdentifier = "rememberOrgIdentifier";
|
||||
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private string _orgIdentifier;
|
||||
|
||||
public LoginSsoPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_passwordGenerationService =
|
||||
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
LogInCommand = new Command(async () => await LogInAsync());
|
||||
}
|
||||
|
||||
public string OrgIdentifier
|
||||
{
|
||||
get => _orgIdentifier;
|
||||
set => SetProperty(ref _orgIdentifier, value);
|
||||
}
|
||||
|
||||
public Command LogInCommand { get; }
|
||||
public bool RememberOrgIdentifier { get; set; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action SsoAuthSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(OrgIdentifier))
|
||||
{
|
||||
OrgIdentifier = await _storageService.GetAsync<string>(Keys_RememberedOrgIdentifier);
|
||||
}
|
||||
var rememberOrgIdentifier = await _storageService.GetAsync<bool?>(Keys_RememberOrgIdentifier);
|
||||
RememberOrgIdentifier = rememberOrgIdentifier.GetValueOrDefault(true);
|
||||
}
|
||||
|
||||
public async Task LogInAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(OrgIdentifier))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||
|
||||
try
|
||||
{
|
||||
await _apiService.PreValidateSso(OrgIdentifier);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
(e?.Error != null ? e.Error.GetSingleMessage() : AppResources.LoginSsoError),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
return;
|
||||
}
|
||||
|
||||
var passwordOptions = new PasswordGenerationOptions(true);
|
||||
passwordOptions.Length = 64;
|
||||
|
||||
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
|
||||
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
|
||||
|
||||
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||
|
||||
var redirectUri = "bitwarden://sso-callback";
|
||||
|
||||
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
|
||||
"client_id=" + _platformUtilsService.IdentityClientId + "&" +
|
||||
"redirect_uri=" + Uri.EscapeDataString(redirectUri) + "&" +
|
||||
"response_type=code&scope=api%20offline_access&" +
|
||||
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
|
||||
"code_challenge_method=S256&response_mode=query&" +
|
||||
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
bool cancelled = false;
|
||||
try
|
||||
{
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
|
||||
new Uri(redirectUri));
|
||||
}
|
||||
catch (TaskCanceledException 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)
|
||||
{
|
||||
var code = GetResultCode(authResult, state);
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
{
|
||||
await LogIn(code, codeVerifier, redirectUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetResultCode(WebAuthenticatorResult authResult, string state)
|
||||
{
|
||||
string code = null;
|
||||
if (authResult != null)
|
||||
{
|
||||
authResult.Properties.TryGetValue("state", out var resultState);
|
||||
if (resultState == state)
|
||||
{
|
||||
authResult.Properties.TryGetValue("code", out var resultCode);
|
||||
code = resultCode;
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private async Task LogIn(string code, string codeVerifier, string redirectUri)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, redirectUri);
|
||||
if (RememberOrgIdentifier)
|
||||
{
|
||||
await _storageService.SaveAsync(Keys_RememberedOrgIdentifier, OrgIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _storageService.RemoveAsync(Keys_RememberedOrgIdentifier);
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (response.TwoFactor)
|
||||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
}
|
||||
else if (response.ResetMasterPassword)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@ -17,12 +18,11 @@ namespace Bit.App.Pages
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as RegisterPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.RegistrationSuccess = async () =>
|
||||
_vm.RegistrationSuccess = () => Device.BeginInvokeOnMainThread(async () => await RegistrationSuccessAsync(homePage));
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
if (homePage != null)
|
||||
{
|
||||
await homePage.DismissRegisterPageAndLogInAsync(_vm.Email);
|
||||
}
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
ConfirmMasterPasswordEntry = _confirmMasterPassword;
|
||||
@ -55,13 +55,20 @@ namespace Bit.App.Pages
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RegistrationSuccessAsync(HomePage homePage)
|
||||
{
|
||||
if (homePage != null)
|
||||
{
|
||||
await homePage.DismissRegisterPageAndLogInAsync(_vm.Email);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ namespace Bit.App.Pages
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public Action RegistrationSuccess { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
|
146
src/App/Pages/Accounts/SetPasswordPage.xaml
Normal file
146
src/App/Pages/Accounts/SetPasswordPage.xaml
Normal file
@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.SetPasswordPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:SetPasswordPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SetPasswordPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label Text="{u:I18n SetMasterPasswordSummary}"
|
||||
StyleClass="text-md"
|
||||
HorizontalTextAlignment="Start"></Label>
|
||||
</StackLayout>
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{Binding PolicySummary}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Start" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n MasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n RetypeMasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_confirmMasterPassword"
|
||||
Text="{Binding ConfirmMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding ToggleConfirmPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHint}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_hint"
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
82
src/App/Pages/Accounts/SetPasswordPage.xaml.cs
Normal file
82
src/App/Pages/Accounts/SetPasswordPage.xaml.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SetPasswordPage : BaseContentPage
|
||||
{
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly SetPasswordPageViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public SetPasswordPage(AppOptions appOptions = null)
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SetPasswordPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.SetPasswordSuccessAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await SetPasswordSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
ConfirmMasterPasswordEntry = _confirmMasterPassword;
|
||||
|
||||
_masterPassword.ReturnType = ReturnType.Next;
|
||||
_masterPassword.ReturnCommand = new Command(() => _confirmMasterPassword.Focus());
|
||||
_confirmMasterPassword.ReturnType = ReturnType.Next;
|
||||
_confirmMasterPassword.ReturnCommand = new Command(() => _hint.Focus());
|
||||
}
|
||||
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
public Entry ConfirmMasterPasswordEntry { get; set; }
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
RequestFocus(_masterPassword);
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetPasswordSuccessAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
}
|
259
src/App/Pages/Accounts/SetPasswordPageViewModel.cs
Normal file
259
src/App/Pages/Accounts/SetPasswordPageViewModel.cs
Normal file
@ -0,0 +1,259 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SetPasswordPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly II18nService _i18nService;
|
||||
|
||||
private bool _showPassword;
|
||||
private bool _isPolicyInEffect;
|
||||
private string _policySummary;
|
||||
private MasterPasswordPolicyOptions _policy;
|
||||
|
||||
public SetPasswordPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_passwordGenerationService =
|
||||
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
|
||||
PageTitle = AppResources.SetMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
}
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new[] { nameof(ShowPasswordIcon) });
|
||||
}
|
||||
|
||||
public bool IsPolicyInEffect
|
||||
{
|
||||
get => _isPolicyInEffect;
|
||||
set => SetProperty(ref _isPolicyInEffect, value);
|
||||
}
|
||||
|
||||
public string PolicySummary
|
||||
{
|
||||
get => _policySummary;
|
||||
set => SetProperty(ref _policySummary, value);
|
||||
}
|
||||
|
||||
public MasterPasswordPolicyOptions Policy
|
||||
{
|
||||
get => _policy;
|
||||
set => SetProperty(ref _policy, value);
|
||||
}
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public Command ToggleConfirmPasswordCommand { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public string MasterPassword { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public Action SetPasswordSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await CheckPasswordPolicy();
|
||||
}
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (IsPolicyInEffect)
|
||||
{
|
||||
var userInput = await GetPasswordStrengthUserInput();
|
||||
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInput);
|
||||
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
|
||||
AppResources.MasterPasswordPolicyValidationMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MasterPassword.Length < 8)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
|
||||
AppResources.MasterPasswordLengthValMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (MasterPassword != ConfirmMasterPassword)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
AppResources.MasterPasswordConfirmationValMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
var kdf = KdfType.PBKDF2_SHA256;
|
||||
var kdfIterations = 100000;
|
||||
var email = await _userService.GetEmailAsync();
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdf, kdfIterations);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
|
||||
Tuple<SymmetricCryptoKey, CipherString> encKey;
|
||||
var existingEncKey = await _cryptoService.GetEncKeyAsync();
|
||||
if (existingEncKey == null)
|
||||
{
|
||||
encKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
encKey = await _cryptoService.RemakeEncKeyAsync(key);
|
||||
}
|
||||
|
||||
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
|
||||
var request = new SetPasswordRequest
|
||||
{
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Key = encKey.Item2.EncryptedString,
|
||||
MasterPasswordHint = Hint,
|
||||
Kdf = kdf,
|
||||
KdfIterations = kdfIterations,
|
||||
Keys = new KeysRequest
|
||||
{
|
||||
PublicKey = keys.Item1,
|
||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
|
||||
await _apiService.SetPasswordAsync(request);
|
||||
await _userService.SetInformationAsync(await _userService.GetUserIdAsync(),
|
||||
await _userService.GetEmailAsync(), kdf, kdfIterations);
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
await _cryptoService.SetKeyHashAsync(masterPasswordHash);
|
||||
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString);
|
||||
await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
SetPasswordSuccessAction?.Invoke();
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as SetPasswordPage).MasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
public void ToggleConfirmPassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as SetPasswordPage).ConfirmMasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
private async Task CheckPasswordPolicy()
|
||||
{
|
||||
Policy = await _policyService.GetMasterPasswordPolicyOptions();
|
||||
IsPolicyInEffect = Policy?.InEffect() ?? false;
|
||||
if (!IsPolicyInEffect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bullet = "\n" + "".PadLeft(4) + "\u2022 ";
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(_i18nService.T("MasterPasswordPolicyInEffect"));
|
||||
if (Policy.MinComplexity > 0)
|
||||
{
|
||||
sb.Append(bullet)
|
||||
.Append(string.Format(_i18nService.T("PolicyInEffectMinComplexity"), Policy.MinComplexity));
|
||||
}
|
||||
if (Policy.MinLength > 0)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectMinLength"), Policy.MinLength));
|
||||
}
|
||||
if (Policy.RequireUpper)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectUppercase"));
|
||||
}
|
||||
if (Policy.RequireLower)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectLowercase"));
|
||||
}
|
||||
if (Policy.RequireNumbers)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectNumbers"));
|
||||
}
|
||||
if (Policy.RequireSpecial)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectSpecial"), "!@#$%^&*"));
|
||||
}
|
||||
PolicySummary = sb.ToString();
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetPasswordStrengthUserInput()
|
||||
{
|
||||
var email = await _userService.GetEmailAsync();
|
||||
List<string> userInput = null;
|
||||
var atPosition = email.IndexOf('@');
|
||||
if (atPosition > -1)
|
||||
{
|
||||
var rx = new Regex("/[^A-Za-z0-9]/", RegexOptions.Compiled);
|
||||
var data = rx.Split(email.Substring(0, atPosition).Trim().ToLower());
|
||||
userInput = new List<string>(data);
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@ -14,22 +14,29 @@ namespace Bit.App.Pages
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
private TwoFactorPageViewModel _vm;
|
||||
private bool _inited;
|
||||
private bool _authingWithSso;
|
||||
|
||||
public TwoFactorPage(AppOptions appOptions = null)
|
||||
public TwoFactorPage(bool? authingWithSso = false, AppOptions appOptions = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator();
|
||||
_authingWithSso = authingWithSso ?? false;
|
||||
_appOptions = appOptions;
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_vm = BindingContext as TwoFactorPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.TwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthAsync());
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.TwoFactorAuthSuccessAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
|
||||
_vm.CloseAction = async () => await Navigation.PopModalAsync();
|
||||
DuoWebView = _duoWebView;
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
@ -141,7 +148,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
private void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
@ -159,28 +166,29 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TwoFactorAuthAsync()
|
||||
|
||||
private async Task StartSetPasswordAsync()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
_vm.CloseAction();
|
||||
var page = new SetPasswordPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task TwoFactorAuthSuccessAsync()
|
||||
{
|
||||
if (_authingWithSso)
|
||||
{
|
||||
if (_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
if (_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
|
||||
if (previousPage != null)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PreviousPageKey);
|
||||
}
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ namespace Bit.App.Pages
|
||||
private TwoFactorProviderType? _selectedProviderType;
|
||||
private string _totpInstruction;
|
||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||
private bool _authingWithSso = false;
|
||||
|
||||
public TwoFactorPageViewModel()
|
||||
{
|
||||
@ -89,19 +90,21 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
public Command SubmitCommand { get; }
|
||||
public Action TwoFactorAction { get; set; }
|
||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public void Init()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_authService.Email) ||
|
||||
string.IsNullOrWhiteSpace(_authService.MasterPasswordHash) ||
|
||||
if ((!_authService.AuthingWithSso() && !_authService.AuthingWithPassword()) ||
|
||||
_authService.TwoFactorProvidersData == null)
|
||||
{
|
||||
// TODO: dismiss modal?
|
||||
return;
|
||||
}
|
||||
|
||||
_authingWithSso = _authService.AuthingWithSso();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
|
||||
{
|
||||
_webVaultUrl = _environmentService.BaseUrl;
|
||||
@ -204,14 +207,21 @@ namespace Bit.App.Pages
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||
await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
|
||||
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
TwoFactorAction?.Invoke();
|
||||
if (_authingWithSso && result.ResetMasterPassword)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
TwoFactorAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
|
6453
src/App/Resources/AppResources.Designer.cs
generated
6453
src/App/Resources/AppResources.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@ -1683,4 +1683,52 @@
|
||||
<data name="EnableSyncOnRefreshDescription" xml:space="preserve">
|
||||
<value>Syncing vault with pull down gesture.</value>
|
||||
</data>
|
||||
<data name="LogInSso" xml:space="preserve">
|
||||
<value>Enterprise Single Sign-On</value>
|
||||
</data>
|
||||
<data name="LogInSsoSummary" xml:space="preserve">
|
||||
<value>Quickly log in using your organization's single sign-on portal. Please enter your organization's identifier to begin.</value>
|
||||
</data>
|
||||
<data name="OrgIdentifier" xml:space="preserve">
|
||||
<value>Organization Identifier</value>
|
||||
</data>
|
||||
<data name="LoginSsoError" xml:space="preserve">
|
||||
<value>Currently unable to login with SSO</value>
|
||||
</data>
|
||||
<data name="SetMasterPassword" xml:space="preserve">
|
||||
<value>Set Master Password</value>
|
||||
</data>
|
||||
<data name="SetMasterPasswordSummary" xml:space="preserve">
|
||||
<value>In order to complete logging in with SSO, please set a master password to access and protect your vault.</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyInEffect" xml:space="preserve">
|
||||
<value>One or more organization policies require your master password to meet the following requirements:</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinComplexity" xml:space="preserve">
|
||||
<value>Minimum complexity score of {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinLength" xml:space="preserve">
|
||||
<value>Minimum length of {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectUppercase" xml:space="preserve">
|
||||
<value>Contain one or more uppercase characters</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectLowercase" xml:space="preserve">
|
||||
<value>Contain one or more lowercase characters</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectNumbers" xml:space="preserve">
|
||||
<value>Contain one or more numbers</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectSpecial" xml:space="preserve">
|
||||
<value>Contain one or more of the following special characters: {0}</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationTitle" xml:space="preserve">
|
||||
<value>Invalid Password</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationMessage" xml:space="preserve">
|
||||
<value>Password does not meet organization requirements. Please check the policy information and try again.</value>
|
||||
</data>
|
||||
<data name="Loading" xml:space="preserve">
|
||||
<value>Loading</value>
|
||||
</data>
|
||||
</root>
|
@ -8,6 +8,7 @@ using Bit.Core.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Utilities
|
||||
@ -192,5 +193,34 @@ namespace Bit.App.Utilities
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool SetAlternateMainPage(AppOptions appOptions)
|
||||
{
|
||||
if (appOptions != null)
|
||||
{
|
||||
if (appOptions.FromAutofillFramework && appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: appOptions));
|
||||
return true;
|
||||
}
|
||||
if (appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(appOptions));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async Task<PreviousPageInfo> ClearPreviousPage()
|
||||
{
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var previousPage = await storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
|
||||
if (previousPage != null)
|
||||
{
|
||||
await storageService.RemoveAsync(Constants.PreviousPageKey);
|
||||
}
|
||||
return previousPage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,11 +26,13 @@ namespace Bit.Core.Abstractions
|
||||
Task<ProfileResponse> GetProfileAsync();
|
||||
Task<SyncResponse> GetSyncAsync();
|
||||
Task PostAccountKeysAsync(KeysRequest request);
|
||||
Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request);
|
||||
Task<CipherResponse> PostCipherAsync(CipherRequest request);
|
||||
Task<CipherResponse> PostCipherCreateAsync(CipherCreateRequest request);
|
||||
Task<FolderResponse> PostFolderAsync(FolderRequest request);
|
||||
Task<Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>> PostIdentityTokenAsync(TokenRequest request);
|
||||
Task PostPasswordHintAsync(PasswordHintRequest request);
|
||||
Task SetPasswordAsync(SetPasswordRequest request);
|
||||
Task<PreloginResponse> PostPreloginAsync(PreloginRequest request);
|
||||
Task PostRegisterAsync(RegisterRequest request);
|
||||
Task<CipherResponse> PutCipherAsync(string id, CipherRequest request);
|
||||
@ -40,6 +42,7 @@ namespace Bit.Core.Abstractions
|
||||
Task PutDeleteCipherAsync(string id);
|
||||
Task PutRestoreCipherAsync(string id);
|
||||
Task RefreshIdentityTokenAsync();
|
||||
Task<object> PreValidateSso(string identifier);
|
||||
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
|
||||
TRequest body, bool authed, bool hasResponse);
|
||||
void SetUrls(EnvironmentUrls urls);
|
||||
|
@ -10,16 +10,22 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
string Email { get; set; }
|
||||
string MasterPasswordHash { get; set; }
|
||||
string Code { get; set; }
|
||||
string CodeVerifier { get; set; }
|
||||
string SsoRedirectUrl { get; set; }
|
||||
TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; }
|
||||
Dictionary<TwoFactorProviderType, TwoFactorProvider> TwoFactorProviders { get; set; }
|
||||
Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProvidersData { get; set; }
|
||||
|
||||
TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported);
|
||||
bool AuthingWithSso();
|
||||
bool AuthingWithPassword();
|
||||
List<TwoFactorProvider> GetSupportedTwoFactorProviders();
|
||||
Task<AuthResult> LogInAsync(string email, string masterPassword);
|
||||
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> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
||||
void LogOut(Action callback);
|
||||
void Init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ namespace Bit.Core.Abstractions
|
||||
Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)> GetOptionsAsync();
|
||||
Task<(PasswordGenerationOptions, PasswordGeneratorPolicyOptions)>
|
||||
EnforcePasswordGeneratorPoliciesOnOptionsAsync(PasswordGenerationOptions options);
|
||||
Task<object> PasswordStrength(string password, List<string> userInputs = null);
|
||||
Zxcvbn.Result PasswordStrength(string password, List<string> userInputs = null);
|
||||
Task SaveOptionsAsync(PasswordGenerationOptions options);
|
||||
void NormalizeOptions(PasswordGenerationOptions options, PasswordGeneratorPolicyOptions enforcedPolicyOptions);
|
||||
}
|
||||
|
@ -12,5 +12,8 @@ namespace Bit.Core.Abstractions
|
||||
Task<IEnumerable<Policy>> GetAll(PolicyType? type);
|
||||
Task Replace(Dictionary<string, PolicyData> policies);
|
||||
Task Clear(string userId);
|
||||
Task<MasterPasswordPolicyOptions> GetMasterPasswordPolicyOptions(IEnumerable<Policy> policies = null);
|
||||
Task<bool> EvaluateMasterPassword(int passwordStrength, string newPassword,
|
||||
MasterPasswordPolicyOptions enforcedPolicyOptions);
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@
|
||||
<PackageReference Include="LiteDB" Version="5.0.8" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="PCLCrypto" Version="2.0.147" />
|
||||
<PackageReference Include="zxcvbn-core" Version="2.1.44" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -6,6 +6,7 @@ namespace Bit.Core.Models.Domain
|
||||
public class AuthResult
|
||||
{
|
||||
public bool TwoFactor { get; set; }
|
||||
public bool ResetMasterPassword { get; set; }
|
||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
|
||||
}
|
||||
}
|
||||
|
22
src/Core/Models/Domain/MasterPasswordPolicyOptions.cs
Normal file
22
src/Core/Models/Domain/MasterPasswordPolicyOptions.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public class MasterPasswordPolicyOptions
|
||||
{
|
||||
public int MinComplexity { get; set; }
|
||||
public int MinLength { get; set; }
|
||||
public bool RequireUpper { get; set; }
|
||||
public bool RequireLower { get; set; }
|
||||
public bool RequireNumbers { get; set; }
|
||||
public bool RequireSpecial { get; set; }
|
||||
|
||||
public bool InEffect()
|
||||
{
|
||||
return MinComplexity > 0 ||
|
||||
MinLength > 0 ||
|
||||
RequireUpper ||
|
||||
RequireLower ||
|
||||
RequireNumbers ||
|
||||
RequireSpecial;
|
||||
}
|
||||
}
|
||||
}
|
7
src/Core/Models/Request/PasswordVerificationRequest.cs
Normal file
7
src/Core/Models/Request/PasswordVerificationRequest.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Request
|
||||
{
|
||||
public class PasswordVerificationRequest
|
||||
{
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
14
src/Core/Models/Request/SetPasswordRequest.cs
Normal file
14
src/Core/Models/Request/SetPasswordRequest.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Request
|
||||
{
|
||||
public class SetPasswordRequest
|
||||
{
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string MasterPasswordHint { get; set; }
|
||||
public KeysRequest Keys { get; set; }
|
||||
public KdfType Kdf { get; set; }
|
||||
public int KdfIterations { get; set; }
|
||||
}
|
||||
}
|
@ -9,21 +9,60 @@ namespace Bit.Core.Models.Request
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public string Code { get; set; }
|
||||
public string CodeVerifier { get; set; }
|
||||
public string RedirectUri { get; set; }
|
||||
public string Token { get; set; }
|
||||
public TwoFactorProviderType? Provider { get; set; }
|
||||
public bool Remember { get; set; }
|
||||
public bool? Remember { get; set; }
|
||||
public DeviceRequest Device { get; set; }
|
||||
|
||||
public TokenRequest(string[] credentials, string[] codes, TwoFactorProviderType? provider, string token,
|
||||
bool? remember, DeviceRequest device = null)
|
||||
{
|
||||
if (credentials != null && credentials.Length > 1)
|
||||
{
|
||||
Email = credentials[0];
|
||||
MasterPasswordHash = credentials[1];
|
||||
}
|
||||
else if (codes != null && codes.Length > 2)
|
||||
{
|
||||
Code = codes[0];
|
||||
CodeVerifier = codes[1];
|
||||
RedirectUri = codes[2];
|
||||
}
|
||||
Token = token;
|
||||
Provider = provider;
|
||||
Remember = remember;
|
||||
Device = device;
|
||||
}
|
||||
|
||||
public Dictionary<string, string> ToIdentityToken(string clientId)
|
||||
{
|
||||
var obj = new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "password",
|
||||
["username"] = Email,
|
||||
["password"] = MasterPasswordHash,
|
||||
["scope"] = "api offline_access",
|
||||
["client_id"] = clientId
|
||||
};
|
||||
|
||||
if (MasterPasswordHash != null && Email != null)
|
||||
{
|
||||
obj.Add("grant_type", "password");
|
||||
obj.Add("username", Email);
|
||||
obj.Add("password", MasterPasswordHash);
|
||||
}
|
||||
else if (Code != null && CodeVerifier != null && RedirectUri != null)
|
||||
{
|
||||
obj.Add("grant_type", "authorization_code");
|
||||
obj.Add("code", Code);
|
||||
obj.Add("code_verifier", CodeVerifier);
|
||||
obj.Add("redirect_uri", RedirectUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("must provide credentials or codes");
|
||||
}
|
||||
|
||||
if (Device != null)
|
||||
{
|
||||
obj.Add("deviceType", ((int)Device.Type).ToString());
|
||||
@ -31,11 +70,11 @@ namespace Bit.Core.Models.Request
|
||||
obj.Add("deviceName", Device.Name);
|
||||
obj.Add("devicePushToken", Device.PushToken);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(Token) && Provider != null)
|
||||
if (!string.IsNullOrWhiteSpace(Token) && Provider != null && Remember.HasValue)
|
||||
{
|
||||
obj.Add("twoFactorToken", Token);
|
||||
obj.Add("twoFactorProvider", ((int)Provider.Value).ToString());
|
||||
obj.Add("twoFactorRemember", Remember ? "1" : "0");
|
||||
obj.Add("twoFactorRemember", Remember.GetValueOrDefault() ? "1" : "0");
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using Bit.Core.Enums;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.Models.Response
|
||||
{
|
||||
@ -12,8 +13,12 @@ namespace Bit.Core.Models.Response
|
||||
public string RefreshToken { get; set; }
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
|
||||
public bool ResetMasterPassword { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string TwoFactorToken { get; set; }
|
||||
public KdfType Kdf { get; set; }
|
||||
public int KdfIterations { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -165,6 +165,12 @@ namespace Bit.Core.Services
|
||||
request, false, false);
|
||||
}
|
||||
|
||||
public Task SetPasswordAsync(SetPasswordRequest request)
|
||||
{
|
||||
return SendAsync<SetPasswordRequest, object>(HttpMethod.Post, "/accounts/set-password", request, true,
|
||||
false);
|
||||
}
|
||||
|
||||
public Task PostRegisterAsync(RegisterRequest request)
|
||||
{
|
||||
return SendAsync<RegisterRequest, object>(HttpMethod.Post, "/accounts/register", request, false, false);
|
||||
@ -175,6 +181,12 @@ namespace Bit.Core.Services
|
||||
return SendAsync<KeysRequest, object>(HttpMethod.Post, "/accounts/keys", request, true, false);
|
||||
}
|
||||
|
||||
public Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request)
|
||||
{
|
||||
return SendAsync<PasswordVerificationRequest, object>(HttpMethod.Post, "/accounts/verify-password", request,
|
||||
true, false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Folder APIs
|
||||
@ -365,6 +377,34 @@ namespace Bit.Core.Services
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public async Task<object> PreValidateSso(string identifier)
|
||||
{
|
||||
var path = "/account/prevalidate?domainHint=" + WebUtility.UrlEncode(identifier);
|
||||
using (var requestMessage = new HttpRequestMessage())
|
||||
{
|
||||
requestMessage.Version = new Version(1, 0);
|
||||
requestMessage.Method = HttpMethod.Get;
|
||||
requestMessage.RequestUri = new Uri(string.Concat(IdentityBaseUrl, path));
|
||||
requestMessage.Headers.Add("Accept", "application/json");
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await _httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ApiException(HandleWebError(e));
|
||||
}
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await HandleErrorAsync(response, false);
|
||||
throw new ApiException(error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path, TRequest body,
|
||||
bool authed, bool hasResponse)
|
||||
{
|
||||
@ -488,13 +528,20 @@ namespace Bit.Core.Services
|
||||
await _logoutCallbackAsync(true);
|
||||
return null;
|
||||
}
|
||||
JObject responseJObject = null;
|
||||
if (IsJsonResponse(response))
|
||||
try
|
||||
{
|
||||
var responseJsonString = await response.Content.ReadAsStringAsync();
|
||||
responseJObject = JObject.Parse(responseJsonString);
|
||||
JObject responseJObject = null;
|
||||
if (IsJsonResponse(response))
|
||||
{
|
||||
var responseJsonString = await response.Content.ReadAsStringAsync();
|
||||
responseJObject = JObject.Parse(responseJsonString);
|
||||
}
|
||||
return new ErrorResponse(responseJObject, response.StatusCode, tokenError);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new ErrorResponse(responseJObject, response.StatusCode, tokenError);
|
||||
}
|
||||
|
||||
private bool IsJsonResponse(HttpResponseMessage response)
|
||||
|
@ -23,8 +23,6 @@ namespace Bit.Core.Services
|
||||
private readonly bool _setCryptoKeys;
|
||||
|
||||
private SymmetricCryptoKey _key;
|
||||
private KdfType? _kdf;
|
||||
private int? _kdfIterations;
|
||||
|
||||
public AuthService(
|
||||
ICryptoService cryptoService,
|
||||
@ -95,6 +93,9 @@ namespace Bit.Core.Services
|
||||
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public string Code { get; set; }
|
||||
public string CodeVerifier { get; set; }
|
||||
public string SsoRedirectUrl { get; set; }
|
||||
public Dictionary<TwoFactorProviderType, TwoFactorProvider> TwoFactorProviders { get; set; }
|
||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProvidersData { get; set; }
|
||||
public TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; }
|
||||
@ -122,13 +123,20 @@ namespace Bit.Core.Services
|
||||
SelectedTwoFactorProviderType = null;
|
||||
var key = await MakePreloginKeyAsync(masterPassword, email);
|
||||
var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key);
|
||||
return await LogInHelperAsync(email, hashedPassword, key);
|
||||
return await LogInHelperAsync(email, hashedPassword, null, null, null, key, null, null, null);
|
||||
}
|
||||
|
||||
public async Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl)
|
||||
{
|
||||
SelectedTwoFactorProviderType = null;
|
||||
return await LogInHelperAsync(null, null, code, codeVerifier, redirectUrl, null, null, null, null);
|
||||
}
|
||||
|
||||
public Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken,
|
||||
bool? remember = null)
|
||||
{
|
||||
return LogInHelperAsync(Email, MasterPasswordHash, _key, twoFactorProvider, twoFactorToken, remember);
|
||||
return LogInHelperAsync(Email, MasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key,
|
||||
twoFactorProvider, twoFactorToken, remember);
|
||||
}
|
||||
|
||||
public async Task<AuthResult> LogInCompleteAsync(string email, string masterPassword,
|
||||
@ -137,7 +145,16 @@ namespace Bit.Core.Services
|
||||
SelectedTwoFactorProviderType = null;
|
||||
var key = await MakePreloginKeyAsync(masterPassword, email);
|
||||
var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key);
|
||||
return await LogInHelperAsync(email, hashedPassword, key, twoFactorProvider, twoFactorToken, remember);
|
||||
return await LogInHelperAsync(email, hashedPassword, null, null, null, key, twoFactorProvider,
|
||||
twoFactorToken, remember);
|
||||
}
|
||||
|
||||
public async Task<AuthResult> LogInSsoCompleteAsync(string code, string codeVerifier, string redirectUrl,
|
||||
TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null)
|
||||
{
|
||||
SelectedTwoFactorProviderType = null;
|
||||
return await LogInHelperAsync(null, null, code, codeVerifier, redirectUrl, null, twoFactorProvider,
|
||||
twoFactorToken, remember);
|
||||
}
|
||||
|
||||
public void LogOut(Action callback)
|
||||
@ -213,20 +230,30 @@ namespace Bit.Core.Services
|
||||
return providerType;
|
||||
}
|
||||
|
||||
public bool AuthingWithSso()
|
||||
{
|
||||
return Code != null && CodeVerifier != null && SsoRedirectUrl != null;
|
||||
}
|
||||
|
||||
public bool AuthingWithPassword()
|
||||
{
|
||||
return Email != null && MasterPasswordHash != null;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private async Task<SymmetricCryptoKey> MakePreloginKeyAsync(string masterPassword, string email)
|
||||
{
|
||||
email = email.Trim().ToLower();
|
||||
_kdf = null;
|
||||
_kdfIterations = null;
|
||||
KdfType? kdf = null;
|
||||
int? kdfIterations = null;
|
||||
try
|
||||
{
|
||||
var preloginResponse = await _apiService.PostPreloginAsync(new PreloginRequest { Email = email });
|
||||
if (preloginResponse != null)
|
||||
{
|
||||
_kdf = preloginResponse.Kdf;
|
||||
_kdfIterations = preloginResponse.KdfIterations;
|
||||
kdf = preloginResponse.Kdf;
|
||||
kdfIterations = preloginResponse.KdfIterations;
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
@ -236,46 +263,64 @@ namespace Bit.Core.Services
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return await _cryptoService.MakeKeyAsync(masterPassword, email, _kdf, _kdfIterations);
|
||||
return await _cryptoService.MakeKeyAsync(masterPassword, email, kdf, kdfIterations);
|
||||
}
|
||||
|
||||
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, SymmetricCryptoKey key,
|
||||
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string code,
|
||||
string codeVerifier, string redirectUrl, SymmetricCryptoKey key,
|
||||
TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null)
|
||||
{
|
||||
var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email);
|
||||
var appId = await _appIdService.GetAppIdAsync();
|
||||
var deviceRequest = new DeviceRequest(appId, _platformUtilsService);
|
||||
var request = new TokenRequest
|
||||
|
||||
string[] emailPassword;
|
||||
string[] codeCodeVerifier;
|
||||
if (email != null && hashedPassword != null)
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordHash = hashedPassword,
|
||||
Device = deviceRequest,
|
||||
Remember = false
|
||||
};
|
||||
emailPassword = new[] { email, hashedPassword };
|
||||
}
|
||||
else
|
||||
{
|
||||
emailPassword = null;
|
||||
}
|
||||
if (code != null && codeVerifier != null && redirectUrl != null)
|
||||
{
|
||||
codeCodeVerifier = new[] { code, codeVerifier, redirectUrl };
|
||||
}
|
||||
else
|
||||
{
|
||||
codeCodeVerifier = null;
|
||||
}
|
||||
|
||||
TokenRequest request;
|
||||
if (twoFactorToken != null && twoFactorProvider != null)
|
||||
{
|
||||
request.Provider = twoFactorProvider;
|
||||
request.Token = twoFactorToken;
|
||||
request.Remember = remember.GetValueOrDefault();
|
||||
request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember,
|
||||
deviceRequest);
|
||||
}
|
||||
else if (storedTwoFactorToken != null)
|
||||
{
|
||||
request.Provider = TwoFactorProviderType.Remember;
|
||||
request.Token = storedTwoFactorToken;
|
||||
request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember,
|
||||
storedTwoFactorToken, false, deviceRequest);
|
||||
}
|
||||
else
|
||||
{
|
||||
request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, deviceRequest);
|
||||
}
|
||||
|
||||
var response = await _apiService.PostIdentityTokenAsync(request);
|
||||
ClearState();
|
||||
var result = new AuthResult
|
||||
{
|
||||
TwoFactor = response.Item2 != null
|
||||
};
|
||||
var result = new AuthResult { TwoFactor = response.Item2 != null };
|
||||
if (result.TwoFactor)
|
||||
{
|
||||
// Two factor required.
|
||||
var twoFactorResponse = response.Item2;
|
||||
Email = email;
|
||||
MasterPasswordHash = hashedPassword;
|
||||
Code = code;
|
||||
CodeVerifier = codeVerifier;
|
||||
SsoRedirectUrl = redirectUrl;
|
||||
_key = _setCryptoKeys ? key : null;
|
||||
TwoFactorProvidersData = twoFactorResponse.TwoFactorProviders2;
|
||||
result.TwoFactorProviders = twoFactorResponse.TwoFactorProviders2;
|
||||
@ -283,13 +328,14 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
var tokenResponse = response.Item1;
|
||||
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
|
||||
if (tokenResponse.TwoFactorToken != null)
|
||||
{
|
||||
await _tokenService.SetTwoFactorTokenAsync(tokenResponse.TwoFactorToken, email);
|
||||
}
|
||||
await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken);
|
||||
await _userService.SetInformationAsync(_tokenService.GetUserId(), _tokenService.GetEmail(),
|
||||
_kdf.Value, _kdfIterations.Value);
|
||||
tokenResponse.Kdf, tokenResponse.KdfIterations);
|
||||
if (_setCryptoKeys)
|
||||
{
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
@ -322,8 +368,12 @@ namespace Bit.Core.Services
|
||||
|
||||
private void ClearState()
|
||||
{
|
||||
_key = null;
|
||||
Email = null;
|
||||
MasterPasswordHash = null;
|
||||
Code = null;
|
||||
CodeVerifier = null;
|
||||
SsoRedirectUrl = null;
|
||||
TwoFactorProvidersData = null;
|
||||
SelectedTwoFactorProviderType = null;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Zxcvbn;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -481,9 +482,28 @@ namespace Bit.Core.Services
|
||||
await _storageService.RemoveAsync(Keys_History);
|
||||
}
|
||||
|
||||
public Task<object> PasswordStrength(string password, List<string> userInputs = null)
|
||||
public Result PasswordStrength(string password, List<string> userInputs = null)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
if (string.IsNullOrEmpty(password))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var globalUserInputs = new List<string>
|
||||
{
|
||||
"bitwarden",
|
||||
"bit",
|
||||
"warden"
|
||||
};
|
||||
if (userInputs != null && userInputs.Any())
|
||||
{
|
||||
globalUserInputs.AddRange(userInputs);
|
||||
}
|
||||
// Use a hash set to get rid of any duplicate user inputs
|
||||
var hashSet = new HashSet<string>(globalUserInputs);
|
||||
var finalUserInputs = new string[hashSet.Count];
|
||||
hashSet.CopyTo(finalUserInputs);
|
||||
var result = Zxcvbn.Zxcvbn.MatchPassword(password, finalUserInputs);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void NormalizeOptions(PasswordGenerationOptions options,
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
@ -66,5 +67,156 @@ namespace Bit.Core.Services
|
||||
await _storageService.RemoveAsync(string.Format(Keys_PoliciesPrefix, userId));
|
||||
_policyCache = null;
|
||||
}
|
||||
|
||||
public async Task<MasterPasswordPolicyOptions> GetMasterPasswordPolicyOptions(
|
||||
IEnumerable<Policy> policies = null)
|
||||
{
|
||||
MasterPasswordPolicyOptions enforcedOptions = null;
|
||||
|
||||
if (policies == null)
|
||||
{
|
||||
policies = await GetAll(PolicyType.MasterPassword);
|
||||
}
|
||||
else
|
||||
{
|
||||
policies = policies.Where(p => p.Type == PolicyType.MasterPassword);
|
||||
}
|
||||
|
||||
if (policies == null || !policies.Any())
|
||||
{
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
foreach (var currentPolicy in policies)
|
||||
{
|
||||
if (!currentPolicy.Enabled || currentPolicy.Data == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enforcedOptions == null)
|
||||
{
|
||||
enforcedOptions = new MasterPasswordPolicyOptions();
|
||||
}
|
||||
|
||||
var minComplexity = GetPolicyInt(currentPolicy, "minComplexity");
|
||||
if (minComplexity != null && (int)(long)minComplexity > enforcedOptions.MinComplexity)
|
||||
{
|
||||
enforcedOptions.MinComplexity = (int)(long)minComplexity;
|
||||
}
|
||||
|
||||
var minLength = GetPolicyInt(currentPolicy, "minLength");
|
||||
if (minLength != null && (int)(long)minLength > enforcedOptions.MinLength)
|
||||
{
|
||||
enforcedOptions.MinLength = (int)(long)minLength;
|
||||
}
|
||||
|
||||
var requireUpper = GetPolicyBool(currentPolicy, "requireUpper");
|
||||
if (requireUpper != null && (bool)requireUpper)
|
||||
{
|
||||
enforcedOptions.RequireUpper = true;
|
||||
}
|
||||
|
||||
var requireLower = GetPolicyBool(currentPolicy, "requireLower");
|
||||
if (requireLower != null && (bool)requireLower)
|
||||
{
|
||||
enforcedOptions.RequireLower = true;
|
||||
}
|
||||
|
||||
var requireNumbers = GetPolicyBool(currentPolicy, "requireNumbers");
|
||||
if (requireNumbers != null && (bool)requireNumbers)
|
||||
{
|
||||
enforcedOptions.RequireNumbers = true;
|
||||
}
|
||||
|
||||
var requireSpecial = GetPolicyBool(currentPolicy, "requireSpecial");
|
||||
if (requireSpecial != null && (bool)requireSpecial)
|
||||
{
|
||||
enforcedOptions.RequireSpecial = true;
|
||||
}
|
||||
}
|
||||
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
public async Task<bool> EvaluateMasterPassword(int passwordStrength, string newPassword,
|
||||
MasterPasswordPolicyOptions enforcedPolicyOptions)
|
||||
{
|
||||
if (enforcedPolicyOptions == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.MinComplexity > 0 && enforcedPolicyOptions.MinComplexity > passwordStrength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.MinLength > 0 && enforcedPolicyOptions.MinLength > newPassword.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.RequireUpper && newPassword.ToLower() == newPassword)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.RequireLower && newPassword.ToUpper() == newPassword)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.RequireNumbers && !newPassword.Any(char.IsDigit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.RequireSpecial && !Regex.IsMatch(newPassword, "^.*[!@#$%\\^&*].*$"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int? GetPolicyInt(Policy policy, string key)
|
||||
{
|
||||
if (policy.Data.ContainsKey(key))
|
||||
{
|
||||
var value = policy.Data[key];
|
||||
if (value != null)
|
||||
{
|
||||
return (int)(long)value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool? GetPolicyBool(Policy policy, string key)
|
||||
{
|
||||
if (policy.Data.ContainsKey(key))
|
||||
{
|
||||
var value = policy.Data[key];
|
||||
if (value != null)
|
||||
{
|
||||
return (bool)value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetPolicyString(Policy policy, string key)
|
||||
{
|
||||
if (policy.Data.ContainsKey(key))
|
||||
{
|
||||
var value = policy.Data[key];
|
||||
if (value != null)
|
||||
{
|
||||
return (string)value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
LaunchLoginFlow();
|
||||
LaunchHomePage();
|
||||
}
|
||||
else if (await IsLocked())
|
||||
{
|
||||
@ -94,7 +94,7 @@ namespace Bit.iOS.Autofill
|
||||
InitAppIfNeeded();
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
LaunchLoginFlow();
|
||||
LaunchHomePage();
|
||||
return;
|
||||
}
|
||||
_context.CredentialIdentity = credentialIdentity;
|
||||
@ -107,7 +107,7 @@ namespace Bit.iOS.Autofill
|
||||
_context.Configuring = true;
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
LaunchLoginFlow();
|
||||
LaunchHomePage();
|
||||
return;
|
||||
}
|
||||
CheckLock(() => PerformSegue("setupSegue", this));
|
||||
@ -294,17 +294,74 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchLoginFlow()
|
||||
private void LaunchHomePage()
|
||||
{
|
||||
var loginPage = new LoginPage();
|
||||
var homePage = new HomePage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(homePage);
|
||||
if (homePage.BindingContext is HomeViewModel vm)
|
||||
{
|
||||
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
||||
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
|
||||
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
|
||||
vm.CloseAction = () => CompleteRequest();
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(homePage);
|
||||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchEnvironmentFlow()
|
||||
{
|
||||
var environmentPage = new EnvironmentPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(environmentPage);
|
||||
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
||||
{
|
||||
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(environmentPage);
|
||||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchRegisterFlow()
|
||||
{
|
||||
var registerPage = new RegisterPage(null);
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(registerPage);
|
||||
if (registerPage.BindingContext is RegisterPageViewModel vm)
|
||||
{
|
||||
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(registerPage);
|
||||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchLoginFlow(string email = null)
|
||||
{
|
||||
var loginPage = new LoginPage(email);
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(loginPage);
|
||||
if (loginPage.BindingContext is LoginPageViewModel vm)
|
||||
{
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow());
|
||||
vm.LoggedInAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => CompleteRequest();
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
vm.HideHintButton = true;
|
||||
}
|
||||
|
||||
@ -314,16 +371,44 @@ namespace Bit.iOS.Autofill
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchTwoFactorFlow()
|
||||
private void LaunchLoginSsoFlow()
|
||||
{
|
||||
var twoFactorPage = new TwoFactorPage();
|
||||
var loginPage = new LoginSsoPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(loginPage);
|
||||
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
|
||||
{
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true));
|
||||
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
|
||||
vm.SsoAuthSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(loginPage);
|
||||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchTwoFactorFlow(bool authingWithSso)
|
||||
{
|
||||
var twoFactorPage = new TwoFactorPage(authingWithSso);
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(twoFactorPage);
|
||||
if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm)
|
||||
{
|
||||
vm.TwoFactorAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
||||
vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue();
|
||||
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
|
||||
if (authingWithSso)
|
||||
{
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
||||
}
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(twoFactorPage);
|
||||
@ -331,5 +416,23 @@ namespace Bit.iOS.Autofill
|
||||
twoFactorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(twoFactorController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchSetPasswordFlow()
|
||||
{
|
||||
var setPasswordPage = new SetPasswordPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(setPasswordPage);
|
||||
if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm)
|
||||
{
|
||||
vm.SetPasswordSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(setPasswordPage);
|
||||
var setPasswordController = navigationPage.CreateViewController();
|
||||
setPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(setPasswordController, true, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ namespace Bit.iOS.Extension
|
||||
}
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
LaunchLoginFlow();
|
||||
LaunchHomePage();
|
||||
return;
|
||||
}
|
||||
else if (await IsLocked())
|
||||
@ -420,16 +420,73 @@ namespace Bit.iOS.Extension
|
||||
return userService.IsAuthenticatedAsync();
|
||||
}
|
||||
|
||||
private void LaunchLoginFlow()
|
||||
private void LaunchHomePage()
|
||||
{
|
||||
var loginPage = new LoginPage();
|
||||
var homePage = new HomePage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(homePage);
|
||||
if (homePage.BindingContext is HomeViewModel vm)
|
||||
{
|
||||
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
||||
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
|
||||
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
|
||||
vm.CloseAction = () => CompleteRequest(null, null);
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(homePage);
|
||||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchEnvironmentFlow()
|
||||
{
|
||||
var environmentPage = new EnvironmentPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(environmentPage);
|
||||
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
||||
{
|
||||
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(environmentPage);
|
||||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchRegisterFlow()
|
||||
{
|
||||
var registerPage = new RegisterPage(null);
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(registerPage);
|
||||
if (registerPage.BindingContext is RegisterPageViewModel vm)
|
||||
{
|
||||
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(registerPage);
|
||||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchLoginFlow(string email = null)
|
||||
{
|
||||
var loginPage = new LoginPage(email);
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(loginPage);
|
||||
if (loginPage.BindingContext is LoginPageViewModel vm)
|
||||
{
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow());
|
||||
vm.LoggedInAction = () => DismissLockAndContinue();
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => CompleteRequest(null, null);
|
||||
vm.HideHintButton = true;
|
||||
}
|
||||
@ -440,7 +497,27 @@ namespace Bit.iOS.Extension
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchTwoFactorFlow()
|
||||
private void LaunchLoginSsoFlow()
|
||||
{
|
||||
var loginPage = new LoginSsoPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(loginPage);
|
||||
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
|
||||
{
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true));
|
||||
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
|
||||
vm.SsoAuthSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(loginPage);
|
||||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchTwoFactorFlow(bool authingWithSso)
|
||||
{
|
||||
var twoFactorPage = new TwoFactorPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
@ -448,8 +525,16 @@ namespace Bit.iOS.Extension
|
||||
ThemeManager.ApplyResourcesToPage(twoFactorPage);
|
||||
if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm)
|
||||
{
|
||||
vm.TwoFactorAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
||||
vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue();
|
||||
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
|
||||
if (authingWithSso)
|
||||
{
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
||||
}
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(twoFactorPage);
|
||||
@ -457,5 +542,23 @@ namespace Bit.iOS.Extension
|
||||
twoFactorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(twoFactorController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchSetPasswordFlow()
|
||||
{
|
||||
var setPasswordPage = new SetPasswordPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(setPasswordPage);
|
||||
if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm)
|
||||
{
|
||||
vm.SetPasswordSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(setPasswordPage);
|
||||
var setPasswordController = navigationPage.CreateViewController();
|
||||
setPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(setPasswordController, true, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -241,6 +241,15 @@ namespace Bit.iOS
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
|
||||
{
|
||||
if (Xamarin.Essentials.Platform.OpenUrl(app, url, options))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return base.OpenUrl(app, url, options);
|
||||
}
|
||||
|
||||
public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
|
||||
{
|
||||
_pushHandler?.OnErrorReceived(error);
|
||||
|
@ -156,7 +156,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Xamarin.Essentials">
|
||||
<Version>1.5.3.1</Version>
|
||||
<Version>1.5.3.2</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
|
||||
|
Loading…
Reference in New Issue
Block a user