mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-29 12:45:20 +01:00
[AC-1070] Enforce master password policy on login/unlock (#2410)
* [AC-1070] Add EnforceOnLogin property to MasterPasswordPolicyOptions
* [AC-1070] Add MasterPasswordPolicy property to Identity responses
* [AC-1070] Add policy service dependency to auth service
* [AC-1070] Introduce logic to evaluate master password after successful login
* [AC-1070] Add optional ForcePasswordResetReason to profile / state service
* [AC-1070] Save ForcePasswordResetReason to state when a weak master password is found during login
- Additionally, save the AdminForcePasswordReset reason if the identity result indicates an admin password reset is in effect.
* [AC-1070] Check for a saved ForcePasswordReset reason on TabsPage load force show the update password page
* [AC-1070] Make InitAsync virtual
Allow the UpdateTempPasswordPage to override the InitAsync method to check for a reset password reason in the state service
* [AC-1070] Modify UpdateTempPassword page appearance
- Load the force password reset reason from the state service
- Make warning text dynamic based on force password reason
- Conditionally show the Current master password field if updating a weak master password
* [AC-1070] Add update password method to Api service
* [AC-1070] Introduce logic to update both temp and regular passwords
- Check the Reason to use the appropriate request/endpoint when submitting.
- Verify the users current password locally using the user verification service.
* [AC-1070] Introduce VerifyMasterPasswordResponse
* [AC-1070] Add logic to evaluate master password on unlock
* [AC-1070] Add support 2FA login flow
Keep track of the reset password reason after a password login requires 2FA. During 2FA submission, check if there is a saved reason, and if so, force the user to update their password.
* [AC-1070] Formatting
* [AC-1070] Remove string key from service resolution
* [AC-1070] Change master password options to method variable to avoid class field
Add null check for password strength result and log an error as this is an unexpected flow
* [AC-1070] Remove usage of i18nService
* [AC-1070] Use AsyncCommand for SubmitCommand
* [AC-1070] Remove type from ShowToast call
* [AC-1070] Simplify UpdatePassword methods to accept string for the new encryption key
* [AC-1070] Use full text for key for the CurrentMasterPassword resource
* [AC-1070] Convert Reason to a private class field
* [AC-1070] Formatting changes
* [AC-1070] Simplify if statements in master password options policy service method
* [AC-1070] Use the saved force password reset reason after 2FA login
* [AC-1070] Use constant for ForceUpdatePassword message command
* [AC-1070] Move shared RequirePasswordChangeOnLogin method into PolicyService
* Revert "[AC-1070] Move shared RequirePasswordChangeOnLogin method into PolicyService"
This reverts commit e4feac130f
.
* [AC-1070] Add check for null password strength response
* [AC-1070] Fix broken show password icon
* [AC-1070] Add show password icon for current master password
This commit is contained in:
parent
a72f267558
commit
b108b4e71d
@ -161,6 +161,14 @@ namespace Bit.App
|
||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||
});
|
||||
}
|
||||
else if (message.Command == Constants.ForceUpdatePassword)
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await Application.Current.MainPage.Navigation.PushModalAsync(
|
||||
new NavigationPage(new UpdateTempPasswordPage()));
|
||||
});
|
||||
}
|
||||
else if (message.Command == Constants.PasswordlessLoginRequestKey
|
||||
|| message.Command == "unlocked"
|
||||
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
||||
|
@ -1,10 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
@ -73,13 +70,13 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _policy, value);
|
||||
}
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string MasterPassword { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
|
||||
public async Task InitAsync(bool forceSync = false)
|
||||
public virtual async Task InitAsync(bool forceSync = false)
|
||||
{
|
||||
if (forceSync)
|
||||
{
|
||||
|
@ -31,6 +31,8 @@ namespace Bit.App.Pages
|
||||
private readonly ILogger _logger;
|
||||
private readonly IWatchDeviceService _watchDeviceService;
|
||||
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
|
||||
private string _email;
|
||||
private string _masterPassword;
|
||||
@ -61,6 +63,8 @@ namespace Bit.App.Pages
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>();
|
||||
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
|
||||
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@ -294,6 +298,7 @@ namespace Bit.App.Pages
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdfConfig);
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
var passwordValid = false;
|
||||
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
|
||||
|
||||
if (storedKeyHash != null)
|
||||
{
|
||||
@ -305,9 +310,11 @@ namespace Bit.App.Pages
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
|
||||
try
|
||||
{
|
||||
await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
|
||||
passwordValid = true;
|
||||
var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
|
||||
await _cryptoService.SetKeyHashAsync(localKeyHash);
|
||||
@ -328,6 +335,14 @@ namespace Bit.App.Pages
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email, kdfConfig);
|
||||
await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key.Key, pinKey));
|
||||
}
|
||||
|
||||
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
|
||||
{
|
||||
// Save the ForcePasswordResetReason to force a password reset after unlock
|
||||
await _stateService.SetForcePasswordResetReasonAsync(
|
||||
ForcePasswordResetReason.WeakMasterPasswordOnLogin);
|
||||
}
|
||||
|
||||
MasterPassword = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
@ -352,6 +367,37 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the master password requires updating to meet the enforced policy requirements
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
private async Task<bool> RequirePasswordChangeAsync(MasterPasswordPolicyOptions options = null)
|
||||
{
|
||||
// If no policy options are provided, attempt to load them from the policy service
|
||||
var enforcedOptions = options ?? await _policyService.GetMasterPasswordPolicyOptions();
|
||||
|
||||
// No policy to enforce on login/unlock
|
||||
if (!(enforcedOptions is { EnforceOnLogin: true }))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var strength = _passwordGenerationService.PasswordStrength(
|
||||
MasterPassword, _passwordGenerationService.GetPasswordStrengthUserInput(_email))?.Score;
|
||||
|
||||
if (!strength.HasValue)
|
||||
{
|
||||
_logger.Error("Unable to evaluate master password strength during unlock");
|
||||
return false;
|
||||
}
|
||||
|
||||
return !await _policyService.EvaluateMasterPassword(
|
||||
strength.Value,
|
||||
MasterPassword,
|
||||
enforcedOptions
|
||||
);
|
||||
}
|
||||
|
||||
public async Task LogOutAsync()
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation,
|
||||
|
@ -46,7 +46,7 @@
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n UpdateMasterPasswordWarning}"
|
||||
Text="{Binding UpdateMasterPasswordWarningText }"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
@ -74,6 +74,40 @@
|
||||
HorizontalTextAlignment="Start" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row" IsVisible="{Binding RequireCurrentPassword }">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n CurrentMasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_currentMasterPassword"
|
||||
Text="{Binding CurrentMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
|
@ -1,27 +1,68 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
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.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class UpdateTempPasswordPageViewModel : BaseChangePasswordViewModel
|
||||
{
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
|
||||
private ForcePasswordResetReason _reason = ForcePasswordResetReason.AdminForcePasswordReset;
|
||||
|
||||
public UpdateTempPasswordPageViewModel()
|
||||
{
|
||||
PageTitle = AppResources.UpdateMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
SubmitCommand = new AsyncCommand(SubmitAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
}
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public AsyncCommand SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public Command ToggleConfirmPasswordCommand { get; }
|
||||
public Action UpdateTempPasswordSuccessAction { get; set; }
|
||||
public Action LogOutAction { get; set; }
|
||||
public string CurrentMasterPassword { get; set; }
|
||||
|
||||
public override async Task InitAsync(bool forceSync = false)
|
||||
{
|
||||
await base.InitAsync(forceSync);
|
||||
|
||||
var forcePasswordResetReason = await _stateService.GetForcePasswordResetReasonAsync();
|
||||
|
||||
if (forcePasswordResetReason.HasValue)
|
||||
{
|
||||
_reason = forcePasswordResetReason.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RequireCurrentPassword
|
||||
{
|
||||
get => _reason == ForcePasswordResetReason.WeakMasterPasswordOnLogin;
|
||||
}
|
||||
|
||||
public string UpdateMasterPasswordWarningText
|
||||
{
|
||||
get
|
||||
{
|
||||
return _reason == ForcePasswordResetReason.WeakMasterPasswordOnLogin
|
||||
? AppResources.UpdateWeakMasterPasswordWarning
|
||||
: AppResources.UpdateMasterPasswordWarning;
|
||||
}
|
||||
}
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
@ -42,6 +83,12 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
if (RequireCurrentPassword &&
|
||||
!await _userVerificationService.VerifyUser(CurrentMasterPassword, VerificationType.MasterPassword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve details for key generation
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
@ -53,21 +100,29 @@ namespace Bit.App.Pages
|
||||
// Create new encKey for the User
|
||||
var newEncKey = await _cryptoService.RemakeEncKeyAsync(key);
|
||||
|
||||
// Create request
|
||||
var request = new UpdateTempPasswordRequest
|
||||
{
|
||||
Key = newEncKey.Item2.EncryptedString,
|
||||
NewMasterPasswordHash = masterPasswordHash,
|
||||
MasterPasswordHint = Hint
|
||||
};
|
||||
|
||||
// Initiate API action
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.UpdatingPassword);
|
||||
await _apiService.PutUpdateTempPasswordAsync(request);
|
||||
|
||||
switch (_reason)
|
||||
{
|
||||
case ForcePasswordResetReason.AdminForcePasswordReset:
|
||||
await UpdateTempPasswordAsync(masterPasswordHash, newEncKey.Item2.EncryptedString);
|
||||
break;
|
||||
case ForcePasswordResetReason.WeakMasterPasswordOnLogin:
|
||||
await UpdatePasswordAsync(masterPasswordHash, newEncKey.Item2.EncryptedString);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
// Clear the force reset password reason
|
||||
await _stateService.SetForcePasswordResetReasonAsync(null);
|
||||
|
||||
_platformUtilsService.ShowToast(null, null, AppResources.UpdatedMasterPassword);
|
||||
|
||||
UpdateTempPasswordSuccessAction?.Invoke();
|
||||
}
|
||||
catch (ApiException e)
|
||||
@ -85,5 +140,32 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateTempPasswordAsync(string newMasterPasswordHash, string newEncKey)
|
||||
{
|
||||
var request = new UpdateTempPasswordRequest
|
||||
{
|
||||
Key = newEncKey,
|
||||
NewMasterPasswordHash = newMasterPasswordHash,
|
||||
MasterPasswordHint = Hint
|
||||
};
|
||||
|
||||
await _apiService.PutUpdateTempPasswordAsync(request);
|
||||
}
|
||||
|
||||
private async Task UpdatePasswordAsync(string newMasterPasswordHash, string newEncKey)
|
||||
{
|
||||
var currentPasswordHash = await _cryptoService.HashPasswordAsync(CurrentMasterPassword, null);
|
||||
|
||||
var request = new PasswordRequest
|
||||
{
|
||||
MasterPasswordHash = currentPasswordHash,
|
||||
Key = newEncKey,
|
||||
NewMasterPasswordHash = newMasterPasswordHash,
|
||||
MasterPasswordHint = Hint
|
||||
};
|
||||
|
||||
await _apiService.PostPasswordAsync(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
||||
using Bit.App.Effects;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
@ -15,6 +16,7 @@ namespace Bit.App.Pages
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
private NavigationPage _groupingsPage;
|
||||
@ -26,6 +28,7 @@ namespace Bit.App.Pages
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
|
||||
_groupingsPage = new NavigationPage(new GroupingsPage(true, previousPage: previousPage))
|
||||
{
|
||||
@ -95,6 +98,13 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_messagingService.Send("convertAccountToKeyConnector");
|
||||
}
|
||||
|
||||
var forcePasswordResetReason = await _stateService.GetForcePasswordResetReasonAsync();
|
||||
|
||||
if (forcePasswordResetReason.HasValue)
|
||||
{
|
||||
_messagingService.Send(Constants.ForceUpdatePassword);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
|
18
src/App/Resources/AppResources.Designer.cs
generated
18
src/App/Resources/AppResources.Designer.cs
generated
@ -1687,6 +1687,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current master password.
|
||||
/// </summary>
|
||||
public static string CurrentMasterPassword {
|
||||
get {
|
||||
return ResourceManager.GetString("CurrentMasterPassword", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Custom.
|
||||
/// </summary>
|
||||
@ -6452,6 +6461,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour..
|
||||
/// </summary>
|
||||
public static string UpdateWeakMasterPasswordWarning {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdateWeakMasterPasswordWarning", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Updating password.
|
||||
/// </summary>
|
||||
|
@ -2610,4 +2610,10 @@ Do you want to switch to this account?</value>
|
||||
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
|
||||
<value>There are no items that match the search</value>
|
||||
</data>
|
||||
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
|
||||
<value>Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</value>
|
||||
</data>
|
||||
<data name="CurrentMasterPassword" xml:space="preserve">
|
||||
<value>Current master password</value>
|
||||
</data>
|
||||
</root>
|
||||
|
@ -27,7 +27,7 @@ namespace Bit.Core.Abstractions
|
||||
Task<ProfileResponse> GetProfileAsync();
|
||||
Task<SyncResponse> GetSyncAsync();
|
||||
Task PostAccountKeysAsync(KeysRequest request);
|
||||
Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request);
|
||||
Task<VerifyMasterPasswordResponse> PostAccountVerifyPasswordAsync(PasswordVerificationRequest request);
|
||||
Task PostAccountRequestOTP();
|
||||
Task PostAccountVerifyOTPAsync(VerifyOTPRequest request);
|
||||
Task<CipherResponse> PostCipherAsync(CipherRequest request);
|
||||
@ -62,6 +62,7 @@ namespace Bit.Core.Abstractions
|
||||
Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request);
|
||||
Task PostEventsCollectAsync(IEnumerable<EventRequest> request);
|
||||
Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request);
|
||||
Task PostPasswordAsync(PasswordRequest request);
|
||||
Task DeleteAccountAsync(DeleteAccountRequest request);
|
||||
Task<OrganizationKeysResponse> GetOrganizationKeysAsync(string id);
|
||||
Task<OrganizationAutoEnrollStatusResponse> GetOrganizationAutoEnrollStatusAsync(string identifier);
|
||||
|
@ -134,6 +134,8 @@ namespace Bit.Core.Abstractions
|
||||
Task SetPushRegisteredTokenAsync(string value);
|
||||
Task<bool> GetUsesKeyConnectorAsync(string userId = null);
|
||||
Task SetUsesKeyConnectorAsync(bool? value, string userId = null);
|
||||
Task<ForcePasswordResetReason?> GetForcePasswordResetReasonAsync(string userId = null);
|
||||
Task SetForcePasswordResetReasonAsync(ForcePasswordResetReason? value, string userId = null);
|
||||
Task<Dictionary<string, OrganizationData>> GetOrganizationsAsync(string userId = null);
|
||||
Task SetOrganizationsAsync(Dictionary<string, OrganizationData> organizations, string userId = null);
|
||||
Task<PasswordGenerationOptions> GetPasswordGenerationOptionsAsync(string userId = null);
|
||||
|
@ -49,6 +49,7 @@
|
||||
public const string OtpAuthScheme = "otpauth";
|
||||
public const string AppLocaleKey = "appLocale";
|
||||
public const string ClearSensitiveFields = "clearSensitiveFields";
|
||||
public const string ForceUpdatePassword = "forceUpdatePassword";
|
||||
public const int SelectFileRequestCode = 42;
|
||||
public const int SelectFilePermissionRequestCode = 43;
|
||||
public const int SaveFileRequestCode = 44;
|
||||
|
@ -52,6 +52,7 @@ namespace Bit.Core.Models.Domain
|
||||
EmailVerified = copy.EmailVerified;
|
||||
HasPremiumPersonally = copy.HasPremiumPersonally;
|
||||
AvatarColor = copy.AvatarColor;
|
||||
ForcePasswordResetReason = copy.ForcePasswordResetReason;
|
||||
}
|
||||
|
||||
public string UserId;
|
||||
@ -66,6 +67,7 @@ namespace Bit.Core.Models.Domain
|
||||
public int? KdfParallelism;
|
||||
public bool? EmailVerified;
|
||||
public bool? HasPremiumPersonally;
|
||||
public ForcePasswordResetReason? ForcePasswordResetReason;
|
||||
}
|
||||
|
||||
public class AccountTokens
|
||||
|
16
src/Core/Models/Domain/ForcePasswordResetReason.cs
Normal file
16
src/Core/Models/Domain/ForcePasswordResetReason.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public enum ForcePasswordResetReason
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when an organization admin forces a user to reset their password.
|
||||
/// </summary>
|
||||
AdminForcePasswordReset,
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a user logs in with a master password that does not meet an organization's master password
|
||||
/// policy that is enforced on login.
|
||||
/// </summary>
|
||||
WeakMasterPasswordOnLogin
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
public bool RequireLower { get; set; }
|
||||
public bool RequireNumbers { get; set; }
|
||||
public bool RequireSpecial { get; set; }
|
||||
public bool EnforceOnLogin { get; set; }
|
||||
|
||||
public bool InEffect()
|
||||
{
|
||||
|
10
src/Core/Models/Request/PasswordRequest.cs
Normal file
10
src/Core/Models/Request/PasswordRequest.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Models.Request
|
||||
{
|
||||
public class PasswordRequest
|
||||
{
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public string NewMasterPasswordHash { get; set; }
|
||||
public string MasterPasswordHint { get; set; }
|
||||
public string Key { get; set; }
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.Enums;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.Models.Response
|
||||
@ -24,6 +26,7 @@ namespace Bit.Core.Models.Response
|
||||
public int? KdfParallelism { get; set; }
|
||||
public bool ForcePasswordReset { get; set; }
|
||||
public string KeyConnectorUrl { get; set; }
|
||||
public MasterPasswordPolicyOptions MasterPasswordPolicy { get; set; }
|
||||
[JsonIgnore]
|
||||
public KdfConfig KdfConfig => new KdfConfig(Kdf, KdfIterations, KdfMemory, KdfParallelism);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.Models.Response
|
||||
@ -8,6 +9,7 @@ namespace Bit.Core.Models.Response
|
||||
{
|
||||
public List<TwoFactorProviderType> TwoFactorProviders { get; set; }
|
||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders2 { get; set; }
|
||||
public MasterPasswordPolicyOptions MasterPasswordPolicy { get; set; }
|
||||
[JsonProperty("CaptchaBypassToken")]
|
||||
public string CaptchaToken { get; set; }
|
||||
}
|
||||
|
9
src/Core/Models/Response/VerifyMasterPasswordResponse.cs
Normal file
9
src/Core/Models/Response/VerifyMasterPasswordResponse.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.Response
|
||||
{
|
||||
public class VerifyMasterPasswordResponse
|
||||
{
|
||||
public MasterPasswordPolicyOptions MasterPasswordPolicy { get; set; }
|
||||
}
|
||||
}
|
@ -176,10 +176,10 @@ namespace Bit.Core.Services
|
||||
return SendAsync<KeysRequest, object>(HttpMethod.Post, "/accounts/keys", request, true, false);
|
||||
}
|
||||
|
||||
public Task PostAccountVerifyPasswordAsync(PasswordVerificationRequest request)
|
||||
public Task<VerifyMasterPasswordResponse> PostAccountVerifyPasswordAsync(PasswordVerificationRequest request)
|
||||
{
|
||||
return SendAsync<PasswordVerificationRequest, object>(HttpMethod.Post, "/accounts/verify-password", request,
|
||||
true, false);
|
||||
return SendAsync<PasswordVerificationRequest, VerifyMasterPasswordResponse>(HttpMethod.Post, "/accounts/verify-password", request,
|
||||
true, true);
|
||||
}
|
||||
|
||||
public Task PostAccountRequestOTP()
|
||||
@ -199,6 +199,11 @@ namespace Bit.Core.Services
|
||||
request, true, false);
|
||||
}
|
||||
|
||||
public Task PostPasswordAsync(PasswordRequest request)
|
||||
{
|
||||
return SendAsync<PasswordRequest, object>(HttpMethod.Post, "/accounts/password", request, true, false);
|
||||
}
|
||||
|
||||
public Task DeleteAccountAsync(DeleteAccountRequest request)
|
||||
{
|
||||
return SendAsync<DeleteAccountRequest, object>(HttpMethod.Delete, "/accounts", request, true, false);
|
||||
|
@ -26,11 +26,16 @@ namespace Bit.Core.Services
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly bool _setCryptoKeys;
|
||||
|
||||
private readonly LazyResolve<IWatchDeviceService> _watchDeviceService = new LazyResolve<IWatchDeviceService>();
|
||||
private SymmetricCryptoKey _key;
|
||||
|
||||
private string _authedUserId;
|
||||
private MasterPasswordPolicyOptions _masterPasswordPolicy;
|
||||
private ForcePasswordResetReason? _2faForcePasswordResetReason;
|
||||
|
||||
public AuthService(
|
||||
ICryptoService cryptoService,
|
||||
ICryptoFunctionService cryptoFunctionService,
|
||||
@ -44,6 +49,7 @@ namespace Bit.Core.Services
|
||||
IVaultTimeoutService vaultTimeoutService,
|
||||
IKeyConnectorService keyConnectorService,
|
||||
IPasswordGenerationService passwordGenerationService,
|
||||
IPolicyService policyService,
|
||||
bool setCryptoKeys = true)
|
||||
{
|
||||
_cryptoService = cryptoService;
|
||||
@ -57,6 +63,7 @@ namespace Bit.Core.Services
|
||||
_messagingService = messagingService;
|
||||
_keyConnectorService = keyConnectorService;
|
||||
_passwordGenerationService = passwordGenerationService;
|
||||
_policyService = policyService;
|
||||
_setCryptoKeys = setCryptoKeys;
|
||||
|
||||
TwoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||
@ -136,11 +143,56 @@ namespace Bit.Core.Services
|
||||
public async Task<AuthResult> LogInAsync(string email, string masterPassword, string captchaToken)
|
||||
{
|
||||
SelectedTwoFactorProviderType = null;
|
||||
_2faForcePasswordResetReason = null;
|
||||
var key = await MakePreloginKeyAsync(masterPassword, email);
|
||||
var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key);
|
||||
var localHashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization);
|
||||
return await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, null, null,
|
||||
null, captchaToken);
|
||||
var result = await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, null, null, null, captchaToken);
|
||||
|
||||
if (await RequirePasswordChangeAsync(email, masterPassword))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_authedUserId))
|
||||
{
|
||||
// Authentication was successful, save the WeakMasterPasswordOnLogin flag for the user
|
||||
result.ForcePasswordReset = true;
|
||||
await _stateService.SetForcePasswordResetReasonAsync(
|
||||
ForcePasswordResetReason.WeakMasterPasswordOnLogin, _authedUserId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Authentication not fully successful (likely 2FA), store flag for LogInTwoFactorAsync()
|
||||
_2faForcePasswordResetReason = ForcePasswordResetReason.WeakMasterPasswordOnLogin;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the supplied master password against the master password policy provided by the Identity response.
|
||||
/// </summary>
|
||||
/// <param name="email"></param>
|
||||
/// <param name="masterPassword"></param>
|
||||
/// <returns>True if the master password does NOT meet any policy requirements, false otherwise (or if no policy present)</returns>
|
||||
private async Task<bool> RequirePasswordChangeAsync(string email, string masterPassword)
|
||||
{
|
||||
// No policy with EnforceOnLogin enabled, we're done.
|
||||
if (!(_masterPasswordPolicy is { EnforceOnLogin: true }))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var strength = _passwordGenerationService.PasswordStrength(
|
||||
masterPassword,
|
||||
_passwordGenerationService.GetPasswordStrengthUserInput(email)
|
||||
)?.Score;
|
||||
|
||||
if (!strength.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !await _policyService.EvaluateMasterPassword(strength.Value, masterPassword, _masterPasswordPolicy);
|
||||
}
|
||||
|
||||
public async Task<AuthResult> LogInPasswordlessAsync(string email, string accessCode, string authRequestId, byte[] decryptionKey, string userKeyCiphered, string localHashedPasswordCiphered)
|
||||
@ -157,15 +209,26 @@ namespace Bit.Core.Services
|
||||
return await LogInHelperAsync(null, null, null, code, codeVerifier, redirectUrl, null, orgId: orgId);
|
||||
}
|
||||
|
||||
public Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken,
|
||||
public async Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken,
|
||||
string captchaToken, bool? remember = null)
|
||||
{
|
||||
if (captchaToken != null)
|
||||
{
|
||||
CaptchaToken = captchaToken;
|
||||
}
|
||||
return LogInHelperAsync(Email, MasterPasswordHash, LocalMasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key,
|
||||
var result = await LogInHelperAsync(Email, MasterPasswordHash, LocalMasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key,
|
||||
twoFactorProvider, twoFactorToken, remember, CaptchaToken, authRequestId: AuthRequestId);
|
||||
|
||||
// If we successfully authenticated and we have a saved _2faForcePasswordResetReason reason from LogInAsync()
|
||||
if (!string.IsNullOrEmpty(_authedUserId) && _2faForcePasswordResetReason.HasValue)
|
||||
{
|
||||
// Save the forcePasswordReset reason with the state service to force a password reset for the user
|
||||
result.ForcePasswordReset = true;
|
||||
await _stateService.SetForcePasswordResetReasonAsync(
|
||||
_2faForcePasswordResetReason, _authedUserId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<AuthResult> LogInCompleteAsync(string email, string masterPassword,
|
||||
@ -367,6 +430,7 @@ namespace Bit.Core.Services
|
||||
TwoFactorProvidersData = response.TwoFactorResponse.TwoFactorProviders2;
|
||||
result.TwoFactorProviders = response.TwoFactorResponse.TwoFactorProviders2;
|
||||
CaptchaToken = response.TwoFactorResponse.CaptchaToken;
|
||||
_masterPasswordPolicy = response.TwoFactorResponse.MasterPasswordPolicy;
|
||||
await _tokenService.ClearTwoFactorTokenAsync(email);
|
||||
return result;
|
||||
}
|
||||
@ -374,6 +438,7 @@ namespace Bit.Core.Services
|
||||
var tokenResponse = response.TokenResponse;
|
||||
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
|
||||
result.ForcePasswordReset = tokenResponse.ForcePasswordReset;
|
||||
_masterPasswordPolicy = tokenResponse.MasterPasswordPolicy;
|
||||
if (tokenResponse.TwoFactorToken != null)
|
||||
{
|
||||
await _tokenService.SetTwoFactorTokenAsync(tokenResponse.TwoFactorToken, email);
|
||||
@ -391,6 +456,9 @@ namespace Bit.Core.Services
|
||||
KdfMemory = tokenResponse.KdfMemory,
|
||||
KdfParallelism = tokenResponse.KdfParallelism,
|
||||
HasPremiumPersonally = _tokenService.GetPremium(),
|
||||
ForcePasswordResetReason = result.ForcePasswordReset
|
||||
? ForcePasswordResetReason.AdminForcePasswordReset
|
||||
: (ForcePasswordResetReason?)null,
|
||||
},
|
||||
new Account.AccountTokens()
|
||||
{
|
||||
@ -473,6 +541,7 @@ namespace Bit.Core.Services
|
||||
|
||||
}
|
||||
|
||||
_authedUserId = _tokenService.GetUserId();
|
||||
await _stateService.SetBiometricLockedAsync(false);
|
||||
_messagingService.Send("loggedIn");
|
||||
return result;
|
||||
@ -490,6 +559,8 @@ namespace Bit.Core.Services
|
||||
SsoRedirectUrl = null;
|
||||
TwoFactorProvidersData = null;
|
||||
SelectedTwoFactorProviderType = null;
|
||||
_masterPasswordPolicy = null;
|
||||
_authedUserId = null;
|
||||
}
|
||||
|
||||
public async Task<List<PasswordlessLoginResponse>> GetPasswordlessLoginRequestsAsync()
|
||||
|
@ -148,28 +148,34 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
var requireUpper = GetPolicyBool(currentPolicy, "requireUpper");
|
||||
if (requireUpper != null && (bool)requireUpper)
|
||||
if (requireUpper == true)
|
||||
{
|
||||
enforcedOptions.RequireUpper = true;
|
||||
}
|
||||
|
||||
var requireLower = GetPolicyBool(currentPolicy, "requireLower");
|
||||
if (requireLower != null && (bool)requireLower)
|
||||
if (requireLower == true)
|
||||
{
|
||||
enforcedOptions.RequireLower = true;
|
||||
}
|
||||
|
||||
var requireNumbers = GetPolicyBool(currentPolicy, "requireNumbers");
|
||||
if (requireNumbers != null && (bool)requireNumbers)
|
||||
if (requireNumbers == true)
|
||||
{
|
||||
enforcedOptions.RequireNumbers = true;
|
||||
}
|
||||
|
||||
var requireSpecial = GetPolicyBool(currentPolicy, "requireSpecial");
|
||||
if (requireSpecial != null && (bool)requireSpecial)
|
||||
if (requireSpecial == true)
|
||||
{
|
||||
enforcedOptions.RequireSpecial = true;
|
||||
}
|
||||
|
||||
var enforceOnLogin = GetPolicyBool(currentPolicy, "enforceOnLogin");
|
||||
if (enforceOnLogin == true)
|
||||
{
|
||||
enforcedOptions.EnforceOnLogin = true;
|
||||
}
|
||||
}
|
||||
|
||||
return enforcedOptions;
|
||||
|
@ -1039,6 +1039,22 @@ namespace Bit.Core.Services
|
||||
await SetValueAsync(Constants.UsesKeyConnectorKey(reconciledOptions.UserId), value, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task<ForcePasswordResetReason?> GetForcePasswordResetReasonAsync(string userId = null)
|
||||
{
|
||||
var reconcileOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
return (await GetAccountAsync(reconcileOptions))?.Profile?.ForcePasswordResetReason;
|
||||
}
|
||||
|
||||
public async Task SetForcePasswordResetReasonAsync(ForcePasswordResetReason? value, string userId = null)
|
||||
{
|
||||
var reconcileOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var account = await GetAccountAsync(reconcileOptions);
|
||||
account.Profile.ForcePasswordResetReason = value;
|
||||
await SaveAccountAsync(account, reconcileOptions);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, OrganizationData>> GetOrganizationsAsync(string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
|
@ -80,7 +80,7 @@ namespace Bit.Core.Utilities
|
||||
var totpService = new TotpService(cryptoFunctionService);
|
||||
var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService,
|
||||
tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService,
|
||||
keyConnectorService, passwordGenerationService);
|
||||
keyConnectorService, passwordGenerationService, policyService);
|
||||
var exportService = new ExportService(folderService, cipherService, cryptoService);
|
||||
var auditService = new AuditService(cryptoFunctionService, apiService);
|
||||
var environmentService = new EnvironmentService(apiService, stateService, conditionedRunner);
|
||||
|
Loading…
Reference in New Issue
Block a user