mirror of
https://github.com/bitwarden/mobile
synced 2025-12-18 09:13:15 +00:00
[PM-3394] Fix login with device for passwordless approvals (#2686)
* set activeUserId to null when logging in a new account - Also stop the user key from being set in inactive accounts * get token for login with device if approving device doesn't have master key * add comment * simplify logic * check for route instead of using isAuthenticated - we don't clear the user id when logging in new account - this means we can't trust the state service, so we have to base our logic off the route in login with device * use authenticated auth request for tde login with device * [PM-3394] Add authingWithSso parameter to LoginPasswordlessRequestPage. * pr feedback * [PM-3394] Refactor condition Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> --------- Co-authored-by: André Bispo <abispo@bitwarden.com> Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
This commit is contained in:
@@ -50,13 +50,13 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task StartLoginWithDeviceAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions);
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions, true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task RequestAdminApprovalAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions);
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions, true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Bit.App.Pages
|
||||
private LoginPasswordlessRequestViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public LoginPasswordlessRequestPage(string email, AuthRequestType authRequestType, AppOptions appOptions = null)
|
||||
public LoginPasswordlessRequestPage(string email, AuthRequestType authRequestType, AppOptions appOptions = null, bool authingWithSso = false)
|
||||
{
|
||||
InitializeComponent();
|
||||
_appOptions = appOptions;
|
||||
@@ -21,6 +21,7 @@ namespace Bit.App.Pages
|
||||
_vm.Page = this;
|
||||
_vm.Email = email;
|
||||
_vm.AuthRequestType = authRequestType;
|
||||
_vm.AuthingWithSso = authingWithSso;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
|
||||
@@ -80,6 +80,7 @@ namespace Bit.App.Pages
|
||||
public Action LogInSuccessAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public bool AuthingWithSso { get; set; }
|
||||
|
||||
public ICommand CreatePasswordlessLoginCommand { get; }
|
||||
public ICommand CloseCommand { get; }
|
||||
@@ -233,7 +234,7 @@ namespace Bit.App.Pages
|
||||
try
|
||||
{
|
||||
PasswordlessLoginResponse response = null;
|
||||
if (await _stateService.IsAuthenticatedAsync())
|
||||
if (AuthingWithSso)
|
||||
{
|
||||
response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId);
|
||||
}
|
||||
@@ -242,14 +243,14 @@ namespace Bit.App.Pages
|
||||
response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode);
|
||||
}
|
||||
|
||||
if (response.RequestApproved == null || !response.RequestApproved.Value)
|
||||
if (response?.RequestApproved != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopCheckLoginRequestStatus();
|
||||
|
||||
var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
|
||||
var authResult = await _authService.LogInPasswordlessAsync(AuthingWithSso, Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
|
||||
@@ -240,9 +240,9 @@ namespace Bit.App.Pages
|
||||
else if (pendingRequest != null)
|
||||
{
|
||||
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||
if (authRequest != null && authRequest.RequestApproved != null && authRequest.RequestApproved.Value)
|
||||
if (authRequest?.RequestApproved == true)
|
||||
{
|
||||
var authResult = await _authService.LogInPasswordlessAsync(await _stateService.GetActiveUserEmailAsync(), authRequest.RequestAccessCode, pendingRequest.Id, pendingRequest.PrivateKey, authRequest.Key, authRequest.MasterPasswordHash);
|
||||
var authResult = await _authService.LogInPasswordlessAsync(true, await _stateService.GetActiveUserEmailAsync(), authRequest.RequestAccessCode, pendingRequest.Id, pendingRequest.PrivateKey, authRequest.Key, authRequest.MasterPasswordHash);
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
await Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(
|
||||
|
||||
@@ -27,7 +27,7 @@ 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<AuthResult> LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered);
|
||||
Task<AuthResult> LogInPasswordlessAsync(bool authingWithSso, string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered);
|
||||
|
||||
Task<List<PasswordlessLoginResponse>> GetPasswordlessLoginRequestsAsync();
|
||||
Task<List<PasswordlessLoginResponse>> GetActivePasswordlessLoginRequestsAsync();
|
||||
|
||||
@@ -200,12 +200,13 @@ namespace Bit.Core.Services
|
||||
return !await _policyService.EvaluateMasterPassword(strength.Value, masterPassword, _masterPasswordPolicy);
|
||||
}
|
||||
|
||||
public async Task<AuthResult> LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string encryptedAuthRequestKey, string masterKeyHash)
|
||||
public async Task<AuthResult> LogInPasswordlessAsync(bool authingWithSso, string email, string accessCode, string authRequestId, byte[] decryptionKey, string encryptedAuthRequestKey, string masterKeyHash)
|
||||
{
|
||||
var decryptedKey = await _cryptoService.RsaDecryptAsync(encryptedAuthRequestKey, decryptionKey);
|
||||
|
||||
// On SSO flow user is already AuthN
|
||||
if (await _stateService.IsAuthenticatedAsync())
|
||||
// If the user is already authenticated, we can just set the key
|
||||
// Note: We can't check for the existance of an access token here because the active user id may not be null
|
||||
if (authingWithSso)
|
||||
{
|
||||
if (string.IsNullOrEmpty(masterKeyHash))
|
||||
{
|
||||
@@ -222,10 +223,13 @@ namespace Bit.Core.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
// The approval device may not have a master key hash if it authenticated with a passwordless method
|
||||
if (string.IsNullOrEmpty(masterKeyHash) && decryptionKey != null)
|
||||
{
|
||||
var authResult = await LogInHelperAsync(email, accessCode, null, null, null, null, null, null, null, null, null, authRequestId: authRequestId);
|
||||
// Only set the user key after the login helper so we have a user id
|
||||
await _cryptoService.SetUserKeyAsync(new UserKey(decryptedKey));
|
||||
return null;
|
||||
return authResult;
|
||||
}
|
||||
|
||||
var decKeyHash = await _cryptoService.RsaDecryptAsync(masterKeyHash, decryptionKey);
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace Bit.Core.Services
|
||||
|
||||
public async Task<UserKey> GetUserKeyWithLegacySupportAsync(string userId = null)
|
||||
{
|
||||
var userKey = await GetUserKeyAsync();
|
||||
var userKey = await GetUserKeyAsync(userId);
|
||||
if (userKey != null)
|
||||
{
|
||||
return userKey;
|
||||
@@ -71,7 +71,7 @@ namespace Bit.Core.Services
|
||||
|
||||
// Legacy support: encryption used to be done with the master key (derived from master password).
|
||||
// Users who have not migrated will have a null user key and must use the master key instead.
|
||||
return new UserKey((await GetMasterKeyAsync()).Key);
|
||||
return new UserKey((await GetMasterKeyAsync(userId)).Key);
|
||||
}
|
||||
|
||||
public async Task<bool> HasUserKeyAsync(string userId = null)
|
||||
|
||||
@@ -59,6 +59,13 @@ namespace Bit.Core.Services
|
||||
|
||||
public async Task<bool> IsLockedAsync(string userId = null)
|
||||
{
|
||||
// When checking if we need to lock inactive account, we don't want to
|
||||
// set the key, so we only check if the account has an auto unlock key
|
||||
if (userId != null && await _stateService.GetActiveUserIdAsync() != userId)
|
||||
{
|
||||
return await _cryptoService.HasAutoUnlockKeyAsync(userId);
|
||||
}
|
||||
|
||||
var biometricSet = await IsBiometricLockSetAsync(userId);
|
||||
if (biometricSet && await _stateService.GetBiometricLockedAsync(userId))
|
||||
{
|
||||
@@ -67,14 +74,11 @@ namespace Bit.Core.Services
|
||||
|
||||
if (!await _cryptoService.HasUserKeyAsync(userId))
|
||||
{
|
||||
if (await _cryptoService.HasAutoUnlockKeyAsync(userId))
|
||||
{
|
||||
await _cryptoService.SetUserKeyAsync(await _cryptoService.GetAutoUnlockKeyAsync(userId));
|
||||
}
|
||||
else
|
||||
if (!await _cryptoService.HasAutoUnlockKeyAsync(userId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
await _cryptoService.SetUserKeyAsync(await _cryptoService.GetAutoUnlockKeyAsync(userId), userId);
|
||||
}
|
||||
|
||||
// Check again to verify auto key was set
|
||||
|
||||
@@ -511,11 +511,11 @@ namespace Bit.iOS.Autofill
|
||||
LogoutIfAuthed();
|
||||
}
|
||||
|
||||
private void LaunchLoginWithDevice(AuthRequestType authRequestType, string email = null)
|
||||
private void LaunchLoginWithDevice(AuthRequestType authRequestType, string email = null, bool authingWithSso = false)
|
||||
{
|
||||
var appOptions = new AppOptions { IosExtension = true };
|
||||
var app = new App.App(appOptions);
|
||||
var loginWithDevicePage = new LoginPasswordlessRequestPage(email, authRequestType, appOptions);
|
||||
var loginWithDevicePage = new LoginPasswordlessRequestPage(email, authRequestType, appOptions, authingWithSso);
|
||||
ThemeManager.SetTheme(app.Resources);
|
||||
ThemeManager.ApplyResourcesTo(loginWithDevicePage);
|
||||
if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm)
|
||||
@@ -632,8 +632,8 @@ namespace Bit.iOS.Autofill
|
||||
if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm)
|
||||
{
|
||||
vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this));
|
||||
vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email));
|
||||
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email));
|
||||
vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email, true));
|
||||
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email, true));
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(loginApproveDevicePage);
|
||||
|
||||
@@ -533,11 +533,11 @@ namespace Bit.iOS.Extension
|
||||
LogoutIfAuthed();
|
||||
}
|
||||
|
||||
private void LaunchLoginWithDevice(AuthRequestType authRequestType,string email = null)
|
||||
private void LaunchLoginWithDevice(AuthRequestType authRequestType, string email = null, bool authingWithSso = false)
|
||||
{
|
||||
var appOptions = new AppOptions { IosExtension = true };
|
||||
var app = new App.App(appOptions);
|
||||
var loginWithDevicePage = new LoginPasswordlessRequestPage(email, authRequestType, appOptions);
|
||||
var loginWithDevicePage = new LoginPasswordlessRequestPage(email, authRequestType, appOptions, authingWithSso);
|
||||
ThemeManager.SetTheme(app.Resources);
|
||||
ThemeManager.ApplyResourcesTo(loginWithDevicePage);
|
||||
if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm)
|
||||
@@ -654,8 +654,8 @@ namespace Bit.iOS.Extension
|
||||
if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm)
|
||||
{
|
||||
vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this));
|
||||
vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email));
|
||||
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email));
|
||||
vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email, true));
|
||||
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email, true));
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(loginApproveDevicePage);
|
||||
|
||||
@@ -348,9 +348,9 @@ namespace Bit.iOS.ShareExtension
|
||||
LogoutIfAuthed();
|
||||
}
|
||||
|
||||
private void LaunchLoginWithDevice(AuthRequestType authRequestType, string email = null)
|
||||
private void LaunchLoginWithDevice(AuthRequestType authRequestType, string email = null, bool authingWithSso = false)
|
||||
{
|
||||
var loginWithDevicePage = new LoginPasswordlessRequestPage(email, authRequestType, _appOptions.Value);
|
||||
var loginWithDevicePage = new LoginPasswordlessRequestPage(email, authRequestType, _appOptions.Value, authingWithSso);
|
||||
SetupAppAndApplyResources(loginWithDevicePage);
|
||||
if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm)
|
||||
{
|
||||
@@ -438,8 +438,8 @@ namespace Bit.iOS.ShareExtension
|
||||
if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm)
|
||||
{
|
||||
vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this));
|
||||
vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email));
|
||||
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email));
|
||||
vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email, true));
|
||||
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email, true));
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(loginApproveDevicePage);
|
||||
|
||||
Reference in New Issue
Block a user