diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 33dc82ed1..4d44dc01c 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -147,7 +147,6 @@ namespace Bit.App } else if (message.Command == Constants.PasswordlessLoginRequestKey || message.Command == "unlocked" - || message.Command == "syncCompleted" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED) { lock (_processingLoginRequestLock) @@ -209,7 +208,7 @@ namespace Bit.App }); await _stateService.SetPasswordlessLoginNotificationAsync(null); _pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId); - if (loginRequestData.CreationDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) > DateTime.UtcNow) + if (!loginRequestData.IsExpired) { await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page))); } diff --git a/src/App/Pages/Accounts/LoginPasswordlessViewModel.cs b/src/App/Pages/Accounts/LoginPasswordlessViewModel.cs index 11c3400b5..ced68d7b7 100644 --- a/src/App/Pages/Accounts/LoginPasswordlessViewModel.cs +++ b/src/App/Pages/Accounts/LoginPasswordlessViewModel.cs @@ -100,7 +100,7 @@ namespace Bit.App.Pages private async Task UpdateRequestTime() { TriggerPropertyChanged(nameof(TimeOfRequestText)); - if (DateTime.UtcNow > LoginRequest?.RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes)) + if (LoginRequest?.IsExpired ?? false) { StopRequestTimeUpdater(); await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired); @@ -110,7 +110,7 @@ namespace Bit.App.Pages private async Task PasswordlessLoginAsync(bool approveRequest) { - if (LoginRequest.RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) <= DateTime.UtcNow) + if (LoginRequest.IsExpired) { await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired); await Page.Navigation.PopModalAsync(); @@ -179,5 +179,7 @@ namespace Bit.App.Pages public string DeviceType { get; set; } public string IpAddress { get; set; } + + public bool IsExpired => RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) < DateTime.UtcNow; } } diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 5c85e41ea..adbbef1a1 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -83,6 +83,7 @@ namespace Bit.Core.Abstractions Task PutSendAsync(string id, SendRequest request); Task PutSendRemovePasswordAsync(string id); Task DeleteSendAsync(string id); + Task> GetAuthRequestAsync(); Task GetAuthRequestAsync(string id); Task GetAuthResponseAsync(string id, string accessCode); Task PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved); diff --git a/src/Core/Abstractions/IAuthService.cs b/src/Core/Abstractions/IAuthService.cs index bebd290d5..e1c947a9d 100644 --- a/src/Core/Abstractions/IAuthService.cs +++ b/src/Core/Abstractions/IAuthService.cs @@ -29,6 +29,7 @@ namespace Bit.Core.Abstractions Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null); Task LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered); + Task> GetPasswordlessLoginRequestsAsync(); Task GetPasswordlessLoginRequestByIdAsync(string id); Task GetPasswordlessLoginResponseAsync(string id, string accessCode); Task PasswordlessLoginAsync(string id, string pubKey, bool requestApproved); diff --git a/src/Core/Models/Response/PasswordlessLoginResponse.cs b/src/Core/Models/Response/PasswordlessLoginResponse.cs index 815036768..4aaedc2a7 100644 --- a/src/Core/Models/Response/PasswordlessLoginResponse.cs +++ b/src/Core/Models/Response/PasswordlessLoginResponse.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using Bit.Core.Enums; +using Bit.Core.Models.Data; namespace Bit.Core.Models.Response { @@ -18,5 +20,14 @@ namespace Bit.Core.Models.Response public string Origin { get; set; } public string RequestAccessCode { get; set; } public Tuple RequestKeyPair { get; set; } + + public bool IsAnswered => RequestApproved != null && ResponseDate != null; + + public bool IsExpired => CreationDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) < DateTime.UtcNow; + } + + public class PasswordlessLoginsResponse + { + public List Data { get; set; } } } diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 938cbecfa..e4c0db826 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -536,6 +536,12 @@ namespace Bit.Core.Services #region PasswordlessLogin + public async Task> GetAuthRequestAsync() + { + var response = await SendAsync(HttpMethod.Get, $"/auth-requests/", null, true, true); + return response.Data; + } + public Task GetAuthRequestAsync(string id) { return SendAsync(HttpMethod.Get, $"/auth-requests/{id}", null, true, true); diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index db09798db..4e95cf847 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -485,6 +485,11 @@ namespace Bit.Core.Services SelectedTwoFactorProviderType = null; } + public async Task> GetPasswordlessLoginRequestsAsync() + { + return await _apiService.GetAuthRequestAsync(); + } + public async Task GetPasswordlessLoginRequestByIdAsync(string id) { return await _apiService.GetAuthRequestAsync(id); diff --git a/src/Core/Services/SyncService.cs b/src/Core/Services/SyncService.cs index ba90d1da9..97f9392c1 100644 --- a/src/Core/Services/SyncService.cs +++ b/src/Core/Services/SyncService.cs @@ -24,6 +24,7 @@ namespace Bit.Core.Services private readonly IPolicyService _policyService; private readonly ISendService _sendService; private readonly IKeyConnectorService _keyConnectorService; + private readonly ILogger _logger; private readonly Func, Task> _logoutCallbackAsync; public SyncService( @@ -39,6 +40,7 @@ namespace Bit.Core.Services IPolicyService policyService, ISendService sendService, IKeyConnectorService keyConnectorService, + ILogger logger, Func, Task> logoutCallbackAsync) { _stateService = stateService; @@ -53,6 +55,7 @@ namespace Bit.Core.Services _policyService = policyService; _sendService = sendService; _keyConnectorService = keyConnectorService; + _logger = logger; _logoutCallbackAsync = logoutCallbackAsync; } @@ -108,6 +111,7 @@ namespace Bit.Core.Services await SyncSettingsAsync(userId, response.Domains); await SyncPoliciesAsync(response.Policies); await SyncSendsAsync(userId, response.Sends); + await SyncPasswordlessLoginRequestsAsync(userId); await SetLastSyncAsync(now); return SyncCompleted(true); } @@ -382,5 +386,44 @@ namespace Bit.Core.Services new Dictionary(); await _sendService.ReplaceAsync(sends); } + + private async Task SyncPasswordlessLoginRequestsAsync(string userId) + { + try + { + // if the user has not enabled passwordless logins ignore requests + if (!await _stateService.GetApprovePasswordlessLoginsAsync(userId)) + { + return; + } + + var loginRequests = await _apiService.GetAuthRequestAsync(); + if (loginRequests == null || !loginRequests.Any()) + { + return; + } + + var validLoginRequest = loginRequests.Where(l => !l.IsAnswered && !l.IsExpired) + .OrderByDescending(x => x.CreationDate) + .FirstOrDefault(); + + if (validLoginRequest is null) + { + return; + } + + await _stateService.SetPasswordlessLoginNotificationAsync(new PasswordlessRequestNotification() + { + Id = validLoginRequest.Id, + UserId = userId + }); + + _messagingService.Send(Constants.PasswordlessLoginRequestKey); + } + catch (Exception ex) + { + _logger.Exception(ex); + } + } } } diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index 8386facc2..003432f30 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -29,6 +29,8 @@ namespace Bit.Core.Utilities var messagingService = Resolve("messagingService"); var cryptoFunctionService = Resolve("cryptoFunctionService"); var cryptoService = Resolve("cryptoService"); + var logger = Resolve(); + SearchService searchService = null; var tokenService = new TokenService(stateService); @@ -67,7 +69,7 @@ namespace Bit.Core.Utilities }); var syncService = new SyncService(stateService, apiService, settingsService, folderService, cipherService, cryptoService, collectionService, organizationService, messagingService, policyService, sendService, - keyConnectorService, (extras) => + keyConnectorService, logger, (extras) => { messagingService.Send("logout", extras); return Task.CompletedTask;