Подписывание JWT токена в .Net
Published: 2023-10-20 15:13:13


## Вводная

Для подписывания JWT токена можно использовать ассиметричный алгоритм шифрования RSA. Физически RSA представляет из себя пару ключей: приватный ключ и публичный ключ. Для подписывания JWT токена используется приватный ключ, а для проверки подлинности токена используется публичный ключ. Зная это можно перейти к практике.

#### Почему именно RSA?

Если генератором и пользователем JWT токена является одно и то же приложение, то проще было бы организовать подписывание JWT токена с помощью симметричного шифрования. Однако если это разные приложения, которые общаются через небезопасную сеть, то лучше всего для подписывания и верификации JWT токена использовать RSA.

## Создание пар ключей RSA

#### Правильный путь

Для создания ключей используем openssl.

Создаем приватный RSA ключ:

```
openssl genpkey -algorithm RSA -out rsa_private_key_for_jwt.pem -pkeyopt rsa_keygen_bits:2048
```

Далее из приватного ключа создаем публичный ключ:

```
openssl rsa -pubout -in rsa_private_key_for_jwt.pem -out rsa_public_key_for_jwt.pem
```

#### Демонстрационный, но не безопасный путь

Так как наше приложение демонстрационное, для генерации RSA ключей можно было воспользоваться сайтом http://travistidwell.com/jsencrypt/demo/
Однако не вздумайте так генерить ключи для PROD и STAGE окружений, так как после генерации ключи можно смело считать скомпрометированными.

## Подписывание токена

Создаем пустое консольное приложение с названием на языке C#.

В корне проекта создаем папку `Secure` и складываем туда файлы с ключами `rsa_private_key_for_jwt.pem` и `rsa_public_key_for_jwt.pem`. В свойствах этих файлов указываем чтобы они копировались во время сборки проекта.

Напишем следующий код программы:

```
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;

using RSA rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText("Secrets/rsa_private_key_for_jwt.pem"));

SecurityKey securityKey = new RsaSecurityKey(rsa);
SigningCredentials signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256);

SecurityTokenDescriptor descriptor = new()
{
    Issuer = "IssuerName",
    Audience = "AudienceName",
    Expires = DateTime.UtcNow.AddMinutes(5),
    SigningCredentials = signingCredentials,
    Subject = new ClaimsIdentity(new Claim[]
    {
        new(ClaimTypes.NameIdentifier, "exampleUser123")
    })
};

JwtSecurityTokenHandler tokenHandler = new();
SecurityToken token = tokenHandler.CreateToken(descriptor);
string jwt = tokenHandler.WriteToken(token);

Console.WriteLine($"JWT: {jwt}");
```

Настало время проверки токена. Для этого будем использовать сайт https://jwt.io/

Давайте запустим программу и запомним получившийся токен. Теперь переходим в разделе Debugger сайта https://jwt.io/ и вводим токен в поле `Encoded`.

Должно получится что-то вроде этого (смотри скриншот ниже). Конечно же при этом токен и время действия токена в вашем случае будут отличаться.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fzhh8wevwg90ia2h5o72.png)

Обратите внимание на надпись "Invalid Signature", которое говорит о том что подпись JWT токена НЕ прошла верификацию. Чтобы верифицировать токена нам нужен публичный ключ из файла `rsa_public_key_for_jwt.pem`. Вводим его в поле ввода для верификации подписи и видим что надпись "Invalid Signature" изменилась на "Signature Verified".

## Верификация токена

Как мы помним, для верификации токена нам нужен публичный ключ, а публичный ключ можно получить из приватного. Поэтому верификацию можно организовать с помощью приватного ключа (`RsaSecurityKey` сам позаботится о извлечении публичного ключа из приватного) или же только с помощью публичного ключа.

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

```
try
{
    tokenHandler.ValidateToken(
        jwt,
        new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidIssuer = "IssuerName",
            ValidAudience = "AudienceName",
            IssuerSigningKey = securityKey
        },
        out SecurityToken _);

    Console.WriteLine("JWT токен прошел верификацию!");
}
catch (Exception ex)
{
    Console.WriteLine("JWT токен НЕ прошел верификацию!");
}
```

Для кейса проверки токена только с помощью публичного ключа нужно написать новую программу:

```
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;

string jwt = "<jwt_token_code>"; // Укажите тут токен, который нужно верифицировать

using RSA rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText("Secrets/rsa_public_key_for_jwt.pem"));

JwtSecurityTokenHandler tokenHandler = new();

try
{
    tokenHandler.ValidateToken(
        jwt,
        new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidIssuer = "IssuerName",
            ValidAudience = "AudienceName",
            IssuerSigningKey = new RsaSecurityKey(rsa)
        },
        out SecurityToken _);

    Console.WriteLine("JWT токен прошел верификацию!");
}
catch (Exception ex)
{
    Console.WriteLine("JWT токен НЕ прошел верификацию!");
}
```

УРА! Теперь мы можем программно подписывать JWT токен с помощью RSA шифрования и проверять валидность подписи у JWT токена.

Ознакомится с проектом можно на github по ссылке https://github.com/izimiky/examples/tree/main/JwtTokenExample