[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:
Shane Melton 2023-04-17 07:35:50 -07:00 committed by GitHub
parent a72f267558
commit b108b4e71d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 379 additions and 33 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
}
}

View file

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

View 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; }
}
}

View file

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

View file

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

View file

@ -0,0 +1,9 @@
using Bit.Core.Models.Domain;
namespace Bit.Core.Models.Response
{
public class VerifyMasterPasswordResponse
{
public MasterPasswordPolicyOptions MasterPasswordPolicy { get; set; }
}
}

View file

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

View file

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

View file

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

View file

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

View file

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