ASP.NET Core. Аутентификация в API. Часть 3: Реализация с JWE

ASP.NET Core logoВ предыдущей части была рассмотрена реализация аутентификации в API с использованием JWT, данные которого подписаны цифровой подписью. Такой подход гарантирует подлинность утверждений в токене, но при этом они могут быть легко прочитаны даже в консоли браузера. Для большинства API такого решения вполне достаточно. Однако если необходимо защитить содержимое токена, то на помощь приходит реализация JWT на базе JWE.

Реализация JWT на базе JWE позволяет не только подписать утверждения, но и зашифровать их. Подробнее стандарт JWE был рассмотрен в первой части.

Принцип использования аутентификации на базе JWE ни чем не отличается от варианта с JWS, рассмотренного во второй части. При этом есть разница в реализации, которую сейчас и рассмотрим. Не будем создавать веб-приложение с нуля, а изменим уже созданное в предыдущей части так, чтобы оно генерировало и использовало JWT с зашифрованными утверждениями.

Ключи для шифрования данных

Для генерации и проверки JWE токена потребуется 4 ключа:

  • два ключа для создания подписи и ее проверки (для асинхронного алгоритма это приватный и публичный).
  • два ключа для шифрования и дешифрования данных (соответственно, публичный и приватный).

Интерфейсы ключей для подписи IJwtSigningDecodingKey и IJwtSigningEncodingKey, а так же их симметричная реализация SigningSymmetricKey были рассмотрены во второй части.

Ключи для шифрования данных опишем следующими интерфейсами:

// Ключ для шифрования данных (публичный)
public interface IJwtEncryptingEncodingKey
{
    string SigningAlgorithm { get; }

    string EncryptingAlgorithm { get; }

    SecurityKey GetKey();
}

// Ключ для дешифрования данных (приватный)
public interface IJwtEncryptingDecodingKey
{
    SecurityKey GetKey();
}

Создадим упрощенную реализацию описанных выше интерфейсов для реализации с симметричным ключом:

public class EncryptingSymmetricKey 
    : IJwtEncryptingEncodingKey, IJwtEncryptingDecodingKey
{
    private readonly SymmetricSecurityKey _secretKey;

    public string SigningAlgorithm { get; } = JwtConstants.DirectKeyUseAlg;

    public string EncryptingAlgorithm { get; } = SecurityAlgorithms.Aes256CbcHmacSha512;

    public EncryptingSymmetricKey(string key)
    {
        this._secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
    }

    public SecurityKey GetKey() => this._secretKey;
}

В контейнере веб-приложения, наряду с IJwtSigningEncodingKey, необходимо зарегистрировать реализацию IJwtEncryptingEncodingKey. Добавим соответствующий код в метод ConfigureServices(…) класса Startup:

public void ConfigureServices(IServiceCollection services)
{
    const string signingSecurityKey = "0d5b3235a8b403c3dab9c3f4f65c07fcalskd234n1k41230";
    var signingKey = new SigningSymmetricKey(signingSecurityKey);
    services.AddSingleton(signingKey);

    const string encodingSecurityKey = "k72gnxq3pkum9toiub48o8s8sdbjhme1tg0m3p4jfkzovsgdqzgv6t47ig3tr5d9";
    var encryptionEncodingKey = new EncryptingSymmetricKey(encodingSecurityKey);
    services.AddSingleton(encryptionEncodingKey);

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    …
}

Генерация JWS

Действие Post в Контроллере AuthenticationController изменится полностью, т.к. теперь оно будет создавать JWT на базе JWS:

[AllowAnonymous]
public ActionResult Post(
    AuthenticationRequest authRequest,
    [FromServices] IJwtSigningEncodingKey signingEncodingKey,
    [FromServices] IJwtEncryptingEncodingKey encryptingEncodingKey)
{
    // 1. Проверяем данные пользователя из запроса.
    // ...

    // 2. Создаем утверждения для токена.
    var claims = new Claim[]
    {
        new Claim(ClaimTypes.NameIdentifier, authRequest.Name)
    };

    // 3. Генерируем JWT.
    var tokenHandler = new JwtSecurityTokenHandler();

    JwtSecurityToken token = tokenHandler.CreateJwtSecurityToken(
        issuer: "DemoApp",
        audience: "DemoAppClient",
        subject: new ClaimsIdentity(claims),
        notBefore: DateTime.Now,
        expires: DateTime.Now.AddMinutes(5),
        issuedAt: DateTime.Now,
        signingCredentials: new SigningCredentials(
            signingEncodingKey.GetKey(),
            signingEncodingKey.SigningAlgorithm),
        encryptingCredentials: new EncryptingCredentials(
            encryptingEncodingKey.GetKey(),
            encryptingEncodingKey.SigningAlgorithm,
            encryptingEncodingKey.EncryptingAlgorithm));

    var jwtToken = tokenHandler.WriteToken(token);
    return jwtToken;
}

