В статье описан способ подключения мультифакторной аутентификации для сайта, работающем на платформе .net core с использованием встроенных механизмов авторизации.
Пару слов зачем вообще нужна мультифакторная аутентификация:
- Безопасность
- Еще раз безопасность
- Удобство
Да, последний пункт не ошибка. Второй и/или третий фактор аутентификации являются не только дополнением к традиционному паролю, но и полноценной заменой. Вместо смски с кодом, который нужно перебивать на сайт, современные способы — это PUSH в мессенджере с кнопкой для подтверждения действия или биометрическая аутентификация с использованием отпечатка пальца на телефоне или ноутбуке.
Принцип работы
- Сайт запрашивает и проверяет логин/пароль пользователя
- Если они указаны корректно, отправляет пользователя на страницу проверки подлинности через второй фактор
- После успешной проверки пользователь возвращается на сайт с токеном доступа и авторизуется, то есть получает разрешение на просмотр закрытой части сайта.
Токен доступа
Формат токена — JWT (JSON Web Token). Является открытым стандартом для обмена аутентификационной информацией между разными сервисами. Внутри, как следует из названия, данные в JSON формате. Токен состоит из трех частей: заголовок, данные и подпись. Части разделены точкой и закодированы base64-url. В заголовке указывается тип токена и алгоритм подписи, обычно вот так
{ "typ": "JWT", //тип : JWT "alg": "HS256" //алгоритм подписи: HMAC с использованием SHA-256 }
Во втором блоке содержатся данные о пользователе, дате выдаче токена, сроке действия, эмитенте, аудитории и прочие параметры. Данные сгруппированы в формате ключ: значение, которые называются claims или заявки на русском. Ключи бывают стандартные, такие как iss, aud, sub и произвольные.
{ "iss": "https://access.multifactor.ru", //кто выдал "aud": "https://example.com", //кому выдал "sub": "user@example.com", //имя пользователя "jti": "RxMEyo9", //id токена "iat": 1571684399, //когда выдан "exp": 1571684699, //срок действия "returnUrl": "/", //произвольный ключ "rememberMe": "False", //произвольный ключ "createdAt": "10/21/19 6:59:55 PM" //произвольный ключ }
Третий блок JWT — подпись токена, которая формируется, как HMAC-SHA256(message, secret), где:
- message — первые две части сообщения, закодированные в base64-url и разделенные точкой;
- secret — общий секрет, известный принимающей и передающей стороне, как правило сайту и серверу аутентификации.
Удобство использования JWT состоит в том, что в него можно вставить всю необходимую информацию о пользователе, например, имя, e-mail, роль и использовать ее без необходимости каждый раз обращаться к базе данных.
При этом недопустимо хранить в JWT данные, которую пользователь не должен знать, так как токен ��е зашифрован. Подпись только гарантирует защиту от подделки, но не от раскрытия информации.
Перейдем к практической части, подключим Мультифактор к шаблонному проекту ASP.NET Core Web Application. Для этого нам нужно:
- Проверить логин и пароль пользователя
- Получить токен доступа от сервера двухфакторной аутентификации
- Передавать и проверять токен при каждом запросе.
Конфигурационный файл
Добавьте в файл appsettings.json раздел с параметрами для подключения к API multifactor.ru
"Multifactor": { "ApiKey": "", "ApiSecret": "", "CallbackUrl": "https://localhost:44300/account/mfa" }
- Параметры ApiKey и ApiSecret доступны в личном кабинете
- CallbackUrl — это адрес возврата пользователя на ваш сайт
Клиент для API
Добавьте в проект службу для взаимодействия с API multifactor.ru
/// <summary> /// Multifactor Client /// </summary> public class MultifactorService { //параметры для подключения к API private string _apiKey; private string _apiSecret; private string _callbackUrl; private string _apiHost = "https://api.multifactor.ru"; public MultifactorService(string apiKey, string apiSecret, string callbackUrl) { _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); _apiSecret = apiSecret ?? throw new ArgumentNullException(nameof(apiSecret)); _callbackUrl = callbackUrl ?? throw new ArgumentNullException(nameof(callbackUrl)); } public async Task<string> GetAccessPage(string identityName, IDictionary<string, string> claims = null) { if (string.IsNullOrEmpty(identityName)) throw new ArgumentNullException(nameof(identityName)); var request = JsonConvert.SerializeObject(new { Identity = identityName, //login пользователя Callback = new { Action = _callbackUrl, //адрес возврата Target = "_self" }, Claims = claims //набор заявок }); var payLoad = Encoding.UTF8.GetBytes(request); //basic authorization var authHeader = Convert.ToBase64String(Encoding.ASCII.GetBytes(_apiKey + ":" + _apiSecret)); using var client = new WebClient(); client.Headers.Add("Authorization", "Basic " + authHeader); client.Headers.Add("Content-Type", "application/json"); var responseData = await client.UploadDataTaskAsync(_apiHost + "/access/requests", "POST", payLoad); var responseJson = Encoding.ASCII.GetString(responseData); var response = JsonConvert.DeserializeObject<MultifactorResponse<MultifactorAccessPage>>(responseJson); return response.Model.Url; //адрес страницы доступа } internal class MultifactorResponse<T> { public bool Success { get; set; } public T Model { get; set; } } internal class MultifactorAccessPage { public string Url { get; set; } } }
Сервис — клиент для API Мультифактора. В конструктор класса передаются параметры для подключения, которые берутся из конфигурационного файла appsettings.json. В методе GetAccessPage формируется запрос для получения адреса страницы проверки доступа, далее запрашивается API и возвращется результат.
Внедрение зависимостей и настройка сервисов
Отредактируйте файл Startup.cs, добавьте в метод ConfigureServices код для загрузки настроек из файла конфигурации и регистрации клиента для API
//load Multifactor settings var multifactorSection = Configuration.GetSection("Multifactor"); var apiKey = multifactorSection["ApiKey"]; var apiSecret = multifactorSection["ApiSecret"]; var callbackUrl = multifactorSection["CallbackUrl"]; //register Multifactor service var multifactorService = new MultifactorService(apiKey, apiSecret, callbackUrl); services.AddSingleton(multifactorService);
В .net core предусмотрено несколько схем проверки подлинности. В едином приложении можно использовать одну или несколько одновременно, в зависимости от сценариев. Наиболее удобная схема для использования аутентификации на базе JWT токена — это Bearer аутентификация. В соответствии с этой схемой, JWT токен передается в HTTP заголовке "Authorization: Bearer", а .NET Core автоматически проверяет подпись, срок действия, прочие атрибуты токена и авторизует пользователя.
Следующий код переключает схему авторизации .net на JWT Bearer
services .AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(x => { x.RequireHttpsMetadata = true; x.SaveToken = true; x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(apiSecret)), //signature validation key ValidateIssuer = true, ValidIssuer = "https://access.multifactor.ru", ValidateAudience = false, NameClaimType = ClaimTypes.NameIdentifier }; });
Здесь необходимо обратить внимание на следующие моменты:
- ValidateIssuerSigningKey — необходимо проверять подпись токена
- IssuerSigningKey совпадает с ApiSecret из настроек доступа к API
- ValidateIssuer — необходимо проверять эмитента токена
- NameClaimType указывает из какой заявки брать идентификатор пользователя
Middleware
Добавьте в метод Configure код, который берет токен доступа из куки и перекладывает в заголовок JWT Bearer
app.Use(async (context, next) => { var token = context.Request.Cookies["jwt"]; if (!string.IsNullOrEmpty(token)) { context.Request.Headers.Add("Authorization", "Bearer " + token); } await next(); });
И укажите адрес формы входа
//redirect to /account/login when unauthorized app.UseStatusCodePages(async context => { var response = context.HttpContext.Response; if (response.StatusCode == (int)HttpStatusCode.Unauthorized) { response.Redirect("/account/login"); } });
AccountController
Вся подготовительная работа завершена, осталось доделать сценарий формы входа и получения токена доступа. Будем считать, что в вашем проекте есть класс AccountController, который запрашивает логин и пароль пользователя.
Добавьте в него сервис для работы с API multifactor.ru
private MultifactorService _multifactorService;
И сделайте запрос на мультифакторную аутентификию после проверки логина и пароля
[HttpPost("/account/login")] public async Task<IActionResult> Login([Required]string login, [Required]string password) { if (ModelState.IsValid) { //ваш identity provider для проверки логина и пароля пользователя var isValidUser = _identityService.ValidateUser(login, password, out string role); if (isValidUser) { var claims = new Dictionary<string, string> //можно добавить роль и любые другие аттрибуты пользователя в токен, чтоб не запрашивать их из базы данных { { "Role", role } }; var url = await _multifactorService.GetAccessPage(login, claims); return RedirectPermanent(url); } } return View(); }
Последний момент — это адрес возврата пользователя после успешной аутентификации с токеном доступа
[HttpPost("/account/mfa")] public IActionResult MultifactorCallback(string accessToken) { //сохраняем токен в куки и отправляем пользователя в авторизованную зону Response.Cookies.Append("jwt", accessToken); return LocalRedirect("/"); }
Для того, чтоб разлогинить пользователя, сделайте метод Logout, который удаляет куки с токеном
[HttpGet("/account/logout")] public IActionResult Logout() { Response.Cookies.Delete("jwt"); return Redirect("/"); }
Рабочий проект с кодом из статьи доступен в GitHub. Для более глубокого понимания, как работает авторизация на базе ClaimsIdentity в ASP.NET Core, посмотрите эту замечательную статью.