Аутентификация и авторизация в ASP.NET Core MVC. Подтверждение адреса электронной почты

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

Как включить подтверждение адреса электронной почты в ASP.NET Core Identity

В нашем приложение, которое мы разрабатываем, подтверждение адреса электронной почты отключено. Убедиться в этом можно, посмотрев в базе данных таблицу пользователей:

Чтобы включить подтверждение адреса электронной почты необходимо внести изменение в конфигурацию сервиса аутентификации:

builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
{
    options.SignIn.RequireConfirmedEmail = true; //включаем подтверждение адреса электронной почты
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 10;
})

Также, в качестве тестирования новой для нас опции, мы можем вынести подтверждение адреса электронной почты в настройки, например, в файл appsettings.json следующим образом:

{
  "RequireConfirmedEmail": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

и теперь использовать такую настройку сервиса аутентификации:

   var requireEmailConfirmed = builder.Configuration.GetValue<bool>("RequireConfirmedEmail");
   builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
   {
       options.SignIn.RequireConfirmedEmail = requireEmailConfirmed; //включаем подтверждение адреса электронной почты
       options.Password.RequireNonAlphanumeric = true;
       options.Password.RequireDigit = true;
       options.Password.RequiredLength = 10;
   })
       .AddRoles<ApplicationRole>() //включаем поддержку ролей
       .AddEntityFrameworkStores<ApplicationContext>();

Теперь нам необходимо разработать необходимые процедуры подтверждения адреса электронной почты пользователя. Для этого нам потребуется внести изменения в действия контроллера AccountController, который мы разрабатывали в этой части.

Использование подтверждения адреса электронной почты

Вначале внесем следующие изменения в действие Register контроллера AccountController:

[Route("{area}/{action}")]
[HttpPost]
public async Task<IActionResult> Register(RegisterModel model)
{
    var user = model.GetUser();
    IdentityResult result = await _userManager.CreateAsync(user, model.Password);
    if (result.Succeeded)
    {
        var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
        var confirmationLink = Url.Action("ConfirmEmail", "Account", new { token, email = user.Email }, Request.Scheme);
        EmailHelper emailHelper = new EmailHelper();
        bool emailResponse = emailHelper.SendEmail(user.Email, confirmationLink);

        if (emailResponse)
        {
            user = await _userManager.FindByEmailAsync(user.Email);
            //добавляем дату рождения в Claimd пользователя
            await _userManager.AddClaimAsync(user, new Claim("Birthday", model.Birthday.ToString("dd.MM.yyyy"), ClaimValueTypes.String));
            return RedirectToAction("Index", "Home");
        }

        else
        {
            ModelState.AddModelError("", "На сервере произошла ошибка. Попробуйте выполнить регистрацию позднее");
            return View();
        }
    }
    else
    {
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError("", error.Description);
        }
        return View();
    }
}

Рассмотрим, что изменилось в этом методе. После того, как мы создаем объект пользователя, то формируется токен подтверждения адреса электронной почты:

var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);

Этот токен вставляется в ссылку по которой должен перейти пользователь, чтобы подтвердить почту:

var confirmationLink = Url.Action("ConfirmEmail", "Account", new { token, email = user.Email }, Request.Scheme);

Здесь ConfirmEmail — это действие, которое мы ниже добавим в наш контроллер. После создания ссылки создается новый объект типа EmailHelper у которого выполняется метод SendEmail:

EmailHelper emailHelper = new EmailHelper(); 
bool emailResponse = emailHelper.SendEmail(user.Email, confirmationLink);

EmailHelper — это нестандартный класс, который необходимо создать, например, в папке Models. Код класса следующий:

public class EmailHelper
{
    public bool SendEmail(string userEmail, string confirmationLink)
    {
        MailMessage mailMessage = new MailMessage
        {
            From = new MailAddress("ваш_адрес_email_с_которого_будет_отправляться_письмо_пользователю")
        };
        mailMessage.To.Add(new MailAddress(userEmail));

        mailMessage.Subject = "Тема_письма";
        mailMessage.IsBodyHtml = true;
        mailMessage.Body = confirmationLink;

        SmtpClient client = new SmtpClient
        {
            DeliveryMethod = SmtpDeliveryMethod.Network,
            Credentials = new System.Net.NetworkCredential("логин_от_почты", "пароль_от_почты"),
            Host = "smtp.yandex.ru",
            Port = 25,
            EnableSsl = true
        };

        try
        {
            client.Send(mailMessage);
            return true;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
        return false;
    }
}

Здесь создается smtp-клиент для отправки почты на адрес пользователя. В настройках клиента оставлены настройки smtp-сервера для Яндекса с которыми мне удалось отправить письмо себе на почту. После того, как письмо со ссылкой для подтверждения адреса электронной почты отправлено, для пользователя создается необходимое для аутентификации утверждение (с датой рождения) и пользователь переходит на главную страницу приложения.

Здесь стоит отметить, что до тех пор, пока пользователь не подтвердит адрес электронной почты, он не сможет войти в приложение. Также в приложение не смогут зайти пользователи, зарегистрированные ранее и не подтвердившие адрес своей электронной почты

Теперь создадим действие контроллера, выполнение которого будет подтверждать адрес электронной почты:

[Route("{area}/{action}")]
public async Task<IActionResult> ConfirmEmail(string token, string email)
{
    var user = await _userManager.FindByEmailAsync(email);
    if (user == null)
        return View("Error");

    var result = await _userManager.ConfirmEmailAsync(user, token);
    return View(result.Succeeded ? "ConfirmEmail" : "Error");
}

здесь значения token и email получаются из параметров запроса и используются в методе:

var result = await _userManager.ConfirmEmailAsync(user, token);

для подтверждения адреса электронной почты пользователя. Если адрес успешно подтвержден, то загружается представление с именем ConfirmEmail, иначе — с именем Error. Эти представления необходимо разместить в папке Areas/Account/Views/Account.

Код этих представлений очень простой:

ConfirmEmail.cshtml

@{
    ViewData["Title"] = "ConfirmEmail";
}

<h1>Подтверждение Email</h1>
<p>Спасибо за подтверждение. Теперь вы можете войти в свой аккаунт.</p>

Error.cshtml

@{
    ViewData["Title"] = "Error";
}

<h1 class="text-danger">Ошибка</h1>
<h2 class="text-danger">
    Ошибка выполнения запроса. Возможно Ваш email ещё не подтвержден.
</h2>

Теперь осталось немного изменить действие, выполняемой при входе пользователя в приложение — это действие SignIn в контроллере AccountController:

[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("/");
    }
    else
    {
        ModelState.AddModelError("", "Неверный логин или пароль");
    }
    return View("Login");
}

