mirror of
https://github.com/bitwarden/android.git
synced 2024-12-26 10:58:29 +03:00
Passwordless feature branch PR (#2100)
* [SG-471] Passwordless device login screen (#2017) * [SSG-471] Added UI for the device login request response. * [SG-471] Added text resources and arguments to Page. * [SG-471] Added properties to speed up page bindings * [SG-471] Added mock services. Added Accept/reject command binding, navigation and toast messages. * [SG-471] fixed code styling with dotnet-format * [SG-471] Fixed back button placement. PR fixes. * [SG-471] Added new Origin parameter to the page. * [SG-471] PR Fixes * [SG-471] PR fixes * [SG-471] PR Fix: added FireAndForget. * [SG-471] Moved fire and forget to run on ui thread task. * [SG-381] Passwordless - Add setting to Mobile (#2037) * [SG-381] Added settings option to approve passwordless login request. If user has notifications disabled, prompt to go to settings and enable them. * [SG-381] Update settings pop up texts. * [SG-381] Added new method to get notifications state on device settings. Added userId to property saved on device to differentiate value between users. * [SG-381] Added text for the popup on selection. * [SG-381] PR Fixes * [SG-408] Implement passwordless api methods (#2055) * [SG-408] Update notification model. * [SG-408] removed duplicated resource * [SG-408] Added implementation to Api Service of new passwordless methods. * removed qa endpoints * [SG-408] Changed auth methods implementation, added method call to viewmodel. * [SG-408] ran code format * [SG-408] PR fixes * [SG-472] Add configuration for new notification type (#2056) * [SG-472] Added methods to present local notification to the user. Configured new notification type for passwordless logins * [SG-472] Updated code to new api service changes. * [SG-472] ran dotnet format * [SG-472] PR Fixes. * [SG-472] PR Fixes * [SG-169] End-to-end testing refactor. (#2073) * [SG-169] Passwordless demo change requests (#2079) * [SG-169] End-to-end testing refactor. * [SG-169] Fixed labels. Changed color of Fingerprint phrase. Waited for app to be in foreground to launch passwordless modal to fix Android issues. * [SG-169] Anchored buttons to the bottom of the screen. * [SG-169] Changed device type from enum to string. * [SG-169] PR fixes * [SG-169] PR fixes * [SG-169] Added comment on static variable
This commit is contained in:
parent
2f4cd36595
commit
f9a32e4abc
35 changed files with 852 additions and 30 deletions
|
@ -12,6 +12,7 @@ using Android.Runtime;
|
|||
using Android.Views;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
|
@ -86,6 +87,7 @@ namespace Bit.Droid
|
|||
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
||||
Xamarin.Forms.Forms.Init(this, savedInstanceState);
|
||||
_appOptions = GetOptions();
|
||||
CreateNotificationChannel();
|
||||
LoadApplication(new App.App(_appOptions));
|
||||
DisableAndroidFontScale();
|
||||
|
||||
|
@ -407,6 +409,25 @@ namespace Bit.Droid
|
|||
await _eventService.UploadEventsAsync();
|
||||
}
|
||||
|
||||
private void CreateNotificationChannel()
|
||||
{
|
||||
#if !FDROID
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||
{
|
||||
// Notification channels are new in API 26 (and not a part of the
|
||||
// support library). There is no need to create a notification
|
||||
// channel on older versions of Android.
|
||||
return;
|
||||
}
|
||||
|
||||
var channel = new NotificationChannel(Constants.AndroidNotificationChannelId, AppResources.AllNotifications, NotificationImportance.Default);
|
||||
if(GetSystemService(NotificationService) is NotificationManager notificationManager)
|
||||
{
|
||||
notificationManager.CreateNotificationChannel(channel);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void DisableAndroidFontScale()
|
||||
{
|
||||
try
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
#if !FDROID
|
||||
using System;
|
||||
using Android.App;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Firebase.Messaging;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -16,34 +18,41 @@ namespace Bit.Droid.Push
|
|||
{
|
||||
public async override void OnNewToken(string token)
|
||||
{
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||
try {
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||
|
||||
await stateService.SetPushRegisteredTokenAsync(token);
|
||||
await pushNotificationService.RegisterAsync();
|
||||
await stateService.SetPushRegisteredTokenAsync(token);
|
||||
await pushNotificationService.RegisterAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async override void OnMessageReceived(RemoteMessage message)
|
||||
{
|
||||
if (message?.Data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
|
||||
if (data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (message?.Data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
|
||||
if (data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var obj = JObject.Parse(data);
|
||||
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
|
||||
"pushNotificationListenerService");
|
||||
await listener.OnMessageAsync(obj, Device.Android);
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ex.ToString());
|
||||
Logger.Instance.Exception(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
#if !FDROID
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using AndroidX.Core.App;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Xamarin.Forms;
|
||||
|
||||
|
@ -23,6 +26,11 @@ namespace Bit.Droid.Services
|
|||
|
||||
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
|
||||
|
||||
public Task<bool> AreNotificationsSettingsEnabledAsync()
|
||||
{
|
||||
return Task.FromResult(IsRegisteredForPush);
|
||||
}
|
||||
|
||||
public async Task<string> GetTokenAsync()
|
||||
{
|
||||
return await _stateService.GetPushCurrentTokenAsync();
|
||||
|
@ -47,6 +55,36 @@ namespace Bit.Droid.Services
|
|||
// Do we ever need to unregister?
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public void DismissLocalNotification(string notificationId)
|
||||
{
|
||||
if (int.TryParse(notificationId, out int intNotificationId))
|
||||
{
|
||||
var notificationManager = NotificationManagerCompat.From(Android.App.Application.Context);
|
||||
notificationManager.Cancel(intNotificationId);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendLocalNotification(string title, string message, string notificationId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(notificationId))
|
||||
{
|
||||
throw new ArgumentNullException("notificationId cannot be null or empty.");
|
||||
}
|
||||
|
||||
var context = Android.App.Application.Context;
|
||||
var intent = new Intent(context, typeof(MainActivity));
|
||||
var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, PendingIntentFlags.UpdateCurrent);
|
||||
var builder = new NotificationCompat.Builder(context, Constants.AndroidNotificationChannelId)
|
||||
.SetContentIntent(pendingIntent)
|
||||
.SetContentTitle(title)
|
||||
.SetContentText(message)
|
||||
.SetSmallIcon(Resource.Mipmap.ic_launcher)
|
||||
.SetAutoCancel(true);
|
||||
|
||||
var notificationManager = NotificationManagerCompat.From(context);
|
||||
notificationManager.Notify(int.Parse(notificationId), builder.Build());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -964,5 +964,14 @@ namespace Bit.Droid.Services
|
|||
}
|
||||
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
|
||||
}
|
||||
|
||||
public void OpenAppSettings()
|
||||
{
|
||||
var intent = new Intent(Android.Provider.Settings.ActionApplicationDetailsSettings);
|
||||
intent.AddFlags(ActivityFlags.NewTask);
|
||||
var uri = Android.Net.Uri.FromParts("package", Application.Context.PackageName, null);
|
||||
intent.SetData(uri);
|
||||
Application.Context.StartActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,5 +49,6 @@ namespace Bit.App.Abstractions
|
|||
float GetSystemFontSizeScale();
|
||||
Task OnAccountSwitchCompleteAsync();
|
||||
Task SetScreenCaptureAllowedAsync();
|
||||
void OpenAppSettings();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IPushNotificationService
|
||||
{
|
||||
bool IsRegisteredForPush { get; }
|
||||
Task<bool> AreNotificationsSettingsEnabledAsync();
|
||||
Task<string> GetTokenAsync();
|
||||
Task RegisterAsync();
|
||||
Task UnregisterAsync();
|
||||
void SendLocalNotification(string title, string message, string notificationId);
|
||||
void DismissLocalNotification(string notificationId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,6 +123,9 @@
|
|||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
|
||||
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
|
||||
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -7,6 +7,7 @@ using Bit.App.Resources;
|
|||
using Bit.App.Services;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
|
@ -25,13 +26,13 @@ namespace Bit.App
|
|||
private readonly IStateService _stateService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IStorageService _secureStorageService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAccountsManager _accountsManager;
|
||||
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private static bool _isResumed;
|
||||
// this variable is static because the app is launching new activities on notification click, creating new instances of App.
|
||||
private static bool _pendingCheckPasswordlessLoginRequests;
|
||||
|
||||
public App(AppOptions appOptions)
|
||||
{
|
||||
|
@ -47,10 +48,9 @@ namespace Bit.App
|
|||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||
|
||||
_accountsManager.Init(() => Options, this);
|
||||
|
||||
|
@ -140,6 +140,10 @@ namespace Bit.App
|
|||
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||
});
|
||||
}
|
||||
else if (message.Command == "passwordlessLoginRequest" || message.Command == "unlocked")
|
||||
{
|
||||
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -148,11 +152,52 @@ namespace Bit.App
|
|||
});
|
||||
}
|
||||
|
||||
private async Task CheckPasswordlessLoginRequestsAsync()
|
||||
{
|
||||
if (!_isResumed)
|
||||
{
|
||||
_pendingCheckPasswordlessLoginRequests = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingCheckPasswordlessLoginRequests = false;
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
|
||||
if (notification == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay to wait for the vault page to appear
|
||||
await Task.Delay(2000);
|
||||
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(notification.Id);
|
||||
var page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
|
||||
{
|
||||
PubKey = loginRequestData.PublicKey,
|
||||
Id = loginRequestData.Id,
|
||||
IpAddress = loginRequestData.RequestIpAddress,
|
||||
Email = await _stateService.GetEmailAsync(),
|
||||
FingerprintPhrase = loginRequestData.RequestFingerprint,
|
||||
RequestDate = loginRequestData.CreationDate,
|
||||
DeviceType = loginRequestData.RequestDeviceType,
|
||||
Origin = loginRequestData.Origin,
|
||||
});
|
||||
await _stateService.SetPasswordlessLoginNotificationAsync(null);
|
||||
_pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId);
|
||||
await Device.InvokeOnMainThreadAsync(async () => await Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
||||
}
|
||||
|
||||
public AppOptions Options { get; private set; }
|
||||
|
||||
protected async override void OnStart()
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
|
||||
_isResumed = true;
|
||||
await ClearCacheIfNeededAsync();
|
||||
Prime();
|
||||
if (string.IsNullOrWhiteSpace(Options.Uri))
|
||||
|
@ -164,6 +209,10 @@ namespace Bit.App
|
|||
SyncIfNeeded();
|
||||
}
|
||||
}
|
||||
if (_pendingCheckPasswordlessLoginRequests)
|
||||
{
|
||||
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||
}
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
|
@ -196,6 +245,10 @@ namespace Bit.App
|
|||
{
|
||||
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
||||
_isResumed = true;
|
||||
if (_pendingCheckPasswordlessLoginRequests)
|
||||
{
|
||||
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||
}
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ResumedAsync().FireAndForget();
|
||||
|
|
85
src/App/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
85
src/App/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?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.LoginPasswordlessPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginPasswordlessViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginPasswordlessViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
<StackLayout
|
||||
Padding="7, 0, 7, 20">
|
||||
<ScrollView
|
||||
VerticalOptions="FillAndExpand">
|
||||
<StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n AreYouTryingToLogIn}"
|
||||
FontSize="Title"
|
||||
FontAttributes="Bold"
|
||||
Margin="0,14,0,21"/>
|
||||
<Label
|
||||
Text="{Binding LogInAttemptByLabel}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,24"/>
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
||||
FontSize="Medium"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
Margin="0,0,0,27"/>
|
||||
<Label
|
||||
Text="{u:I18n DeviceType}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<Label
|
||||
Text="{Binding LoginRequest.DeviceType}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,21"/>
|
||||
<Label
|
||||
Text="{u:I18n IpAddress}"
|
||||
IsVisible="{Binding ShowIpAddress}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<Label
|
||||
Text="{Binding LoginRequest.IpAddress}"
|
||||
IsVisible="{Binding ShowIpAddress}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,21"/>
|
||||
<Label
|
||||
Text="{u:I18n Time}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<Label
|
||||
Text="{Binding TimeOfRequestText}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,57"/>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
Text="{u:I18n ConfirmLogIn}"
|
||||
Command="{Binding AcceptRequestCommand}"
|
||||
Margin="0,0,0,17"
|
||||
StyleClass="btn-primary"/>
|
||||
<Button
|
||||
Text="{u:I18n DenyLogIn}"
|
||||
Command="{Binding RejectRequestCommand}"
|
||||
StyleClass="btn-secundary"/>
|
||||
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
31
src/App/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
31
src/App/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginPasswordlessPage : BaseContentPage
|
||||
{
|
||||
private LoginPasswordlessViewModel _vm;
|
||||
|
||||
public LoginPasswordlessPage(LoginPasswordlessDetails loginPasswordlessDetails)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginPasswordlessViewModel;
|
||||
_vm.Page = this;
|
||||
|
||||
_vm.LoginRequest = loginPasswordlessDetails;
|
||||
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(_closeItem);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
126
src/App/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
126
src/App/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
|
@ -0,0 +1,126 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginPasswordlessViewModel : BaseViewModel
|
||||
{
|
||||
private IDeviceActionService _deviceActionService;
|
||||
private IAuthService _authService;
|
||||
private IPlatformUtilsService _platformUtilsService;
|
||||
private ILogger _logger;
|
||||
private LoginPasswordlessDetails _resquest;
|
||||
|
||||
public LoginPasswordlessViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
PageTitle = AppResources.LogInRequested;
|
||||
|
||||
AcceptRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(true),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
RejectRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(false),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ICommand AcceptRequestCommand { get; }
|
||||
|
||||
public ICommand RejectRequestCommand { get; }
|
||||
|
||||
public string LogInAttemptByLabel => LoginRequest != null ? string.Format(AppResources.LogInAttemptByXOnY, LoginRequest.Email, LoginRequest.Origin) : string.Empty;
|
||||
|
||||
public string TimeOfRequestText => CreateRequestDate(LoginRequest?.RequestDate);
|
||||
|
||||
public bool ShowIpAddress => !string.IsNullOrEmpty(LoginRequest?.IpAddress);
|
||||
|
||||
public LoginPasswordlessDetails LoginRequest
|
||||
{
|
||||
get => _resquest;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _resquest, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(LogInAttemptByLabel),
|
||||
nameof(TimeOfRequestText),
|
||||
nameof(ShowIpAddress),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PasswordlessLoginAsync(bool approveRequest)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
await _authService.PasswordlessLoginAsync(LoginRequest.Id, LoginRequest.PubKey, approveRequest);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.Navigation.PopModalAsync();
|
||||
_platformUtilsService.ShowToast("info", null, approveRequest ? AppResources.LogInAccepted : AppResources.LogInDenied);
|
||||
}
|
||||
|
||||
private string CreateRequestDate(DateTime? requestDate)
|
||||
{
|
||||
if (!requestDate.HasValue)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var minutesSinceRequest = requestDate.Value.ToUniversalTime().Minute - DateTime.UtcNow.Minute;
|
||||
if (minutesSinceRequest < 5)
|
||||
{
|
||||
return AppResources.JustNow;
|
||||
}
|
||||
if (minutesSinceRequest < 59)
|
||||
{
|
||||
return string.Format(AppResources.XMinutesAgo, minutesSinceRequest);
|
||||
}
|
||||
|
||||
return requestDate.Value.ToShortTimeString();
|
||||
}
|
||||
|
||||
private void HandleException(Exception ex)
|
||||
{
|
||||
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
|
||||
}).FireAndForget();
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public class LoginPasswordlessDetails
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public string PubKey { get; set; }
|
||||
|
||||
public string Origin { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
|
||||
public string FingerprintPhrase { get; set; }
|
||||
|
||||
public DateTime RequestDate { get; set; }
|
||||
|
||||
public string DeviceType { get; set; }
|
||||
|
||||
public string IpAddress { get; set; }
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ namespace Bit.App.Pages
|
|||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger _loggerService;
|
||||
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private const int CustomVaultTimeoutValue = -100;
|
||||
|
||||
private bool _supportsBiometric;
|
||||
|
@ -42,6 +42,7 @@ namespace Bit.App.Pages
|
|||
private string _vaultTimeoutActionDisplayValue;
|
||||
private bool _showChangeMasterPassword;
|
||||
private bool _reportLoggingEnabled;
|
||||
private bool _approvePasswordlessLoginRequests;
|
||||
|
||||
private List<KeyValuePair<string, int?>> _vaultTimeouts =
|
||||
new List<KeyValuePair<string, int?>>
|
||||
|
@ -83,6 +84,7 @@ namespace Bit.App.Pages
|
|||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||
|
||||
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
|
||||
PageTitle = AppResources.Settings;
|
||||
|
@ -133,6 +135,7 @@ namespace Bit.App.Pages
|
|||
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
|
||||
!await _keyConnectorService.GetUsesKeyConnector();
|
||||
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
||||
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
|
||||
BuildList();
|
||||
}
|
||||
|
||||
|
@ -326,6 +329,38 @@ namespace Bit.App.Pages
|
|||
BuildList();
|
||||
}
|
||||
|
||||
public async Task ApproveLoginRequestsAsync()
|
||||
{
|
||||
var options = new[]
|
||||
{
|
||||
CreateSelectableOption(AppResources.Yes, _approvePasswordlessLoginRequests),
|
||||
CreateSelectableOption(AppResources.No, !_approvePasswordlessLoginRequests),
|
||||
};
|
||||
|
||||
var selection = await Page.DisplayActionSheet(AppResources.UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices, AppResources.Cancel, null, options);
|
||||
|
||||
if (selection == null || selection == AppResources.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_approvePasswordlessLoginRequests = CompareSelection(selection, AppResources.Yes);
|
||||
await _stateService.SetApprovePasswordlessLoginsAsync(_approvePasswordlessLoginRequests);
|
||||
|
||||
BuildList();
|
||||
|
||||
if (!_approvePasswordlessLoginRequests || await _pushNotificationService.AreNotificationsSettingsEnabledAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(AppResources.ReceivePushNotificationsForNewLoginRequests, title: string.Empty, confirmText: AppResources.Settings, cancelText: AppResources.NoThanks);
|
||||
if (openAppSettingsResult)
|
||||
{
|
||||
_deviceActionService.OpenAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task VaultTimeoutActionAsync()
|
||||
{
|
||||
var options = _vaultTimeoutActions.Select(o =>
|
||||
|
@ -504,6 +539,12 @@ namespace Bit.App.Pages
|
|||
ExecuteAsync = () => UpdatePinAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ApproveLoginRequests,
|
||||
SubLabel = _approvePasswordlessLoginRequests ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => ApproveLoginRequestsAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LockNow,
|
||||
ExecuteAsync = () => LockAsync()
|
||||
|
|
121
src/App/Resources/AppResources.Designer.cs
generated
121
src/App/Resources/AppResources.Designer.cs
generated
|
@ -4151,6 +4151,127 @@ namespace Bit.App.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
public static string LogInRequested {
|
||||
get {
|
||||
return ResourceManager.GetString("LogInRequested", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string AreYouTryingToLogIn {
|
||||
get {
|
||||
return ResourceManager.GetString("AreYouTryingToLogIn", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string LogInAttemptByXOnY {
|
||||
get {
|
||||
return ResourceManager.GetString("LogInAttemptByXOnY", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string DeviceType {
|
||||
get {
|
||||
return ResourceManager.GetString("DeviceType", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string IpAddress {
|
||||
get {
|
||||
return ResourceManager.GetString("IpAddress", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Time {
|
||||
get {
|
||||
return ResourceManager.GetString("Time", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Near {
|
||||
get {
|
||||
return ResourceManager.GetString("Near", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ConfirmLogIn {
|
||||
get {
|
||||
return ResourceManager.GetString("ConfirmLogIn", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string DenyLogIn {
|
||||
get {
|
||||
return ResourceManager.GetString("DenyLogIn", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string JustNow {
|
||||
get {
|
||||
return ResourceManager.GetString("JustNow", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string XMinutesAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("XMinutesAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string LogInAccepted {
|
||||
get {
|
||||
return ResourceManager.GetString("LogInAccepted", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string LogInDenied {
|
||||
get {
|
||||
return ResourceManager.GetString("LogInDenied", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ApproveLoginRequests {
|
||||
get {
|
||||
return ResourceManager.GetString("ApproveLoginRequests", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices {
|
||||
get {
|
||||
return ResourceManager.GetString("UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string AllowNotifications {
|
||||
get {
|
||||
return ResourceManager.GetString("AllowNotifications", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ReceivePushNotificationsForNewLoginRequests {
|
||||
get {
|
||||
return ResourceManager.GetString("ReceivePushNotificationsForNewLoginRequests", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string NoThanks {
|
||||
get {
|
||||
return ResourceManager.GetString("NoThanks", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ConfimLogInAttempForX {
|
||||
get {
|
||||
return ResourceManager.GetString("ConfimLogInAttempForX", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string AllNotifications
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResourceManager.GetString("AllNotifications", resourceCulture);
|
||||
}
|
||||
}
|
||||
public static string PasswordType {
|
||||
get {
|
||||
return ResourceManager.GetString("PasswordType", resourceCulture);
|
||||
|
|
|
@ -2314,6 +2314,66 @@ select Add TOTP to store the key safely</value>
|
|||
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||
<value>Are you sure you want to enable Screen Capture?</value>
|
||||
</data>
|
||||
<data name="LogInRequested" xml:space="preserve">
|
||||
<value>Login requested</value>
|
||||
</data>
|
||||
<data name="AreYouTryingToLogIn" xml:space="preserve">
|
||||
<value>Are you trying to log in?</value>
|
||||
</data>
|
||||
<data name="LogInAttemptByXOnY" xml:space="preserve">
|
||||
<value>Login attempt by {0} on {1}</value>
|
||||
</data>
|
||||
<data name="DeviceType" xml:space="preserve">
|
||||
<value>Device type</value>
|
||||
</data>
|
||||
<data name="IpAddress" xml:space="preserve">
|
||||
<value>IP address</value>
|
||||
</data>
|
||||
<data name="Time" xml:space="preserve">
|
||||
<value>Time</value>
|
||||
</data>
|
||||
<data name="Near" xml:space="preserve">
|
||||
<value>Near</value>
|
||||
</data>
|
||||
<data name="ConfirmLogIn" xml:space="preserve">
|
||||
<value>Confirm login</value>
|
||||
</data>
|
||||
<data name="DenyLogIn" xml:space="preserve">
|
||||
<value>Deny login</value>
|
||||
</data>
|
||||
<data name="JustNow" xml:space="preserve">
|
||||
<value>Just now</value>
|
||||
</data>
|
||||
<data name="XMinutesAgo" xml:space="preserve">
|
||||
<value>{0} minutes ago</value>
|
||||
</data>
|
||||
<data name="LogInAccepted" xml:space="preserve">
|
||||
<value>Login confirmed</value>
|
||||
</data>
|
||||
<data name="LogInDenied" xml:space="preserve">
|
||||
<value>Login denied</value>
|
||||
</data>
|
||||
<data name="ApproveLoginRequests" xml:space="preserve">
|
||||
<value>Approve login requests</value>
|
||||
</data>
|
||||
<data name="UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices" xml:space="preserve">
|
||||
<value>Use this device to approve login requests made from other devices.</value>
|
||||
</data>
|
||||
<data name="AllowNotifications" xml:space="preserve">
|
||||
<value>Allow notifications</value>
|
||||
</data>
|
||||
<data name="ReceivePushNotificationsForNewLoginRequests" xml:space="preserve">
|
||||
<value>Receive push notifications for new login requests</value>
|
||||
</data>
|
||||
<data name="NoThanks" xml:space="preserve">
|
||||
<value>No thanks</value>
|
||||
</data>
|
||||
<data name="ConfimLogInAttempForX" xml:space="preserve">
|
||||
<value>Confirm login attempt for {0}</value>
|
||||
</data>
|
||||
<data name="AllNotifications" xml:space="preserve">
|
||||
<value>All notifications</value>
|
||||
</data>
|
||||
<data name="PasswordType" xml:space="preserve">
|
||||
<value>Password Type</value>
|
||||
</data>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
|
||||
namespace Bit.App.Services
|
||||
|
@ -7,6 +8,11 @@ namespace Bit.App.Services
|
|||
{
|
||||
public bool IsRegisteredForPush => false;
|
||||
|
||||
public Task<bool> AreNotificationsSettingsEnabledAsync()
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public Task<string> GetTokenAsync()
|
||||
{
|
||||
return Task.FromResult(null as string);
|
||||
|
@ -21,5 +27,9 @@ namespace Bit.App.Services
|
|||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public void DismissLocalNotification(string notificationId) { }
|
||||
|
||||
public void SendLocalNotification(string title, string message, string notificationId) { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
#if !FDROID
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Pages;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
|
@ -26,6 +29,7 @@ namespace Bit.App.Services
|
|||
private IAppIdService _appIdService;
|
||||
private IApiService _apiService;
|
||||
private IMessagingService _messagingService;
|
||||
private IPushNotificationService _pushNotificationService;
|
||||
|
||||
public async Task OnMessageAsync(JObject value, string deviceType)
|
||||
{
|
||||
|
@ -125,6 +129,27 @@ namespace Bit.App.Services
|
|||
_messagingService.Send("logout");
|
||||
}
|
||||
break;
|
||||
case NotificationType.AuthRequest:
|
||||
var passwordlessLoginMessage = JsonConvert.DeserializeObject<PasswordlessRequestNotification>(notification.Payload);
|
||||
|
||||
// if the user has not enabled passwordless logins ignore requests
|
||||
if (!await _stateService.GetApprovePasswordlessLoginsAsync(passwordlessLoginMessage?.UserId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is a request modal opened ignore all incoming requests
|
||||
if (App.Current.MainPage.Navigation.ModalStack.Any(p => p is NavigationPage navPage && navPage.CurrentPage is LoginPasswordlessPage))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage, passwordlessLoginMessage?.UserId);
|
||||
var userEmail = await _stateService.GetEmailAsync(passwordlessLoginMessage?.UserId);
|
||||
|
||||
_pushNotificationService.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), Constants.PasswordlessNotificationId);
|
||||
_messagingService.Send("passwordlessLoginRequest", passwordlessLoginMessage);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -204,6 +229,7 @@ namespace Bit.App.Services
|
|||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||
_resolved = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,6 +71,6 @@
|
|||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
||||
|
||||
<Color x:Key="FingerprintPhrase">#F08DC7</Color>
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
|
|
@ -71,6 +71,6 @@
|
|||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
||||
|
||||
<Color x:Key="FingerprintPhrase">#F08DC7</Color>
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
|
|
@ -71,6 +71,6 @@
|
|||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#175DDC</Color>
|
||||
|
||||
<Color x:Key="FingerprintPhrase">#C01176</Color>
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
|
|
@ -71,6 +71,6 @@
|
|||
<Color x:Key="NavigationBarTextColor">#e5e9f0</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#81a1c1</Color>
|
||||
|
||||
<Color x:Key="FingerprintPhrase">#F08DC7</Color>
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
|
|
@ -83,6 +83,8 @@ namespace Bit.Core.Abstractions
|
|||
Task<SendResponse> PutSendAsync(string id, SendRequest request);
|
||||
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
||||
Task DeleteSendAsync(string id);
|
||||
Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id);
|
||||
Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
|
||||
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
|
@ -25,6 +26,10 @@ namespace Bit.Core.Abstractions
|
|||
Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId);
|
||||
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
||||
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null);
|
||||
|
||||
Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id);
|
||||
Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved);
|
||||
|
||||
void LogOut(Action callback);
|
||||
void Init();
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
|||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Models.View;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
|
@ -151,6 +152,10 @@ namespace Bit.Core.Abstractions
|
|||
Task<bool> GetScreenCaptureAllowedAsync(string userId = null);
|
||||
Task SetScreenCaptureAllowedAsync(bool value, string userId = null);
|
||||
Task SaveExtensionActiveUserIdToStorageAsync(string userId);
|
||||
Task<bool> GetApprovePasswordlessLoginsAsync(string userId = null);
|
||||
Task SetApprovePasswordlessLoginsAsync(bool? value, string userId = null);
|
||||
Task<PasswordlessRequestNotification> GetPasswordlessLoginNotificationAsync(string userId = null);
|
||||
Task SetPasswordlessLoginNotificationAsync(PasswordlessRequestNotification value, string userId = null);
|
||||
Task<UsernameGenerationOptions> GetUsernameGenerationOptionsAsync(string userId = null);
|
||||
Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
public static string EventCollectionKey = "eventCollection";
|
||||
public static string RememberedEmailKey = "rememberedEmail";
|
||||
public static string RememberedOrgIdentifierKey = "rememberedOrgIdentifier";
|
||||
public const string PasswordlessNotificationId = "26072022";
|
||||
public const string AndroidNotificationChannelId = "general_notification_channel";
|
||||
public const int SelectFileRequestCode = 42;
|
||||
public const int SelectFilePermissionRequestCode = 43;
|
||||
public const int SaveFileRequestCode = 44;
|
||||
|
@ -85,6 +87,8 @@
|
|||
public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}";
|
||||
public static string LastSyncKey(string userId) => $"lastSync_{userId}";
|
||||
public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}";
|
||||
public static string ApprovePasswordlessLoginsKey(string userId) => $"approvePasswordlessLogins_{userId}";
|
||||
public static string PasswordlessLoginNofiticationKey(string userId) => $"passwordlessLoginNofitication_{userId}";
|
||||
public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,5 +16,12 @@
|
|||
SyncSettings = 10,
|
||||
|
||||
LogOut = 11,
|
||||
|
||||
SyncSendCreate = 12,
|
||||
SyncSendUpdate = 13,
|
||||
SyncSendDelete = 14,
|
||||
|
||||
AuthRequest = 15,
|
||||
AuthRequestResponse = 16,
|
||||
}
|
||||
}
|
||||
|
|
21
src/Core/Models/Request/PasswordlessLoginRequest.cs
Normal file
21
src/Core/Models/Request/PasswordlessLoginRequest.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace Bit.Core.Models.Request
|
||||
{
|
||||
public class PasswordlessLoginRequest
|
||||
{
|
||||
public PasswordlessLoginRequest(string key, string masterPasswordHash, string deviceIdentifier,
|
||||
bool requestApproved)
|
||||
{
|
||||
Key = key ?? throw new ArgumentNullException(nameof(key));
|
||||
MasterPasswordHash = masterPasswordHash ?? throw new ArgumentNullException(nameof(masterPasswordHash));
|
||||
DeviceIdentifier = deviceIdentifier ?? throw new ArgumentNullException(nameof(deviceIdentifier));
|
||||
RequestApproved = requestApproved;
|
||||
}
|
||||
|
||||
public string Key { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public string DeviceIdentifier { get; set; }
|
||||
public bool RequestApproved { get; set; }
|
||||
}
|
||||
}
|
|
@ -33,4 +33,10 @@ namespace Bit.Core.Models.Response
|
|||
public string UserId { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
|
||||
public class PasswordlessRequestNotification
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
19
src/Core/Models/Response/PasswordlessLoginResponse.cs
Normal file
19
src/Core/Models/Response/PasswordlessLoginResponse.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Response
|
||||
{
|
||||
public class PasswordlessLoginResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
public string RequestDeviceType { get; set; }
|
||||
public string RequestIpAddress { get; set; }
|
||||
public string RequestFingerprint { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public bool RequestApproved { get; set; }
|
||||
public string Origin { get; set; }
|
||||
}
|
||||
}
|
|
@ -534,6 +534,21 @@ namespace Bit.Core.Services
|
|||
|
||||
#endregion
|
||||
|
||||
#region PasswordlessLogin
|
||||
|
||||
public Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id)
|
||||
{
|
||||
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Get, $"/auth-requests/{id}", null, true, true);
|
||||
}
|
||||
|
||||
public Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string encKey, string encMasterPasswordHash, string deviceIdentifier, bool requestApproved)
|
||||
{
|
||||
var request = new PasswordlessLoginRequest(encKey, encMasterPasswordHash, deviceIdentifier, requestApproved);
|
||||
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Put, $"/auth-requests/{id}", request, true, true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
public async Task<string> GetActiveBearerTokenAsync()
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
|
@ -468,5 +470,20 @@ namespace Bit.Core.Services
|
|||
TwoFactorProvidersData = null;
|
||||
SelectedTwoFactorProviderType = null;
|
||||
}
|
||||
|
||||
public async Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id)
|
||||
{
|
||||
return await _apiService.GetAuthRequestAsync(id);
|
||||
}
|
||||
|
||||
public async Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved)
|
||||
{
|
||||
var publicKey = CoreHelpers.Base64UrlDecode(pubKey);
|
||||
var masterKey = await _cryptoService.GetKeyAsync();
|
||||
var encryptedKey = await _cryptoService.RsaEncryptAsync(masterKey.EncKey, publicKey);
|
||||
var encryptedMasterPassword = await _cryptoService.RsaEncryptAsync(Encoding.UTF8.GetBytes(await _stateService.GetKeyHashAsync()), publicKey);
|
||||
var deviceId = await _appIdService.GetAppIdAsync();
|
||||
return await _apiService.PutAuthRequestAsync(id, encryptedKey.EncryptedString, encryptedMasterPassword.EncryptedString, deviceId, requestApproved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using Bit.Core.Abstractions;
|
|||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
|
@ -1260,6 +1261,37 @@ namespace Bit.Core.Services
|
|||
await SetValueAsync(key, value, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task<bool> GetApprovePasswordlessLoginsAsync(string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var key = Constants.ApprovePasswordlessLoginsKey(reconciledOptions.UserId);
|
||||
return await GetValueAsync<bool?>(key, reconciledOptions) ?? false;
|
||||
}
|
||||
|
||||
public async Task SetApprovePasswordlessLoginsAsync(bool? value, string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var key = Constants.ApprovePasswordlessLoginsKey(reconciledOptions.UserId);
|
||||
await SetValueAsync(key, value, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task<PasswordlessRequestNotification> GetPasswordlessLoginNotificationAsync(string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var key = Constants.PasswordlessLoginNofiticationKey(reconciledOptions.UserId);
|
||||
return await GetValueAsync<PasswordlessRequestNotification>(key, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task SetPasswordlessLoginNotificationAsync(PasswordlessRequestNotification value, string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var key = Constants.PasswordlessLoginNofiticationKey(reconciledOptions.UserId);
|
||||
await SetValueAsync(key, value, reconciledOptions);
|
||||
}
|
||||
// Helpers
|
||||
|
||||
private async Task<T> GetValueAsync<T>(string key, StorageOptions options)
|
||||
|
@ -1455,6 +1487,7 @@ namespace Bit.Core.Services
|
|||
await SetEncryptedPasswordGenerationHistoryAsync(null, userId);
|
||||
await SetEncryptedSendsAsync(null, userId);
|
||||
await SetSettingsAsync(null, userId);
|
||||
await SetApprovePasswordlessLoginsAsync(null, userId);
|
||||
|
||||
if (userInitiated)
|
||||
{
|
||||
|
@ -1474,6 +1507,7 @@ namespace Bit.Core.Services
|
|||
await SetAutoDarkThemeAsync(null, userId);
|
||||
await SetAddSitePromptShownAsync(null, userId);
|
||||
await SetPasswordGenerationOptionsAsync(null, userId);
|
||||
await SetApprovePasswordlessLoginsAsync(null, userId);
|
||||
await SetUsernameGenerationOptionsAsync(null, userId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -643,5 +643,11 @@ namespace Bit.iOS.Core.Services
|
|||
_deviceActionService.PickedDocument(url);
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenAppSettings()
|
||||
{
|
||||
var url = new NSUrl(UIApplication.OpenSettingsUrlString);
|
||||
UIApplication.SharedApplication.OpenUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ using Bit.iOS.Services;
|
|||
using CoreNFC;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using UserNotifications;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.iOS;
|
||||
|
||||
|
@ -56,7 +57,6 @@ namespace Bit.iOS
|
|||
LoadApplication(new App.App(null));
|
||||
iOSCoreHelpers.AppearanceAdjustments();
|
||||
ZXing.Net.Mobile.Forms.iOS.Platform.Init();
|
||||
|
||||
_broadcasterService.Subscribe(nameof(AppDelegate), async (message) =>
|
||||
{
|
||||
try
|
||||
|
|
|
@ -83,7 +83,6 @@ namespace Bit.iOS.Services
|
|||
public void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
|
||||
{
|
||||
Debug.WriteLine($"{TAG} WillPresentNotification {notification?.Request?.Content?.UserInfo}");
|
||||
|
||||
OnMessageReceived(notification?.Request?.Content?.UserInfo);
|
||||
completionHandler(UNNotificationPresentationOptions.Alert);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using UserNotifications;
|
||||
|
@ -19,6 +23,12 @@ namespace Bit.iOS.Services
|
|||
|
||||
public bool IsRegisteredForPush => UIApplication.SharedApplication.IsRegisteredForRemoteNotifications;
|
||||
|
||||
public async Task<bool> AreNotificationsSettingsEnabledAsync()
|
||||
{
|
||||
var settings = await UNUserNotificationCenter.Current.GetNotificationSettingsAsync();
|
||||
return settings.AlertSetting == UNNotificationSetting.Enabled;
|
||||
}
|
||||
|
||||
public async Task RegisterAsync()
|
||||
{
|
||||
Debug.WriteLine($"{TAG} RegisterAsync");
|
||||
|
@ -58,5 +68,39 @@ namespace Bit.iOS.Services
|
|||
NSUserDefaults.StandardUserDefaults.Synchronize();
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public void SendLocalNotification(string title, string message, string notificationId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(notificationId))
|
||||
{
|
||||
throw new ArgumentNullException("notificationId cannot be null or empty.");
|
||||
}
|
||||
|
||||
var content = new UNMutableNotificationContent()
|
||||
{
|
||||
Title = title,
|
||||
Body = message
|
||||
};
|
||||
|
||||
var request = UNNotificationRequest.FromIdentifier(notificationId, content, null);
|
||||
UNUserNotificationCenter.Current.AddNotificationRequest(request, (err) =>
|
||||
{
|
||||
if (err != null)
|
||||
{
|
||||
Logger.Instance.Exception(new Exception($"Failed to schedule notification: {err}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void DismissLocalNotification(string notificationId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(notificationId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UNUserNotificationCenter.Current.RemovePendingNotificationRequests(new string[] { notificationId });
|
||||
UNUserNotificationCenter.Current.RemoveDeliveredNotifications(new string[] { notificationId });
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue