720
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Аутентификация с использованием асимметричной криптографии в ASP.NET Core с JWT и RSA

Время чтения 13 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
720
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Всем привет! Меня зовут Дмитрий Бахтенков, и я .NET-разработчик. Сегодня поделюсь с вами интересным кейсом, связанным с настройкой безопасной аутентификации в ASP.NET Core с помощью JWT — использованием асимметричного ключа вместо симметричного. Погрузимся в основы асимметричной криптографии, разберем преимущества RSA и пошагово настроим ваше приложение для работы с JWT. 

Аутентификация с использованием асимметричной криптографии в ASP.NET Core с JWT и RSA

Введение

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

В этой статье мы разберем, как настроить аутентификацию в ASP.NET Core с использованием JWT (JSON Web Token) и асимметричной криптографии на основе RSA. JWT предоставляет удобный и безопасный способ передачи информации между участниками, а использование асимметричной криптографии позволяет улучшить безопасность за счет разделения ключей на публичные и приватные. Мы обсудим основные концепции асимметричной криптографии, преимущества использования RSA в сравнении с симметричными методами и подробно рассмотрим процесс настройки вашего приложения для использования этих технологий.

Что такое JWT и зачем он нужен

Перед определением JSON Web Token давайте определимся, что такое клейм (claim, утверждение).

Claims — это информация о пользователе, на основе которой можно принимать решения по авторизации. Это набор утверждений, обычно в формате key-value, которые содержат информацию о пользователе: идентификатор, роль, логин. 

JSON Web Token используется для передачи информации о пользователе. Полезной нагрузкой как раз является набор клеймов.

Итак, JSON Web Token — это формат для передачи данных между приложениями, который содержит набор утверждений (клеймов) о пользователе и подписан специальным ключом.

Из чего состоит JSON Web Token?

1. Заголовок (Header) содержит метаданные о токене. В них входит алгоритм подписи (поле alg) и опционально тип контента (typ). Стандарт рекомендует в поле typ указывать значение JWT, если не используется каких-либо кастомных типов в токене.

2. Полезная нагрузка (Payload) — набор клеймов. Стандарт определяет клеймы по умолчанию, например:

  • Issuer (iss) — некоторый идентификатор того, кто выпустил токен.
  • Subject (sub) — идентификатор того, кто запрашивает токен.
  • Expiration Time (exp) — время жизни токена.

3. Signature — Header и Payload в формате base64, подписанные с помощью специального алгоритма и секретного ключа

JWT — это просто строка в формате header.payload.signature. Аутентифицировать пользователя можно с помощью верификации signature, а авторизовывать — с помощью информации из клеймов.

Процесс работы с JWT

«Поиграться» с JWT, а также почитать о нем подробнее можно на сайте JSON Web Tokens — jwt.io .

Симметричная vs асимметричная криптография

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

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

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

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

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

В таком случае мы имеем:

  1. Сервис, который выпускает токены, — например, AuthService. Он имеет приватный и публичный ключи и выпускает токены, подписывая их приватным ключом.
  2. Сервис, к которому мы обращаемся. Для каждого запроса он получает JWT из заголовков и проверяет подпись с помощью публичного ключа сервиса из п. 1.

Создаем проект на ASP.NET Core

Попробуем создать проект на ASP.NET Core, который выписывает и проверяет токены с помощью пары «приватный — публичный ключ». Я добавил проект по шаблону Web API. Его можно создать через IDE или с помощью консольной команды:

dotnet new webapi

Теперь нам необходимо сгенерировать ключи. Добавим папку Keys. Далее, если у вас Windows, можно использовать утилиту ssh-keygen. Просто вызываем ssh-keygen из командной строки, отказываемся от passphrase и вводим путь к папке с проектом.

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

ssh-keygen -f ./Keys/key.pub -m pem -e > ./Keys/key.pub.pem

В папке Keys должно появиться три файла:

  1. key — приватный ключ в формате .pem;
  2. key.pub — публичный ключ в формате ssh-rsa;
  3. key.pub.pem — публичный ключ в формате .pem.

