mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 16:12:49 -05:00
initial commit of source
This commit is contained in:
16
src/Core/Services/IMailService.cs
Normal file
16
src/Core/Services/IMailService.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Domains;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public interface IMailService
|
||||
{
|
||||
Task SendAlreadyRegisteredEmailAsync(string registrantEmailAddress);
|
||||
Task SendRegisterEmailAsync(string registrantEmailAddress, string token);
|
||||
Task SendWelcomeEmailAsync(User user);
|
||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||
Task SendNoMasterPasswordHintEmailAsync(string email);
|
||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||
}
|
||||
}
|
22
src/Core/Services/IUserService.cs
Normal file
22
src/Core/Services/IUserService.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Bit.Core.Domains;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public interface IUserService
|
||||
{
|
||||
Task<User> GetUserByIdAsync(string userId);
|
||||
Task SaveUserAsync(User user);
|
||||
Task InitiateRegistrationAsync(string email);
|
||||
Task<IdentityResult> RegisterUserAsync(string token, User user, string masterPassword);
|
||||
Task SendMasterPasswordHintAsync(string email);
|
||||
Task InitiateEmailChangeAsync(User user, string newEmail);
|
||||
Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, IEnumerable<dynamic> ciphers);
|
||||
Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable<dynamic> ciphers);
|
||||
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
|
||||
Task GetTwoFactorAsync(User user, Enums.TwoFactorProvider provider);
|
||||
}
|
||||
}
|
138
src/Core/Services/MailService.cs
Normal file
138
src/Core/Services/MailService.cs
Normal file
@ -0,0 +1,138 @@
|
||||
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 AlreadyRegisteredTemplateId = "8af9cd2b-e4dd-497a-bcc6-1d5b317ff811";
|
||||
private const string RegisterTemplateId = "7382e1f9-50c7-428d-aa06-bf584f03cd6a";
|
||||
private const string WelcomeTemplateId = "d24aa21e-5ead-45d8-a14e-f96ba7ec63ff";
|
||||
private const string ChangeEmailAlreadyExistsTemplateId = "b28bc69e-9592-4320-b274-bfb955667add";
|
||||
private const string ChangeEmailTemplateId = "b8d17dd7-c883-4b47-8170-5b845d487929";
|
||||
private const string NoMasterPasswordHint = "d5d13bba-3f67-4899-9995-514c1bd6dae7";
|
||||
private const string MasterPasswordHint = "804a9897-1284-42e8-8aed-ab318c378b71";
|
||||
|
||||
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 SendAlreadyRegisteredEmailAsync(string registrantEmailAddress)
|
||||
{
|
||||
var message = CreateDefaultMessage(AlreadyRegisteredTemplateId);
|
||||
|
||||
message.Subject = "Your Registration";
|
||||
message.AddTo(registrantEmailAddress);
|
||||
message.AddSubstitution("{{email}}", new List<string> { registrantEmailAddress });
|
||||
message.SetCategories(new List<string> { AdministrativeCategoryName, "Already Registered" });
|
||||
|
||||
await _web.DeliverAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendRegisterEmailAsync(string registrantEmailAddress, string token)
|
||||
{
|
||||
var message = CreateDefaultMessage(RegisterTemplateId);
|
||||
|
||||
message.Subject = "Complete Your Registration";
|
||||
message.AddTo(registrantEmailAddress);
|
||||
message.AddSubstitution("{{token}}", new List<string> { Uri.EscapeDataString(token) });
|
||||
message.AddSubstitution("{{email}}", new List<string> { Uri.EscapeDataString(registrantEmailAddress) });
|
||||
message.SetCategories(new List<string> { AdministrativeCategoryName, "Register" });
|
||||
message.DisableBypassListManagement();
|
||||
|
||||
await _web.DeliverAsync(message);
|
||||
}
|
||||
|
||||
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(NoMasterPasswordHint);
|
||||
|
||||
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(MasterPasswordHint);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
308
src/Core/Services/UserService.cs
Normal file
308
src/Core/Services/UserService.cs
Normal file
@ -0,0 +1,308 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.DataProtection;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.OptionsModel;
|
||||
using Bit.Core.Domains;
|
||||
using Bit.Core.Repositories;
|
||||
using OtpSharp;
|
||||
using Base32;
|
||||
using System.Linq;
|
||||
|
||||
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 ITimeLimitedDataProtector _registrationEmailDataProtector;
|
||||
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,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
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,
|
||||
IHttpContextAccessor contextAccessor)
|
||||
: base(
|
||||
store,
|
||||
optionsAccessor,
|
||||
passwordHasher,
|
||||
userValidators,
|
||||
passwordValidators,
|
||||
keyNormalizer,
|
||||
errors,
|
||||
services,
|
||||
logger,
|
||||
contextAccessor)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_cipherRepository = cipherRepository;
|
||||
_mailService = mailService;
|
||||
_registrationEmailDataProtector = dataProtectionProvider.CreateProtector("RegistrationEmail").ToTimeLimitedDataProtector();
|
||||
_identityOptions = optionsAccessor?.Value ?? new IdentityOptions();
|
||||
_identityErrorDescriber = errors;
|
||||
_passwordHasher = passwordHasher;
|
||||
_passwordValidators = passwordValidators;
|
||||
}
|
||||
|
||||
public async Task<User> GetUserByIdAsync(string userId)
|
||||
{
|
||||
return await _userRepository.GetByIdAsync(userId);
|
||||
}
|
||||
|
||||
public async Task SaveUserAsync(User user)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(user.Id))
|
||||
{
|
||||
throw new ApplicationException("Use register method to create a new user.");
|
||||
}
|
||||
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
}
|
||||
|
||||
public async Task InitiateRegistrationAsync(string email)
|
||||
{
|
||||
var existingUser = await _userRepository.GetByEmailAsync(email);
|
||||
if(existingUser != null)
|
||||
{
|
||||
await _mailService.SendAlreadyRegisteredEmailAsync(email);
|
||||
return;
|
||||
}
|
||||
|
||||
var token = _registrationEmailDataProtector.Protect(email, TimeSpan.FromDays(5));
|
||||
await _mailService.SendRegisterEmailAsync(email, token);
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> RegisterUserAsync(string token, User user, string masterPassword)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenEmail = _registrationEmailDataProtector.Unprotect(token);
|
||||
if(tokenEmail != user.Email)
|
||||
{
|
||||
return IdentityResult.Failed(_identityErrorDescriber.InvalidToken());
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return IdentityResult.Failed(_identityErrorDescriber.InvalidToken());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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<dynamic> 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)
|
||||
{
|
||||
return IdentityResult.Failed(_identityErrorDescriber.DuplicateEmail(newEmail));
|
||||
}
|
||||
|
||||
user.OldEmail = user.Email;
|
||||
user.OldMasterPassword = user.MasterPassword;
|
||||
user.Email = newEmail;
|
||||
user.MasterPassword = _passwordHasher.HashPassword(user, newMasterPassword);
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
await _userRepository.ReplaceAndDirtyCiphersAsync(user);
|
||||
await _cipherRepository.UpdateDirtyCiphersAsync(ciphers);
|
||||
|
||||
// TODO: what if something fails? rollback?
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
public override Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable<dynamic> ciphers)
|
||||
{
|
||||
if(user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if(await base.CheckPasswordAsync(user, currentMasterPasswordHash))
|
||||
{
|
||||
var result = await UpdatePasswordHash(user, newMasterPasswordHash);
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
await _userRepository.ReplaceAndDirtyCiphersAsync(user);
|
||||
await _cipherRepository.UpdateDirtyCiphersAsync(ciphers);
|
||||
|
||||
// TODO: what if something fails? rollback?
|
||||
|
||||
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 masterPasswordHash)
|
||||
{
|
||||
if(user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if(await base.CheckPasswordAsync(user, masterPasswordHash))
|
||||
{
|
||||
var result = await base.UpdateSecurityStampAsync(user);
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
await _userRepository.ReplaceAndDirtyCiphersAsync(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 _userRepository.ReplaceAsync(user);
|
||||
}
|
||||
|
||||
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.OldMasterPassword = user.MasterPassword;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user