Здесь, перед тем как осуществить вход с логином/паролем проводится проверка подтверждения пользователем адреса электронной почты:

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");
}

Если поста не подтверждена, то пользователю возвращается соответствующая ошибка. Если почта подтверждена, то осуществляется вход с логином и паролем пользователя.

Проверка подтверждения адреса электронной почты

Создадим нового пользователя с реальным адресом электронной почты:

Через несколько секунд на адрес электронной почты, указанной при регистрации придет письмо:

После перехода по ссылки из письма мы получим следующее сообщение:


и сможем аутентифицировать пользователя

Сервис отправки почты

Теперь, когда мы убедились, что письмо с подтверждением почты отправляется и само подтверждение адреса электронной почты работает, можно немного улучшить наше приложение. Дело в том, что на данный момент мы жестко «зашиваем» в код программы конфиденциальные данные, такие как логин и пароль от почты отправителя. Так в рабочих проектах или проектах, код которых публикуется на сторонних ресурсах, делать, конечно, не стоит. Поэтому, немного улучшим наше приложение и сделаем класс EmailHelper сервисом, который будет получать настройки из секретов пользователя.

Во-первых, создадим класс в котором будем хранить настройки smtp-клиента:

namespace AspMvcAuth.Models
{
    public class EmailHelperOptions
    {
        public string Subject { get; set; }
        public string From { get; set; }
        public string Login { get; set; }
        public string Password { get; set; }
        public string Host { get; set; }
        public int Port { get; set; }
        public bool EnableSSL { get; set; }
    }
}

во-вторых, перепишем класс EmailHelper следующим образом:

public class EmailHelper
{
    public EmailHelperOptions Options { get; }

    public EmailHelper(EmailHelperOptions options)
    {
        Options = options;
    }

    public bool SendEmail(string userEmail, string confirmationLink)
    {
        MailMessage mailMessage = new()
        {
            From = new MailAddress(Options.From)
        };
        mailMessage.To.Add(new MailAddress(userEmail));

        mailMessage.Subject = Options.Subject;
        mailMessage.IsBodyHtml = true;
        mailMessage.Body = confirmationLink;

        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 (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
        return false;
    }
}

Во-первых, теперь у класса определен конструктор в который передаются настройки smtp-клиента:

public EmailHelperOptions Options { get; }

public EmailHelper(EmailHelperOptions options)
{
    Options = options;
}

Во-вторых, из метода SendEmail() убраны все конфиденциальные данные — они берутся из настроек. Для удобства, добавим метод расширения для регистрации нового сервиса:

public static class EmailHelperExtensions
{
    public static void AddEmailHelper(this IServiceCollection services, EmailHelperOptions options)
    {
        services.AddTransient((service)=>new EmailHelper(options));
    }
}

Теперь создадим в файле секретов следующие настройки smtp-клиента:

{
  "EmailSender": {
    "Subject": "Email Confirmation",
    "From": "адрес_с_которого_отправляется_письмо",
    "Login": "логин_почты",
    "Password": "пароль_почты",
    "Host": "smtp.yandex.ru",
    "Port": 25,
    "EnableSSL": true
  },
  "ConnectionStrings": {
    "DefaultConnection": "DataSource=Identity.sqlite"
  }
}

Все настройки smtp-клинта находятся в секции EmailSender. Теперь изменим класс Program, добавив новый сервис:

var emailOptions = builder.Configuration.GetSection("EmailSender").Get<EmailHelperOptions>() ?? throw new InvalidOperationException("Email Sender options not found."); ;
builder.Services.AddEmailHelper(emailOptions);

и, наконец, запросим новый сервис в контроллере AccountController:

private readonly EmailHelper _emailHelper;


public AccountController(UserManager<ApplicationUser> manager, SignInManager<ApplicationUser> signInManager, EmailHelper emailHelper)
{
    _userManager = manager;
    _signInManager = signInManager;
    _emailHelper = emailHelper;
}

а в методе Register() используем этот сервис (приведу только часть измененного кода метода):

//EmailHelper emailHelper = new();
bool emailResponse = _emailHelper.SendEmail(user.Email, confirmationLink);

На этом всё — теперь для отправки письма для подтверждения электронной почты будет использоваться сервис, настройки для которого будут храниться в секретах пользователя.

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

Итого

В этой части мы научились использовать ещё один полезный механизм ASP.NET Core Identity — подтверждение адреса электронной почты нового пользователя. Подтверждение адреса используется для того, чтобы, во-первых, убедиться, что указанный адрес гарантированно доступен для отправки сообщений, а, во-вторых, в дальнейшем, такое подтверждение будет нам необходимо для выполнения двухфакторной аутентификации, которую мы рассмотрим в следующей части.

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