Теперь добавим настройки к этим файлам для того, чтобы они копировались в директорию проекта при сборке. Это можно сделать с помощью свойств файла:

Или вручную в файле .csproj добавить строки


<ItemGroup>
<None Update="Keyskey">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Keyskey.pub.pem">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

 

Должно получиться вот так:

Теперь подключим необходимые библиотеки. Их также можно установить с помощью командной строки или в IDE:


dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer;
dotnet add package System.IdentityModel.Tokens.Jwt.

 

Наш проект готов к дальнейшей разработке.

Стандартные механизмы

Сначала разберем аутентификацию с помощью стандартных механизмов ASP.NET Core. 

В файле Program.cs указываем параметры аутентификации — схему по умолчанию и параметры валидации JWT:


builder.Services.AddAuthentication(options =>
    {
        // устанавливаем дефолтную схему как JWT
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        // создаем объект RSA
        var publicKey = RSA.Create();
        // импортируем публичный ключ для проверки подписи
        publicKey.ImportFromPem(File.ReadAllText("Keys/key.pub"));

        // устанавливаем параметры токена
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "yourIssuer",
            ValidAudience = "yourAudience",
            // указываем ключ для проверки подписи
            IssuerSigningKey = new RsaSecurityKey(publicKey),
            CryptoProviderFactory = new CryptoProviderFactory
            {
                // отключаем кеширование ключа. Объект RSA — Disposable,
                // и если его закешировать, возможны ObjectDisposedException
                CacheSignatureProviders = false
            }
        };
    });

Далее вызываем метод AddAuthorization, в котором обязываем приложение проверять факт успешной аутентификации:


builder.Services.AddAuthorization(x =>
{
    x.AddPolicy("Default", o => o.RequireAuthenticatedUser());
});

 

И ниже, на этапе настройки middleware указываем использование аутентификации и авторизации:

app.UseAuthentication();

app.UseAuthorization();

 

Теперь добавим два эндпоинта: первый будет выписывать токены, а второй — проверять факт авторизации пользователя (как раз по токену). Мы воспользуемся подходом Minimal API, который подразумевает добавление эндпоинтов сразу в Program.cs без создания контроллеров.

В этом методе должна быть реализована проверка логина и пароля, например, из БД. Для упрощения примера я упустил этот момент, но в реальном приложении токены нужно выписывать после проверки логина и пароля 🙂


app.MapPost("api/v1/token", ([FromBody] Credentials credentials) =>
{
    using var privateKey = RSA.Create();
    privateKey.ImportFromPem(File.ReadAllText("Keys/key"));

    // указываем клеймы. Тут могут быть также кастомные параметры,
    // например телефон или почта пользователя
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, credentials.Login),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
    };

    // создаем объект токена с параметрами
    var token = new JwtSecurityToken(
        issuer: "yourIssuer",
        audience: "yourAudience",
        claims: claims,
        expires: DateTime.Now.AddHours(1),
        signingCredentials: new SigningCredentials(new RsaSecurityKey(privateKey), SecurityAlgorithms.RsaSha256)
    );

    // конвертируем токен в строку
    var stringToken = new JwtSecurityTokenHandler().WriteToken(token);

    return new { Token = stringToken };
});

И добавим метод получения информации о пользователе:


app.MapGet("api/v1/check", (HttpContext context) =>
{

    var claims = context.User.Claims.ToDictionary(x => x.Type, x => x.Value);

    return claims;

}).RequireAuthorization("Default");

 

Запустим приложение для проверки. У нас откроется Swagger, где мы сможем попробовать выполнить HTTP-запросы:

При выполнении запроса мы получили токен:

Теперь скопируем этот ключ, чтобы добавить его в заголовок на следующий запрос. Для отправки данных с заголовками можно воспользоваться утилитой Postman. Во вкладке Authorization выберем тип Bearer, добавим туда наш токен и выполним запрос:

