Содержание
Двухфакторная аутентификация позволяет дополнительно защитить ваш аккаунт от несанкционированного доступа. В общих чертах, двухфакторная аутентификация выглядит следующим образом: после ввода логина и пароля, пользователю на почту или по SMS направляется код подтверждения (одноразовый пароль), которые необходимо передать в приложение. После передачи кода подтверждения пользователь получает доступ ко всем возможностям, доступным его аккаунту. По умолчанию, двухэтапная аутентификация отключена для новых пользователей приложения ASP.NET Core MVC и в этой части мы рассмотрим процесс включения и использования двухэтапной аутентификации в ASP.NET Core.
Включение двухэтапной аутентификации для пользователя
Вернемся к нашему приложению. На данный момент, для всех новых пользователей двухэтапная аутентификация отключена. Убедиться в этом можно, посмотрев в базе данных таблицу пользователей:
Для того, чтобы заработала двухэтапная аутентификация, у объекта пользователя два флага должны быть равны true
— это TwoFactorEnabled
и EmailConfirmed
(адрес электронной почты подтвержден). Что касается подтверждения адреса электронной почты, то мы это делали в предыдущей части и уже зарегистрировали пользователя с подтвержденным адресом электронной почты.
Чтобы включить двухфакторную аутентификацию для пользователя, нам необходимо внести изменения в базу данных, а именно в таблицу пользователей. Для этого изменим немного код компонента, который мы разрабытывали в одной из частей этой главы
а именно — создадим новую ссылку на страницу для включения двухфакторной аутентификации для конкретного пользователя. Добавим в файл Default.cshtml следующий код:
@using System.Security.Claims @model ClaimsPrincipal <div> @if (Model.Identity.IsAuthenticated) { <p>Привет, @Model.Identity.Name</p> <a href="/account/SignOut">Выйти из приложения</a><br/> <a href="/account/updateuser/@Model.Identity.Name">Настройки аккаунта</a> } else { <p>Привет, Анонимус!</p> <a href="/account/signin">Войти в приложение</a><br/> <a href="/account/register">Зарегистрироваться</a> } </div>
По сравнению с тем, чтобы ло ранее в этом файле, мы добавили ссылку:
<a href="/account/updateuser/@Model.Identity.Name">Настройки аккаунта</a>
Для изменения настроек пользователя создадим новое представление в папке Areas/Account/Views/Account с названием UpdateUser.cshtml:
Добавим в файл следующий код:
@using AspMvcAuth.Models @model ApplicationUser @{ ViewData["Title"] = "Update user"; } <h1 class="text-primary">Обновление настроек аккаунта</h1> <form asp-area="Account" asp-action="UpdateUser" asp-controller="Account" method="post"> <div class="form-check"> <input type="hidden" asp-for="@Model.Id" /> <input asp-for="@Model.TwoFactorEnabled" class="form-check-input" type="checkbox" id="twoFactorAuth"> <label class="form-check-label" for="twoFactorAuth"> Использовать двухфакторную аутентификацию </label> <br/> <button type="submit" class="btn btn-primary">Сохранить</button> </form>
Здесь определена web-форма с помощью которой мы устанавливаем или снимаем флаг TwoFactorEnabled
для объекта пользователя. Соответственно, в качестве модели выступает класс ApplicationUser
.
Теперь добавим необходимые методы в контроллер AccountController
:
[Route("{area}/{action}/{name}")] [HttpGet] public async Task<IActionResult> UpdateUser(string name) { var user = await _userManager.FindByNameAsync(name); if (user == null) return BadRequest(); return View(user); } [Route("{area}/{action}/{name}")] [HttpPost] public async Task<IActionResult> UpdateUser(ApplicationUser user) { var updatedUser = await _userManager.FindByIdAsync(user.Id.ToString()); if (updatedUser == null) return BadRequest(); updatedUser.TwoFactorEnabled = user.TwoFactorEnabled; await _userManager.UpdateAsync(updatedUser); return LocalRedirect("/"); }
В первом методе UpdateUser()
мы находим пользователя по его имени и отправляем объект в представление. Во втором методе, обрабатывающем POST-запрос, мы получаем в качестве параметра метода объект пользователя в котором содержится только Id
пользователя и значение флага TwoFactorEnabled
. Поэтому, вначале, мы получаем объект пользователя из хранилища:
var updatedUser = await _userManager.FindByIdAsync(user.Id.ToString());
а, затем, вносим изменения и обновляем запись пользователя в хранилище:
updatedUser.TwoFactorEnabled = user.TwoFactorEnabled; await _userManager.UpdateAsync(updatedUser);
Проверим работу. Запустим приложение и войдем с логином и паролем пользователя у которого подтвержден адрес электронной почты.
Зайдем в настройки аккаунта, включим двухфакторную авторизацию и сохраним результат: Проверим результат в базе данных:
Как можно видеть на рисунке, флаг TwoFactorEnabled
установлен в значение 1
(true
) значит мы успешно включили двухфакторную аутентификацию для пользователя. Можно переходить к следующему этапу — разработке механизма двухфакторной аутентификации.
Использование двухфакторной аутентификации в приложении
Для того, чтобы использовать двухфакторную аутентификацию нам необходимо выполнить следующие шаги:
- Разработать представление в котором пользователю будет предложено ввести код подтверждения, который он будет получать по email
- Разработать необходимые действия контроллера, в которых будет формироваться, отправляться и проверяться код подтверждения
- Внести изменения в действие входа пользователя в приложение.
Начнем с самого простого — создадим необходимое представление. Для этого, в папке Areas/Account/Views/Account создадим файл представления с именем LoginTwoStep.cshtml и разместим в нем следующий код:
@using AspMvcAuth.Models @model TwoFactor @{ ViewData["Title"] = "Login Two Step"; } <h1 class="bg-info text-white">Login Two Step</h1> <div class="text-danger" asp-validation-summary="All"></div> <p>Введите ваш код подтверждения</p> <p>Код был отправлен на email, указанный при регистрации</p> <form asp-action="LoginTwoStep" method="post"> <input type="hidden" asp-for="@Model.ReturnUrl" /> <div class="form-group"> <label>Код</label> <input asp-for="@Model.TwoFactorCode" class="form-control" /> </div> <button class="btn btn-primary" type="submit">Отправить код и войти</button> </form>
В web-форме представления одно пле ввода — для кода подтверждения. После нажатия на кнопку «Отправить код и войти» должно выполнится действие контроллера с названием LoginTwoStep
(его мы сейчас разработаем). При этом, в качестве модели представления указан класс TwoFactor
, который выглядит максимально просто:
using System.ComponentModel.DataAnnotations; namespace AspMvcAuth.Models { public class TwoFactor { [Required] public string TwoFactorCode { get; set; } = ""; public string ReturnUrl { get; set; } = "/"; } }
В свойстве TwoFactorCode
мы будем передавать код подтверждения, а в ReturnUrl
, при необходимости путь редиректа пользователя при успешной аутентификации. Теперь рассмотрим действия контроллера LoginTwoStep
[AllowAnonymous] [HttpGet] [Route("{area}/{action}")] public async Task<IActionResult> LoginTwoStep(string email) { var user = await _userManager.FindByEmailAsync(email); var token = await _userManager.GenerateTwoFactorTokenAsync(user, "Email"); _ = _emailHelper.SendEmailTwoFactorCode(user.Email, token); return View(new TwoFactor()); } [HttpPost] [AllowAnonymous] [Route("{area}/{action}")] public async Task<IActionResult> LoginTwoStep(TwoFactor twoFactor) { if (!ModelState.IsValid) { return View(twoFactor.TwoFactorCode); } var result = await _signInManager.TwoFactorSignInAsync("Email", twoFactor.TwoFactorCode, false, false); if (result.Succeeded) { return Redirect(twoFactor.ReturnUrl ?? "/"); } else { ModelState.AddModelError("", "Неверный код подтверждения"); return View(twoFactor); } }
В первой версии метода, обрабатывающей GET-запрос, мы формируем код подтверждения и отправляем его пользователю по email, задействуя сервис отправки писем, который мы разработали в предыдущей части, при этом, сервис немного изменился. Так как необходимо отправлять письма с разными темами («подтверждение адреса», «код подтверждения»), то сервис теперь будет выглядеть следующим образом:
public class EmailHelper { public EmailHelperOptions Options { get; } public EmailHelper(EmailHelperOptions options) { Options = options; } private bool Send(string userEmail, string message, string subject = "") { MailMessage mailMessage = new() { From = new MailAddress(Options.From) }; mailMessage.To.Add(new MailAddress(userEmail)); if (string.IsNullOrEmpty(subject)) mailMessage.Subject = Options.Subject; else mailMessage.Subject = subject; mailMessage.IsBodyHtml = true; mailMessage.Body = message; SmtpClient client = new() { DeliveryMethod = SmtpDeliveryMethod.Network, Credentials = new System.Net.NetworkCredential(Options.Login, Options.Password), Host = Options.Host, Port = Options.Port, EnableSsl = Options.EnableSSL }; try { client.Send(mailMessage); return true; } catch { return false; } } public bool SendEmail(string userEmail, string confirmationLink) { return Send(userEmail, confirmationLink); } public bool SendEmailTwoFactorCode(string userEmail, string code) { return Send(userEmail, code, "Код подтверждения"); } }
Что касается второй версии метода LoginTwoStep()
, то здесь обрабатывается POST-запрос. На первом шаге проверяется наличие кода подтверждения как такового:
if (!ModelState.IsValid) { return View(twoFactor.TwoFactorCode); }
Если код предоставлен, то мы пытаемся аутентифицировать пользователя, используя полученный код:
var result = await _signInManager.TwoFactorSignInAsync("Email", twoFactor.TwoFactorCode, false, false);
В зависимости от результата, мы либо перенаправляем пользователя на определенный URL, либо выдаем ошибку, что код подтверждения задан неверно:
if (result.Succeeded) { return Redirect(twoFactor.ReturnUrl ?? "/"); } else { ModelState.AddModelError("", "Неверный код подтверждения"); return View(twoFactor); }
Осталось внести изменения в действие входа пользователя в приложение (SignIn()
). Теперь этот метод будет выглядеть следующим образом:
[Route("{area}/{action}")] [HttpPost] [AllowAnonymous] public async Task<IActionResult> SignIn(LoginModel model) { ApplicationUser user = await _userManager.FindByNameAsync(model.Login); bool emailStatus = await _userManager.IsEmailConfirmedAsync(user); if (emailStatus == false) { ModelState.AddModelError(nameof(user.Email), "Email не подтвержден. Пожалуйста, пройдите по ссылке из отправленного вам письма"); return View("Login"); } var result = await _signInManager.PasswordSignInAsync(model.Login, model.Password, false, lockoutOnFailure: false); if (result.Succeeded) { return LocalRedirect("/"); } if (result.RequiresTwoFactor) { return RedirectToAction("LoginTwoStep", new { user.Email }); } ModelState.AddModelError("", "Неверный логин или пароль"); return View("Login"); }
Так как у разных пользователей могут быть разные настройки аккаунта, то вначале, после проверки подтверждения адреса электронной почты мы пытаемся залогинить пользователя используя логин/пароль:
var result = await _signInManager.PasswordSignInAsync(model.Login, model.Password, false, lockoutOnFailure: false);
Далее, происходит следующее:
если у пользователя двухфакторная аутентификация отключена, то он сразу войдет в приложение и его перенаправят на главную страницу приложения:
if (result.Succeeded) { return LocalRedirect("/"); }
На этом этапе, для пользователей с двухфакторной аутентификацией объект result
будет содержать ошибку — поэтому такие пользователи пройдут дальше до
if (result.RequiresTwoFactor) { return RedirectToAction("LoginTwoStep", new { user.Email }); }
то есть их мы перенаправим на наше новое представление, указав при этом их email в качестве параметра действия LoginTwoStep()
. Если же пользователь доходит до этого шага и проверка показывает, что двухфакторная аутентификация выключена, то это может означать только то, что пользователь ввел неверный логин/пароль, о чем мы ему и сообщаем:
ModelState.AddModelError("", "Неверный логин или пароль"); return View("Login");
Осталось проверить работу нового механизма. Запустим приложение и попробуем войти в приложение, используя пользователя с включенной двухфакторной аутентификацией:
Как только будет нажата кнопка «Отправить» мы перейдем на представление для ввода кода подтверждения:
А на почту придет письмо с кодом подтверждения:
Вводим этот код в форму:
При попытке ввести неверный код мы получим сообщение:
Приложение работает корректно.
Итого
Двухфакторная аутентификация позволяет дополнительно защитить аккаунт от несанкционированного доступа. Для использования такой схемы аутентификации мы должны её включить в настройках конкретного пользователя. При этом, код подтверждения может приходить только на подтвержденную почту пользователя.