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.
-
+
+
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);
+ }
}
}