1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-28 14:13:25 +00:00

[PM-2287][PM-2289][PM-2293] Approval Options (#2608)

* [PM-2293] Add AuthRequestType to PasswordlessLoginPage.

* [PM-2293] Add Actions to ApproveWithDevicePage

* [PM-2293] Change screen text based on AuthRequestType

* [PM-2293] Refactor AuthRequestType enum. Add label. Remove unnecessary actions.

* [PM-2293] Change boolean variable expression.

* [PM-2293] Trust device after admin request login.

* code format

* [PM-2287] Add trust device to master password unlock. Change trust device method. Remove email from SSO login page.

* [PM-2293] Fix state variable get set.

* [PM-2287][PM-2289][PM-2293] Rename method
This commit is contained in:
André Bispo
2023-07-12 19:12:57 +01:00
committed by GitHub
parent 548bd12a8e
commit a5df6c0c65
22 changed files with 261 additions and 29 deletions

View File

@@ -33,7 +33,7 @@ namespace Bit.App.Pages
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
private readonly IPolicyService _policyService;
private readonly IPasswordGenerationService _passwordGenerationService;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
private string _email;
private string _masterPassword;
private string _pin;
@@ -65,6 +65,7 @@ namespace Bit.App.Pages
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
_policyService = ServiceContainer.Resolve<IPolicyService>();
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
PageTitle = AppResources.VerifyMasterPassword;
TogglePasswordCommand = new Command(TogglePassword);
@@ -454,6 +455,7 @@ namespace Bit.App.Pages
{
await _cryptoService.SetKeyAsync(key);
}
await _deviceTrustCryptoService.TrustDeviceIfNeededAsync();
await DoContinueAsync();
}

View File

@@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -9,14 +13,22 @@ namespace Bit.App.Pages
{
private readonly LoginApproveDeviceViewModel _vm;
private readonly AppOptions _appOptions;
public LoginApproveDevicePage(AppOptions appOptions = null)
{
InitializeComponent();
_vm = BindingContext as LoginApproveDeviceViewModel;
_vm.LogInWithMasterPassword = () => StartLogInWithMasterPassword().FireAndForget();
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
_vm.RequestAdminApprovalAction = () => RequestAdminApprovalAsync().FireAndForget();
_vm.CloseAction = () => { Navigation.PopModalAsync(); };
_vm.Page = this;
_appOptions = appOptions;
}
protected override void OnAppearing(){
protected override void OnAppearing()
{
_vm.InitAsync();
}
@@ -27,6 +39,24 @@ namespace Bit.App.Pages
_vm.CloseAction();
}
}
private async Task StartLogInWithMasterPassword()
{
var page = new LockPage(_appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task StartLoginWithDeviceAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task RequestAdminApprovalAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
}

View File

@@ -7,9 +7,11 @@ using Bit.App.Utilities.AccountManagement;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -23,29 +25,35 @@ namespace Bit.App.Pages
private string _email;
private readonly IStateService _stateService;
private readonly IApiService _apiService;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
public ICommand ApproveWithMyOtherDeviceCommand { get; }
public ICommand RequestAdminApprovalCommand { get; }
public ICommand ApproveWithMasterPasswordCommand { get; }
public ICommand ContinueCommand { get; }
public Action LogInWithMasterPassword { get; set; }
public Action LogInWithDeviceAction { get; set; }
public Action RequestAdminApprovalAction { get; set; }
public Action CloseAction { get; set; }
public LoginApproveDeviceViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>();
_stateService = ServiceContainer.Resolve<IStateService>();
_apiService = ServiceContainer.Resolve<IApiService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
PageTitle = AppResources.LoggedIn;
ApproveWithMyOtherDeviceCommand = new AsyncCommand(InitAsync,
ApproveWithMyOtherDeviceCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithDeviceAction),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
RequestAdminApprovalCommand = new AsyncCommand(InitAsync,
RequestAdminApprovalCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(RequestAdminApprovalAction),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
ApproveWithMasterPasswordCommand = new AsyncCommand(InitAsync,
ApproveWithMasterPasswordCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithMasterPassword),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
@@ -97,13 +105,12 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
// Appears if the browser is trusted and shared the key with the app
ContinueEnabled = true;
try
{
Email = await _stateService.GetRememberedEmailAsync();
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
RequestAdminApprovalEnabled = decryptOptions.TrustedDeviceOption.HasAdminApproval;
ApproveWithMasterPasswordEnabled = decryptOptions.HasMasterPassword;
RequestAdminApprovalEnabled = decryptOptions?.TrustedDeviceOption?.HasAdminApproval ?? false;
ApproveWithMasterPasswordEnabled = decryptOptions?.HasMasterPassword ?? false;
}
catch (Exception ex)
{
@@ -118,6 +125,15 @@ namespace Bit.App.Pages
{
HandleException(ex);
}
// TODO: Change this expression to, Appear if the browser is trusted and shared the key with the app
ContinueEnabled = !RequestAdminApprovalEnabled && !ApproveWithMasterPasswordEnabled && !ApproveWithMyOtherDeviceEnabled;
}
private async Task SetDeviceTrustAndInvokeAsync(Action action)
{
await _deviceTrustCryptoService.SetShouldTrustDeviceAsync(RememberThisDevice);
await Device.InvokeOnMainThreadAsync(action);
}
}
}

