mirror of
https://github.com/bitwarden/android.git
synced 2024-12-24 01:48:25 +03:00
[Reset Password v1] Update Temp Password (#1492)
* [Reset Password v1] Update Temp Password * fixed order of operations for reset temp password flow * Refactored bool with auth result * Finished removal of temp password flow from set password * iOS extension support plus extension bugfixes Co-authored-by: addison <addisonbeck1@gmail.com> Co-authored-by: Matt Portune <mportune@bitwarden.com>
This commit is contained in:
parent
750faf8a83
commit
271e6b3d92
30 changed files with 795 additions and 97 deletions
|
@ -17,21 +17,12 @@ namespace Bit.App
|
|||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
private readonly MobileI18nService _i18nService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ICollectionService _collectionService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly ISearchService _searchService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IStorageService _storageService;
|
||||
|
@ -54,20 +45,10 @@ namespace Bit.App
|
|||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_tokenService = ServiceContainer.Resolve<ITokenService>("tokenService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
_settingsService = ServiceContainer.Resolve<ISettingsService>("settingsService");
|
||||
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
|
||||
"passwordGenerationService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
|
||||
Bootstrap();
|
||||
|
@ -240,22 +221,7 @@ namespace Bit.App
|
|||
|
||||
private async Task LogOutAsync(bool expired)
|
||||
{
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
await Task.WhenAll(
|
||||
_syncService.SetLastSyncAsync(DateTime.MinValue),
|
||||
_tokenService.ClearTokenAsync(),
|
||||
_cryptoService.ClearKeysAsync(),
|
||||
_userService.ClearAsync(),
|
||||
_settingsService.ClearAsync(userId),
|
||||
_cipherService.ClearAsync(userId),
|
||||
_folderService.ClearAsync(userId),
|
||||
_collectionService.ClearAsync(userId),
|
||||
_passwordGenerationService.ClearAsync(),
|
||||
_vaultTimeoutService.ClearAsync(),
|
||||
_stateService.PurgeAsync(),
|
||||
_deviceActionService.ClearCacheAsync());
|
||||
_vaultTimeoutService.BiometricLocked = true;
|
||||
_searchService.ClearIndex();
|
||||
await AppHelpers.LogOutAsync();
|
||||
_authService.LogOut(() =>
|
||||
{
|
||||
Current.MainPage = new HomePage();
|
||||
|
|
177
src/App/Pages/Accounts/BaseChangePasswordViewModel.cs
Normal file
177
src/App/Pages/Accounts/BaseChangePasswordViewModel.cs
Normal file
|
@ -0,0 +1,177 @@
|
|||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class BaseChangePasswordViewModel : BaseViewModel
|
||||
{
|
||||
protected readonly IPlatformUtilsService _platformUtilsService;
|
||||
protected readonly IUserService _userService;
|
||||
protected readonly IPolicyService _policyService;
|
||||
protected readonly IPasswordGenerationService _passwordGenerationService;
|
||||
protected readonly II18nService _i18nService;
|
||||
protected readonly ICryptoService _cryptoService;
|
||||
protected readonly IDeviceActionService _deviceActionService;
|
||||
protected readonly IApiService _apiService;
|
||||
|
||||
private bool _showPassword;
|
||||
private bool _isPolicyInEffect;
|
||||
private string _policySummary;
|
||||
private MasterPasswordPolicyOptions _policy;
|
||||
|
||||
protected BaseChangePasswordViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_passwordGenerationService =
|
||||
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
}
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new[] { nameof(ShowPasswordIcon) });
|
||||
}
|
||||
|
||||
public bool IsPolicyInEffect
|
||||
{
|
||||
get => _isPolicyInEffect;
|
||||
set => SetProperty(ref _isPolicyInEffect, value);
|
||||
}
|
||||
|
||||
public string PolicySummary
|
||||
{
|
||||
get => _policySummary;
|
||||
set => SetProperty(ref _policySummary, value);
|
||||
}
|
||||
|
||||
public MasterPasswordPolicyOptions Policy
|
||||
{
|
||||
get => _policy;
|
||||
set => SetProperty(ref _policy, value);
|
||||
}
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public string MasterPassword { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await CheckPasswordPolicy();
|
||||
}
|
||||
|
||||
private async Task CheckPasswordPolicy()
|
||||
{
|
||||
Policy = await _policyService.GetMasterPasswordPolicyOptions();
|
||||
IsPolicyInEffect = Policy?.InEffect() ?? false;
|
||||
if (!IsPolicyInEffect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bullet = "\n" + "".PadLeft(4) + "\u2022 ";
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(_i18nService.T("MasterPasswordPolicyInEffect"));
|
||||
if (Policy.MinComplexity > 0)
|
||||
{
|
||||
sb.Append(bullet)
|
||||
.Append(string.Format(_i18nService.T("PolicyInEffectMinComplexity"), Policy.MinComplexity));
|
||||
}
|
||||
if (Policy.MinLength > 0)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectMinLength"), Policy.MinLength));
|
||||
}
|
||||
if (Policy.RequireUpper)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectUppercase"));
|
||||
}
|
||||
if (Policy.RequireLower)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectLowercase"));
|
||||
}
|
||||
if (Policy.RequireNumbers)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectNumbers"));
|
||||
}
|
||||
if (Policy.RequireSpecial)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectSpecial"), "!@#$%^&*"));
|
||||
}
|
||||
PolicySummary = sb.ToString();
|
||||
}
|
||||
|
||||
protected async Task<bool> ValidateMasterPasswordAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
if (IsPolicyInEffect)
|
||||
{
|
||||
var userInput = await GetPasswordStrengthUserInput();
|
||||
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInput);
|
||||
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordPolicyValidationMessage,
|
||||
AppResources.MasterPasswordPolicyValidationTitle, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MasterPassword.Length < 8)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordLengthValMessage,
|
||||
AppResources.MasterPasswordPolicyValidationTitle, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (MasterPassword != ConfirmMasterPassword)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordConfirmationValMessage,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetPasswordStrengthUserInput()
|
||||
{
|
||||
var email = await _userService.GetEmailAsync();
|
||||
List<string> userInput = null;
|
||||
var atPosition = email.IndexOf('@');
|
||||
if (atPosition > -1)
|
||||
{
|
||||
var rx = new Regex("/[^A-Za-z0-9]/", RegexOptions.Compiled);
|
||||
var data = rx.Split(email.Substring(0, atPosition).Trim().ToLower());
|
||||
userInput = new List<string>(data);
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,6 +29,8 @@ namespace Bit.App.Pages
|
|||
_vm.Page = this;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
|
@ -122,5 +124,11 @@ namespace Bit.App.Pages
|
|||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
|
||||
private async Task UpdateTempPasswordAsync()
|
||||
{
|
||||
var page = new UpdateTempPasswordPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,6 @@ using System;
|
|||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using Xamarin.Essentials;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
|
@ -78,6 +73,7 @@ namespace Bit.App.Pages
|
|||
public bool RememberEmail { get; set; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action LogInSuccessAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
protected override II18nService i18nService => _i18nService;
|
||||
|
@ -100,15 +96,14 @@ namespace Bit.App.Pages
|
|||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (!Email.Contains("@"))
|
||||
|
@ -121,8 +116,7 @@ namespace Bit.App.Pages
|
|||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -163,6 +157,10 @@ namespace Bit.App.Pages
|
|||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
}
|
||||
else if (response.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
|
@ -179,7 +177,7 @@ namespace Bit.App.Pages
|
|||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ namespace Bit.App.Pages
|
|||
_vm.StartSetPasswordAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
|
@ -103,6 +105,12 @@ namespace Bit.App.Pages
|
|||
var page = new SetPasswordPage(_appOptions, _vm.OrgIdentifier);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task UpdateTempPasswordAsync()
|
||||
{
|
||||
var page = new UpdateTempPasswordPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task SsoAuthSuccessAsync()
|
||||
{
|
||||
|
|
|
@ -60,6 +60,7 @@ namespace Bit.App.Pages
|
|||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action SsoAuthSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
|
@ -185,6 +186,10 @@ namespace Bit.App.Pages
|
|||
else if (response.ResetMasterPassword)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
}
|
||||
else if (response.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -137,7 +137,7 @@
|
|||
<FormattedString>
|
||||
<Span Text="{u:I18n AcceptPolicies}" />
|
||||
<Span Text="{u:I18n TermsOfService}"
|
||||
TextColor="{StaticResource HyperlinkColor}">
|
||||
TextColor="{DynamicResource HyperlinkColor}">
|
||||
<Span.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding PoliciesClickCommand}"
|
||||
CommandParameter="https://bitwarden.com/terms/" />
|
||||
|
@ -145,7 +145,7 @@
|
|||
</Span>
|
||||
<Span Text=", " />
|
||||
<Span Text="{u:I18n PrivacyPolicy}"
|
||||
TextColor="{StaticResource HyperlinkColor}">
|
||||
TextColor="{DynamicResource HyperlinkColor}">
|
||||
<Span.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding PoliciesClickCommand}"
|
||||
CommandParameter="https://bitwarden.com/privacy/" />
|
||||
|
|
|
@ -90,44 +90,47 @@ namespace Bit.App.Pages
|
|||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (!Email.Contains("@"))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidEmail, AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (MasterPassword.Length < 8)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
AppResources.MasterPasswordLengthValMessage, AppResources.Ok);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordLengthValMessage,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (MasterPassword != ConfirmMasterPassword)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
AppResources.MasterPasswordConfirmationValMessage, AppResources.Ok);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordConfirmationValMessage,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (ShowTerms && !AcceptPolicies)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
AppResources.AcceptPoliciesError, AppResources.Ok);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AcceptPoliciesError,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -190,7 +193,7 @@ namespace Bit.App.Pages
|
|||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,6 +90,7 @@ namespace Bit.App.Pages
|
|||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public Action SetPasswordSuccessAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public string OrgIdentifier { get; set; }
|
||||
public string OrgId { get; set; }
|
||||
|
@ -221,7 +222,6 @@ namespace Bit.App.Pages
|
|||
}
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
SetPasswordSuccessAction?.Invoke();
|
||||
}
|
||||
catch (ApiException e)
|
||||
|
|
|
@ -40,6 +40,8 @@ namespace Bit.App.Pages
|
|||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.TwoFactorAuthSuccessAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.CloseAction = async () => await Navigation.PopModalAsync();
|
||||
DuoWebView = _duoWebView;
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
|
@ -189,6 +191,12 @@ namespace Bit.App.Pages
|
|||
var page = new SetPasswordPage(_appOptions, _orgIdentifier);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task UpdateTempPasswordAsync()
|
||||
{
|
||||
var page = new UpdateTempPasswordPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task TwoFactorAuthSuccessAsync()
|
||||
{
|
||||
|
|
|
@ -49,7 +49,7 @@ namespace Bit.App.Pages
|
|||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
|
||||
PageTitle = AppResources.TwoStepLogin;
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
|
@ -113,6 +113,7 @@ namespace Bit.App.Pages
|
|||
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
|
||||
public void Init()
|
||||
{
|
||||
|
@ -243,12 +244,13 @@ namespace Bit.App.Pages
|
|||
if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError))
|
||||
{
|
||||
var message = AppResources.Fido2CheckBrowser + "\n\n" + resultError;
|
||||
await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred);
|
||||
await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.Fido2CheckBrowser,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -262,14 +264,14 @@ namespace Bit.App.Pages
|
|||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Token))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.VerificationCode),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (SelectedProviderType == TwoFactorProviderType.Email ||
|
||||
|
@ -293,6 +295,10 @@ namespace Bit.App.Pages
|
|||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
}
|
||||
else if (result.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
|
@ -306,7 +312,7 @@ namespace Bit.App.Pages
|
|||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -344,7 +350,7 @@ namespace Bit.App.Pages
|
|||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
try
|
||||
|
@ -375,7 +381,8 @@ namespace Bit.App.Pages
|
|||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.VerificationEmailNotSent);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.VerificationEmailNotSent,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
160
src/App/Pages/Accounts/UpdateTempPasswordPage.xaml
Normal file
160
src/App/Pages/Accounts/UpdateTempPasswordPage.xaml
Normal file
|
@ -0,0 +1,160 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.UpdateTempPasswordPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:UpdateTempPasswordPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:UpdateTempPasswordPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n LogOut}" Clicked="LogOut_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid Margin="0, 12, 0, 0"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{u:I18n UpdateMasterPasswordWarning}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{Binding PolicySummary}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Start" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n MasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<controls:FaButton
|
||||
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}" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n RetypeMasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_confirmMasterPassword"
|
||||
Text="{Binding ConfirmMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding ToggleConfirmPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHint}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_hint"
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
83
src/App/Pages/Accounts/UpdateTempPasswordPage.xaml.cs
Normal file
83
src/App/Pages/Accounts/UpdateTempPasswordPage.xaml.cs
Normal file
|
@ -0,0 +1,83 @@
|
|||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class UpdateTempPasswordPage : BaseContentPage
|
||||
{
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly UpdateTempPasswordPageViewModel _vm;
|
||||
|
||||
public UpdateTempPasswordPage()
|
||||
{
|
||||
// Service Init
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
|
||||
// Service Use
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
|
||||
// Binding
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as UpdateTempPasswordPageViewModel;
|
||||
_vm.Page = this;
|
||||
|
||||
// Actions Declaration
|
||||
_vm.LogOutAction = () =>
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
};
|
||||
_vm.UpdateTempPasswordSuccessAction = () => Device.BeginInvokeOnMainThread(UpdateTempPasswordSuccess);
|
||||
|
||||
// Link fields that will be referenced in codebehind
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
ConfirmMasterPasswordEntry = _confirmMasterPassword;
|
||||
|
||||
// Return Types and Commands
|
||||
_masterPassword.ReturnType = ReturnType.Next;
|
||||
_masterPassword.ReturnCommand = new Command(() => _confirmMasterPassword.Focus());
|
||||
_confirmMasterPassword.ReturnType = ReturnType.Next;
|
||||
_confirmMasterPassword.ReturnCommand = new Command(() => _hint.Focus());
|
||||
}
|
||||
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
public Entry ConfirmMasterPasswordEntry { get; set; }
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
RequestFocus(_masterPassword);
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void LogOut_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation,
|
||||
AppResources.LogOut, AppResources.Yes, AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_vm.LogOutAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTempPasswordSuccess()
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
}
|
||||
}
|
||||
}
|
90
src/App/Pages/Accounts/UpdateTempPasswordPageViewModel.cs
Normal file
90
src/App/Pages/Accounts/UpdateTempPasswordPageViewModel.cs
Normal file
|
@ -0,0 +1,90 @@
|
|||
using Bit.App.Resources;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Request;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class UpdateTempPasswordPageViewModel : BaseChangePasswordViewModel
|
||||
{
|
||||
public UpdateTempPasswordPageViewModel()
|
||||
{
|
||||
PageTitle = AppResources.UpdateMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
}
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public Command ToggleConfirmPasswordCommand { get; }
|
||||
public Action UpdateTempPasswordSuccessAction { get; set; }
|
||||
public Action LogOutAction { get; set; }
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as UpdateTempPasswordPage).MasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
public void ToggleConfirmPassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as UpdateTempPasswordPage).ConfirmMasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (!await ValidateMasterPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve details for key generation
|
||||
var kdf = await _userService.GetKdfAsync();
|
||||
var kdfIterations = await _userService.GetKdfIterationsAsync();
|
||||
var email = await _userService.GetEmailAsync();
|
||||
|
||||
// Create new key and hash new password
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdf, kdfIterations);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
|
||||
// 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);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
UpdateTempPasswordSuccessAction?.Invoke();
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UpdatePasswordError,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
src/App/Resources/AppResources.Designer.cs
generated
30
src/App/Resources/AppResources.Designer.cs
generated
|
@ -5362,6 +5362,36 @@ namespace Bit.App.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
public static string UpdatedMasterPassword {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdatedMasterPassword", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string UpdateMasterPassword {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdateMasterPassword", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string UpdateMasterPasswordWarning {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdateMasterPasswordWarning", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string UpdatingPassword {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdatingPassword", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string UpdatePasswordError {
|
||||
get {
|
||||
return ResourceManager.GetString("UpdatePasswordError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your account's fingerprint phrase.
|
||||
/// </summary>
|
||||
|
|
|
@ -2016,6 +2016,21 @@
|
|||
<data name="CaptchaFailed" xml:space="preserve">
|
||||
<value>Captcha Failed. Please try again.</value>
|
||||
</data>
|
||||
<data name="UpdatedMasterPassword" xml:space="preserve">
|
||||
<value>Updated Master Password</value>
|
||||
</data>
|
||||
<data name="UpdateMasterPassword" xml:space="preserve">
|
||||
<value>Update Master Password</value>
|
||||
</data>
|
||||
<data name="UpdateMasterPasswordWarning" xml:space="preserve">
|
||||
<value>Your Master Password was recently changed by an administrator in your organization. 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="UpdatingPassword" xml:space="preserve">
|
||||
<value>Updating Password</value>
|
||||
</data>
|
||||
<data name="UpdatePasswordError" xml:space="preserve">
|
||||
<value>Currently unable to update password</value>
|
||||
</data>
|
||||
<data name="Fido2Title" xml:space="preserve">
|
||||
<value>FIDO2 WebAuthn</value>
|
||||
</data>
|
||||
|
|
|
@ -462,5 +462,40 @@ namespace Bit.App.Utilities
|
|||
var multiByteEscaped = Regex.Replace(escaped, "%([0-9A-F]{2})", EncodeMultibyte);
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped));
|
||||
}
|
||||
|
||||
public static async Task LogOutAsync()
|
||||
{
|
||||
var userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
var syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
var tokenService = ServiceContainer.Resolve<ITokenService>("tokenService");
|
||||
var cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
var settingsService = ServiceContainer.Resolve<ISettingsService>("settingsService");
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
var folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
var collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
var passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
|
||||
"passwordGenerationService");
|
||||
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
var searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||
|
||||
var userId = await userService.GetUserIdAsync();
|
||||
await Task.WhenAll(
|
||||
syncService.SetLastSyncAsync(DateTime.MinValue),
|
||||
tokenService.ClearTokenAsync(),
|
||||
cryptoService.ClearKeysAsync(),
|
||||
userService.ClearAsync(),
|
||||
settingsService.ClearAsync(userId),
|
||||
cipherService.ClearAsync(userId),
|
||||
folderService.ClearAsync(userId),
|
||||
collectionService.ClearAsync(userId),
|
||||
passwordGenerationService.ClearAsync(),
|
||||
vaultTimeoutService.ClearAsync(),
|
||||
stateService.PurgeAsync(),
|
||||
deviceActionService.ClearCacheAsync());
|
||||
vaultTimeoutService.BiometricLocked = true;
|
||||
searchService.ClearIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ namespace Bit.Core.Abstractions
|
|||
Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request);
|
||||
Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request);
|
||||
Task PostEventsCollectAsync(IEnumerable<EventRequest> request);
|
||||
Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request);
|
||||
Task<OrganizationKeysResponse> GetOrganizationKeysAsync(string id);
|
||||
Task<OrganizationAutoEnrollStatusResponse> GetOrganizationAutoEnrollStatusAsync(string identifier);
|
||||
Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId,
|
||||
|
|
|
@ -19,11 +19,13 @@ namespace Bit.Core.Abstractions
|
|||
Task<Organization> GetOrganizationByIdentifierAsync(string identifier);
|
||||
Task<string> GetSecurityStampAsync();
|
||||
Task<bool> GetEmailVerifiedAsync();
|
||||
Task<bool> GetForcePasswordReset();
|
||||
Task<string> GetUserIdAsync();
|
||||
Task<bool> IsAuthenticatedAsync();
|
||||
Task ReplaceOrganizationsAsync(Dictionary<string, OrganizationData> organizations);
|
||||
Task SetInformationAsync(string userId, string email, KdfType kdf, int? kdfIterations);
|
||||
Task SetSecurityStampAsync(string stamp);
|
||||
Task SetEmailVerifiedAsync(bool emailVerified);
|
||||
Task SetForcePasswordReset(bool forcePasswordReset);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace Bit.Core.Models.Domain
|
|||
public bool CaptchaNeeded => !string.IsNullOrWhiteSpace(CaptchaSiteKey);
|
||||
public string CaptchaSiteKey { get; set; }
|
||||
public bool ResetMasterPassword { get; set; }
|
||||
public bool ForcePasswordReset { get; set; }
|
||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
9
src/Core/Models/Request/UpdateTempPasswordRequest.cs
Normal file
9
src/Core/Models/Request/UpdateTempPasswordRequest.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Bit.Core.Models.Request
|
||||
{
|
||||
public class UpdateTempPasswordRequest
|
||||
{
|
||||
public string NewMasterPasswordHash { get; set; }
|
||||
public string MasterPasswordHint { get; set; }
|
||||
public string Key { get; set; }
|
||||
}
|
||||
}
|
|
@ -20,5 +20,6 @@ namespace Bit.Core.Models.Response
|
|||
public string TwoFactorToken { get; set; }
|
||||
public KdfType Kdf { get; set; }
|
||||
public int? KdfIterations { get; set; }
|
||||
public bool ForcePasswordReset { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.Core.Models.Response
|
||||
{
|
||||
|
@ -15,6 +16,7 @@ namespace Bit.Core.Models.Response
|
|||
public string Key { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public string SecurityStamp { get; set; }
|
||||
public bool ForcePasswordReset { get; set; }
|
||||
public List<ProfileOrganizationResponse> Organizations { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -177,6 +177,12 @@ namespace Bit.Core.Services
|
|||
return SendAsync<PasswordVerificationRequest, object>(HttpMethod.Post, "/accounts/verify-password", request,
|
||||
true, false);
|
||||
}
|
||||
|
||||
public Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request)
|
||||
{
|
||||
return SendAsync<UpdateTempPasswordRequest, object>(HttpMethod.Put, "/accounts/update-temp-password",
|
||||
request, true, false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
|
@ -343,6 +343,7 @@ namespace Bit.Core.Services
|
|||
|
||||
var tokenResponse = response.TokenResponse;
|
||||
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
|
||||
result.ForcePasswordReset = tokenResponse.ForcePasswordReset;
|
||||
if (tokenResponse.TwoFactorToken != null)
|
||||
{
|
||||
await _tokenService.SetTwoFactorTokenAsync(tokenResponse.TwoFactorToken, email);
|
||||
|
|
|
@ -328,6 +328,7 @@ namespace Bit.Core.Services
|
|||
var organizations = response.Organizations.ToDictionary(o => o.Id, o => new OrganizationData(o));
|
||||
await _userService.ReplaceOrganizationsAsync(organizations);
|
||||
await _userService.SetEmailVerifiedAsync(response.EmailVerified);
|
||||
await _userService.SetForcePasswordReset(response.ForcePasswordReset);
|
||||
}
|
||||
|
||||
private async Task SyncFoldersAsync(string userId, List<FolderResponse> response)
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace Bit.Core.Services
|
|||
private KdfType? _kdf;
|
||||
private int? _kdfIterations;
|
||||
private bool? _emailVerified;
|
||||
private bool? _forcePasswordReset;
|
||||
|
||||
private const string Keys_UserId = "userId";
|
||||
private const string Keys_UserEmail = "userEmail";
|
||||
|
@ -24,6 +25,7 @@ namespace Bit.Core.Services
|
|||
private const string Keys_KdfIterations = "kdfIterations";
|
||||
private const string Keys_OrganizationsFormat = "organizations_{0}";
|
||||
private const string Keys_EmailVerified = "emailVerified";
|
||||
private const string Keys_ForcePasswordReset = "forcePasswordReset";
|
||||
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly ITokenService _tokenService;
|
||||
|
@ -59,6 +61,12 @@ namespace Bit.Core.Services
|
|||
await _storageService.SaveAsync(Keys_EmailVerified, emailVerified);
|
||||
}
|
||||
|
||||
public async Task SetForcePasswordReset(bool forcePasswordReset)
|
||||
{
|
||||
_forcePasswordReset = forcePasswordReset;
|
||||
await _storageService.SaveAsync(Keys_ForcePasswordReset, forcePasswordReset);
|
||||
}
|
||||
|
||||
public async Task<string> GetUserIdAsync()
|
||||
{
|
||||
if (_userId == null)
|
||||
|
@ -113,6 +121,15 @@ namespace Bit.Core.Services
|
|||
return _kdfIterations;
|
||||
}
|
||||
|
||||
public async Task<bool> GetForcePasswordReset()
|
||||
{
|
||||
if (_forcePasswordReset == null)
|
||||
{
|
||||
_forcePasswordReset = await _storageService.GetAsync<bool>(Keys_ForcePasswordReset);
|
||||
}
|
||||
return _forcePasswordReset.GetValueOrDefault();
|
||||
}
|
||||
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
var userId = await GetUserIdAsync();
|
||||
|
@ -122,6 +139,7 @@ namespace Bit.Core.Services
|
|||
_storageService.RemoveAsync(Keys_Stamp),
|
||||
_storageService.RemoveAsync(Keys_Kdf),
|
||||
_storageService.RemoveAsync(Keys_KdfIterations),
|
||||
_storageService.RemoveAsync(Keys_ForcePasswordReset),
|
||||
ClearOrganizationsAsync(userId));
|
||||
_userId = _email = _stamp = null;
|
||||
_kdf = null;
|
||||
|
|
|
@ -300,6 +300,22 @@ namespace Bit.iOS.Autofill
|
|||
return userService.IsAuthenticatedAsync();
|
||||
}
|
||||
|
||||
private void LogoutIfAuthed()
|
||||
{
|
||||
NSRunLoop.Main.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
if (await IsAuthed())
|
||||
{
|
||||
await AppHelpers.LogOutAsync();
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
if (deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void InitApp()
|
||||
{
|
||||
// Init Xamarin Forms
|
||||
|
@ -353,6 +369,8 @@ namespace Bit.iOS.Autofill
|
|||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
|
||||
LogoutIfAuthed();
|
||||
}
|
||||
|
||||
private void LaunchEnvironmentFlow()
|
||||
|
@ -400,6 +418,7 @@ namespace Bit.iOS.Autofill
|
|||
if (loginPage.BindingContext is LoginPageViewModel vm)
|
||||
{
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
@ -408,6 +427,8 @@ namespace Bit.iOS.Autofill
|
|||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
|
||||
LogoutIfAuthed();
|
||||
}
|
||||
|
||||
private void LaunchLoginSsoFlow()
|
||||
|
@ -420,6 +441,7 @@ namespace Bit.iOS.Autofill
|
|||
{
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true));
|
||||
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
|
||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||
vm.SsoAuthSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
@ -428,6 +450,8 @@ namespace Bit.iOS.Autofill
|
|||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
|
||||
LogoutIfAuthed();
|
||||
}
|
||||
|
||||
private void LaunchTwoFactorFlow(bool authingWithSso)
|
||||
|
@ -448,6 +472,7 @@ namespace Bit.iOS.Autofill
|
|||
{
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
||||
}
|
||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(twoFactorPage);
|
||||
|
@ -464,6 +489,7 @@ namespace Bit.iOS.Autofill
|
|||
ThemeManager.ApplyResourcesToPage(setPasswordPage);
|
||||
if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm)
|
||||
{
|
||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||
vm.SetPasswordSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
@ -473,5 +499,23 @@ namespace Bit.iOS.Autofill
|
|||
setPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(setPasswordController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchUpdateTempPasswordFlow()
|
||||
{
|
||||
var updateTempPasswordPage = new UpdateTempPasswordPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(updateTempPasswordPage);
|
||||
if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm)
|
||||
{
|
||||
vm.UpdateTempPasswordSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
vm.LogOutAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(updateTempPasswordPage);
|
||||
var updateTempPasswordController = navigationPage.CreateViewController();
|
||||
updateTempPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(updateTempPasswordController, true, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -288,34 +288,8 @@ namespace Bit.iOS.Core.Controllers
|
|||
|
||||
private async Task LogOutAsync()
|
||||
{
|
||||
var syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
var tokenService = ServiceContainer.Resolve<ITokenService>("tokenService");
|
||||
var settingsService = ServiceContainer.Resolve<ISettingsService>("settingsService");
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
var folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
var collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
var passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
|
||||
"passwordGenerationService");
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||
await AppHelpers.LogOutAsync();
|
||||
var authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
await Task.WhenAll(
|
||||
syncService.SetLastSyncAsync(DateTime.MinValue),
|
||||
tokenService.ClearTokenAsync(),
|
||||
_cryptoService.ClearKeysAsync(),
|
||||
_userService.ClearAsync(),
|
||||
settingsService.ClearAsync(userId),
|
||||
cipherService.ClearAsync(userId),
|
||||
folderService.ClearAsync(userId),
|
||||
collectionService.ClearAsync(userId),
|
||||
passwordGenerationService.ClearAsync(),
|
||||
_vaultTimeoutService.ClearAsync(),
|
||||
stateService.PurgeAsync(),
|
||||
_deviceActionService.ClearCacheAsync());
|
||||
_vaultTimeoutService.BiometricLocked = true;
|
||||
searchService.ClearIndex();
|
||||
authService.LogOut(() =>
|
||||
{
|
||||
Cancel?.Invoke();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using AuthenticationServices;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
@ -431,6 +432,22 @@ namespace Bit.iOS.Extension
|
|||
return userService.IsAuthenticatedAsync();
|
||||
}
|
||||
|
||||
private void LogoutIfAuthed()
|
||||
{
|
||||
NSRunLoop.Main.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
if (await IsAuthed())
|
||||
{
|
||||
await AppHelpers.LogOutAsync();
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
if (deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void LaunchHomePage()
|
||||
{
|
||||
var homePage = new HomePage();
|
||||
|
@ -450,6 +467,8 @@ namespace Bit.iOS.Extension
|
|||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
|
||||
LogoutIfAuthed();
|
||||
}
|
||||
|
||||
private void LaunchEnvironmentFlow()
|
||||
|
@ -497,6 +516,7 @@ namespace Bit.iOS.Extension
|
|||
if (loginPage.BindingContext is LoginPageViewModel vm)
|
||||
{
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => CompleteRequest(null, null);
|
||||
}
|
||||
|
@ -505,6 +525,8 @@ namespace Bit.iOS.Extension
|
|||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
|
||||
LogoutIfAuthed();
|
||||
}
|
||||
|
||||
private void LaunchLoginSsoFlow()
|
||||
|
@ -517,6 +539,7 @@ namespace Bit.iOS.Extension
|
|||
{
|
||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true));
|
||||
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
|
||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||
vm.SsoAuthSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
@ -525,6 +548,8 @@ namespace Bit.iOS.Extension
|
|||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
|
||||
LogoutIfAuthed();
|
||||
}
|
||||
|
||||
private void LaunchTwoFactorFlow(bool authingWithSso)
|
||||
|
@ -545,6 +570,7 @@ namespace Bit.iOS.Extension
|
|||
{
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
||||
}
|
||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(twoFactorPage);
|
||||
|
@ -561,6 +587,7 @@ namespace Bit.iOS.Extension
|
|||
ThemeManager.ApplyResourcesToPage(setPasswordPage);
|
||||
if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm)
|
||||
{
|
||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||
vm.SetPasswordSuccessAction = () => DismissLockAndContinue();
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
@ -570,5 +597,23 @@ namespace Bit.iOS.Extension
|
|||
setPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(setPasswordController, true, null);
|
||||
}
|
||||
|
||||
private void LaunchUpdateTempPasswordFlow()
|
||||
{
|
||||
var updateTempPasswordPage = new UpdateTempPasswordPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(false, app.Resources);
|
||||
ThemeManager.ApplyResourcesToPage(updateTempPasswordPage);
|
||||
if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm)
|
||||
{
|
||||
vm.UpdateTempPasswordSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
vm.LogOutAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(updateTempPasswordPage);
|
||||
var updateTempPasswordController = navigationPage.CreateViewController();
|
||||
updateTempPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(updateTempPasswordController, true, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue