1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-18 17:23:18 +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() 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)); await Navigation.PushModalAsync(new NavigationPage(page));
} }
private async Task RequestAdminApprovalAsync() 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)); await Navigation.PushModalAsync(new NavigationPage(page));
} }
} }

View File

@@ -13,7 +13,7 @@ namespace Bit.App.Pages
private LoginPasswordlessRequestViewModel _vm; private LoginPasswordlessRequestViewModel _vm;
private readonly AppOptions _appOptions; 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(); InitializeComponent();
_appOptions = appOptions; _appOptions = appOptions;
@@ -21,6 +21,7 @@ namespace Bit.App.Pages
_vm.Page = this; _vm.Page = this;
_vm.Email = email; _vm.Email = email;
_vm.AuthRequestType = authRequestType; _vm.AuthRequestType = authRequestType;
_vm.AuthingWithSso = authingWithSso;
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync()); _vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync()); _vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); _vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());

View File

@@ -80,6 +80,7 @@ namespace Bit.App.Pages
public Action LogInSuccessAction { get; set; } public Action LogInSuccessAction { get; set; }
public Action UpdateTempPasswordAction { get; set; } public Action UpdateTempPasswordAction { get; set; }
public Action CloseAction { get; set; } public Action CloseAction { get; set; }
public bool AuthingWithSso { get; set; }
public ICommand CreatePasswordlessLoginCommand { get; } public ICommand CreatePasswordlessLoginCommand { get; }
public ICommand CloseCommand { get; } public ICommand CloseCommand { get; }
@@ -233,7 +234,7 @@ namespace Bit.App.Pages
try try
{ {
PasswordlessLoginResponse response = null; PasswordlessLoginResponse response = null;
if (await _stateService.IsAuthenticatedAsync()) if (AuthingWithSso)
{ {
response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId); response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId);
} }
@@ -242,14 +243,14 @@ namespace Bit.App.Pages
response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode); response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode);
} }
if (response.RequestApproved == null || !response.RequestApproved.Value) if (response?.RequestApproved != true)
{ {
return; return;
} }
StopCheckLoginRequestStatus(); 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(); await AppHelpers.ResetInvalidUnlockAttemptsAsync();
if (authResult == null && await _stateService.IsAuthenticatedAsync()) if (authResult == null && await _stateService.IsAuthenticatedAsync())

View File

@@ -240,9 +240,9 @@ namespace Bit.App.Pages
else if (pendingRequest != null) else if (pendingRequest != null)
{ {
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id); 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()) if (authResult == null && await _stateService.IsAuthenticatedAsync())
{ {
await Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync( 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> 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> 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> 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>> GetPasswordlessLoginRequestsAsync();
Task<List<PasswordlessLoginResponse>> GetActivePasswordlessLoginRequestsAsync(); Task<List<PasswordlessLoginResponse>> GetActivePasswordlessLoginRequestsAsync();

View File

@@ -200,12 +200,13 @@ namespace Bit.Core.Services
return !await _policyService.EvaluateMasterPassword(strength.Value, masterPassword, _masterPasswordPolicy); 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); var decryptedKey = await _cryptoService.RsaDecryptAsync(encryptedAuthRequestKey, decryptionKey);
// On SSO flow user is already AuthN // If the user is already authenticated, we can just set the key
if (await _stateService.IsAuthenticatedAsync()) // 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)) if (string.IsNullOrEmpty(masterKeyHash))
{ {
@@ -222,10 +223,13 @@ namespace Bit.Core.Services
return null; 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) 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)); await _cryptoService.SetUserKeyAsync(new UserKey(decryptedKey));
return null; return authResult;
} }
var decKeyHash = await _cryptoService.RsaDecryptAsync(masterKeyHash, decryptionKey); 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) public async Task<UserKey> GetUserKeyWithLegacySupportAsync(string userId = null)
{ {
var userKey = await GetUserKeyAsync(); var userKey = await GetUserKeyAsync(userId);
if (userKey != null) if (userKey != null)
{ {
return userKey; 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). // 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. // 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) 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) 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); var biometricSet = await IsBiometricLockSetAsync(userId);
if (biometricSet && await _stateService.GetBiometricLockedAsync(userId)) if (biometricSet && await _stateService.GetBiometricLockedAsync(userId))
{ {
@@ -67,14 +74,11 @@ namespace Bit.Core.Services
if (!await _cryptoService.HasUserKeyAsync(userId)) if (!await _cryptoService.HasUserKeyAsync(userId))
{ {
if (await _cryptoService.HasAutoUnlockKeyAsync(userId)) if (!await _cryptoService.HasAutoUnlockKeyAsync(userId))
{
await _cryptoService.SetUserKeyAsync(await _cryptoService.GetAutoUnlockKeyAsync(userId));
}
else
{ {
return true; return true;
} }
await _cryptoService.SetUserKeyAsync(await _cryptoService.GetAutoUnlockKeyAsync(userId), userId);
} }
// Check again to verify auto key was set // Check again to verify auto key was set

View File

@@ -511,11 +511,11 @@ namespace Bit.iOS.Autofill
LogoutIfAuthed(); 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 appOptions = new AppOptions { IosExtension = true };
var app = new App.App(appOptions); 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.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(loginWithDevicePage); ThemeManager.ApplyResourcesTo(loginWithDevicePage);
if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm) if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm)
@@ -632,8 +632,8 @@ namespace Bit.iOS.Autofill
if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm) if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm)
{ {
vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this)); vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this));
vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email)); vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email, true));
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email)); vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email, true));
} }
var navigationPage = new NavigationPage(loginApproveDevicePage); var navigationPage = new NavigationPage(loginApproveDevicePage);

View File

@@ -533,11 +533,11 @@ namespace Bit.iOS.Extension
LogoutIfAuthed(); 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 appOptions = new AppOptions { IosExtension = true };
var app = new App.App(appOptions); 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.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(loginWithDevicePage); ThemeManager.ApplyResourcesTo(loginWithDevicePage);
if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm) if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm)
@@ -654,8 +654,8 @@ namespace Bit.iOS.Extension
if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm) if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm)
{ {
vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this)); vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this));
vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email)); vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email, true));
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email)); vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email, true));
} }
var navigationPage = new NavigationPage(loginApproveDevicePage); var navigationPage = new NavigationPage(loginApproveDevicePage);

View File

@@ -348,9 +348,9 @@ namespace Bit.iOS.ShareExtension
LogoutIfAuthed(); 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); SetupAppAndApplyResources(loginWithDevicePage);
if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm) if (loginWithDevicePage.BindingContext is LoginPasswordlessRequestViewModel vm)
{ {
@@ -438,8 +438,8 @@ namespace Bit.iOS.ShareExtension
if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm) if (loginApproveDevicePage.BindingContext is LoginApproveDeviceViewModel vm)
{ {
vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this)); vm.LogInWithMasterPasswordAction = () => DismissViewController(false, () => PerformSegue("lockPasswordSegue", this));
vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email)); vm.RequestAdminApprovalAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AdminApproval, vm.Email, true));
vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email)); vm.LogInWithDeviceAction = () => DismissViewController(false, () => LaunchLoginWithDevice(AuthRequestType.AuthenticateAndUnlock, vm.Email, true));
} }
var navigationPage = new NavigationPage(loginApproveDevicePage); var navigationPage = new NavigationPage(loginApproveDevicePage);