View File

@@ -4,6 +4,7 @@ using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
@@ -135,7 +136,7 @@ namespace Bit.App.Pages
private async Task StartLoginWithDeviceAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, _appOptions);
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}

View File

@@ -21,17 +21,17 @@
<StackLayout
Padding="7, 0, 7, 20">
<Label
Text="{u:I18n LogInInitiated}"
Text="{Binding Tittle}"
FontSize="Title"
FontAttributes="Bold"
Margin="0,14,0,21"
AutomationId="LogInInitiatedLabel" />
<Label
Text="{u:I18n ANotificationHasBeenSentToYourDevice}"
Text="{Binding SubTittle}"
FontSize="Small"
Margin="0,0,0,10"/>
<Label
Text="{u:I18n PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice}"
Text="{Binding Description}"
FontSize="Small"
Margin="0,0,0,24"/>
<Label
@@ -45,6 +45,7 @@
AutomationId="FingerprintPhraseValue" />
<Label
Text="{u:I18n ResendNotification}"
IsVisible="{Binding ResendNotificationVisible}"
StyleClass="text-md"
HorizontalOptions="Start"
Margin="0,40,0,0"
@@ -58,7 +59,7 @@
Orientation="Horizontal"
Margin="0,30,0,0">
<Label
Text="{u:I18n NeedAnotherOption}"
Text="{Binding OtherOptions}"
FontSize="Small"
VerticalTextAlignment="End"/>
<Label

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Enums;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -12,13 +13,14 @@ namespace Bit.App.Pages
private LoginPasswordlessRequestViewModel _vm;
private readonly AppOptions _appOptions;
public LoginPasswordlessRequestPage(string email, AppOptions appOptions = null)
public LoginPasswordlessRequestPage(string email, AuthRequestType authRequestType, AppOptions appOptions = null)
{
InitializeComponent();
_appOptions = appOptions;
_vm = BindingContext as LoginPasswordlessRequestViewModel;
_vm.Page = this;
_vm.Email = email;
_vm.AuthRequestType = authRequestType;
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -32,6 +33,7 @@ namespace Bit.App.Pages
private IPlatformUtilsService _platformUtilsService;
private IEnvironmentService _environmentService;
private ILogger _logger;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
protected override II18nService i18nService => _i18nService;
protected override IEnvironmentService environmentService => _environmentService;
@@ -44,6 +46,7 @@ namespace Bit.App.Pages
private string _email;
private string _requestId;
private string _requestAccessCode;
private AuthRequestType _authRequestType;
// Item1 publicKey, Item2 privateKey
private Tuple<byte[], byte[]> _requestKeyPair;
@@ -57,6 +60,7 @@ namespace Bit.App.Pages
_i18nService = ServiceContainer.Resolve<II18nService>();
_stateService = ServiceContainer.Resolve<IStateService>();
_logger = ServiceContainer.Resolve<ILogger>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
PageTitle = AppResources.LogInWithAnotherDevice;
@@ -77,6 +81,70 @@ namespace Bit.App.Pages
public ICommand CreatePasswordlessLoginCommand { get; }
public ICommand CloseCommand { get; }
public string Tittle
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.LogInInitiated;
case AuthRequestType.AdminApproval:
return AppResources.AdminApprovalRequested;
default:
return string.Empty;
};
}
}
public string SubTittle
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.ANotificationHasBeenSentToYourDevice;
case AuthRequestType.AdminApproval:
return AppResources.YourRequestHasBeenSentToYourAdmin;
default:
return string.Empty;
};
}
}
public string Description
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice;
case AuthRequestType.AdminApproval:
return AppResources.YouWillBeNotifiedOnceApproved;
default:
return string.Empty;
};
}
}
public string OtherOptions
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.NeedAnotherOption;
case AuthRequestType.AdminApproval:
return AppResources.TroubleLoggingIn;
default:
return string.Empty;
};
}
}
public string FingerprintPhrase
{
get => _fingerprintPhrase;
@@ -89,6 +157,21 @@ namespace Bit.App.Pages
set => SetProperty(ref _email, value);
}
public AuthRequestType AuthRequestType
{
get => _authRequestType;
set => SetProperty(ref _authRequestType, value, additionalPropertyNames: new string[]
{
nameof(Tittle),
nameof(SubTittle),
nameof(Description),
nameof(OtherOptions),
nameof(ResendNotificationVisible)
});
}
public bool ResendNotificationVisible => AuthRequestType == AuthRequestType.AuthenticateAndUnlock;
public void StartCheckLoginRequestStatus()
{
try
@@ -154,6 +237,7 @@ namespace Bit.App.Pages
else
{
_syncService.FullSyncAsync(true).FireAndForget();
await _deviceTrustCryptoService.TrustDeviceIfNeededAsync();
LogInSuccessAction?.Invoke();
}
}
@@ -168,7 +252,7 @@ namespace Bit.App.Pages
{
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email);
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType);
if (response != null)
{
FingerprintPhrase = response.FingerprintPhrase;

View File

@@ -418,6 +418,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Admin approval requested.
/// </summary>
public static string AdminApprovalRequested {
get {
return ResourceManager.GetString("AdminApprovalRequested", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to All.
/// </summary>
@@ -6308,6 +6317,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Trouble logging in?.
/// </summary>
public static string TroubleLoggingIn {
get {
return ResourceManager.GetString("TroubleLoggingIn", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Try again.
/// </summary>
@@ -7208,6 +7226,24 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Your request has been sent to your admin..
/// </summary>
public static string YourRequestHasBeenSentToYourAdmin {
get {
return ResourceManager.GetString("YourRequestHasBeenSentToYourAdmin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You will be notified once approved. .
/// </summary>
public static string YouWillBeNotifiedOnceApproved {
get {
return ResourceManager.GetString("YouWillBeNotifiedOnceApproved", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to To continue, hold your YubiKey NEO against the back of the device or insert your YubiKey into your device&apos;s USB port, then touch its button..
/// </summary>

View File

@@ -2661,6 +2661,18 @@ Do you want to switch to this account?</value>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Invalid API token</value>
</data>
<data name="AdminApprovalRequested" xml:space="preserve">
<value>Admin approval requested</value>
</data>
<data name="YourRequestHasBeenSentToYourAdmin" xml:space="preserve">
<value>Your request has been sent to your admin.</value>
</data>
<data name="YouWillBeNotifiedOnceApproved" xml:space="preserve">
<value>You will be notified once approved. </value>
</data>
<data name="TroubleLoggingIn" xml:space="preserve">
<value>Trouble logging in?</value>
</data>
<data name="LoggingInAsX" xml:space="preserve">
<value>Logging in as {0}</value>
</data>