1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 08:02:49 -05:00

organization service implementations into their own folder

This commit is contained in:
Kyle Spearrin
2016-12-02 23:32:36 -05:00
parent 4aa5292a50
commit bfb98131e5
6 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,37 @@
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;
using System;
namespace Bit.Core.Services
{
public class AzureBlockIpService : IBlockIpService
{
private readonly CloudQueue _blockIpQueue;
private readonly CloudQueue _unblockIpQueue;
public AzureBlockIpService(
GlobalSettings globalSettings)
{
var storageAccount = CloudStorageAccount.Parse(globalSettings.Storage.ConnectionString);
var queueClient = storageAccount.CreateCloudQueueClient();
_blockIpQueue = queueClient.GetQueueReference("blockip");
_blockIpQueue.CreateIfNotExists();
_unblockIpQueue = queueClient.GetQueueReference("unblockip");
_unblockIpQueue.CreateIfNotExists();
}
public async Task BlockIpAsync(string ipAddress, bool permanentBlock)
{
var message = new CloudQueueMessage(ipAddress);
await _blockIpQueue.AddMessageAsync(message);
if(!permanentBlock)
{
await _unblockIpQueue.AddMessageAsync(message, null, new TimeSpan(12, 0, 0), null, null);
}
}
}
}

View File