Свой обработчик аутентификации

Что, если нам необходимо кастомизировать обработку аутентификации? Например, получать ключ динамически из БД или выполнить дополнительную логику. В таком случае нужно написать свой обработчик аутентификации и реализовать логику самостоятельно. Разберем, как это сделать.

Добавим в проект классы CustomAuthenticationOptions и CustomAuthenticationHandler. CustomAuthenticationHandler унаследуем от AuthenticationHandler и реализуем в нем следующую логику:


public class CustomAuthenticationOptions : AuthenticationSchemeOptions
{
    // тут могут быть дополнительные параметры
}
    public class CustomAuthenticationHandler : AuthenticationHandler
    {
        public CustomAuthenticationHandler(
            IOptionsMonitor options, 
            ILoggerFactory logger, 
            UrlEncoder encoder, 
            ISystemClock clock) 
            : base(options, logger, encoder, clock)
        {
        }

        protected override async Task HandleAuthenticateAsync()
        {
            // получаем токен
            var token = Request.Headers.Authorization.ToString().Replace("Bearer ", string.Empty);

            if (string.IsNullOrEmpty(token))
            {
                return AuthenticateResult.Fail("No token");
            }

            var securityTokenHandler = new JwtSecurityTokenHandler();
            using var rsa = RSA.Create();
            var publicKey = File.ReadAllText("./Keys/key.pub.pem");
            rsa.ImportFromPem(publicKey);
            var rsaSecurityKey = new RsaSecurityKey(rsa);
            try
            {
                // описываем параметры валидации
                var validationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = rsaSecurityKey,
                    ValidateIssuer = true,
                    ValidateLifetime = true,
                    ValidateAudience = true,
                    ClockSkew = TimeSpan.Zero,
                    CryptoProviderFactory = new CryptoProviderFactory()
                    {
                        CacheSignatureProviders = false
                    },
                };

                // выполняем валидацию. Если прошла неуспешно, будет исключение
                var claims = securityTokenHandler.ValidateToken(token, validationParameters, out _);
                
                // создаем объект Identity и наполняем его клеймами
                // Тут может быть кастомная логика получения информации о пользователе
                var ticket = new AuthenticationTicket(claims, "custom");

                return AuthenticateResult.Success(ticket);
            }
            catch (Exception ex)
            {
                return AuthenticateResult.Fail(ex);
            }
        }
    }

Созданный кастомный обработчик аутентификации нам надо подключить в файле Program.cs вместо AddJwtBearer:


builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = "custom";
        options.DefaultChallengeScheme = "custom";
    })
    .AddScheme<CustomAuthenticationOptions, CustomAuthenticationHandler>("custom", "custom", 
        x => { /* тут можно настроить параметры */ }
    );

 

Готово! Теперь снова запускаем и тестируем приложение. Получаем токен и используем его:

Теперь мы знаем, как использовать JWT-токены и асиметричную подпись в ASP.NET Core. Разберем еще один важный вопрос.

Как и где хранить ключи?

Это сложная и немного «холиварная» тема. В реальных проектах на production-среде хранить ключи в файловой системе проекта точно не стоит. Но какие есть варианты?

Менеджеры секретов

Можно воспользоваться решением HashiCorp Vault или аналогами от AWS или Azure. Они удобны, безопасны, и их легко интегрировать в приложение на ASP.NET Core.

CI/CD и переменные окружения

В системе CI/CD, например в GitLab, можно генерировать ключи в рамках пайплайнов и добавлять их в переменные среды приложения. В конфигурацию ASP.NET Core их можно добавить при настройке конфигурации с помощью метода Configuration.AddEnvironmentVariables — Configuration in ASP.NET Core | Microsoft Learn.

Заключение

В рамках этой статьи мы разобрали работу с JWT и парой «публичный — приватный ключ». Мы использовали объект RSA для генерации ключа, который использовался для подписи токена.

Также я веду телеграм-канал, где пишу про .NET и не только.

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники