From b2295f867ba681783b41736ff3bae0b3dff2fa9d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 9 Aug 2017 10:53:42 -0400 Subject: [PATCH] account recovery to delete via email --- src/Api/Controllers/AccountsController.cs | 32 +++++++++++++++++++ src/Core/Core.csproj | 2 ++ src/Core/MailTemplates/VerifyDelete.cshtml | 9 ++++++ .../MailTemplates/VerifyDelete.text.cshtml | 10 ++++++ .../Accounts/DeleteRecoverRequestModel.cs | 12 +++++++ .../VerifyDeleteRecoverRequestModel.cs | 12 +++++++ src/Core/Models/Mail/VerifyDeleteModel.cs | 18 +++++++++++ src/Core/Services/IMailService.cs | 1 + src/Core/Services/IUserService.cs | 2 ++ .../Implementations/RazorViewMailService.cs | 19 +++++++++++ .../SendGridTemplateMailService.cs | 20 +++++++++++- .../Services/Implementations/UserService.cs | 23 +++++++++++++ .../NoopImplementations/NoopMailService.cs | 5 +++ 13 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/Core/MailTemplates/VerifyDelete.cshtml create mode 100644 src/Core/MailTemplates/VerifyDelete.text.cshtml create mode 100644 src/Core/Models/Api/Request/Accounts/DeleteRecoverRequestModel.cs create mode 100644 src/Core/Models/Api/Request/Accounts/VerifyDeleteRecoverRequestModel.cs create mode 100644 src/Core/Models/Mail/VerifyDeleteModel.cs diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index e8049a6c8..210a9e254 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -345,6 +345,38 @@ namespace Bit.Api.Controllers throw new BadRequestException(ModelState); } + [AllowAnonymous] + [HttpPost("delete-recover")] + public async Task PostDeleteRecover([FromBody]DeleteRecoverRequestModel model) + { + await _userService.SendDeleteConfirmationAsync(model.Email); + } + + [HttpPost("delete-recover-token")] + [AllowAnonymous] + public async Task PostDeleteRecoverToken([FromBody]VerifyDeleteRecoverRequestModel model) + { + var user = await _userService.GetUserByIdAsync(new Guid(model.UserId)); + if(user == null) + { + throw new UnauthorizedAccessException(); + } + + var result = await _userService.DeleteAsync(user, model.Token); + if(result.Succeeded) + { + return; + } + + foreach(var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + + await Task.Delay(2000); + throw new BadRequestException(ModelState); + } + [HttpPost("premium")] public async Task PostPremium([FromBody]PremiumRequestModel model) { diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 9bbbf5f8c..0f13525ee 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -7,6 +7,8 @@ + + diff --git a/src/Core/MailTemplates/VerifyDelete.cshtml b/src/Core/MailTemplates/VerifyDelete.cshtml new file mode 100644 index 000000000..c50936be5 --- /dev/null +++ b/src/Core/MailTemplates/VerifyDelete.cshtml @@ -0,0 +1,9 @@ +@model Bit.Core.Models.Mail.VerifyDeleteModel +@{ + Layout = "_BasicMailLayout"; +} +

+ Click the link below to delete your bitwarden account (@Model.Email). + If you did not request this email to delete your bitwarden account, you can safely ignore it. +

+

@Model.Url

diff --git a/src/Core/MailTemplates/VerifyDelete.text.cshtml b/src/Core/MailTemplates/VerifyDelete.text.cshtml new file mode 100644 index 000000000..5061c2db3 --- /dev/null +++ b/src/Core/MailTemplates/VerifyDelete.text.cshtml @@ -0,0 +1,10 @@ +@model Bit.Core.Models.Mail.VerifyDeleteModel +@{ + Layout = "_BasicMailLayout.text"; +} +Click the link below to delete your bitwarden +account (@Model.Email). If you did not request +this email to delete your bitwarden account, +you can safely ignore it. + +@Model.Url diff --git a/src/Core/Models/Api/Request/Accounts/DeleteRecoverRequestModel.cs b/src/Core/Models/Api/Request/Accounts/DeleteRecoverRequestModel.cs new file mode 100644 index 000000000..39721eb25 --- /dev/null +++ b/src/Core/Models/Api/Request/Accounts/DeleteRecoverRequestModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class DeleteRecoverRequestModel + { + [Required] + [EmailAddress] + [StringLength(50)] + public string Email { get; set; } + } +} diff --git a/src/Core/Models/Api/Request/Accounts/VerifyDeleteRecoverRequestModel.cs b/src/Core/Models/Api/Request/Accounts/VerifyDeleteRecoverRequestModel.cs new file mode 100644 index 000000000..481b00b15 --- /dev/null +++ b/src/Core/Models/Api/Request/Accounts/VerifyDeleteRecoverRequestModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class VerifyDeleteRecoverRequestModel + { + [Required] + public string UserId { get; set; } + [Required] + public string Token { get; set; } + } +} diff --git a/src/Core/Models/Mail/VerifyDeleteModel.cs b/src/Core/Models/Mail/VerifyDeleteModel.cs new file mode 100644 index 000000000..8d788d2ea --- /dev/null +++ b/src/Core/Models/Mail/VerifyDeleteModel.cs @@ -0,0 +1,18 @@ +using System; + +namespace Bit.Core.Models.Mail +{ + public class VerifyDeleteModel : BaseMailModel + { + public string Url => string.Format("{0}/verify-recover-delete?userId={1}&token={2}&email={3}", + WebVaultUrl, + UserId, + Token, + EmailEncoded); + + public Guid UserId { get; set; } + public string Email { get; set; } + public string EmailEncoded { get; set; } + public string Token { get; set; } + } +} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 1ed6370b6..cc0f4a8d0 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -9,6 +9,7 @@ namespace Bit.Core.Services { Task SendWelcomeEmailAsync(User user); Task SendVerifyEmailEmailAsync(string email, Guid userId, string token); + Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token); Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail); Task SendChangeEmailEmailAsync(string newEmailAddress, string token); Task SendTwoFactorEmailAsync(string email, string token); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index a31e965e6..7016a1427 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -38,6 +38,8 @@ namespace Bit.Core.Services Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); Task GenerateUserTokenAsync(User user, string tokenProvider, string purpose); Task DeleteAsync(User user); + Task DeleteAsync(User user, string token); + Task SendDeleteConfirmationAsync(string email); Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb); Task AdjustStorageAsync(User user, short storageAdjustmentGb); Task ReplacePaymentMethodAsync(User user, string paymentToken); diff --git a/src/Core/Services/Implementations/RazorViewMailService.cs b/src/Core/Services/Implementations/RazorViewMailService.cs index 6664d76e2..a8304a685 100644 --- a/src/Core/Services/Implementations/RazorViewMailService.cs +++ b/src/Core/Services/Implementations/RazorViewMailService.cs @@ -47,6 +47,25 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token) + { + var message = CreateDefaultMessage("Delete Your Account", email); + var model = new VerifyDeleteModel + { + Token = WebUtility.UrlEncode(token), + UserId = userId, + WebVaultUrl = _globalSettings.BaseServiceUri.Vault, + SiteName = _globalSettings.SiteName, + Email = email, + EmailEncoded = WebUtility.UrlEncode(email) + }; + message.HtmlContent = _engine.Parse("VerifyDelete", model); + message.TextContent = _engine.Parse("VerifyDelete.text", model); + message.MetaData.Add("SendGridBypassListManagement", true); + + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail) { var message = CreateDefaultMessage("Your Email Change", toEmail); diff --git a/src/Core/Services/Implementations/SendGridTemplateMailService.cs b/src/Core/Services/Implementations/SendGridTemplateMailService.cs index 736b95918..5a83671f7 100644 --- a/src/Core/Services/Implementations/SendGridTemplateMailService.cs +++ b/src/Core/Services/Implementations/SendGridTemplateMailService.cs @@ -11,6 +11,7 @@ namespace Bit.Core.Services { private const string WelcomeTemplateId = "045f8ad5-5547-4fa2-8d3d-6d46e401164d"; private const string VerifyEmailTemplateId = "TODO"; + private const string VerifyDeleteTemplateId = "TODO"; private const string ChangeEmailAlreadyExistsTemplateId = "b69d2038-6ad9-4cf6-8f7f-7880921cba43"; private const string ChangeEmailTemplateId = "ec2c1471-8292-4f17-b6b6-8223d514f86e"; private const string TwoFactorEmailTemplateId = "264cfe69-5258-4c89-8d90-76b4659de589"; @@ -53,7 +54,7 @@ namespace Bit.Core.Services email, VerifyEmailTemplateId); - AddSubstitution(message, "{{token}}", Uri.EscapeDataString(token)); + AddSubstitution(message, "{{token}}", WebUtility.UrlEncode(token)); AddSubstitution(message, "{{userId}}", userId.ToString()); AddCategories(message, new List { AdministrativeCategoryName, "Verify Email" }); message.MetaData.Add("SendGridBypassListManagement", true); @@ -61,6 +62,23 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token) + { + var message = CreateDefaultMessage( + "Delete Your Account", + email, + VerifyDeleteTemplateId); + + AddSubstitution(message, "{{token}}", WebUtility.UrlEncode(token)); + AddSubstitution(message, "{{email}}", email); + AddSubstitution(message, "{{emailUrlEncoded}}", WebUtility.UrlEncode(email)); + AddSubstitution(message, "{{userId}}", userId.ToString()); + AddCategories(message, new List { AdministrativeCategoryName, "Verify Delete" }); + message.MetaData.Add("SendGridBypassListManagement", true); + + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail) { var message = CreateDefaultMessage( diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 9748a222e..acf058e06 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -178,6 +178,29 @@ namespace Bit.Core.Services return IdentityResult.Success; } + public async Task 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 RegisterUserAsync(User user, string masterPassword) { var result = await base.CreateAsync(user, masterPassword); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 705cccdfc..967572121 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -56,5 +56,10 @@ namespace Bit.Core.Services { return Task.FromResult(0); } + + public Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token) + { + return Task.FromResult(0); + } } }