В статье описан способ подключения мультифакторной аутентификации для сайта, работающем на платформе .net core с использованием встроенных механизмов авторизации.


Пару слов зачем вообще нужна мультифакторная аутентификация:


  1. Безопасность
  2. Еще раз безопасность
  3. Удобство

Да, последний пункт не ошибка. Второй и/или третий фактор аутентификации являются не только дополнением к традиционному паролю, но и полноценной заменой. Вместо смски с кодом, который нужно перебивать на сайт, современные способы — это PUSH в мессенджере с кнопкой для подтверждения действия или биометрическая аутентификация с использованием отпечатка пальца на телефоне или ноутбуке.


Принцип работы


  1. Сайт запрашивает и проверяет логин/пароль пользователя
  2. Если они указаны корректно, отправляет пользователя на страницу проверки подлинности через второй фактор
  3. После успешной проверки пользователь возвращается на сайт с токеном доступа и авторизуется, то есть получает разрешение на просмотр закрытой части сайта.

Токен доступа


Формат токена — 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. Для этого нам нужно:


  1. Проверить логин и пароль пользователя
  2. Получить токен доступа от сервера двухфакторной аутентификации
  3. Передавать и проверять токен при каждом запросе.

Конфигурационный файл


Добавьте в файл 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, посмотрите эту замечательную статью.