From 6e16581fe841cf9544224f113474ffb839818aae Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 21 Mar 2018 21:19:03 -0400 Subject: [PATCH] passwordless signin email --- src/Admin/Controllers/LoginController.cs | 3 ++- src/Admin/Views/Login/Index.cshtml | 7 ++++--- src/Billing/Controllers/LoginController.cs | 3 ++- src/Core/Core.csproj | 9 +++++++++ src/Core/Identity/PasswordlessSignInManager.cs | 14 ++++++++------ .../MailTemplates/Markdown/PasswordlessSignIn.md | 5 +++++ .../MailTemplates/Razor/PasswordlessSignIn.cshtml | 11 +++++++++++ .../Razor/PasswordlessSignIn.text.cshtml | 9 +++++++++ src/Core/Models/Mail/PasswordlessSignInModel.cs | 7 +++++++ src/Core/Services/IMailService.cs | 3 ++- .../Services/Implementations/BackupMailService.cs | 13 +++++++++++++ .../Implementations/MarkdownMailService.cs | 13 +++++++++++++ .../Services/Implementations/RazorMailService.cs | 13 +++++++++++++ .../NoopImplementations/NoopMailService.cs | 5 +++++ 14 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 src/Core/MailTemplates/Markdown/PasswordlessSignIn.md create mode 100644 src/Core/MailTemplates/Razor/PasswordlessSignIn.cshtml create mode 100644 src/Core/MailTemplates/Razor/PasswordlessSignIn.text.cshtml create mode 100644 src/Core/Models/Mail/PasswordlessSignInModel.cs diff --git a/src/Admin/Controllers/LoginController.cs b/src/Admin/Controllers/LoginController.cs index ee568c67b2..b4bb678a94 100644 --- a/src/Admin/Controllers/LoginController.cs +++ b/src/Admin/Controllers/LoginController.cs @@ -27,7 +27,8 @@ namespace Bit.Admin.Controllers { if(ModelState.IsValid) { - await _signInManager.PasswordlessSignInAsync(model.Email); + await _signInManager.PasswordlessSignInAsync(model.Email, + Url.Action("Confirm", "Login", null, Request.Scheme)); return RedirectToAction("Index", "Home"); } diff --git a/src/Admin/Views/Login/Index.cshtml b/src/Admin/Views/Login/Index.cshtml index 930f5613f7..a3522399f0 100644 --- a/src/Admin/Views/Login/Index.cshtml +++ b/src/Admin/Views/Login/Index.cshtml @@ -3,8 +3,8 @@ ViewData["Title"] = "Login"; } -
-
+
+

Please enter your email address below to log in.