@ -0,0 +1,88 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Domains;
using Bit.Core.Repositories;
namespace Bit.Core.Services
{
public class CipherService : ICipherService
{
private readonly ICipherRepository _cipherRepository;
private readonly IPushService _pushService;
public CipherService(
ICipherRepository cipherRepository,
IPushService pushService)
{
_cipherRepository = cipherRepository;
_pushService = pushService;
}
public async Task SaveAsync(Cipher cipher)
{
if(cipher.Id == default(Guid))
{
await _cipherRepository.CreateAsync(cipher);
// push
await _pushService.PushSyncCipherCreateAsync(cipher);
}
else
{
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync(cipher);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher);
}
}
public async Task DeleteAsync(Cipher cipher)
{
await _cipherRepository.DeleteAsync(cipher);
// push
await _pushService.PushSyncCipherDeleteAsync(cipher);
}
public async Task ImportCiphersAsync(
List<Cipher> folders,
List<Cipher> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships)
{
// create all the folders
var folderTasks = new List<Task>();
foreach(var folder in folders)
{
folderTasks.Add(_cipherRepository.CreateAsync(folder));
}
await Task.WhenAll(folderTasks);
// associate the newly created folders to the ciphers
foreach(var relationship in folderRelationships)
{
var cipher = ciphers.ElementAtOrDefault(relationship.Key);
var folder = folders.ElementAtOrDefault(relationship.Value);
if(cipher == null || folder == null)
{
continue;
}
cipher.FolderId = folder.Id;
}
// create all the ciphers
await _cipherRepository.CreateAsync(ciphers);
// push
var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId;
if(userId.HasValue)
{
await _pushService.PushSyncCiphersAsync(userId.Value);
}
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Domains;
using Bit.Core.Repositories;
namespace Bit.Core.Services
{
public class DeviceService : IDeviceService
{
private readonly IDeviceRepository _deviceRepository;
public DeviceService(
IDeviceRepository deviceRepository)
{
_deviceRepository = deviceRepository;
}
public async Task SaveAsync(Device device)
{
if(device.Id == default(Guid))
{
await _deviceRepository.CreateAsync(device);
}
else
{
device.RevisionDate = DateTime.UtcNow;
await _deviceRepository.ReplaceAsync(device);
}
}
}
}

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Net.Mail;
using System.Threading.Tasks;
using Bit.Core.Domains;
using SendGrid;
namespace Bit.Core.Services
{
public class MailService : IMailService
{
private const string WelcomeTemplateId = "045f8ad5-5547-4fa2-8d3d-6d46e401164d";
private const string ChangeEmailAlreadyExistsTemplateId = "b69d2038-6ad9-4cf6-8f7f-7880921cba43";
private const string ChangeEmailTemplateId = "ec2c1471-8292-4f17-b6b6-8223d514f86e";
private const string NoMasterPasswordHintTemplateId = "136eb299-e102-495a-88bd-f96736eea159";
private const string MasterPasswordHintTemplateId = "be77cfde-95dd-4cb9-b5e0-8286b53885f1";
private const string AdministrativeCategoryName = "Administrative";
private const string MarketingCategoryName = "Marketing";
private readonly GlobalSettings _globalSettings;
private readonly Web _web;
public MailService(GlobalSettings globalSettings)
{
_globalSettings = globalSettings;
_web = new Web(_globalSettings.Mail.ApiKey);
}
public async Task SendWelcomeEmailAsync(User user)
{
var message = CreateDefaultMessage(WelcomeTemplateId);
message.Subject = "Welcome";
message.AddTo(user.Email);
message.SetCategories(new List<string> { AdministrativeCategoryName, "Welcome" });
await _web.DeliverAsync(message);
}
public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)
{
var message = CreateDefaultMessage(ChangeEmailAlreadyExistsTemplateId);
message.Subject = "Your Email Change";
message.AddTo(toEmail);
message.AddSubstitution("{{fromEmail}}", new List<string> { fromEmail });
message.AddSubstitution("{{toEmail}}", new List<string> { toEmail });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Change Email Alrady Exists" });
await _web.DeliverAsync(message);
}
public async Task SendChangeEmailEmailAsync(string newEmailAddress, string token)
{
var message = CreateDefaultMessage(ChangeEmailTemplateId);
message.Subject = "Change Your Email";
message.AddTo(newEmailAddress);
message.AddSubstitution("{{token}}", new List<string> { Uri.EscapeDataString(token) });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Change Email" });
message.DisableBypassListManagement();
await _web.DeliverAsync(message);
}
public async Task SendNoMasterPasswordHintEmailAsync(string email)
{
var message = CreateDefaultMessage(NoMasterPasswordHintTemplateId);
message.Subject = "Your Master Password Hint";
message.AddTo(email);
message.SetCategories(new List<string> { AdministrativeCategoryName, "No Master Password Hint" });
await _web.DeliverAsync(message);
}
public async Task SendMasterPasswordHintEmailAsync(string email, string hint)
{
var message = CreateDefaultMessage(MasterPasswordHintTemplateId);
message.Subject = "Your Master Password Hint";
message.AddTo(email);
message.AddSubstitution("{{hint}}", new List<string> { hint });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Master Password Hint" });
await _web.DeliverAsync(message);
}
private SendGridMessage CreateDefaultMessage(string templateId)
{
var message = new SendGridMessage
{
From = new MailAddress(_globalSettings.Mail.ReplyToEmail, _globalSettings.SiteName),
Html = " ",
Text = " "
};
if(!string.IsNullOrWhiteSpace(templateId))
{
message.EnableTemplateEngine(templateId);
}
message.AddSubstitution("{{siteName}}", new List<string> { _globalSettings.SiteName });
message.AddSubstitution("{{baseVaultUri}}", new List<string> { _globalSettings.BaseVaultUri });
return message;
}
}
}

View File

@ -0,0 +1,334 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Repositories;
using Newtonsoft.Json.Linq;
using PushSharp.Google;
using PushSharp.Apple;
using Microsoft.AspNetCore.Hosting;
using PushSharp.Core;
using System.Security.Cryptography.X509Certificates;
using Bit.Core.Domains;
using Bit.Core.Enums;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
namespace Bit.Core.Services
{
public class PushService : IPushService
{
private readonly IDeviceRepository _deviceRepository;
private readonly ILogger<IPushService> _logger;
private readonly CurrentContext _currentContext;
private GcmServiceBroker _gcmBroker;
private ApnsServiceBroker _apnsBroker;
public PushService(
IDeviceRepository deviceRepository,
ILogger<IPushService> logger,
CurrentContext currentContext,
IHostingEnvironment hostingEnvironment,
GlobalSettings globalSettings)
{
_deviceRepository = deviceRepository;
_logger = logger;
_currentContext = currentContext;
InitGcmBroker(globalSettings);
InitApnsBroker(globalSettings, hostingEnvironment);
}
public async Task PushSyncCipherCreateAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncCipherCreate);
}
public async Task PushSyncCipherUpdateAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncCipherUpdate);
}
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
{
switch(cipher.Type)
{
case CipherType.Folder:
await PushCipherAsync(cipher, PushType.SyncFolderDelete);
break;
case CipherType.Site:
await PushCipherAsync(cipher, PushType.SyncSiteDelete);
break;
default:
break;
}
}
private async Task PushCipherAsync(Cipher cipher, PushType type)
{
var message = new SyncCipherPushNotification
{
Type = type,
Id = cipher.Id,
UserId = cipher.UserId,
RevisionDate = cipher.RevisionDate,
Aps = new PushNotification.AppleData { ContentAvailable = 1 }
};
var excludedTokens = new List<string>();
if(!string.IsNullOrWhiteSpace(_currentContext.DeviceIdentifier))
{
excludedTokens.Add(_currentContext.DeviceIdentifier);
}
await PushToAllUserDevicesAsync(cipher.UserId, JObject.FromObject(message), excludedTokens);
}
public async Task PushSyncCiphersAsync(Guid userId)
{
var message = new SyncCiphersPushNotification
{
Type = PushType.SyncCiphers,
UserId = userId,
Date = DateTime.UtcNow,
Aps = new PushNotification.AppleData { ContentAvailable = 1 }
};
await PushToAllUserDevicesAsync(userId, JObject.FromObject(message), null);
}
private void InitGcmBroker(GlobalSettings globalSettings)
{
if(string.IsNullOrWhiteSpace(globalSettings.Push.GcmSenderId) || string.IsNullOrWhiteSpace(globalSettings.Push.GcmApiKey)
|| string.IsNullOrWhiteSpace(globalSettings.Push.GcmAppPackageName))
{
return;
}
var gcmConfig = new GcmConfiguration(globalSettings.Push.GcmSenderId, globalSettings.Push.GcmApiKey,
globalSettings.Push.GcmAppPackageName);
_gcmBroker = new GcmServiceBroker(gcmConfig);
_gcmBroker.OnNotificationFailed += GcmBroker_OnNotificationFailed;
_gcmBroker.OnNotificationSucceeded += (notification) =>
{
Debug.WriteLine("GCM Notification Sent!");
};
_gcmBroker.Start();
}
private void GcmBroker_OnNotificationFailed(GcmNotification notification, AggregateException exception)
{
exception.Handle(ex =>
{
// See what kind of exception it was to further diagnose
if(ex is GcmNotificationException)
{
var notificationException = ex as GcmNotificationException;
// Deal with the failed notification
var gcmNotification = notificationException.Notification;
var description = notificationException.Description;
Debug.WriteLine($"GCM Notification Failed: ID={gcmNotification.MessageId}, Desc={description}");
}
else if(ex is GcmMulticastResultException)
{
var multicastException = ex as GcmMulticastResultException;
foreach(var succeededNotification in multicastException.Succeeded)
{
Debug.WriteLine($"GCM Notification Failed: ID={succeededNotification.MessageId}");
}
foreach(var failedKvp in multicastException.Failed)
{
var n = failedKvp.Key;
var e = failedKvp.Value;
Debug.WriteLine($"GCM Notification Failed: ID={n.MessageId}, Desc={e.Message}");
}
}
else if(ex is DeviceSubscriptionExpiredException)
{
var expiredException = ex as DeviceSubscriptionExpiredException;
var oldId = expiredException.OldSubscriptionId;
var newId = expiredException.NewSubscriptionId;
Debug.WriteLine($"Device RegistrationId Expired: {oldId}");
if(!string.IsNullOrWhiteSpace(newId))
{
// If this value isn't null, our subscription changed and we should update our database
Debug.WriteLine($"Device RegistrationId Changed To: {newId}");
}
}
else if(ex is RetryAfterException)
{
var retryException = (RetryAfterException)ex;
// If you get rate limited, you should stop sending messages until after the RetryAfterUtc date
Debug.WriteLine($"GCM Rate Limited, don't send more until after {retryException.RetryAfterUtc}");
}
else
{
Debug.WriteLine("GCM Notification Failed for some unknown reason");
}
// Mark it as handled
return true;
});
}
private void InitApnsBroker(GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
{
if(string.IsNullOrWhiteSpace(globalSettings.Push.ApnsCertificatePassword)
|| string.IsNullOrWhiteSpace(globalSettings.Push.ApnsCertificateThumbprint))
{
return;
}
var apnsCertificate = GetCertificate(globalSettings.Push.ApnsCertificateThumbprint);
if(apnsCertificate == null)
{
return;
}
var apnsConfig = new ApnsConfiguration(hostingEnvironment.IsProduction() ?
ApnsConfiguration.ApnsServerEnvironment.Production : ApnsConfiguration.ApnsServerEnvironment.Sandbox,
apnsCertificate.RawData, globalSettings.Push.ApnsCertificatePassword);
_apnsBroker = new ApnsServiceBroker(apnsConfig);
_apnsBroker.OnNotificationFailed += ApnsBroker_OnNotificationFailed;
_apnsBroker.OnNotificationSucceeded += (notification) =>
{
Debug.WriteLine("Apple Notification Sent!");
};
_apnsBroker.Start();
var feedbackService = new FeedbackService(apnsConfig);
feedbackService.FeedbackReceived += FeedbackService_FeedbackReceived;
feedbackService.Check();
}
private void ApnsBroker_OnNotificationFailed(ApnsNotification notification, AggregateException exception)
{
exception.Handle(ex =>
{
// See what kind of exception it was to further diagnose
if(ex is ApnsNotificationException)
{
var notificationException = ex as ApnsNotificationException;
// Deal with the failed notification
var apnsNotification = notificationException.Notification;
var statusCode = notificationException.ErrorStatusCode;
Debug.WriteLine($"Apple Notification Failed: ID={apnsNotification.Identifier}, Code={statusCode}");
}
else
{
// Inner exception might hold more useful information like an ApnsConnectionException
Debug.WriteLine($"Apple Notification Failed for some unknown reason : {ex.InnerException}");
}
// Mark it as handled
return true;
});
}
private X509Certificate2 GetCertificate(string thumbprint)
{
// Clean possible garbage characters from thumbprint copy/paste
// ref http://stackoverflow.com/questions/8448147/problems-with-x509store-certificates-find-findbythumbprint
thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper();
X509Certificate2 cert = null;
var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
if(certCollection.Count > 0)
{
cert = certCollection[0];
}
certStore.Close();
return cert;
}
private void FeedbackService_FeedbackReceived(string deviceToken, DateTime timestamp)
{
// Remove the deviceToken from your database
// timestamp is the time the token was reported as expired
}
private async Task PushToAllUserDevicesAsync(Guid userId, JObject message, IEnumerable<string> tokensToSkip)
{
var devices = (await _deviceRepository.GetManyByUserIdAsync(userId))
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken) && (!tokensToSkip?.Contains(d.PushToken) ?? true));
if(devices.Count() == 0)
{
return;
}
if(_apnsBroker != null)
{
// Send to each iOS device
foreach(var device in devices.Where(d => d.Type == DeviceType.iOS))
{
_apnsBroker.QueueNotification(new ApnsNotification
{
DeviceToken = device.PushToken,
Payload = message
});
}
}
// Android can send to many devices at once
var androidDevices = devices.Where(d => d.Type == DeviceType.Android);
if(_gcmBroker != null && androidDevices.Count() > 0)
{
_gcmBroker.QueueNotification(new GcmNotification
{
RegistrationIds = androidDevices.Select(d => d.PushToken).ToList(),
Data = message
});
}
}
private class PushNotification
{
public PushType Type { get; set; }
[JsonProperty(PropertyName = "aps")]
public AppleData Aps { get; set; }
public class AppleData
{
[JsonProperty(PropertyName = "badge")]
public dynamic Badge { get; set; } = null;
[JsonProperty(PropertyName = "alert")]
public string Alert { get; set; }
[JsonProperty(PropertyName = "content-available")]
public int ContentAvailable { get; set; }
}
}
private abstract class SyncPushNotification : PushNotification
{
public Guid UserId { get; set; }
}
private class SyncCipherPushNotification : SyncPushNotification
{
public Guid Id { get; set; }
public DateTime RevisionDate { get; set; }
}
private class SyncCiphersPushNotification : SyncPushNotification
{
public DateTime Date { get; set; }
}
}
}

