diff --git a/src/App/Pages/Accounts/SetPasswordPage.xaml b/src/App/Pages/Accounts/SetPasswordPage.xaml index ce0b2286f..2393ce0ab 100644 --- a/src/App/Pages/Accounts/SetPasswordPage.xaml +++ b/src/App/Pages/Accounts/SetPasswordPage.xaml @@ -32,6 +32,28 @@ StyleClass="text-md" HorizontalTextAlignment="Start"> + + + + + + + + + + + @@ -44,7 +66,7 @@ diff --git a/src/App/Pages/Accounts/SetPasswordPageViewModel.cs b/src/App/Pages/Accounts/SetPasswordPageViewModel.cs index 78f850017..60f53a2c6 100644 --- a/src/App/Pages/Accounts/SetPasswordPageViewModel.cs +++ b/src/App/Pages/Accounts/SetPasswordPageViewModel.cs @@ -30,6 +30,7 @@ namespace Bit.App.Pages private bool _showPassword; private bool _isPolicyInEffect; + private bool _resetPasswordAutoEnroll; private string _policySummary; private MasterPasswordPolicyOptions _policy; @@ -50,7 +51,6 @@ namespace Bit.App.Pages ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword); SubmitCommand = new Command(async () => await SubmitAsync()); } - public bool ShowPassword { get => _showPassword; @@ -63,6 +63,12 @@ namespace Bit.App.Pages get => _isPolicyInEffect; set => SetProperty(ref _isPolicyInEffect, value); } + + public bool ResetPasswordAutoEnroll + { + get => _resetPasswordAutoEnroll; + set => SetProperty(ref _resetPasswordAutoEnroll, value); + } public string PolicySummary { @@ -86,10 +92,17 @@ namespace Bit.App.Pages public Action SetPasswordSuccessAction { get; set; } public Action CloseAction { get; set; } public string OrgIdentifier { get; set; } + public string OrgId { get; set; } public async Task InitAsync() { await CheckPasswordPolicy(); + + var org = await _userService.GetOrganizationByIdentifierAsync(OrgIdentifier); + OrgId = org?.Id; + var policyList = await _policyService.GetAll(PolicyType.ResetPassword); + var policyResult = _policyService.GetResetPasswordPolicyOptions(policyList, OrgId); + ResetPasswordAutoEnroll = policyResult.Item2 && policyResult.Item1.AutoEnrollEnabled; } public async Task SubmitAsync() @@ -171,6 +184,7 @@ namespace Bit.App.Pages try { await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount); + // Set Password and relevant information await _apiService.SetPasswordAsync(request); await _userService.SetInformationAsync(await _userService.GetUserIdAsync(), await _userService.GetEmailAsync(), kdf, kdfIterations); @@ -178,6 +192,25 @@ namespace Bit.App.Pages await _cryptoService.SetKeyHashAsync(localMasterPasswordHash); await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString); await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString); + + if (ResetPasswordAutoEnroll) + { + // Grab Organization Keys + var response = await _apiService.GetOrganizationKeysAsync(OrgId); + var publicKey = CoreHelpers.Base64UrlDecode(response.PublicKey); + // Grab user's Encryption Key and encrypt with Org Public Key + var userEncKey = await _cryptoService.GetEncKeyAsync(); + var encryptedKey = await _cryptoService.RsaEncryptAsync(userEncKey.Key, publicKey); + // Request + var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest + { + ResetPasswordKey = encryptedKey.EncryptedString + }; + var userId = await _userService.GetUserIdAsync(); + // Enroll user + await _apiService.PutOrganizationUserResetPasswordEnrollmentAsync(OrgId, userId, resetRequest); + } + await _deviceActionService.HideLoadingAsync(); SetPasswordSuccessAction?.Invoke(); diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index b7dee1148..576767f43 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -3590,5 +3590,11 @@ namespace Bit.App.Resources { return ResourceManager.GetString("Fido2SomethingWentWrong", resourceCulture); } } + + public static string ResetPasswordAutoEnrollInviteWarning { + get { + return ResourceManager.GetString("ResetPasswordAutoEnrollInviteWarning", resourceCulture); + } + } } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 5301e085d..180109bd3 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2031,4 +2031,7 @@ Something Went Wrong. Try again. + + This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password. + diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 01394172a..8a29eb535 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -58,6 +58,9 @@ namespace Bit.Core.Abstractions Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request); Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request); Task PostEventsCollectAsync(IEnumerable request); + Task GetOrganizationKeysAsync(string id); + Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId, + OrganizationUserResetPasswordEnrollmentRequest request); Task GetSendAsync(string id); Task PostSendAsync(SendRequest request); diff --git a/src/Core/Abstractions/IPolicyService.cs b/src/Core/Abstractions/IPolicyService.cs index cab335f0f..ef6b7e44f 100644 --- a/src/Core/Abstractions/IPolicyService.cs +++ b/src/Core/Abstractions/IPolicyService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Enums; @@ -15,5 +16,7 @@ namespace Bit.Core.Abstractions Task GetMasterPasswordPolicyOptions(IEnumerable policies = null); Task EvaluateMasterPassword(int passwordStrength, string newPassword, MasterPasswordPolicyOptions enforcedPolicyOptions); + Tuple GetResetPasswordPolicyOptions(IEnumerable policies, + string orgId); } } diff --git a/src/Core/Abstractions/IUserService.cs b/src/Core/Abstractions/IUserService.cs index fd640569d..dbdd17e83 100644 --- a/src/Core/Abstractions/IUserService.cs +++ b/src/Core/Abstractions/IUserService.cs @@ -16,6 +16,7 @@ namespace Bit.Core.Abstractions Task GetKdfAsync(); Task GetKdfIterationsAsync(); Task GetOrganizationAsync(string id); + Task GetOrganizationByIdentifierAsync(string identifier); Task GetSecurityStampAsync(); Task GetEmailVerifiedAsync(); Task GetUserIdAsync(); diff --git a/src/Core/Enums/PolicyType.cs b/src/Core/Enums/PolicyType.cs index 8dd175da9..ab3b20bfd 100644 --- a/src/Core/Enums/PolicyType.cs +++ b/src/Core/Enums/PolicyType.cs @@ -10,5 +10,6 @@ PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items DisableSend = 6, // Disables the ability to create and edit Sends SendOptions = 7, // Sets restrictions or defaults for Bitwarden Sends + ResetPassword = 8, // Allows orgs to use reset password : also can enable auto-enrollment during invite flow } } diff --git a/src/Core/Models/Data/OrganizationData.cs b/src/Core/Models/Data/OrganizationData.cs index 7fe8f94be..562a589a3 100644 --- a/src/Core/Models/Data/OrganizationData.cs +++ b/src/Core/Models/Data/OrganizationData.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using System.Data.Common; +using Bit.Core.Enums; using Bit.Core.Models.Response; namespace Bit.Core.Models.Data @@ -27,6 +28,7 @@ namespace Bit.Core.Models.Data MaxCollections = response.MaxCollections; MaxStorageGb = response.MaxStorageGb; Permissions = response.Permissions ?? new Permissions(); + Identifier = response.Identifier; } public string Id { get; set; } @@ -47,5 +49,6 @@ namespace Bit.Core.Models.Data public short? MaxCollections { get; set; } public short? MaxStorageGb { get; set; } public Permissions Permissions { get; set; } = new Permissions(); + public string Identifier { get; set; } } } diff --git a/src/Core/Models/Domain/Organization.cs b/src/Core/Models/Domain/Organization.cs index 7e2046212..fb8ee271a 100644 --- a/src/Core/Models/Domain/Organization.cs +++ b/src/Core/Models/Domain/Organization.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using System.Data.Common; +using Bit.Core.Enums; using Bit.Core.Models.Data; namespace Bit.Core.Models.Domain @@ -27,6 +28,7 @@ namespace Bit.Core.Models.Domain MaxCollections = obj.MaxCollections; MaxStorageGb = obj.MaxStorageGb; Permissions = obj.Permissions ?? new Permissions(); + Identifier = obj.Identifier; } public string Id { get; set; } @@ -47,6 +49,7 @@ namespace Bit.Core.Models.Domain public short? MaxCollections { get; set; } public short? MaxStorageGb { get; set; } public Permissions Permissions { get; set; } = new Permissions(); + public string Identifier { get; set; } public bool CanAccess { diff --git a/src/Core/Models/Domain/ResetPasswordPolicyOptions.cs b/src/Core/Models/Domain/ResetPasswordPolicyOptions.cs new file mode 100644 index 000000000..1653aa774 --- /dev/null +++ b/src/Core/Models/Domain/ResetPasswordPolicyOptions.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Domain +{ + public class ResetPasswordPolicyOptions + { + public bool AutoEnrollEnabled { get; set; } + } +} diff --git a/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs b/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs new file mode 100644 index 000000000..b751a9a4f --- /dev/null +++ b/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Request +{ + public class OrganizationUserResetPasswordEnrollmentRequest + { + public string ResetPasswordKey { get; set; } + } +} diff --git a/src/Core/Models/Response/OrganizationKeysResponse.cs b/src/Core/Models/Response/OrganizationKeysResponse.cs new file mode 100644 index 000000000..28d350f7b --- /dev/null +++ b/src/Core/Models/Response/OrganizationKeysResponse.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Response +{ + public class OrganizationKeysResponse + { + public string PrivateKey { get; set; } + public string PublicKey { get; set; } + } +} diff --git a/src/Core/Models/Response/ProfileOrganizationResponse.cs b/src/Core/Models/Response/ProfileOrganizationResponse.cs index 504a75db8..b83f0e295 100644 --- a/src/Core/Models/Response/ProfileOrganizationResponse.cs +++ b/src/Core/Models/Response/ProfileOrganizationResponse.cs @@ -24,5 +24,6 @@ namespace Bit.Core.Models.Response public OrganizationUserType Type { get; set; } public bool Enabled { get; set; } public Permissions Permissions { get; set; } = new Permissions(); + public string Identifier { get; set; } } } diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 78a06e80d..e0a6fa6ae 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -177,7 +177,7 @@ namespace Bit.Core.Services return SendAsync(HttpMethod.Post, "/accounts/verify-password", request, true, false); } - + #endregion #region Folder APIs @@ -402,6 +402,26 @@ namespace Bit.Core.Services string.Concat("/hibp/breach?username=", username), null, true, true); } + #endregion + + #region Organizations APIs + + public Task GetOrganizationKeysAsync(string id) + { + return SendAsync(HttpMethod.Get, $"/organizations/{id}/keys", null, true, true); + } + + #endregion + + #region Organization User APIs + + public Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId, + OrganizationUserResetPasswordEnrollmentRequest request) + { + return SendAsync(HttpMethod.Put, + $"/organizations/{orgId}/users/{userId}/reset-password-enrollment", request, true, false); + } + #endregion #region Helpers diff --git a/src/Core/Services/PolicyService.cs b/src/Core/Services/PolicyService.cs index 938356e93..9dcd1a2cd 100644 --- a/src/Core/Services/PolicyService.cs +++ b/src/Core/Services/PolicyService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -180,6 +181,23 @@ namespace Bit.Core.Services return true; } + public Tuple GetResetPasswordPolicyOptions(IEnumerable policies, + string orgId) + { + var resetPasswordPolicyOptions = new ResetPasswordPolicyOptions(); + + if (policies == null || orgId == null) + { + return new Tuple(resetPasswordPolicyOptions, false); + } + + var policy = policies.FirstOrDefault(p => + p.OrganizationId == orgId && p.Type == PolicyType.ResetPassword && p.Enabled); + resetPasswordPolicyOptions.AutoEnrollEnabled = GetPolicyBool(policy, "autoEnrollEnabled") ?? false; + + return new Tuple(resetPasswordPolicyOptions, policy != null); + } + private int? GetPolicyInt(Policy policy, string key) { if (policy.Data.ContainsKey(key)) diff --git a/src/Core/Services/UserService.cs b/src/Core/Services/UserService.cs index d59c07325..519e27f41 100644 --- a/src/Core/Services/UserService.cs +++ b/src/Core/Services/UserService.cs @@ -167,6 +167,19 @@ namespace Bit.Core.Services } return new Organization(organizations[id]); } + + public async Task GetOrganizationByIdentifierAsync(string identifier) + { + var userId = await GetUserIdAsync(); + var organizations = await GetAllOrganizationAsync(); + + if (organizations == null || organizations.Count == 0) + { + return null; + } + + return organizations.FirstOrDefault(o => o.Identifier == identifier); + } public async Task> GetAllOrganizationAsync() {