В предыдущей части была рассмотрена реализация аутентификации в API с использованием JWT, данные которого подписаны цифровой подписью. Такой подход гарантирует подлинность утверждений в токене, но при этом они могут быть легко прочитаны даже в консоли браузера. Для большинства API такого решения вполне достаточно. Однако если необходимо защитить содержимое токена, то на помощь приходит реализация JWT на базе JWE. |
Реализация JWT на базе JWE позволяет не только подписать утверждения, но и зашифровать их. Подробнее стандарт JWE был рассмотрен в первой части.
Принцип использования аутентификации на базе JWE ни чем не отличается от варианта с JWS, рассмотренного во второй части. При этом есть разница в реализации, которую сейчас и рассмотрим. Не будем создавать веб-приложение с нуля, а изменим уже созданное в предыдущей части так, чтобы оно генерировало и использовало JWT с зашифрованными утверждениями.
Исходный код проекта JwsAuthentication доступен на GitHub.
Ключи для шифрования данных
Для генерации и проверки 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<IJwtSigningEncodingKey>(signingKey);
const string encodingSecurityKey = "k72gnxq3pkum9toiub48o8s8sdbjhme1tg0m3p4jfkzovsgdqzgv6t47ig3tr5d9";
var encryptionEncodingKey = new EncryptingSymmetricKey(encodingSecurityKey);
services.AddSingleton<IJwtEncryptingEncodingKey>(encryptionEncodingKey);
services.AddControllers();
…
}
Генерация JWS
Действие Post в Контроллере AuthenticationController изменится полностью, т.к. теперь оно будет создавать JWT на базе JWS:
[AllowAnonymous]
public ActionResult<string> 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):
- Явно разрешаем доступ к данному Действию, указав для него атрибут [AllowAnonymous].
- Проверяем данные пользователя. Для упрощения кода сама проверка пропущена. Считаем что клиент API передал существующее имя пользователя и корректный пароль.
- Подготавливаем набор утверждений (claim), который будет сохранен в токене в зашифрованном виде в блоке CIPHERTEXT.
- Создаем 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<IJwtSigningEncodingKey>(signingKey);
const string encodingSecurityKey = "k72gnxq3pkum9toiub48o8s8sdbjhme1tg0m3p4jfkzovsgdqzgv6t47ig3tr5d9";
var encryptionEncodingKey = new EncryptingSymmetricKey(encodingSecurityKey);
services.AddSingleton<IJwtEncryptingEncodingKey>(encryptionEncodingKey);
services.AddControllers();
const string jwtSchemeName = "JwtBearer";
var signingDecodingKey = (IJwtSigningDecodingKey)signingKey;
var 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/.