Skip to content

Commit

Permalink
Adding 2FA Authenticator Support (dotnet-architecture#66)
Browse files Browse the repository at this point in the history
* Adding support for 2fa, more auth options

* WIP getting auth stuff working

* Added Manage views. 2FA working now for MVC app.

* Switching to using a controller for no-UI logout scenario

* Adding Razor Pages impl of 2FA auth stuff. Works.
  • Loading branch information
ardalis authored Oct 24, 2017
1 parent 101b7ba commit 3d46c80
Show file tree
Hide file tree
Showing 75 changed files with 2,702 additions and 58 deletions.
10 changes: 10 additions & 0 deletions src/ApplicationCore/Interfaces/IEmailSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.Tasks;

namespace ApplicationCore.Interfaces
{

public interface IEmailSender
{
Task SendEmailAsync(string email, string subject, string message);
}
}
1 change: 0 additions & 1 deletion src/Infrastructure/Identity/ApplicationUser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Identity;


namespace Infrastructure.Identity
{
public class ApplicationUser : IdentityUser
Expand Down
1 change: 0 additions & 1 deletion src/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Data\Migrations\" />
<Folder Include="Services\" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions src/Infrastructure/Services/EmailSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using ApplicationCore.Interfaces;
using System.Threading.Tasks;

namespace Infrastructure.Services
{
// This class is used by the application to send email for account confirmation and password reset.
// For more details see https://go.microsoft.com/fwlink/?LinkID=532713
public class EmailSender : IEmailSender
{
public Task SendEmailAsync(string email, string subject, string message)
{
// TODO: Wire this up to actual email sending logic via SendGrid, local SMTP, etc.
return Task.CompletedTask;
}
}
}
116 changes: 109 additions & 7 deletions src/Web/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
using Microsoft.eShopWeb.ViewModels;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using ApplicationCore.Interfaces;
using Infrastructure.Identity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Infrastructure.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ViewModels.Account;
using System;
using Microsoft.AspNetCore.Authentication;
using ApplicationCore.Interfaces;
using System.Threading.Tasks;
using Web.ViewModels.Account;

namespace Microsoft.eShopWeb.Controllers
{

[Route("[controller]/[action]")]
[Authorize]
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IBasketService _basketService;
private readonly IAppLogger<AccountController> _logger;

public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IBasketService basketService)
IBasketService basketService,
IAppLogger<AccountController> logger)
{
_userManager = userManager;
_signInManager = signInManager;
_basketService = basketService;
_logger = logger;
}

// GET: /Account/SignIn
Expand Down Expand Up @@ -58,6 +63,10 @@ public async Task<IActionResult> SignIn(LoginViewModel model, string returnUrl =
ViewData["ReturnUrl"] = returnUrl;

var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe });
}
if (result.Succeeded)
{
string anonymousBasketId = Request.Cookies[Constants.BASKET_COOKIENAME];
Expand All @@ -72,6 +81,70 @@ public async Task<IActionResult> SignIn(LoginViewModel model, string returnUrl =
return View(model);
}

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
{
// Ensure the user has gone through the username & password screen first
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();

if (user == null)
{
throw new ApplicationException($"Unable to load two-factor authentication user.");
}

var model = new LoginWith2faViewModel { RememberMe = rememberMe };
ViewData["ReturnUrl"] = returnUrl;

return View(model);
}

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return View(model);
}

var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);

var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);

if (result.Succeeded)
{
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
return RedirectToLocal(returnUrl);
}
else if (result.IsLockedOut)
{
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
return RedirectToAction(nameof(Lockout));
}
else
{
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View();
}
}

[HttpGet]
[AllowAnonymous]
public IActionResult Lockout()
{
return View();
}


[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> SignOut()
Expand Down Expand Up @@ -107,6 +180,35 @@ public async Task<IActionResult> Register(RegisterViewModel model, string return
return View(model);
}

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return RedirectToAction(nameof(CatalogController.Index), "Catalog");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}

[HttpGet]
[AllowAnonymous]
public IActionResult ResetPassword(string code = null)
{
if (code == null)
{
throw new ApplicationException("A code must be supplied for password reset.");
}
var model = new ResetPasswordViewModel { Code = code };
return View(model);
}

private IActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
Expand Down
Loading

0 comments on commit 3d46c80

Please sign in to comment.