1
0
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:
Jake Fink
2023-08-15 19:06:39 -04:00
committed by GitHub
parent 9c1a206751
commit 001e08e347
11 changed files with 42 additions and 32 deletions

View File

@@ -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));
}
}

View File

@@ -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());

View File

@@ -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())

View File

@@ -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(

View File

@@ -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();

View File

@@ -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);

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);