@@ -15,7 +15,8 @@ We'll email you a secure login link.
- +
+ diff --git a/src/Billing/Controllers/LoginController.cs b/src/Billing/Controllers/LoginController.cs index 38088a93de..9094ba8882 100644 --- a/src/Billing/Controllers/LoginController.cs +++ b/src/Billing/Controllers/LoginController.cs @@ -27,7 +27,8 @@ namespace Billing.Controllers { if(ModelState.IsValid) { - var result = await _signInManager.PasswordlessSignInAsync(model.Email); + var result = await _signInManager.PasswordlessSignInAsync(model.Email, + Url.Action("Confirm", "Login", null, Request.Scheme)); if(result.Succeeded) { return RedirectToAction("Index", "Home"); diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 779914eb71..4848e3a947 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -6,6 +6,12 @@ false + + + + + + @@ -14,11 +20,14 @@ + + + diff --git a/src/Core/Identity/PasswordlessSignInManager.cs b/src/Core/Identity/PasswordlessSignInManager.cs index c64c89ad63..67957aaf23 100644 --- a/src/Core/Identity/PasswordlessSignInManager.cs +++ b/src/Core/Identity/PasswordlessSignInManager.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Threading.Tasks; +using Bit.Core.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -13,17 +14,21 @@ namespace Bit.Core.Identity { public const string PasswordlessSignInPurpose = "PasswordlessSignIn"; + private readonly IMailService _mailService; + public PasswordlessSignInManager(UserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, - IAuthenticationSchemeProvider schemes) + IAuthenticationSchemeProvider schemes, + IMailService mailService) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes) { + _mailService = mailService; } - public async Task PasswordlessSignInAsync(string email) + public async Task PasswordlessSignInAsync(string email, string loginConfirmUrl) { var user = await UserManager.FindByEmailAsync(email); if(user == null) @@ -33,10 +38,7 @@ namespace Bit.Core.Identity var token = await UserManager.GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, PasswordlessSignInPurpose); - - // TODO: send email - var encodedToken = WebUtility.UrlEncode(token); - + await _mailService.SendPasswordlessSignInAsync(loginConfirmUrl, token, email); return SignInResult.Success; } diff --git a/src/Core/MailTemplates/Markdown/PasswordlessSignIn.md b/src/Core/MailTemplates/Markdown/PasswordlessSignIn.md new file mode 100644 index 0000000000..ad22071a3d --- /dev/null +++ b/src/Core/MailTemplates/Markdown/PasswordlessSignIn.md @@ -0,0 +1,5 @@ +Click the following link to log in: + +<{{url}}> + +If you did not request to log in, you can safely ignore this email. diff --git a/src/Core/MailTemplates/Razor/PasswordlessSignIn.cshtml b/src/Core/MailTemplates/Razor/PasswordlessSignIn.cshtml new file mode 100644 index 0000000000..0675fbc0f4 --- /dev/null +++ b/src/Core/MailTemplates/Razor/PasswordlessSignIn.cshtml @@ -0,0 +1,11 @@ +@model Bit.Core.Models.Mail.PasswordlessSignInModel +@{ + Layout = "_BasicMailLayout"; +} +

+ Click the following link to log in: +

+

@Model.Url

+

+ If you did not request to log in, you can safely ignore this email. +

\ No newline at end of file diff --git a/src/Core/MailTemplates/Razor/PasswordlessSignIn.text.cshtml b/src/Core/MailTemplates/Razor/PasswordlessSignIn.text.cshtml new file mode 100644 index 0000000000..b68df45d46 --- /dev/null +++ b/src/Core/MailTemplates/Razor/PasswordlessSignIn.text.cshtml @@ -0,0 +1,9 @@ +@model Bit.Core.Models.Mail.PasswordlessSignInModel +@{ + Layout = "_BasicMailLayout.text"; +} +Click the following link to log in: + +@Raw(Model.Url) + +If you did not request to log in, you can safely ignore this email. diff --git a/src/Core/Models/Mail/PasswordlessSignInModel.cs b/src/Core/Models/Mail/PasswordlessSignInModel.cs new file mode 100644 index 0000000000..a09d5f7b0e --- /dev/null +++ b/src/Core/Models/Mail/PasswordlessSignInModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Mail +{ + public class PasswordlessSignInModel + { + public string Url { get; set; } + } +} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index cc0f4a8d00..a1925f658f 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -18,5 +18,6 @@ namespace Bit.Core.Services Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, string token); Task SendOrganizationAcceptedEmailAsync(string organizationName, string userEmail, IEnumerable adminEmails); Task SendOrganizationConfirmedEmailAsync(string organizationName, string email); + Task SendPasswordlessSignInAsync(string baseUrl, string token, string email); } -} \ No newline at end of file +} diff --git a/src/Core/Services/Implementations/BackupMailService.cs b/src/Core/Services/Implementations/BackupMailService.cs index c804181c46..08d00bd400 100644 --- a/src/Core/Services/Implementations/BackupMailService.cs +++ b/src/Core/Services/Implementations/BackupMailService.cs @@ -153,6 +153,19 @@ namespace Bit.Core.Services } } + public async Task SendPasswordlessSignInAsync(string baseUrl, string token, string email) + { + try + { + await _primaryMailService.SendPasswordlessSignInAsync(baseUrl, token, email); + } + catch(Exception e) + { + LogError(e); + await _backupMailService.SendPasswordlessSignInAsync(baseUrl, token, email); + } + } + public async Task SendWelcomeEmailAsync(User user) { try diff --git a/src/Core/Services/Implementations/MarkdownMailService.cs b/src/Core/Services/Implementations/MarkdownMailService.cs index d669b71ba8..f26f17e370 100644 --- a/src/Core/Services/Implementations/MarkdownMailService.cs +++ b/src/Core/Services/Implementations/MarkdownMailService.cs @@ -170,6 +170,19 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendPasswordlessSignInAsync(string baseUrl, string token, string email) + { + var model = new Dictionary + { + ["url"] = string.Format("{0}?email={1}&token={2}", baseUrl, WebUtility.UrlEncode(email), + WebUtility.UrlEncode(token)) + }; + + var message = await CreateMessageAsync("Continue Logging In", email, "PasswordlessSignIn", model); + message.MetaData.Add("SendGridBypassListManagement", true); + await _mailDeliveryService.SendEmailAsync(message); + } + private async Task CreateMessageAsync(string subject, string toEmail, string fileName, Dictionary model) { diff --git a/src/Core/Services/Implementations/RazorMailService.cs b/src/Core/Services/Implementations/RazorMailService.cs index c47126ec8c..6e4609ceab 100644 --- a/src/Core/Services/Implementations/RazorMailService.cs +++ b/src/Core/Services/Implementations/RazorMailService.cs @@ -203,6 +203,19 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendPasswordlessSignInAsync(string baseUrl, string token, string email) + { + var message = CreateDefaultMessage("Continue Logging In", email); + var model = new PasswordlessSignInModel + { + Url = string.Format("{0}?email={1}&token={2}", baseUrl, WebUtility.UrlEncode(email), + WebUtility.UrlEncode(token)) + }; + message.HtmlContent = await _engine.CompileRenderAsync("PasswordlessSignIn", model); + message.TextContent = await _engine.CompileRenderAsync("PasswordlessSignIn.text", model); + await _mailDeliveryService.SendEmailAsync(message); + } + private MailMessage CreateDefaultMessage(string subject, string toEmail) { return CreateDefaultMessage(subject, new List { toEmail }); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 967572121d..871c30f34c 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -61,5 +61,10 @@ namespace Bit.Core.Services { return Task.FromResult(0); } + + public Task SendPasswordlessSignInAsync(string baseUrl, string token, string email) + { + return Task.FromResult(0); + } } }