Рассмотрим данный код подробнее (аналогично тому, как это ранее было сделано для реализации на базе JWS):

  1. Явно разрешаем доступ к данному Действию, указав для него атрибут [AllowAnonymous].
  2. Проверяем данные пользователя. Для упрощения кода сама проверка пропущена. Считаем что клиент API передал существующее имя пользователя и корректный пароль.
  3. Подготавливаем набор утверждений (claim), который будет сохранен в токене в зашифрованном виде в блоке CIPHERTEXT. 
  4. Создаем JWT. Стоит обратить внимание на данные, передаваемые в JwtSecurityToken:
    • issuer - имя приложения, сгенерировавшего JWT (утверждение iss в CIPHERTEXT).
    • audience - для кого был сгенерирован JWT (утверждение aud в CIPHERTEXT).
    • notBefore – дата начала действия токена. До нее он будет считаться не валидным (утверждение nbf в CIPHERTEXT).
    • expiers – дата, после который JWT будет считаться не валидным и клиенту потребуется сгенерировать новый JWT (утверждение exp в CIPHERTEXT).
    • issuedAt – дата создания JWT (утверждение iat в CIPHERTEXT).
    • signingCredentials – определяет способ создания цифровой подписи, которая подтверждает валидность JWT (параметр alg в HEADER).
    • encryptingCredentials – определяет алгоритм шифрования данных в блоке CIPHERTEXT (параметр enc в HEADER).

В текущем состоянии веб-приложение уже способно создавать JWT с зашифрованными утверждениями. Для этого достаточно сделать POST запрос к /api/authentication, передав в качестве данных JSON объект с любыми именем пользователем и паролем. В ответ будет содержаться JWT в формате "HEADER..INITVECTOR.CIPHERTEXT.AUTHTAG". Например:

eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwidHlwIjoiSldUIn0..35M000vhK1deYj2OdXzLIw.4wwly3ohGZ9tjypYw6CE7vr6CISF7eTthiM2hvBT-Q9KzyM2LqIy-yTSSIAkLMCTjTLH5rg5T4VtX70PsPD2WCh7cVqXAW_nF6CpFzDmReVpDXiAiY7TSH1gpjMhZVSpjqqRlWKepHV45UOKZkkX43xAmlldpzh_CbvtZLJxxG95z7Otmy6E89DElzYXbwRmiY8vS8Gpa21uOIAc-iebbdCfLTZFD_bNd8t7oZuPrCQF-PVp70SgjCT7LKUsWgqCfLlkt_89KXakt0rQVkxOPCenedilFHz4buhEDi5-hN0.LmNiOoHMUuO-qjdLRy7YWofHhXvOZHEU_bwoC8Zc42k

Блок ENCRYPTEDKEY отсутствует поскольку используется симметричный ключ.

Блок HEADER, содержащий служебные данные, по прежнему можно легко преобразовать в JSON даже в браузере, передав его содержимое в функцию atob(). Для токена, приведенного выше в качестве примера, получится следующее:

{
    "alg":"dir",
    "enc":"A256CBC-HS512",
    "typ":"JWT"
}

А вот получить утверждения, содержащиеся в CIPHERTEXT, уже не получится. Для этого потребуется приватный ключ, использованный для шифрования.

Аутентификация пользователя с JWT

Методе Configure() класса Startup остается без изменений. Он по прежнему должен вызывать UseAuthentication().

Изменения коснутся настройки проверки JWT. Как можно догадаться, в методе ConfigureServices() в вызове AddJwtBearer() необходимо указать ключ для дешифровки данных (утверждений):

public void ConfigureServices(IServiceCollection services)
{
    const string signingSecurityKey = "0d5b3235a8b403c3dab9c3f4f65c07fcalskd234n1k41230";
    var signingKey = new SigningSymmetricKey(signingSecurityKey);
    services.AddSingleton(signingKey);

    const string encodingSecurityKey = "k72gnxq3pkum9toiub48o8s8sdbjhme1tg0m3p4jfkzovsgdqzgv6t47ig3tr5d9";
    var encryptionEncodingKey = new EncryptingSymmetricKey(encodingSecurityKey);
    services.AddSingleton(encryptionEncodingKey);

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    const string jwtSchemeName = "JwtBearer";
    IJwtSigningDecodingKey signingDecodingKey = (IJwtSigningDecodingKey)signingKey;
    IJwtEncryptingDecodingKey encryptingDecodingKey = (IJwtEncryptingDecodingKey)encryptionEncodingKey;
    services
        .AddAuthentication(options => {
            options.DefaultAuthenticateScheme = jwtSchemeName;
            options.DefaultChallengeScheme = jwtSchemeName;
        })
        .AddJwtBearer(jwtSchemeName, jwtBearerOptions => {
            jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingDecodingKey.GetKey(),
                TokenDecryptionKey = encryptingDecodingKey.GetKey(),

                ValidateIssuer = true,
                ValidIssuer = "DemoApp",

                ValidateAudience = true,
                ValidAudience = "DemoAppClient",

                ValidateLifetime = true,

                ClockSkew = TimeSpan.FromSeconds(5)
            };
        });
}

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

После аутентификации утверждения будут доступны также в this.HttpContext.User.Claims.

Для проверки можно запустить веб-приложение и, получив JWT, обратиться к /api/values/.

Комментарии (2) -

Большое спасибо за статью. Подскажите, а каково должно быть содержание метода EncryptingSymmetricKey?

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

Добавить комментарий