View File

@ -0,0 +1,318 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Bit.Core.Domains;
using Bit.Core.Repositories;
using OtpSharp;
using Base32;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Bit.Core.Enums;
namespace Bit.Core.Services
{
public class UserService : UserManager<User>, IUserService, IDisposable
{
private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IMailService _mailService;
private readonly IdentityErrorDescriber _identityErrorDescriber;
private readonly IdentityOptions _identityOptions;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;
public UserService(
IUserRepository userRepository,
ICipherRepository cipherRepository,
IMailService mailService,
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)
: base(
store,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger)
{
_userRepository = userRepository;
_cipherRepository = cipherRepository;
_mailService = mailService;
_identityOptions = optionsAccessor?.Value ?? new IdentityOptions();
_identityErrorDescriber = errors;
_passwordHasher = passwordHasher;
_passwordValidators = passwordValidators;
}
public async Task<User> GetUserByIdAsync(Guid userId)
{
return await _userRepository.GetByIdAsync(userId);
}
public async Task SaveUserAsync(User user)
{
if(user.Id == default(Guid))
{
throw new ApplicationException("Use register method to create a new user.");
}
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
public async Task<IdentityResult> RegisterUserAsync(User user, string masterPassword)
{
var result = await base.CreateAsync(user, masterPassword);
if(result == IdentityResult.Success)
{
await _mailService.SendWelcomeEmailAsync(user);
}
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 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, IEnumerable<Cipher> ciphers)
{
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 result = await UpdatePasswordHash(user, newMasterPassword);
if(!result.Succeeded)
{
return result;
}
user.Email = newEmail;
user.EmailVerified = true;
user.RevisionDate = DateTime.UtcNow;
if(ciphers.Any())
{
await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers);
}
else
{
await _userRepository.ReplaceAsync(user);
}
return IdentityResult.Success;
}
public override Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword)
{
throw new NotImplementedException();
}
public async Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, IEnumerable<Cipher> ciphers)
{
if(user == null)
{
throw new ArgumentNullException(nameof(user));
}
if(await base.CheckPasswordAsync(user, masterPassword))
{
var result = await UpdatePasswordHash(user, newMasterPassword);
if(!result.Succeeded)
{
return result;
}
user.RevisionDate = DateTime.UtcNow;
if(ciphers.Any())
{
await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers);
}
else
{
await _userRepository.ReplaceAsync(user);
}
return IdentityResult.Success;
}
Logger.LogWarning("Change password failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPassword)
{
if(user == null)
{
throw new ArgumentNullException(nameof(user));
}
if(await base.CheckPasswordAsync(user, masterPassword))
{
var result = await base.UpdateSecurityStampAsync(user);
if(!result.Succeeded)
{
return result;
}
await SaveUserAsync(user);
return IdentityResult.Success;
}
Logger.LogWarning("Refresh security stamp failed for user {userId}.", user.Id);
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task GetTwoFactorAsync(User user, Enums.TwoFactorProvider provider)
{
if(user.TwoFactorEnabled && user.TwoFactorProvider.HasValue && user.TwoFactorProvider.Value == provider)
{
switch(provider)
{
case Enums.TwoFactorProvider.Authenticator:
if(!string.IsNullOrWhiteSpace(user.AuthenticatorKey))
{
return;
}
break;
default:
throw new ArgumentException(nameof(provider));
}
}
user.TwoFactorProvider = provider;
// Reset authenticator key.
user.AuthenticatorKey = null;
switch(provider)
{
case Enums.TwoFactorProvider.Authenticator:
var key = KeyGeneration.GenerateRandomKey(20);
user.AuthenticatorKey = Base32Encoder.Encode(key);
break;
default:
throw new ArgumentException(nameof(provider));
}
await SaveUserAsync(user);
}
public async Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode)
{
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 base.CheckPasswordAsync(user, masterPassword))
{
return false;
}
if(string.Compare(user.TwoFactorRecoveryCode, recoveryCode, true) != 0)
{
return false;
}
user.TwoFactorProvider = TwoFactorProvider.Authenticator;
user.TwoFactorEnabled = false;
user.TwoFactorRecoveryCode = null;
await SaveUserAsync(user);
return true;
}
private async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword, bool validatePassword = true)
{
if(validatePassword)
{
var validate = await ValidatePasswordInternal(user, newPassword);
if(!validate.Succeeded)
{
return validate;
}
}
user.MasterPassword = _passwordHasher.HashPassword(user, newPassword);
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;
}
}
}