diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index dc6221e2e..6d097f888 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -34,6 +34,7 @@ namespace Bit.App.Pages private readonly IPolicyService _policyService; private readonly IPasswordGenerationService _passwordGenerationService; private IDeviceTrustCryptoService _deviceTrustCryptoService; + private readonly ISyncService _syncService; private string _email; private string _masterPassword; private string _pin; @@ -65,6 +66,7 @@ namespace Bit.App.Pages _policyService = ServiceContainer.Resolve(); _passwordGenerationService = ServiceContainer.Resolve(); _deviceTrustCryptoService = ServiceContainer.Resolve(); + _syncService = ServiceContainer.Resolve(); PageTitle = AppResources.VerifyMasterPassword; TogglePasswordCommand = new Command(TogglePassword); @@ -480,6 +482,7 @@ namespace Bit.App.Pages private async Task DoContinueAsync() { + _syncService.FullSyncAsync(false).FireAndForget(); await _stateService.SetBiometricLockedAsync(false); _watchDeviceService.SyncDataToWatchAsync().FireAndForget(); _messagingService.Send("unlocked"); diff --git a/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs b/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs index 8d9e116a2..87b004c24 100644 --- a/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs +++ b/src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs @@ -13,6 +13,7 @@ using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; @@ -34,6 +35,8 @@ namespace Bit.App.Pages private IEnvironmentService _environmentService; private ILogger _logger; private IDeviceTrustCryptoService _deviceTrustCryptoService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private readonly ICryptoService _cryptoService; protected override II18nService i18nService => _i18nService; protected override IEnvironmentService environmentService => _environmentService; @@ -61,6 +64,8 @@ namespace Bit.App.Pages _stateService = ServiceContainer.Resolve(); _logger = ServiceContainer.Resolve(); _deviceTrustCryptoService = ServiceContainer.Resolve(); + _cryptoFunctionService = ServiceContainer.Resolve(); + _cryptoService = ServiceContainer.Resolve(); PageTitle = AppResources.LogInWithAnotherDevice; @@ -202,14 +207,22 @@ namespace Bit.App.Pages private async Task CheckLoginRequestStatus() { - if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode)) + if (string.IsNullOrEmpty(_requestId)) { return; } try { - var response = await _authService.GetPasswordlessLoginResponseAsync(_requestId, _requestAccessCode); + PasswordlessLoginResponse response = null; + if (await _stateService.IsAuthenticatedAsync()) + { + response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId); + } + else + { + response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode); + } if (response.RequestApproved == null || !response.RequestApproved.Value) { @@ -221,6 +234,12 @@ namespace Bit.App.Pages var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash); await AppHelpers.ResetInvalidUnlockAttemptsAsync(); + if (authResult == null && await _stateService.IsAuthenticatedAsync()) + { + await HandleLoginCompleteAsync(); + return; + } + if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus)) { return; @@ -236,9 +255,7 @@ namespace Bit.App.Pages } else { - _syncService.FullSyncAsync(true).FireAndForget(); - await _deviceTrustCryptoService.TrustDeviceIfNeededAsync(); - LogInSuccessAction?.Invoke(); + await HandleLoginCompleteAsync(); } } catch (Exception ex) @@ -248,22 +265,66 @@ namespace Bit.App.Pages } } + private async Task HandleLoginCompleteAsync() + { + await _stateService.SetPendingAdminAuthRequestAsync(null); + _syncService.FullSyncAsync(true).FireAndForget(); + LogInSuccessAction?.Invoke(); + } + private async Task CreatePasswordlessLoginAsync() { await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading)); - var response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType); - if (response != null) + PasswordlessLoginResponse response = null; + var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync(); + if (pendingRequest != null && _authRequestType == AuthRequestType.AdminApproval) { - FingerprintPhrase = response.FingerprintPhrase; - _requestId = response.Id; - _requestAccessCode = response.RequestAccessCode; - _requestKeyPair = response.RequestKeyPair; + response = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id); + if (response == null || (response.IsAnswered && !response.RequestApproved.Value)) + { + // handle pending auth request not valid remove it from state + await _stateService.SetPendingAdminAuthRequestAsync(null); + pendingRequest = null; + } + else + { + // Derive pubKey from privKey in state to avoid MITM attacks + // Also generate FingerprintPhrase locally for the same reason + var derivedPublicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(pendingRequest.PrivateKey); + response.FingerprintPhrase = string.Join("-", await _cryptoService.GetFingerprintAsync(Email, derivedPublicKey)); + response.RequestKeyPair = new Tuple(derivedPublicKey, pendingRequest.PrivateKey); + } } + if (response == null) + { + response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType); + } + + await HandlePasswordlessLoginAsync(response, pendingRequest == null && _authRequestType == AuthRequestType.AdminApproval); await _deviceActionService.HideLoadingAsync(); } + private async Task HandlePasswordlessLoginAsync(PasswordlessLoginResponse response, bool createPendingAdminRequest) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (createPendingAdminRequest) + { + var pendingAuthRequest = new PendingAdminAuthRequest { Id = response.Id, PrivateKey = response.RequestKeyPair.Item2 }; + await _stateService.SetPendingAdminAuthRequestAsync(pendingAuthRequest); + } + + FingerprintPhrase = response.FingerprintPhrase; + _requestId = response.Id; + _requestAccessCode = response.RequestAccessCode; + _requestKeyPair = response.RequestKeyPair; + } + private void HandleException(Exception ex) { Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () => diff --git a/src/App/Pages/Accounts/TwoFactorPage.xaml.cs b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs index 29f4b7c14..e3c1ec755 100644 --- a/src/App/Pages/Accounts/TwoFactorPage.xaml.cs +++ b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs @@ -37,6 +37,8 @@ namespace Bit.App.Pages Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync()); _vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); + _vm.StartDeviceApprovalOptionsAction = + () => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync()); _vm.CloseAction = async () => await Navigation.PopModalAsync(); DuoWebView = _duoWebView; if (Device.RuntimePlatform == Device.Android) @@ -180,6 +182,12 @@ 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 TwoFactorAuthSuccessAsync() { if (_authingWithSso) diff --git a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs index 486b54f80..54bd90983 100644 --- a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs +++ b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs @@ -11,6 +11,7 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Request; +using Bit.Core.Services; using Bit.Core.Utilities; using Newtonsoft.Json; using Xamarin.CommunityToolkit.ObjectModel; @@ -33,7 +34,7 @@ namespace Bit.App.Pages private readonly II18nService _i18nService; private readonly IAppIdService _appIdService; private readonly ILogger _logger; - + private readonly IDeviceTrustCryptoService _deviceTrustCryptoService; private TwoFactorProviderType? _selectedProviderType; private string _totpInstruction; private string _webVaultUrl = "https://vault.bitwarden.com"; @@ -55,6 +56,7 @@ namespace Bit.App.Pages _i18nService = ServiceContainer.Resolve("i18nService"); _appIdService = ServiceContainer.Resolve("appIdService"); _logger = ServiceContainer.Resolve(); + _deviceTrustCryptoService = ServiceContainer.Resolve(); PageTitle = AppResources.TwoStepLogin; SubmitCommand = new Command(async () => await SubmitAsync()); @@ -118,6 +120,7 @@ namespace Bit.App.Pages public Command SubmitCommand { get; } public ICommand MoreCommand { get; } public Action TwoFactorAuthSuccessAction { get; set; } + public Action StartDeviceApprovalOptionsAction { get; set; } public Action StartSetPasswordAction { get; set; } public Action CloseAction { get; set; } public Action UpdateTempPasswordAction { get; set; } @@ -315,6 +318,7 @@ namespace Bit.App.Pages var task = Task.Run(() => _syncService.FullSyncAsync(true)); await _deviceActionService.HideLoadingAsync(); + var decryptOptions = await _stateService.GetAccountDecryptionOptions(); _messagingService.Send("listenYubiKeyOTP", false); _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); @@ -326,6 +330,27 @@ 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 (result.ForcePasswordReset) + { + UpdateTempPasswordAction?.Invoke(); + } + else if (await _deviceTrustCryptoService.IsDeviceTrustedAsync()) + { + TwoFactorAuthSuccessAction?.Invoke(); + } + else + { + StartDeviceApprovalOptionsAction?.Invoke(); + } + } else { TwoFactorAuthSuccessAction?.Invoke(); diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 269cb04ec..68d0340be 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -91,7 +91,7 @@ namespace Bit.Core.Abstractions Task GetAuthRequestAsync(string id); Task GetAuthResponseAsync(string id, string accessCode); Task PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved); - Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest); + Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest, AuthRequestType authRequestType); Task GetKnownDeviceAsync(string email, string deviceIdentifier); Task GetDeviceByIdentifierAsync(string deviceIdentifier); Task UpdateTrustedDeviceKeysAsync(string deviceIdentifier, TrustedDeviceKeysRequest deviceRequest); diff --git a/src/Core/Abstractions/IAuthService.cs b/src/Core/Abstractions/IAuthService.cs index d463b83a9..aa5ec47f0 100644 --- a/src/Core/Abstractions/IAuthService.cs +++ b/src/Core/Abstractions/IAuthService.cs @@ -32,7 +32,10 @@ namespace Bit.Core.Abstractions Task> GetPasswordlessLoginRequestsAsync(); Task> GetActivePasswordlessLoginRequestsAsync(); Task GetPasswordlessLoginRequestByIdAsync(string id); - Task GetPasswordlessLoginResponseAsync(string id, string accessCode); + /// + /// Gets a passwordless login request by and . No authentication required. + /// + Task GetPasswordlessLoginResquestAsync(string id, string accessCode); Task PasswordlessLoginAsync(string id, string pubKey, bool requestApproved); Task PasswordlessCreateLoginRequestAsync(string email, AuthRequestType authRequestType); diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs index 7a7d5a8e6..12a2f80d3 100644 --- a/src/Core/Abstractions/IStateService.cs +++ b/src/Core/Abstractions/IStateService.cs @@ -183,6 +183,8 @@ namespace Bit.Core.Abstractions Task GetPreLoginEmailAsync(); Task SetPreLoginEmailAsync(string value); Task GetAccountDecryptionOptions(string userId = null); + Task GetPendingAdminAuthRequestAsync(string userId = null); + Task SetPendingAdminAuthRequestAsync(PendingAdminAuthRequest value, string userId = null); string GetLocale(); void SetLocale(string locale); ConfigResponse GetConfigs(); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7bfd1eb30..38fad57d6 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -124,6 +124,7 @@ namespace Bit.Core public static string PushCurrentTokenKey(string userId) => $"pushCurrentToken_{userId}"; public static string ShouldConnectToWatchKey(string userId) => $"shouldConnectToWatch_{userId}"; public static string ScreenCaptureAllowedKey(string userId) => $"screenCaptureAllowed_{userId}"; + public static string PendingAdminAuthRequest(string userId) => $"pendingAdminAuthRequest_{userId}"; [Obsolete] public static string KeyKey(string userId) => $"key_{userId}"; [Obsolete] diff --git a/src/Core/Models/Domain/PendingAdminAuthRequest.cs b/src/Core/Models/Domain/PendingAdminAuthRequest.cs new file mode 100644 index 000000000..15727e368 --- /dev/null +++ b/src/Core/Models/Domain/PendingAdminAuthRequest.cs @@ -0,0 +1,10 @@ +using System; +namespace Bit.Core.Models.Domain +{ + public class PendingAdminAuthRequest + { + public string Id { get; set; } + public byte[] PrivateKey { get; set; } + } +} + diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 95af20a55..d8c0ed7d4 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -591,9 +591,9 @@ namespace Bit.Core.Services return SendAsync(HttpMethod.Get, $"/auth-requests/{id}/response?code={accessCode}", null, false, true); } - public Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest) + public Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest, AuthRequestType authRequestType) { - return SendAsync(HttpMethod.Post, $"/auth-requests", passwordlessCreateLoginRequest, false, true); + return SendAsync(HttpMethod.Post, authRequestType == AuthRequestType.AdminApproval ? "/auth-requests/admin-request" : "/auth-requests", passwordlessCreateLoginRequest, authRequestType == AuthRequestType.AdminApproval, true); } public Task PutAuthRequestAsync(string id, string encKey, string encMasterPasswordHash, string deviceIdentifier, bool requestApproved) diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index 9fda8f9f2..d8d506913 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -198,11 +198,34 @@ namespace Bit.Core.Services return !await _policyService.EvaluateMasterPassword(strength.Value, masterPassword, _masterPasswordPolicy); } - public async Task LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered) + public async Task LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string encryptedAuthRequestKey, string masterKeyHash) { - var decKey = await _cryptoService.RsaDecryptAsync(userKeyCiphered, decryptionKey); - var decPasswordHash = await _cryptoService.RsaDecryptAsync(localHashedPasswordCiphered, decryptionKey); - return await LogInHelperAsync(email, accessCode, Encoding.UTF8.GetString(decPasswordHash), null, null, null, new MasterKey(decKey), null, null, + var decryptedKey = await _cryptoService.RsaDecryptAsync(encryptedAuthRequestKey, decryptionKey); + + // On SSO flow user is already AuthN + if (await _stateService.IsAuthenticatedAsync()) + { + if (string.IsNullOrEmpty(masterKeyHash)) + { + await _cryptoService.SetUserKeyAsync(new UserKey(decryptedKey)); + } + else + { + var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(new MasterKey(decryptedKey)); + await _cryptoService.SetUserKeyAsync(userKey); + } + await _deviceTrustCryptoService.TrustDeviceIfNeededAsync(); + return null; + } + + if (string.IsNullOrEmpty(masterKeyHash) && decryptionKey != null) + { + await _cryptoService.SetUserKeyAsync(new UserKey(decryptedKey)); + return null; + } + + var decKeyHash = await _cryptoService.RsaDecryptAsync(masterKeyHash, decryptionKey); + return await LogInHelperAsync(email, accessCode, Encoding.UTF8.GetString(decKeyHash), null, null, null, new MasterKey(decryptedKey), null, null, null, null, authRequestId: authRequestId); } @@ -474,10 +497,12 @@ namespace Bit.Core.Services _messagingService.Send("accountAdded"); if (_setCryptoKeys) { - if (localHashedPassword != null) { await _cryptoService.SetPasswordHashAsync(localHashedPassword); + await _cryptoService.SetMasterKeyAsync(masterKey); + var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey); + await _cryptoService.SetUserKeyAsync(userKey); } if (code == null || tokenResponse.Key != null) @@ -505,6 +530,14 @@ namespace Bit.Core.Services } } + // Login with Device + if (masterKey != null && !string.IsNullOrEmpty(authRequestId)) + { + 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. if (tokenResponse.PrivateKey == null) { @@ -557,7 +590,6 @@ namespace Bit.Core.Services ); await _apiService.PostSetKeyConnectorKey(setPasswordRequest); } - } _authedUserId = _tokenService.GetUserId(); @@ -594,14 +626,14 @@ namespace Bit.Core.Services var activeRequests = requests.Where(r => !r.IsAnswered && !r.IsExpired).OrderByDescending(r => r.CreationDate).ToList(); return await PopulateFingerprintPhrasesAsync(activeRequests); } - public async Task GetPasswordlessLoginRequestByIdAsync(string id) { var response = await _apiService.GetAuthRequestAsync(id); return await PopulateFingerprintPhraseAsync(response, await _stateService.GetEmailAsync()); } - public async Task GetPasswordlessLoginResponseAsync(string id, string accessCode) + /// + public async Task GetPasswordlessLoginResquestAsync(string id, string accessCode) { return await _apiService.GetAuthResponseAsync(id, accessCode); } @@ -631,7 +663,7 @@ namespace Bit.Core.Services var publicB64 = Convert.ToBase64String(keyPair.Item1); var accessCode = await _passwordGenerationService.GeneratePasswordAsync(PasswordGenerationOptions.CreateDefault.WithLength(25)); var passwordlessCreateLoginRequest = new PasswordlessCreateLoginRequest(email, publicB64, deviceId, accessCode, authRequestType, fingerprintPhrase); - var response = await _apiService.PostCreateRequestAsync(passwordlessCreateLoginRequest); + var response = await _apiService.PostCreateRequestAsync(passwordlessCreateLoginRequest, authRequestType); if (response != null) { diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index 480bacaaa..738b5346c 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -336,12 +336,16 @@ namespace Bit.Core.Services public async Task GetUserKeyMasterKeyAsync(string userId = null) { - return await _storageMediatorService.GetAsync(Constants.UserKeyKey(userId), false); + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + return await _storageMediatorService.GetAsync(Constants.UserKeyKey(reconciledOptions.UserId), false); } public async Task SetUserKeyMasterKeyAsync(string value, string userId = null) { - await _storageMediatorService.SaveAsync(Constants.UserKeyKey(userId), value, false); + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultInMemoryOptionsAsync()); + await _storageMediatorService.SaveAsync(Constants.UserKeyKey(reconciledOptions.UserId), value, false); } public async Task CanAccessPremiumAsync(string userId = null) @@ -1347,6 +1351,20 @@ namespace Bit.Core.Services await _storageMediatorService.SaveAsync(Constants.ShouldTrustDevice, value); } + public async Task GetPendingAdminAuthRequestAsync(string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + return await _storageMediatorService.GetAsync(Constants.PendingAdminAuthRequest(reconciledOptions.UserId), true); + } + + public async Task SetPendingAdminAuthRequestAsync(PendingAdminAuthRequest value, string userId = null) + { + var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, + await GetDefaultStorageOptionsAsync()); + await _storageMediatorService.SaveAsync(Constants.PendingAdminAuthRequest(reconciledOptions.UserId), value, true); + } + public ConfigResponse GetConfigs() { return _storageMediatorService.Get(Constants.ConfigsKey); diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index ed0ae3ec4..0ba9ba458 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -568,6 +568,7 @@ namespace Bit.iOS.Autofill { vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue(); vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); + vm.StartDeviceApprovalOptionsAction = () => DismissViewController(false, () => LaunchDeviceApprovalOptionsFlow()); if (authingWithSso) { vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 597ee90ff..7f9d451c2 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -590,6 +590,7 @@ namespace Bit.iOS.Extension { vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue(); vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); + vm.StartDeviceApprovalOptionsAction = () => DismissViewController(false, () => LaunchDeviceApprovalOptionsFlow()); if (authingWithSso) { vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs index 95b753b08..200946cfa 100644 --- a/src/iOS.ShareExtension/LoadingViewController.cs +++ b/src/iOS.ShareExtension/LoadingViewController.cs @@ -390,6 +390,7 @@ namespace Bit.iOS.ShareExtension { vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue(); vm.StartSetPasswordAction = () => DismissAndLaunch(() => LaunchSetPasswordFlow()); + vm.StartDeviceApprovalOptionsAction = () => DismissViewController(false, () => LaunchDeviceApprovalOptionsFlow()); if (authingWithSso) { vm.CloseAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());