1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-04 20:50:21 -05:00
Jared Snider ee618328c0
Auth/PM-3275 - Changes to support TDE User without MP being able to Set a Password + misc refactoring (#3242)
* PM-3275 - Add new GetMasterPasswordPolicy endpoint which will allow authenticated clients to get an enabled MP org policy if it exists for the purposes of enforcing those policy requirements when setting a password.

* PM-3275 - AccountsController.cs - PostSetPasswordAsync - (1) Convert UserService.setPasswordAsync into new SetInitialMasterPasswordCommand (2) Refactor SetInitialMasterPasswordCommand to only accept post SSO users who are in the invited state
(3) Add TODOs for more cleanup work and more commands

* PM-3275 - Update AccountsControllerTests.cs to add new SetInitialMasterPasswordCommand

* PM-3275 - UserService.cs - Remove non implemented ChangePasswordAsync method

* PM-3275 - The new SetInitialMasterPasswordCommand leveraged the OrganizationService.cs AcceptUserAsync method so while I was in here I converted the AcceptUserAsync methods into a new AcceptOrgUserCommand.cs and turned the private method which accepted an existing org user public for use in the SetInitialMasterPasswordCommand

* PM-3275 - Dotnet format

* PM-3275 - Test SetInitialMasterPasswordCommand

* Dotnet format

* PM-3275 - In process AcceptOrgUserCommandTests.cs

* PM-3275 - Migrate changes from AC-244 / #3199 over into new AcceptOrgUserCommand

* PM-3275 - AcceptOrgUserCommand.cs - create data protector specifically for this command

* PM-3275 - Add TODO for renaming / removing overloading of methods to improve readability / clarity

* PM-3275 - AcceptOrgUserCommand.cs - refactor AcceptOrgUserAsync by OrgId to retrieve orgUser with _organizationUserRepository.GetByOrganizationAsync which gets a single user instead of a collection

* PM-3275 - AcceptOrgUserCommand.cs - update name in TODO for evaluation later

* PM-3275 / PM-1196 - (1) Slightly refactor SsoEmail2faSessionTokenable to provide public static GetTokenLifeTime() method for testing (2) Add missed tests to SsoEmail2faSessionTokenable in preparation for building tests for new OrgUserInviteTokenable.cs

* PM-3275 / PM-1196 - Removing SsoEmail2faSessionTokenable.cs changes + tests as I've handled that separately in a new PR (#3270) for newly created task PM-3925

* PM-3275 - ExpiringTokenable.cs - add clarifying comments to help distinguish between the Valid property and the TokenIsValid method.

* PM-3275 - Create OrgUserInviteTokenable.cs and add tests in OrgUserInviteTokenableTests.cs

* PM-3275 - OrganizationService.cs - Refactor Org User Invite methods to use new OrgUserInviteTokenable instead of manual creation of a token

* PM-3275 - OrgUserInviteTokenable.cs - clarify backwards compat note

* PM-3275 - AcceptOrgUserCommand.cs - Add TODOs + minor name refactor

* PM-3275 - AcceptOrgUserCommand.cs - replace method overloading with more easily readable names.

* PM-3275 - AcceptOrgUserCommand.cs - Update ValidateOrgUserInviteToken to add new token validation while maintaining backwards compatibility for 1 release.

* dotnet format

* PM-3275 - AcceptOrgUserCommand.cs - Move private method below where it is used

* PM-3275 - ServiceCollectionExtensions.cs - Must register IDataProtectorTokenFactory<OrgUserInviteTokenable> for new tokenable

* PM-3275 - OrgUserInviteTokenable needed access to global settings to set its token lifetime to the _globalSettings.OrganizationInviteExpirationHours value.  Creating a factory seemed the most straightforward way to encapsulate the desired creation logic. Unsure if in the correct location in ServiceCollectionExtensions.cs but will figure that out later.

* PM-3275 - In process work of creating AcceptOrgUserCommandTests.cs

* PM-3275 - Remove no longer relevant AcceptOrgUser tests from OrganizationServiceTests.cs

* PM-3275 - Register OrgUserInviteTokenableFactory alongside tokenizer

* PM-3275 - AcceptOrgUserCommandTests.cs - AcceptOrgUserAsync basic test suite completed.

* PM-3275 - AcceptOrgUserCommandTests.cs - tweak test names

* PM-3275 - AcceptOrgUserCommandTests.cs - (1) Remove old tests from OrganizationServiceTests as no longer needed to reference (2) Add summary for SetupCommonAcceptOrgUserMocks (3) Get AcceptOrgUserByToken_OldToken_AcceptsUserAndVerifiesEmail passing

* PM-3275 - Create interface for OrgUserInviteTokenableFactory b/c that's the right thing to do + enables test substitution

* PM-3275 - AcceptOrgUserCommandTests.cs - (1) Start work on AcceptOrgUserByToken_NewToken_AcceptsUserAndVerifiesEmail (2) Create and use SetupCommonAcceptOrgUserByTokenMocks() (3) Create generic FakeDataProtectorTokenFactory for tokenable testing

* PM-3275 - (1) Get AcceptOrgUserByToken_NewToken_AcceptsUserAndVerifiesEmail test passing (2) Move FakeDataProtectorTokenFactory to own file

* PM-3275 - AcceptOrgUserCommandTests.cs - Finish up tests for AcceptOrgUserByTokenAsync

* PM-3275 - Add pseudo section comments

* PM-3275 - Clean up unused params on AcceptOrgUserByToken_EmailMismatch_ThrowsBadRequest test

* PM-3275 - (1) Tests written for AcceptOrgUserByOrgSsoIdAsync (2) Refactor happy path assertions into helper function AssertValidAcceptedOrgUser to reduce code duplication

* PM-3275 - Finish up testing AcceptOrgUserCommandTests.cs by adding tests for AcceptOrgUserByOrgIdAsync

* PM-3275 - Tweaking test naming to ensure consistency.

* PM-3275 - Bugfix - OrgUserInviteTokenableFactory implementation required when declaring singleton service in ServiceCollectionExtensions.cs

* PM-3275 - Resolve failing OrganizationServiceTests.cs

* dotnet format

* PM-3275 - PoliciesController.cs - GetMasterPasswordPolicy bugfix - for orgs without a MP policy, policy comes back as null and we should return notFound in that case.

* PM-3275 - Add PoliciesControllerTests.cs specifically for new GetMasterPasswordPolicy(...) endpoint.

* PM-3275 - dotnet format PoliciesControllerTests.cs

* PM-3275 - PoliciesController.cs - (1) Add tech debt task number (2) Properly flag endpoint as deprecated

* PM-3275 - Add new hasManageResetPasswordPermission property to ProfileResponseModel.cs primarily for sync so that we can condition client side if TDE user obtains elevated permissions

* PM-3275 - Fix AccountsControllerTests.cs

* PM-3275 - OrgUserInviteTokenable.cs - clarify TODO

* PM-3275 - AcceptOrgUserCommand.cs - Refactor token validation to use short circuiting to only run old token validation if new token validation fails.

* PM-3275 - OrgUserInviteTokenable.cs - (1) Add new static methods to centralize validation logic to avoid repetition (2) Add new token validation method so we can avoid having to pass in a full org user (and hitting the db to do so)

* PM-3275 - Realized that the old token validation was used in the PoliciesController.cs (existing user clicks invite link in email and goes to log in) and UserService.cs (user clicks invite link in email and registers for a new acct). Added tech debt item for cleaning up backwards compatibility in future.

* dotnet format

* PM-3275 - (1) AccountsController.cs - Update PostSetPasswordAsync SetPasswordRequestModel to allow null keys for the case where we have a TDE user who obtains elevated permissions - they already have a user public and user encrypted private key saved in the db. (2) AccountsControllerTests.cs - test PostSetPasswordAsync scenarios to ensure changes will work as expected.

* PM-3275 - PR review feedback - (1) set CurrentContext to private (2) Refactor GetProfile to use variables to improve clarity and simplify debugging.

* PM-3275 - SyncController.cs - PR Review Feedback - Set current context as private instead of protected.

* PM-3275 - CurrentContextExtensions.cs - PR Feedback - move parenthesis up from own line.

* PM-3275 - SetInitialMasterPasswordCommandTests.cs - Replace unnecessary variable

* PM-3275 - SetInitialMasterPasswordCommandTests.cs - PR Feedback - Add expected outcome statement to test name

* PM-3275 - Set Initial Password command and tests - PR Feedback changes - (1) Rename orgIdentifier --> OrgSsoIdentifier for clarity (2) Update SetInitialMasterPasswordAsync to not allow null orgSsoId with explicit message saying this vs letting null org trigger invalid organization (3) Add test to cover this new scenario.

* PM-3275 - SetInitialMasterPasswordCommand.cs - Move summary from implementation to interface to better respect standards and the fact that the interface is the more seen piece of code.

* PM-3275 - AcceptOrgUserCommand.cs - Per PR feedback, rename AcceptOrgUserByTokenAsync -> AcceptOrgUserByEmailTokenAsync + replace generic name token with emailToken

* PM-3275 - OrganizationService.cs - Per PR feedback, remove dupe line

* PM-3275 - AcceptOrgUserCommand.cs - Per PR feedback, remove new lines in error messages for consistency.

* PM-3275 - SetInitialMasterPasswordCommand.cs - Per PR feedback, adjust formatting of constructor for improved readability.

* PM-3275 - CurrentContextExtensions.cs - Refactor AnyOrgUserHasManageResetPasswordPermission per PR feedback to remove unnecessary var.

* PM-3275 - AcceptOrgUserCommand.cs - Per PR feedback, remove completed TODO

* PM-3275 - PoliciesController.cs - Per PR feedback, update GetByInvitedUser param to be guid instead of string.

* PM-3275 - OrgUserInviteTokenable.cs - per PR feedback, add tech debt item info.

* PM-3275 - AcceptOrgUserCommand.cs - Per PR feedback, use const purpose from tokenable instead of magic string.

* PM-3275 - Restore non duplicate line to fix tests

* PM-3275 - Per PR feedback, revert all sync controller changes as the ProfileResponseModel.organizations array has org objects which have permissions which have the ManageResetPassword permission.  So, I have the information that I need clientside already to determine if the user has the ManageResetPassword in any org.

* PM-3275 - PoliciesControllerTests.cs - Update imports as the PoliciesController was moved under the admin console team's domain.

* PM-3275 - Resolve issues from merge conflict resolutions to get solution building.

* PM-3275 / PM-4633 - PoliciesController.cs - use orgUserId to look up user instead of orgId. Oops.

* Fix user service tests

* Resolve merge conflict
2023-11-02 08:02:25 -07:00

1590 lines
57 KiB
C#

using System.Security.Claims;
using System.Text.Json;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Fido2NetLib;
using Fido2NetLib.Objects;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using File = System.IO.File;
namespace Bit.Core.Services;
public class UserService : UserManager<User>, IUserService, IDisposable
{
private const string PremiumPlanId = "premium-annually";
private const string StoragePlanId = "storage-gb-annually";
private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IMailService _mailService;
private readonly IPushNotificationService _pushService;
private readonly IdentityErrorDescriber _identityErrorDescriber;
private readonly IdentityOptions _identityOptions;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;
private readonly ILicensingService _licenseService;
private readonly IEventService _eventService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IPaymentService _paymentService;
private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService;
private readonly IDataProtector _organizationServiceDataProtector;
private readonly IReferenceEventService _referenceEventService;
private readonly IFido2 _fido2;
private readonly ICurrentContext _currentContext;
private readonly IGlobalSettings _globalSettings;
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IStripeSyncService _stripeSyncService;
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
private readonly IDataProtectorTokenFactory<WebAuthnLoginTokenable> _webAuthnLoginTokenizer;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
public UserService(
IUserRepository userRepository,
ICipherRepository cipherRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IMailService mailService,
IPushNotificationService pushService,
IUserStore<User> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<User> passwordHasher,
IEnumerable<IUserValidator<User>> userValidators,
IEnumerable<IPasswordValidator<User>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<User>> logger,
ILicensingService licenseService,
IEventService eventService,
IApplicationCacheService applicationCacheService,
IDataProtectionProvider dataProtectionProvider,
IPaymentService paymentService,
IPolicyRepository policyRepository,
IPolicyService policyService,
IReferenceEventService referenceEventService,
IFido2 fido2,
ICurrentContext currentContext,
IGlobalSettings globalSettings,
IAcceptOrgUserCommand acceptOrgUserCommand,
IProviderUserRepository providerUserRepository,
IStripeSyncService stripeSyncService,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IWebAuthnCredentialRepository webAuthnRepository,
IDataProtectorTokenFactory<WebAuthnLoginTokenable> webAuthnLoginTokenizer)
: base(
store,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger)
{
_userRepository = userRepository;
_cipherRepository = cipherRepository;
_organizationUserRepository = organizationUserRepository;
_organizationRepository = organizationRepository;
_mailService = mailService;
_pushService = pushService;
_identityOptions = optionsAccessor?.Value ?? new IdentityOptions();
_identityErrorDescriber = errors;
_passwordHasher = passwordHasher;
_passwordValidators = passwordValidators;
_licenseService = licenseService;
_eventService = eventService;
_applicationCacheService = applicationCacheService;
_paymentService = paymentService;
_policyRepository = policyRepository;
_policyService = policyService;
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
"OrganizationServiceDataProtector");
_referenceEventService = referenceEventService;
_fido2 = fido2;
_currentContext = currentContext;
_globalSettings = globalSettings;
_acceptOrgUserCommand = acceptOrgUserCommand;
_providerUserRepository = providerUserRepository;
_stripeSyncService = stripeSyncService;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_webAuthnCredentialRepository = webAuthnRepository;
_webAuthnLoginTokenizer = webAuthnLoginTokenizer;
}
public Guid? GetProperUserId(ClaimsPrincipal principal)
{
if (!Guid.TryParse(GetUserId(principal), out var userIdGuid))
{
return null;
}
return userIdGuid;
}
public async Task<User> GetUserByIdAsync(string userId)
{
if (_currentContext?.User != null &&
string.Equals(_currentContext.User.Id.ToString(), userId, StringComparison.InvariantCultureIgnoreCase))
{
return _currentContext.User;
}
if (!Guid.TryParse(userId, out var userIdGuid))
{
return null;
}
_currentContext.User = await _userRepository.GetByIdAsync(userIdGuid);
return _currentContext.User;
}
public async Task<User> GetUserByIdAsync(Guid userId)
{
if (_currentContext?.User != null && _currentContext.User.Id == userId)
{
return _currentContext.User;
}
_currentContext.User = await _userRepository.GetByIdAsync(userId);
return _currentContext.User;
}
public async Task<User> GetUserByPrincipalAsync(ClaimsPrincipal principal)
{
var userId = GetProperUserId(principal);
if (!userId.HasValue)
{
return null;
}
return await GetUserByIdAsync(userId.Value);
}
public async Task<DateTime> GetAccountRevisionDateByIdAsync(Guid userId)
{
return await _userRepository.GetAccountRevisionDateAsync(userId);
}
public async Task SaveUserAsync(User user, bool push = false)
{
if (user.Id == default(Guid))
{
throw new ApplicationException("Use register method to create a new user.");
}
// if the name is empty, set it to null
if (String.Equals(user.Name, String.Empty))
{
user.Name = null;
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
if (push)
{
// push
await _pushService.PushSyncSettingsAsync(user.Id);
}
}
public override async Task<IdentityResult> DeleteAsync(User user)
{
// Check if user is the only owner of any organizations.
var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id);
if (onlyOwnerCount > 0)
{
var deletedOrg = false;
var orgs = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id,
OrganizationUserStatusType.Confirmed);
if (orgs.Count == 1)
{
var org = await _organizationRepository.GetByIdAsync(orgs.First().OrganizationId);
if (org != null && (!org.Enabled || string.IsNullOrWhiteSpace(org.GatewaySubscriptionId)))
{
var orgCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(org.Id);
if (orgCount <= 1)
{
await _organizationRepository.DeleteAsync(org);
deletedOrg = true;
}
}
}
if (!deletedOrg)
{
return IdentityResult.Failed(new IdentityError
{
Description = "Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.",
});
}
}
var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id);
if (onlyOwnerProviderCount > 0)
{
return IdentityResult.Failed(new IdentityError
{
Description = "Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.",
});
}
if (!string.IsNullOrWhiteSpace(user.GatewaySubscriptionId))
{
try
{
await CancelPremiumAsync(user, null, true);
}
catch (GatewayException) { }
}
await _userRepository.DeleteAsync(user);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext));
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
}
public async Task<IdentityResult> DeleteAsync(User user, string token)
{
if (!(await VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount", token)))
{
return IdentityResult.Failed(ErrorDescriber.InvalidToken());
}
return await DeleteAsync(user);
}
public async Task SendDeleteConfirmationAsync(string email)
{
var user = await _userRepository.GetByEmailAsync(email);
if (user == null)
{
// No user exists.
return;
}
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);
}
public async Task<IdentityResult> RegisterUserAsync(User user, string masterPassword,
string token, Guid? orgUserId)
{
var tokenValid = false;
if (_globalSettings.DisableUserRegistration && !string.IsNullOrWhiteSpace(token) && orgUserId.HasValue)
{
// TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete
var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
_orgUserInviteTokenDataFactory, token, orgUserId.Value, user.Email);
tokenValid = newTokenValid ||
CoreHelpers.UserInviteTokenIsValid(_organizationServiceDataProtector, token,
user.Email, orgUserId.Value, _globalSettings);
}
if (_globalSettings.DisableUserRegistration && !tokenValid)
{
throw new BadRequestException("Open registration has been disabled by the system administrator.");
}
if (orgUserId.HasValue)
{
var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserId.Value);
if (orgUser != null)
{
var twoFactorPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgUser.OrganizationId,
PolicyType.TwoFactorAuthentication);
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
Enabled = true
}
});
SetTwoFactorProvider(user, TwoFactorProviderType.Email);
}
}
}
user.ApiKey = CoreHelpers.SecureRandomString(30);
var result = await base.CreateAsync(user, masterPassword);
if (result == IdentityResult.Success)
{
await _mailService.SendWelcomeEmailAsync(user);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
}
return result;
}
public async Task<IdentityResult> RegisterUserAsync(User user)
{
var result = await base.CreateAsync(user);
if (result == IdentityResult.Success)
{
await _mailService.SendWelcomeEmailAsync(user);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
}
return result;
}
public async Task SendMasterPasswordHintAsync(string email)
{
var user = await _userRepository.GetByEmailAsync(email);
if (user == null)
{
// No user exists. Do we want to send an email telling them this in the future?
return;
}
if (string.IsNullOrWhiteSpace(user.MasterPasswordHint))
{
await _mailService.SendNoMasterPasswordHintEmailAsync(email);
return;
}
await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint);
}
public async Task SendTwoFactorEmailAsync(User user)
{
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
if (provider == null || provider.MetaData == null || !provider.MetaData.ContainsKey("Email"))
{
throw new ArgumentNullException("No email.");
}
var email = ((string)provider.MetaData["Email"]).ToLowerInvariant();
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"2faEmail:" + email);
await _mailService.SendTwoFactorEmailAsync(email, token);
}
public async Task<bool> VerifyTwoFactorEmailAsync(User user, string token)
{
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
if (provider == null || provider.MetaData == null || !provider.MetaData.ContainsKey("Email"))
{
throw new ArgumentNullException("No email.");
}
var email = ((string)provider.MetaData["Email"]).ToLowerInvariant();
return await base.VerifyUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"2faEmail:" + email, token);
}
public async Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user)
{
var providers = user.GetTwoFactorProviders();
if (providers == null)
{
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
}
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
if (provider == null)
{
provider = new TwoFactorProvider
{
Enabled = false
};
}
if (provider.MetaData == null)
{
provider.MetaData = new Dictionary<string, object>();
}
var fidoUser = new Fido2User
{
DisplayName = user.Name,
Name = user.Email,
Id = user.Id.ToByteArray(),
};
var excludeCredentials = provider.MetaData
.Where(k => k.Key.StartsWith("Key"))
.Select(k => new TwoFactorProvider.WebAuthnData((dynamic)k.Value).Descriptor)
.ToList();
var authenticatorSelection = new AuthenticatorSelection
{
AuthenticatorAttachment = null,
RequireResidentKey = false,
UserVerification = UserVerificationRequirement.Discouraged
};
var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, authenticatorSelection, AttestationConveyancePreference.None);
provider.MetaData["pending"] = options.ToJson();
providers[TwoFactorProviderType.WebAuthn] = provider;
user.SetTwoFactorProviders(providers);
await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false);
return options;
}
public async Task<bool> CompleteWebAuthRegistrationAsync(User user, int id, string name, AuthenticatorAttestationRawResponse attestationResponse)
{
var keyId = $"Key{id}";
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
if (!provider?.MetaData?.ContainsKey("pending") ?? true)
{
return false;
}
var options = CredentialCreateOptions.FromJson((string)provider.MetaData["pending"]);
// Callback to ensure credential ID is unique. Always return true since we don't care if another
// account uses the same 2FA key.
IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) => Task.FromResult(true);
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);
provider.MetaData.Remove("pending");
provider.MetaData[keyId] = new TwoFactorProvider.WebAuthnData
{
Name = name,
Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
PublicKey = success.Result.PublicKey,
UserHandle = success.Result.User.Id,
SignatureCounter = success.Result.Counter,
CredType = success.Result.CredType,
RegDate = DateTime.Now,
AaGuid = success.Result.Aaguid
};
var providers = user.GetTwoFactorProviders();
providers[TwoFactorProviderType.WebAuthn] = provider;
user.SetTwoFactorProviders(providers);
await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
return true;
}
public async Task<bool> DeleteWebAuthnKeyAsync(User user, int id)
{
var providers = user.GetTwoFactorProviders();
if (providers == null)
{
return false;
}
var keyName = $"Key{id}";
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
if (!provider?.MetaData?.ContainsKey(keyName) ?? true)
{
return false;
}
if (provider.MetaData.Count < 2)
{
return false;
}
provider.MetaData.Remove(keyName);
providers[TwoFactorProviderType.WebAuthn] = provider;
user.SetTwoFactorProviders(providers);
await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
return true;
}
public async Task<CredentialCreateOptions> StartWebAuthnLoginRegistrationAsync(User user)
{
var fidoUser = new Fido2User
{
DisplayName = user.Name,
Name = user.Email,
Id = user.Id.ToByteArray(),
};
// Get existing keys to exclude
var existingKeys = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
var excludeCredentials = existingKeys
.Select(k => new PublicKeyCredentialDescriptor(CoreHelpers.Base64UrlDecode(k.CredentialId)))
.ToList();
var authenticatorSelection = new AuthenticatorSelection
{
AuthenticatorAttachment = null,
RequireResidentKey = false, // TODO: This is using the old residentKey selection variant, we need to update our lib so that we can set this to preferred
UserVerification = UserVerificationRequirement.Preferred
};
var extensions = new AuthenticationExtensionsClientInputs { };
var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, authenticatorSelection,
AttestationConveyancePreference.None, extensions);
return options;
}
public async Task<bool> CompleteWebAuthLoginRegistrationAsync(User user, string name,
CredentialCreateOptions options,
AuthenticatorAttestationRawResponse attestationResponse)
{
var existingCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
if (existingCredentials.Count >= 5)
{
return false;
}
var existingCredentialIds = existingCredentials.Select(c => c.CredentialId);
IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) => Task.FromResult(!existingCredentialIds.Contains(CoreHelpers.Base64UrlEncode(args.CredentialId)));
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);
var credential = new WebAuthnCredential
{
Name = name,
CredentialId = CoreHelpers.Base64UrlEncode(success.Result.CredentialId),
PublicKey = CoreHelpers.Base64UrlEncode(success.Result.PublicKey),
Type = success.Result.CredType,
AaGuid = success.Result.Aaguid,
Counter = (int)success.Result.Counter,
UserId = user.Id
};
await _webAuthnCredentialRepository.CreateAsync(credential);
return true;
}
public async Task<AssertionOptions> StartWebAuthnLoginAssertionAsync(User user)
{
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
var existingKeys = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
var existingCredentials = existingKeys
.Select(k => new PublicKeyCredentialDescriptor(CoreHelpers.Base64UrlDecode(k.CredentialId)))
.ToList();
if (existingCredentials.Count == 0)
{
return null;
}
// TODO: PRF?
var exts = new AuthenticationExtensionsClientInputs
{
UserVerificationMethod = true
};
var options = _fido2.GetAssertionOptions(existingCredentials, UserVerificationRequirement.Preferred, exts);
// TODO: temp save options to user record somehow
return options;
}
public async Task<string> CompleteWebAuthLoginAssertionAsync(AuthenticatorAssertionRawResponse assertionResponse, User user)
{
// TODO: Get options from user record somehow, then clear them
var options = AssertionOptions.FromJson("");
var userCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
var assertionId = CoreHelpers.Base64UrlEncode(assertionResponse.Id);
var credential = userCredentials.FirstOrDefault(c => c.CredentialId == assertionId);
if (credential == null)
{
return null;
}
// TODO: Callback to ensure credential ID is unique. Do we care? I don't think so.
IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true);
var credentialPublicKey = CoreHelpers.Base64UrlDecode(credential.PublicKey);
var assertionVerificationResult = await _fido2.MakeAssertionAsync(
assertionResponse, options, credentialPublicKey, (uint)credential.Counter, callback);
// Update SignatureCounter
credential.Counter = (int)assertionVerificationResult.Counter;
await _webAuthnCredentialRepository.ReplaceAsync(credential);
if (assertionVerificationResult.Status == "ok")
{
var token = _webAuthnLoginTokenizer.Protect(new WebAuthnLoginTokenable(user));
return token;
}
else
{
return null;
}
}
public async Task SendEmailVerificationAsync(User user)
{
if (user.EmailVerified)
{
throw new BadRequestException("Email already verified.");
}
var token = await base.GenerateEmailConfirmationTokenAsync(user);
await _mailService.SendVerifyEmailEmailAsync(user.Email, user.Id, token);
}
public async Task InitiateEmailChangeAsync(User user, string newEmail)
{
var existingUser = await _userRepository.GetByEmailAsync(newEmail);
if (existingUser != null)
{
await _mailService.SendChangeEmailAlreadyExistsEmailAsync(user.Email, newEmail);
return;
}
var token = await base.GenerateChangeEmailTokenAsync(user, newEmail);
await _mailService.SendChangeEmailEmailAsync(newEmail, token);
}
public async Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail,
string newMasterPassword, string token, string key)
{
var verifyPasswordResult = _passwordHasher.VerifyHashedPassword(user, user.MasterPassword, masterPassword);
if (verifyPasswordResult == PasswordVerificationResult.Failed)
{
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
if (!await base.VerifyUserTokenAsync(user, _identityOptions.Tokens.ChangeEmailTokenProvider,
GetChangeEmailTokenPurpose(newEmail), token))
{
return IdentityResult.Failed(_identityErrorDescriber.InvalidToken());
}
var existingUser = await _userRepository.GetByEmailAsync(newEmail);
if (existingUser != null && existingUser.Id != user.Id)
{
return IdentityResult.Failed(_identityErrorDescriber.DuplicateEmail(newEmail));
}
var previousState = new
{
Key = user.Key,
MasterPassword = user.MasterPassword,
SecurityStamp = user.SecurityStamp,
Email = user.Email
};
var result = await UpdatePasswordHash(user, newMasterPassword);
if (!result.Succeeded)
{
return result;
}
var now = DateTime.UtcNow;
user.Key = key;
user.Email = newEmail;
user.EmailVerified = true;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastEmailChangeDate = now;
await _userRepository.ReplaceAsync(user);
if (user.Gateway == GatewayType.Stripe)
{
try
{
await _stripeSyncService.UpdateCustomerEmailAddress(user.GatewayCustomerId,
user.BillingEmailAddress());
}
catch (Exception ex)
{
//if sync to strip fails, update email and securityStamp to previous
user.Key = previousState.Key;
user.Email = previousState.Email;
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.MasterPassword = previousState.MasterPassword;
user.SecurityStamp = previousState.SecurityStamp;
await _userRepository.ReplaceAsync(user);
return IdentityResult.Failed(new IdentityError
{
Description = ex.Message
});
}
}
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
}
public async Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string passwordHint,
string key)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (await CheckPasswordAsync(user, masterPassword))
{
var result = await UpdatePasswordHash(user, newMasterPassword);
if (!result.Succeeded)
{
return result;
}
var now = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastPasswordChangeDate = now;
user.Key = key;
user.MasterPasswordHint = passwordHint;
await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
await _pushService.PushLogOutAsync(user.Id, true);
return IdentityResult.Success;
}
Logger.LogWarning("Change password failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier)
{
var identityResult = CheckCanUseKeyConnector(user);
if (identityResult != null)
{
return identityResult;
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.Key = key;
user.UsesKeyConnector = true;
await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
await _acceptOrgUserCommand.AcceptOrgUserByOrgSsoIdAsync(orgIdentifier, user, this);
return IdentityResult.Success;
}
public async Task<IdentityResult> ConvertToKeyConnectorAsync(User user)
{
var identityResult = CheckCanUseKeyConnector(user);
if (identityResult != null)
{
return identityResult;
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.MasterPassword = null;
user.UsesKeyConnector = true;
await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
return IdentityResult.Success;
}
private IdentityResult CheckCanUseKeyConnector(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.UsesKeyConnector)
{
Logger.LogWarning("Already uses Key Connector.");
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
}
if (_currentContext.Organizations.Any(u =>
u.Type is OrganizationUserType.Owner or OrganizationUserType.Admin))
{
throw new BadRequestException("Cannot use Key Connector when admin or owner of an organization.");
}
return null;
}
public async Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType callingUserType, Guid orgId, Guid id, string newMasterPassword, string key)
{
// Org must be able to use reset password
var org = await _organizationRepository.GetByIdAsync(orgId);
if (org == null || !org.UseResetPassword)
{
throw new BadRequestException("Organization does not allow password reset.");
}
// Enterprise policy must be enabled
var resetPasswordPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
{
throw new BadRequestException("Organization does not have the password reset policy enabled.");
}
// Org User must be confirmed and have a ResetPasswordKey
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed ||
orgUser.OrganizationId != orgId || string.IsNullOrEmpty(orgUser.ResetPasswordKey) ||
!orgUser.UserId.HasValue)
{
throw new BadRequestException("Organization User not valid");
}
// Calling User must be of higher/equal user type to reset user's password
var canAdjustPassword = false;
switch (callingUserType)
{
case OrganizationUserType.Owner:
canAdjustPassword = true;
break;
case OrganizationUserType.Admin:
canAdjustPassword = orgUser.Type != OrganizationUserType.Owner;
break;
case OrganizationUserType.Custom:
canAdjustPassword = orgUser.Type != OrganizationUserType.Owner &&
orgUser.Type != OrganizationUserType.Admin;
break;
}
if (!canAdjustPassword)
{
throw new BadRequestException("Calling user does not have permission to reset this user's master password");
}
var user = await GetUserByIdAsync(orgUser.UserId.Value);
if (user == null)
{
throw new NotFoundException();
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("Cannot reset password of a user with Key Connector.");
}
var result = await UpdatePasswordHash(user, newMasterPassword);
if (!result.Succeeded)
{
return result;
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.Key = key;
user.ForcePasswordReset = true;
await _userRepository.ReplaceAsync(user);
await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.Name);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_AdminResetPassword);
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
}
public async Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint)
{
if (!user.ForcePasswordReset)
{
throw new BadRequestException("User does not have a temporary password to update.");
}
var result = await UpdatePasswordHash(user, newMasterPassword);
if (!result.Succeeded)
{
return result;
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.ForcePasswordReset = false;
user.Key = key;
user.MasterPasswordHint = hint;
await _userRepository.ReplaceAsync(user);
await _mailService.SendUpdatedTempPasswordEmailAsync(user.Email, user.Name);
await _eventService.LogUserEventAsync(user.Id, EventType.User_UpdatedTempPassword);
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
}
public async Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword,
string key, KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (await CheckPasswordAsync(user, masterPassword))
{
var result = await UpdatePasswordHash(user, newMasterPassword);
if (!result.Succeeded)
{
return result;
}
var now = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastKdfChangeDate = now;
user.Key = key;
user.Kdf = kdf;
user.KdfIterations = kdfIterations;
user.KdfMemory = kdfMemory;
user.KdfParallelism = kdfParallelism;
await _userRepository.ReplaceAsync(user);
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
}
Logger.LogWarning("Change KDF failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey,
IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders, IEnumerable<Send> sends)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (await CheckPasswordAsync(user, masterPassword))
{
var now = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastKeyRotationDate = now;
user.SecurityStamp = Guid.NewGuid().ToString();
user.Key = key;
user.PrivateKey = privateKey;
if (ciphers.Any() || folders.Any() || sends.Any())
{
await _cipherRepository.UpdateUserKeysAndCiphersAsync(user, ciphers, folders, sends);
}
else
{
await _userRepository.ReplaceAsync(user);
}
await _pushService.PushLogOutAsync(user.Id, excludeCurrentContextFromPush: true);
return IdentityResult.Success;
}
Logger.LogWarning("Update key failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string secret)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (await VerifySecretAsync(user, secret))
{
var result = await base.UpdateSecurityStampAsync(user);
if (!result.Succeeded)
{
return result;
}
await SaveUserAsync(user);
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
}
Logger.LogWarning("Refresh security stamp failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true)
{
SetTwoFactorProvider(user, type, setEnabled);
await SaveUserAsync(user);
if (logEvent)
{
await _eventService.LogUserEventAsync(user.Id, EventType.User_Updated2fa);
}
}
public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type,
IOrganizationService organizationService)
{
var providers = user.GetTwoFactorProviders();
if (!providers?.ContainsKey(type) ?? true)
{
return;
}
providers.Remove(type);
user.SetTwoFactorProviders(providers);
await SaveUserAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
if (!await TwoFactorIsEnabledAsync(user))
{
await CheckPoliciesOnTwoFactorRemovalAsync(user, organizationService);
}
}
public async Task<bool> RecoverTwoFactorAsync(string email, string secret, string recoveryCode,
IOrganizationService organizationService)
{
var user = await _userRepository.GetByEmailAsync(email);
if (user == null)
{
// No user exists. Do we want to send an email telling them this in the future?
return false;
}
if (!await VerifySecretAsync(user, secret))
{
return false;
}
if (!CoreHelpers.FixedTimeEquals(user.TwoFactorRecoveryCode, recoveryCode))
{
return false;
}
user.TwoFactorProviders = null;
user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);
await SaveUserAsync(user);
await _mailService.SendRecoverTwoFactorEmail(user.Email, DateTime.UtcNow, _currentContext.IpAddress);
await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa);
await CheckPoliciesOnTwoFactorRemovalAsync(user, organizationService);
return true;
}
public async Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken,
PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license,
TaxInfo taxInfo)
{
if (user.Premium)
{
throw new BadRequestException("Already a premium user.");
}
if (additionalStorageGb < 0)
{
throw new BadRequestException("You can't subtract storage!");
}
if ((paymentMethodType == PaymentMethodType.GoogleInApp ||
paymentMethodType == PaymentMethodType.AppleInApp) && additionalStorageGb > 0)
{
throw new BadRequestException("You cannot add storage with this payment method.");
}
string paymentIntentClientSecret = null;
IPaymentService paymentService = null;
if (_globalSettings.SelfHosted)
{
if (license == null || !_licenseService.VerifyLicense(license))
{
throw new BadRequestException("Invalid license.");
}
if (!license.CanUse(user))
{
throw new BadRequestException("This license is not valid for this user.");
}
var dir = $"{_globalSettings.LicenseDirectory}/user";
Directory.CreateDirectory(dir);
using var fs = File.OpenWrite(Path.Combine(dir, $"{user.Id}.json"));
await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);
}
else
{
paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType,
paymentToken, additionalStorageGb, taxInfo);
}
user.Premium = true;
user.RevisionDate = DateTime.UtcNow;
if (_globalSettings.SelfHosted)
{
user.MaxStorageGb = 10240; // 10 TB
user.LicenseKey = license.LicenseKey;
user.PremiumExpirationDate = license.Expires;
}
else
{
user.MaxStorageGb = (short)(1 + additionalStorageGb);
user.LicenseKey = CoreHelpers.SecureRandomString(20);
}
try
{
await SaveUserAsync(user);
await _pushService.PushSyncVaultAsync(user.Id);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.UpgradePlan, user, _currentContext)
{
Storage = user.MaxStorageGb,
PlanName = PremiumPlanId,
});
}
catch when (!_globalSettings.SelfHosted)
{
await paymentService.CancelAndRecoverChargesAsync(user);
throw;
}
return new Tuple<bool, string>(string.IsNullOrWhiteSpace(paymentIntentClientSecret),
paymentIntentClientSecret);
}
public async Task IapCheckAsync(User user, PaymentMethodType paymentMethodType)
{
if (paymentMethodType != PaymentMethodType.AppleInApp)
{
throw new BadRequestException("Payment method not supported for in-app purchases.");
}
if (user.Premium)
{
throw new BadRequestException("Already a premium user.");
}
if (!string.IsNullOrWhiteSpace(user.GatewayCustomerId))
{
var customerService = new Stripe.CustomerService();
var customer = await customerService.GetAsync(user.GatewayCustomerId);
if (customer != null && customer.Balance != 0)
{
throw new BadRequestException("Customer balance cannot exist when using in-app purchases.");
}
}
}
public async Task UpdateLicenseAsync(User user, UserLicense license)
{
if (!_globalSettings.SelfHosted)
{
throw new InvalidOperationException("Licenses require self hosting.");
}
if (license?.LicenseType != null && license.LicenseType != LicenseType.User)
{
throw new BadRequestException("Organization licenses cannot be applied to a user. "
+ "Upload this license from the Organization settings page.");
}
if (license == null || !_licenseService.VerifyLicense(license))
{
throw new BadRequestException("Invalid license.");
}
if (!license.CanUse(user))
{
throw new BadRequestException("This license is not valid for this user.");
}
var dir = $"{_globalSettings.LicenseDirectory}/user";
Directory.CreateDirectory(dir);
using var fs = File.OpenWrite(Path.Combine(dir, $"{user.Id}.json"));
await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);
user.Premium = license.Premium;
user.RevisionDate = DateTime.UtcNow;
user.MaxStorageGb = _globalSettings.SelfHosted ? 10240 : license.MaxStorageGb; // 10 TB
user.LicenseKey = license.LicenseKey;
user.PremiumExpirationDate = license.Expires;
await SaveUserAsync(user);
}
public async Task<string> AdjustStorageAsync(User user, short storageAdjustmentGb)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (!user.Premium)
{
throw new BadRequestException("Not a premium user.");
}
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb,
StoragePlanId);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.AdjustStorage, user, _currentContext)
{
Storage = storageAdjustmentGb,
PlanName = StoragePlanId,
});
await SaveUserAsync(user);
return secret;
}
public async Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType, TaxInfo taxInfo)
{
if (paymentToken.StartsWith("btok_"))
{
throw new BadRequestException("Invalid token.");
}
var updated = await _paymentService.UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo: taxInfo);
if (updated)
{
await SaveUserAsync(user);
}
}
public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null, bool accountDelete = false)
{
var eop = endOfPeriod.GetValueOrDefault(true);
if (!endOfPeriod.HasValue && user.PremiumExpirationDate.HasValue &&
user.PremiumExpirationDate.Value < DateTime.UtcNow)
{
eop = false;
}
await _paymentService.CancelSubscriptionAsync(user, eop, accountDelete);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.CancelSubscription, user, _currentContext)
{
EndOfPeriod = eop,
});
}
public async Task ReinstatePremiumAsync(User user)
{
await _paymentService.ReinstateSubscriptionAsync(user);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.ReinstateSubscription, user, _currentContext));
}
public async Task EnablePremiumAsync(Guid userId, DateTime? expirationDate)
{
var user = await _userRepository.GetByIdAsync(userId);
await EnablePremiumAsync(user, expirationDate);
}
public async Task EnablePremiumAsync(User user, DateTime? expirationDate)
{
if (user != null && !user.Premium && user.Gateway.HasValue)
{
user.Premium = true;
user.PremiumExpirationDate = expirationDate;
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
}
public async Task DisablePremiumAsync(Guid userId, DateTime? expirationDate)
{
var user = await _userRepository.GetByIdAsync(userId);
await DisablePremiumAsync(user, expirationDate);
}
public async Task DisablePremiumAsync(User user, DateTime? expirationDate)
{
if (user != null && user.Premium)
{
user.Premium = false;
user.PremiumExpirationDate = expirationDate;
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
}
public async Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate)
{
var user = await _userRepository.GetByIdAsync(userId);
if (user != null)
{
user.PremiumExpirationDate = expirationDate;
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
}
public async Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,
int? version = null)
{
if (user == null)
{
throw new NotFoundException();
}
if (subscriptionInfo == null && user.Gateway != null)
{
subscriptionInfo = await _paymentService.GetSubscriptionAsync(user);
}
return subscriptionInfo == null ? new UserLicense(user, _licenseService) :
new UserLicense(user, subscriptionInfo, _licenseService);
}
public override async Task<bool> CheckPasswordAsync(User user, string password)
{
if (user == null)
{
return false;
}
var result = await base.VerifyPasswordAsync(Store as IUserPasswordStore<User>, user, password);
if (result == PasswordVerificationResult.SuccessRehashNeeded)
{
await UpdatePasswordHash(user, password, false, false);
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
var success = result != PasswordVerificationResult.Failed;
if (!success)
{
Logger.LogWarning(0, "Invalid password for user {userId}.", user.Id);
}
return success;
}
public async Task<bool> CanAccessPremium(ITwoFactorProvidersUser user)
{
var userId = user.GetUserId();
if (!userId.HasValue)
{
return false;
}
return user.GetPremium() || await this.HasPremiumFromOrganization(user);
}
public async Task<bool> HasPremiumFromOrganization(ITwoFactorProvidersUser user)
{
var userId = user.GetUserId();
if (!userId.HasValue)
{
return false;
}
// orgUsers in the Invited status are not associated with a userId yet, so this will get
// orgUsers in Accepted and Confirmed states only
var orgUsers = await _organizationUserRepository.GetManyByUserAsync(userId.Value);
if (!orgUsers.Any())
{
return false;
}
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
return orgUsers.Any(ou =>
orgAbilities.TryGetValue(ou.OrganizationId, out var orgAbility) &&
orgAbility.UsersGetPremium &&
orgAbility.Enabled);
}
public async Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user)
{
var providers = user.GetTwoFactorProviders();
if (providers == null)
{
return false;
}
foreach (var p in providers)
{
if (p.Value?.Enabled ?? false)
{
if (!TwoFactorProvider.RequiresPremium(p.Key))
{
return true;
}
if (await CanAccessPremium(user))
{
return true;
}
}
}
return false;
}
public async Task<bool> TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user)
{
var providers = user.GetTwoFactorProviders();
if (providers == null || !providers.ContainsKey(provider) || !providers[provider].Enabled)
{
return false;
}
if (!TwoFactorProvider.RequiresPremium(provider))
{
return true;
}
return await CanAccessPremium(user);
}
public async Task<string> GenerateSignInTokenAsync(User user, string purpose)
{
var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
purpose);
return token;
}
public async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,
bool validatePassword = true, bool refreshStamp = true)
{
if (validatePassword)
{
var validate = await ValidatePasswordInternal(user, newPassword);
if (!validate.Succeeded)
{
return validate;
}
}
user.MasterPassword = _passwordHasher.HashPassword(user, newPassword);
if (refreshStamp)
{
user.SecurityStamp = Guid.NewGuid().ToString();
}
return IdentityResult.Success;
}
private async Task<IdentityResult> ValidatePasswordInternal(User user, string password)
{
var errors = new List<IdentityError>();
foreach (var v in _passwordValidators)
{
var result = await v.ValidateAsync(this, user, password);
if (!result.Succeeded)
{
errors.AddRange(result.Errors);
}
}
if (errors.Count > 0)
{
Logger.LogWarning("User {userId} password validation failed: {errors}.", await GetUserIdAsync(user),
string.Join(";", errors.Select(e => e.Code)));
return IdentityResult.Failed(errors.ToArray());
}
return IdentityResult.Success;
}
public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true)
{
var providers = user.GetTwoFactorProviders();
if (!providers?.ContainsKey(type) ?? true)
{
return;
}
if (setEnabled)
{
providers[type].Enabled = true;
}
user.SetTwoFactorProviders(providers);
if (string.IsNullOrWhiteSpace(user.TwoFactorRecoveryCode))
{
user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);
}
}
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user, IOrganizationService organizationService)
{
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
{
await organizationService.DeleteUserAsync(p.OrganizationId, user.Id);
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
organization.Name, user.Email);
}).ToArray();
await Task.WhenAll(removeOrgUserTasks);
}
public override async Task<IdentityResult> ConfirmEmailAsync(User user, string token)
{
var result = await base.ConfirmEmailAsync(user, token);
if (result.Succeeded)
{
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.ConfirmEmailAddress, user, _currentContext));
}
return result;
}
public async Task RotateApiKeyAsync(User user)
{
user.ApiKey = CoreHelpers.SecureRandomString(30);
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
public async Task SendOTPAsync(User user)
{
if (user.Email == null)
{
throw new BadRequestException("No user email.");
}
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email);
await _mailService.SendOTPEmailAsync(user.Email, token);
}
public async Task<bool> VerifyOTPAsync(User user, string token)
{
return await base.VerifyUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email, token);
}
public async Task<bool> VerifySecretAsync(User user, string secret)
{
bool isVerified;
if (user.HasMasterPassword())
{
// If the user has a master password the secret is most likely going to be a hash
// of their password, but in certain scenarios, like when the user has logged into their
// device without a password (trusted device encryption) but the account
// does still have a password we will allow the use of OTP.
isVerified = await CheckPasswordAsync(user, secret) ||
await VerifyOTPAsync(user, secret);
}
else
{
// If they don't have a password at all they can only do OTP
isVerified = await VerifyOTPAsync(user, secret);
}
return isVerified;
}
}