Аутентификация и авторизация в ASP.NET Core MVC. Двухфакторная аутентификация

Двухфакторная аутентификация позволяет дополнительно защитить ваш аккаунт от несанкционированного доступа. В общих чертах, двухфакторная аутентификация выглядит следующим образом: после ввода логина и пароля, пользователю на почту или по 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) значит мы успешно включили двухфакторную аутентификацию для пользователя. Можно переходить к следующему этапу — разработке механизма двухфакторной аутентификации.

Использование двухфакторной аутентификации в приложении

Для того, чтобы использовать двухфакторную аутентификацию нам необходимо выполнить следующие шаги:

  1. Разработать представление в котором пользователю будет предложено ввести код подтверждения, который он будет получать по email
  2. Разработать необходимые действия контроллера, в которых будет формироваться, отправляться и проверяться код подтверждения
  3. Внести изменения в действие входа пользователя в приложение.

Начнем с самого простого — создадим необходимое представление. Для этого, в папке 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");

Осталось проверить работу нового механизма. Запустим приложение и попробуем войти в приложение, используя пользователя с включенной двухфакторной аутентификацией:

Как только будет нажата кнопка «Отправить» мы перейдем на представление для ввода кода подтверждения:

А на почту придет письмо с кодом подтверждения:

Вводим этот код в форму:

Вход осуществлен успешно:

При попытке ввести неверный код мы получим сообщение:

Приложение работает корректно.

Скачать код проекта из Github

Итого

Двухфакторная аутентификация позволяет дополнительно защитить аккаунт от несанкционированного доступа. Для использования такой схемы аутентификации мы должны её включить в настройках конкретного пользователя. При этом, код подтверждения может приходить только на подтвержденную почту пользователя.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии