From dfc7c55b776c3809f222be417cc10a30d0b16772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Thu, 27 Jul 2023 16:55:06 +0100 Subject: [PATCH] [PM-2297] Login with trusted device (Flow 2) (#2623) * [PM-2297] Add DecryptUserKeyWithDeviceKey method * [PM-2297] Add methods to DeviceTrustCryptoService update decryption options model * [PM-2297] Update account decryption options model * [PM-2297] Fix TrustedDeviceOption and DeviceResponse model. Change StateService device key get set to have default user id * [PM-2297] Update navigation to decryption options * [PM-2297] Add missing action navigations to iOS extensions * [PM-2297] Fix trust device bug/typo * [PM-2297] Fix model bug * [PM-2297] Fix state var crash * [PM-2297] Add trust device login logic to auth service * [PM-2297] Refactor auth service key connector code * [PM-2297] Remove reconciledOptions for deviceKey in state service * [PM-2297] Remove unnecessary user id params --- .../Accounts/LoginApproveDevicePage.xaml.cs | 2 +- .../Accounts/LoginApproveDeviceViewModel.cs | 14 ++------ src/App/Pages/Accounts/LoginSsoPage.xaml.cs | 12 ++++--- .../Pages/Accounts/LoginSsoPageViewModel.cs | 32 +++++++++++++++++-- .../Abstractions/IDeviceTrustCryptoService.cs | 2 ++ .../Models/Domain/AccountDecryptionOptions.cs | 6 +++- src/Core/Models/Response/DeviceResponse.cs | 2 +- src/Core/Services/AuthService.cs | 30 +++++++++++------ src/Core/Services/DeviceTrustCryptoService.cs | 28 +++++++++++++++- src/Core/Services/StateService.cs | 12 +++++-- src/Core/Utilities/ServiceContainer.cs | 4 +-- .../CredentialProviderViewController.cs | 27 ++++++++++++++-- src/iOS.Extension/LoadingViewController.cs | 27 ++++++++++++++-- .../LoadingViewController.cs | 27 ++++++++++++++-- 14 files changed, 182 insertions(+), 43 deletions(-) diff --git a/src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs b/src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs index 35c15313f..71b66b4ae 100644 --- a/src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs +++ b/src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs @@ -19,7 +19,7 @@ namespace Bit.App.Pages { InitializeComponent(); _vm = BindingContext as LoginApproveDeviceViewModel; - _vm.LogInWithMasterPassword = () => StartLogInWithMasterPassword().FireAndForget(); + _vm.LogInWithMasterPasswordAction = () => StartLogInWithMasterPassword().FireAndForget(); _vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget(); _vm.RequestAdminApprovalAction = () => RequestAdminApprovalAsync().FireAndForget(); _vm.CloseAction = () => { Navigation.PopModalAsync(); }; diff --git a/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs b/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs index 0cee8f216..f25aeb8cf 100644 --- a/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs +++ b/src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs @@ -32,7 +32,7 @@ namespace Bit.App.Pages public ICommand ApproveWithMasterPasswordCommand { get; } public ICommand ContinueCommand { get; } - public Action LogInWithMasterPassword { get; set; } + public Action LogInWithMasterPasswordAction { get; set; } public Action LogInWithDeviceAction { get; set; } public Action RequestAdminApprovalAction { get; set; } public Action CloseAction { get; set; } @@ -53,7 +53,7 @@ namespace Bit.App.Pages onException: ex => HandleException(ex), allowsMultipleExecutions: false); - ApproveWithMasterPasswordCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithMasterPassword), + ApproveWithMasterPasswordCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithMasterPasswordAction), onException: ex => HandleException(ex), allowsMultipleExecutions: false); @@ -111,15 +111,7 @@ namespace Bit.App.Pages var decryptOptions = await _stateService.GetAccountDecryptionOptions(); RequestAdminApprovalEnabled = decryptOptions?.TrustedDeviceOption?.HasAdminApproval ?? false; ApproveWithMasterPasswordEnabled = decryptOptions?.HasMasterPassword ?? false; - } - catch (Exception ex) - { - HandleException(ex); - } - - try - { - ApproveWithMyOtherDeviceEnabled = await _apiService.GetDevicesExistenceByTypes(DeviceTypeExtensions.GetDesktopAndMobileTypes().ToArray()); + ApproveWithMyOtherDeviceEnabled = decryptOptions?.TrustedDeviceOption?.HasLoginApprovingDevice ?? false; } catch (Exception ex) { diff --git a/src/App/Pages/Accounts/LoginSsoPage.xaml.cs b/src/App/Pages/Accounts/LoginSsoPage.xaml.cs index a910c9454..f8a145b4a 100644 --- a/src/App/Pages/Accounts/LoginSsoPage.xaml.cs +++ b/src/App/Pages/Accounts/LoginSsoPage.xaml.cs @@ -29,6 +29,8 @@ namespace Bit.App.Pages _vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync()); _vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); + _vm.StartDeviceApprovalOptionsAction = + () => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync()); _vm.CloseAction = async () => { await Navigation.PopModalAsync(); @@ -106,15 +108,17 @@ namespace Bit.App.Pages await Navigation.PushModalAsync(new NavigationPage(page)); } + private async Task StartDeviceApprovalOptionsAsync() + { + var page = new LoginApproveDevicePage(); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + private async Task SsoAuthSuccessAsync() { RestoreAppOptionsFromCopy(); await AppHelpers.ClearPreviousPage(); - // Just for testing the screen - Application.Current.MainPage = new NavigationPage(new LoginApproveDevicePage(_appOptions)); - return; - if (await _vaultTimeoutService.IsLockedAsync()) { Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions)); diff --git a/src/App/Pages/Accounts/LoginSsoPageViewModel.cs b/src/App/Pages/Accounts/LoginSsoPageViewModel.cs index fbf9bf4f2..af804ed53 100644 --- a/src/App/Pages/Accounts/LoginSsoPageViewModel.cs +++ b/src/App/Pages/Accounts/LoginSsoPageViewModel.cs @@ -9,6 +9,7 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Essentials; @@ -29,6 +30,8 @@ namespace Bit.App.Pages private readonly IStateService _stateService; private readonly ILogger _logger; private readonly IOrganizationService _organizationService; + private readonly IDeviceTrustCryptoService _deviceTrustCryptoService; + private readonly ICryptoService _cryptoService; private string _orgIdentifier; @@ -45,7 +48,8 @@ namespace Bit.App.Pages _stateService = ServiceContainer.Resolve("stateService"); _logger = ServiceContainer.Resolve("logger"); _organizationService = ServiceContainer.Resolve(); - + _deviceTrustCryptoService = ServiceContainer.Resolve(); + _cryptoService = ServiceContainer.Resolve(); PageTitle = AppResources.Bitwarden; LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false); @@ -61,6 +65,7 @@ namespace Bit.App.Pages public Action StartTwoFactorAction { get; set; } public Action StartSetPasswordAction { get; set; } public Action SsoAuthSuccessAction { get; set; } + public Action StartDeviceApprovalOptionsAction { get; set; } public Action CloseAction { get; set; } public Action UpdateTempPasswordAction { get; set; } @@ -197,6 +202,7 @@ namespace Bit.App.Pages try { var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId); + var decryptOptions = await _stateService.GetAccountDecryptionOptions(); await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier); await _deviceActionService.HideLoadingAsync(); @@ -212,9 +218,31 @@ namespace Bit.App.Pages { UpdateTempPasswordAction?.Invoke(); } + else if (decryptOptions?.TrustedDeviceOption != null) + { + // If user doesn't have a MP, but has reset password permission, they must set a MP + if (!decryptOptions.HasMasterPassword && + decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission) + { + StartSetPasswordAction?.Invoke(); + } + else if (response.ForcePasswordReset) + { + UpdateTempPasswordAction?.Invoke(); + } + else if (await _deviceTrustCryptoService.IsDeviceTrustedAsync()) + { + _syncService.FullSyncAsync(true).FireAndForget(); + SsoAuthSuccessAction?.Invoke(); + } + else + { + StartDeviceApprovalOptionsAction?.Invoke(); + } + } else { - var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); + _syncService.FullSyncAsync(true).FireAndForget(); SsoAuthSuccessAction?.Invoke(); } } diff --git a/src/Core/Abstractions/IDeviceTrustCryptoService.cs b/src/Core/Abstractions/IDeviceTrustCryptoService.cs index 7410b8da4..87b7bea12 100644 --- a/src/Core/Abstractions/IDeviceTrustCryptoService.cs +++ b/src/Core/Abstractions/IDeviceTrustCryptoService.cs @@ -10,5 +10,7 @@ namespace Bit.Core.Abstractions Task TrustDeviceIfNeededAsync(); Task GetShouldTrustDeviceAsync(); Task SetShouldTrustDeviceAsync(bool value); + Task DecryptUserKeyWithDeviceKeyAsync(string encryptedDevicePrivateKey, string encryptedUserKey); + Task IsDeviceTrustedAsync(); } } diff --git a/src/Core/Models/Domain/AccountDecryptionOptions.cs b/src/Core/Models/Domain/AccountDecryptionOptions.cs index 8101bd9f3..95969f678 100644 --- a/src/Core/Models/Domain/AccountDecryptionOptions.cs +++ b/src/Core/Models/Domain/AccountDecryptionOptions.cs @@ -11,11 +11,15 @@ namespace Bit.Core.Models.Domain public class TrustedDeviceOption { public bool HasAdminApproval { get; set; } + public bool HasLoginApprovingDevice { get; set; } + public bool HasManageResetPasswordPermission { get; set; } + public string EncryptedPrivateKey { get; set; } + public string EncryptedUserKey { get; set; } } public class KeyConnectorOption { - public bool KeyConnectorUrl { get; set; } + public string KeyConnectorUrl { get; set; } } } diff --git a/src/Core/Models/Response/DeviceResponse.cs b/src/Core/Models/Response/DeviceResponse.cs index 331d0562a..9cd3fb9e0 100644 --- a/src/Core/Models/Response/DeviceResponse.cs +++ b/src/Core/Models/Response/DeviceResponse.cs @@ -3,7 +3,7 @@ public class DeviceResponse { public string Id { get; set; } - public int Name { get; set; } + public string Name { get; set; } public string Identifier { get; set; } public DeviceType Type { get; set; } public string CreationDate { get; set; } diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index d7203c9bd..9fda8f9f2 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -27,6 +27,7 @@ namespace Bit.Core.Services private readonly IKeyConnectorService _keyConnectorService; private readonly IPasswordGenerationService _passwordGenerationService; private readonly IPolicyService _policyService; + private readonly IDeviceTrustCryptoService _deviceTrustCryptoService; private readonly bool _setCryptoKeys; private readonly LazyResolve _watchDeviceService = new LazyResolve(); @@ -50,6 +51,7 @@ namespace Bit.Core.Services IKeyConnectorService keyConnectorService, IPasswordGenerationService passwordGenerationService, IPolicyService policyService, + IDeviceTrustCryptoService deviceTrustCryptoService, bool setCryptoKeys = true) { _cryptoService = cryptoService; @@ -64,6 +66,7 @@ namespace Bit.Core.Services _keyConnectorService = keyConnectorService; _passwordGenerationService = passwordGenerationService; _policyService = policyService; + _deviceTrustCryptoService = deviceTrustCryptoService; _setCryptoKeys = setCryptoKeys; TwoFactorProviders = new Dictionary(); @@ -479,18 +482,27 @@ namespace Bit.Core.Services if (code == null || tokenResponse.Key != null) { - if (tokenResponse.KeyConnectorUrl != null) - { - await _keyConnectorService.GetAndSetKey(tokenResponse.KeyConnectorUrl); - } - + var decryptOptions = await _stateService.GetAccountDecryptionOptions(); await _cryptoService.SetMasterKeyEncryptedUserKeyAsync(tokenResponse.Key); - if (masterKey != null) + if (decryptOptions?.TrustedDeviceOption != null) { - await _cryptoService.SetMasterKeyAsync(masterKey); - var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey); - await _cryptoService.SetUserKeyAsync(userKey); + var key = await _deviceTrustCryptoService.DecryptUserKeyWithDeviceKeyAsync(decryptOptions.TrustedDeviceOption.EncryptedPrivateKey, decryptOptions.TrustedDeviceOption.EncryptedUserKey); + if (key != null) + { + await _cryptoService.SetUserKeyAsync(key); + } + } + else if (!string.IsNullOrEmpty(tokenResponse.KeyConnectorUrl) || !string.IsNullOrEmpty(decryptOptions?.KeyConnectorOption?.KeyConnectorUrl)) + { + + await _cryptoService.SetMasterKeyEncryptedUserKeyAsync(tokenResponse.Key); + if (masterKey != null) + { + await _cryptoService.SetMasterKeyAsync(masterKey); + var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey); + await _cryptoService.SetUserKeyAsync(userKey); + } } // User doesn't have a key pair yet (old account), let's generate one for them. diff --git a/src/Core/Services/DeviceTrustCryptoService.cs b/src/Core/Services/DeviceTrustCryptoService.cs index e25739fb2..6a0e85689 100644 --- a/src/Core/Services/DeviceTrustCryptoService.cs +++ b/src/Core/Services/DeviceTrustCryptoService.cs @@ -59,7 +59,7 @@ namespace Bit.Core.Services var deviceIdentifier = await _appIdService.GetAppIdAsync(); var deviceRequest = new TrustedDeviceKeysRequest { - EncryptedUserKey = (await _cryptoService.RsaEncryptAsync(userKey.EncKey, devicePublicKey)).EncryptedString, + EncryptedUserKey = (await _cryptoService.RsaEncryptAsync(userKey.Key, devicePublicKey)).EncryptedString, EncryptedPublicKey = (await _cryptoService.EncryptAsync(devicePublicKey, userKey)).EncryptedString, EncryptedPrivateKey = (await _cryptoService.EncryptAsync(devicePrivateKey, deviceKey)).EncryptedString, }; @@ -99,5 +99,31 @@ namespace Bit.Core.Services await SetShouldTrustDeviceAsync(false); return response; } + + public async Task IsDeviceTrustedAsync() + { + var existingDeviceKey = await GetDeviceKeyAsync(); + return existingDeviceKey != null; + } + + public async Task DecryptUserKeyWithDeviceKeyAsync(string encryptedDevicePrivateKey, string encryptedUserKey) + { + var existingDeviceKey = await GetDeviceKeyAsync(); + if (existingDeviceKey == null) + { + // User doesn't have a device key anymore so device is untrusted + return null; + } + + // Attempt to decrypt encryptedDevicePrivateKey with device key + var devicePrivateKeyBytes = await _cryptoService.DecryptToBytesAsync( + new EncString(encryptedDevicePrivateKey), + existingDeviceKey + ); + + // Attempt to decrypt encryptedUserDataKey with devicePrivateKey + var userKeyBytes = await _cryptoService.RsaDecryptAsync(encryptedUserKey, devicePrivateKeyBytes); + return new UserKey(userKeyBytes); + } } } diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index c3b1cd7ad..480bacaaa 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -515,13 +515,21 @@ namespace Bit.Core.Services public async Task GetDeviceKeyAsync(string userId = null) { - var deviceKeyB64 = await _storageMediatorService.GetAsync(Constants.DeviceKeyKey(userId), true); + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + var deviceKeyB64 = await _storageMediatorService.GetAsync(Constants.DeviceKeyKey(reconciledOptions.UserId), true); + if (string.IsNullOrEmpty(deviceKeyB64)) + { + return null; + } return new SymmetricCryptoKey(Convert.FromBase64String(deviceKeyB64)); } public async Task SetDeviceKeyAsync(SymmetricCryptoKey value, string userId = null) { - await _storageMediatorService.SaveAsync(Constants.DeviceKeyKey(userId), value.KeyB64, true); + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await _storageMediatorService.SaveAsync(Constants.DeviceKeyKey(reconciledOptions.UserId), value.KeyB64, true); } public async Task> GetAutofillBlacklistedUrisAsync(string userId = null) diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index 36dd7efc9..c9a2255df 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -77,9 +77,10 @@ namespace Bit.Core.Utilities }); var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService, cryptoFunctionService, policyService); var totpService = new TotpService(cryptoFunctionService); + var deviceTrustCryptoService = new DeviceTrustCryptoService(apiService, appIdService, cryptoFunctionService, cryptoService, stateService); var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService, - keyConnectorService, passwordGenerationService, policyService); + keyConnectorService, passwordGenerationService, policyService, deviceTrustCryptoService); var exportService = new ExportService(folderService, cipherService, cryptoService); var auditService = new AuditService(cryptoFunctionService, apiService); var environmentService = new EnvironmentService(apiService, stateService, conditionedRunner); @@ -88,7 +89,6 @@ namespace Bit.Core.Utilities cryptoService); var usernameGenerationService = new UsernameGenerationService(cryptoService, apiService, stateService); var configService = new ConfigService(apiService, stateService, logger); - var deviceTrustCryptoService = new DeviceTrustCryptoService(apiService, appIdService, cryptoFunctionService, cryptoService, stateService); Register(conditionedRunner); Register("tokenService", tokenService); diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index 71c8cf660..ed0ae3ec4 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -498,7 +498,7 @@ namespace Bit.iOS.Autofill vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false)); vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); - vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(email)); + vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, email)); vm.LogInSuccessAction = () => DismissLockAndContinue(); vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); } @@ -511,11 +511,11 @@ namespace Bit.iOS.Autofill LogoutIfAuthed(); } - private void LaunchLoginWithDevice(string email = null) + private void LaunchLoginWithDevice(AuthRequestType authRequestType, string email = null) { var appOptions = new AppOptions { IosExtension = true }; var app = new App.App(appOptions); - var loginWithDevicePage = new LoginPasswordlessRequestPage(email, AuthRequestType.AuthenticateAndUnlock, appOptions); + var loginWithDevicePage = new LoginPasswordlessRequestPage(email, authRequestType, appOptions); ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesTo(loginWithDevicePage); if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm) @@ -545,6 +545,7 @@ namespace Bit.iOS.Autofill vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true)); vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); + vm.StartDeviceApprovalOptionsAction = () => DismissViewController(false, () => LaunchDeviceApprovalOptionsFlow()); vm.SsoAuthSuccessAction = () => DismissLockAndContinue(); vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); } @@ -621,6 +622,26 @@ namespace Bit.iOS.Autofill PresentViewController(updateTempPasswordController, true, null); } + private void LaunchDeviceApprovalOptionsFlow() + { + var loginApproveDevicePage = new LoginApproveDevicePage(); + var app = new App.App(new AppOptions { IosExtension = true }); + ThemeManager.SetTheme(app.Resources); + ThemeManager.ApplyResourcesTo(loginApproveDevicePage); + if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm) + { + vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this)); + vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email)); + vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email)); + vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); + } + + var navigationPage = new NavigationPage(loginApproveDevicePage); + var loginApproveDeviceController = navigationPage.CreateViewController(); + loginApproveDeviceController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; + PresentViewController(loginApproveDeviceController, true, null); + } + public Task SetPreviousPageInfoAsync() => Task.CompletedTask; public Task UpdateThemeAsync() => Task.CompletedTask; diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index f9e2bfd3d..597ee90ff 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -520,7 +520,7 @@ namespace Bit.iOS.Extension vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false)); vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); - vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(email)); + vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, email)); vm.LogInSuccessAction = () => DismissLockAndContinue(); vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); } @@ -533,11 +533,11 @@ namespace Bit.iOS.Extension LogoutIfAuthed(); } - private void LaunchLoginWithDevice(string email = null) + private void LaunchLoginWithDevice(AuthRequestType authRequestType,string email = null) { var appOptions = new AppOptions { IosExtension = true }; var app = new App.App(appOptions); - var loginWithDevicePage = new LoginPasswordlessRequestPage(email, AuthRequestType.AuthenticateAndUnlock, appOptions); + var loginWithDevicePage = new LoginPasswordlessRequestPage(email, authRequestType, appOptions); ThemeManager.SetTheme(app.Resources); ThemeManager.ApplyResourcesTo(loginWithDevicePage); if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm) @@ -567,6 +567,7 @@ namespace Bit.iOS.Extension vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true)); vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); + vm.StartDeviceApprovalOptionsAction = () => DismissViewController(false, () => LaunchDeviceApprovalOptionsFlow()); vm.SsoAuthSuccessAction = () => DismissLockAndContinue(); vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); } @@ -642,5 +643,25 @@ namespace Bit.iOS.Extension updateTempPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; PresentViewController(updateTempPasswordController, true, null); } + + private void LaunchDeviceApprovalOptionsFlow() + { + var loginApproveDevicePage = new LoginApproveDevicePage(); + var app = new App.App(new AppOptions { IosExtension = true }); + ThemeManager.SetTheme(app.Resources); + ThemeManager.ApplyResourcesTo(loginApproveDevicePage); + if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm) + { + vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this)); + vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email)); + vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email)); + vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); + } + + var navigationPage = new NavigationPage(loginApproveDevicePage); + var loginApproveDeviceController = navigationPage.CreateViewController(); + loginApproveDeviceController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; + PresentViewController(loginApproveDeviceController, true, null); + } } } diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs index 46629d457..95b753b08 100644 --- a/src/iOS.ShareExtension/LoadingViewController.cs +++ b/src/iOS.ShareExtension/LoadingViewController.cs @@ -339,7 +339,7 @@ namespace Bit.iOS.ShareExtension vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false)); vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow()); vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow()); - vm.LogInWithDeviceAction = () => DismissAndLaunch(() => LaunchLoginWithDevice(email)); + vm.LogInWithDeviceAction = () => DismissAndLaunch(() => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, email)); vm.LogInSuccessAction = () => { DismissLockAndContinue(); }; vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage()); } @@ -348,9 +348,9 @@ namespace Bit.iOS.ShareExtension LogoutIfAuthed(); } - private void LaunchLoginWithDevice(string email = null) + private void LaunchLoginWithDevice(AuthRequestType authRequestType, string email = null) { - var loginWithDevicePage = new LoginPasswordlessRequestPage(email, AuthRequestType.AuthenticateAndUnlock, _appOptions.Value); + var loginWithDevicePage = new LoginPasswordlessRequestPage(email, authRequestType, _appOptions.Value); SetupAppAndApplyResources(loginWithDevicePage); if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm) { @@ -373,6 +373,7 @@ namespace Bit.iOS.ShareExtension vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(true)); vm.StartSetPasswordAction = () => DismissAndLaunch(() => LaunchSetPasswordFlow()); vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow()); + vm.StartDeviceApprovalOptionsAction = () => DismissViewController(false, () => LaunchDeviceApprovalOptionsFlow()); vm.SsoAuthSuccessAction = () => DismissLockAndContinue(); vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage()); } @@ -427,6 +428,26 @@ namespace Bit.iOS.ShareExtension NavigateToPage(updateTempPasswordPage); } + private void LaunchDeviceApprovalOptionsFlow() + { + var loginApproveDevicePage = new LoginApproveDevicePage(); + var app = new App.App(new AppOptions { IosExtension = true }); + ThemeManager.SetTheme(app.Resources); + ThemeManager.ApplyResourcesTo(loginApproveDevicePage); + if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm) + { + vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this)); + vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email)); + vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email)); + vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); + } + + var navigationPage = new NavigationPage(loginApproveDevicePage); + var loginApproveDeviceController = navigationPage.CreateViewController(); + loginApproveDeviceController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; + PresentViewController(loginApproveDeviceController, true, null); + } + public void Navigate(NavigationTarget navTarget, INavigationParams navParams = null) { if (ExtNavigationController?.ViewControllers?.Any() ?? false)