Страницы

Поиск по вопросам

среда, 5 февраля 2020 г.

ASP.NET Core. Защита jwt токенов

#c_sharp #net #aspnet #aspnet_core #jwt


Я использую в своем проекте jwt токены. Код для создания ClaimsIdentity:

public async Task GetClaimsIdentityAsync(User user)
{
    var roles = await _roleService.GetUserRolesAsync(user);

    var claims = new List
    {
        new Claim(ClaimsIdentity.DefaultNameClaimType, user.Username),
    };
    foreach (var role in roles)
    {
        claims.Add(new Claim(ClaimsIdentity.DefaultRoleClaimType, role.Name));
    }
    ClaimsIdentity claimsIdentity =
        new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType,
            ClaimsIdentity.DefaultRoleClaimType);
    return claimsIdentity;
}


Код для создания токена:

public async Task GetJwtToken(User user)
{
    var identity = await _sinInService.GetClaimsIdentityAsync(user);

    var now = DateTime.UtcNow;

    var jwt = new JwtSecurityToken(
        issuer: AuthOptions.ISSUER,
        audience: AuthOptions.AUDIENCE,
        notBefore: now,
        claims: identity.Claims,
        expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)),
        signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(),
SecurityAlgorithms.HmacSha256));
    return new JwtSecurityTokenHandler().WriteToken(jwt);
}


Код в классе Startup:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = AuthOptions.ISSUER,

            ValidateAudience = true,
            ValidAudience = AuthOptions.AUDIENCE,
            ValidateLifetime = true,

            IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(),
            ValidateIssuerSigningKey = true,
        };
    });


Код был взят из этой статьи: https://metanit.com/sharp/aspnet5/23.7.php

Получив строку токена можно авторизоваться с любого браузера или устройства.

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

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


Ответы

Ответ 1



Получив строку токена, можно авторизоваться с любого браузера или устройства. Да, эта проблема, которую токены аутентификации не решают. Поэтому важно реализовать следующие возможности: Использовать HTTPS вместо HTTP, это защитит от перехвата токенов. При сохранении токенов, использовать средства ОС, которые прячут их от других программ/пользователей. Например, в Андроид-приложениях задействуйте Internal Storage с флагом MODE_PRIVATE. Делайте время жизни токена небольшим, например, 20 или 30 минут. Плюсом токена, относительно логина и пароля является время жизни: даже если его удалось перехватить, обычно это случается слишком поздно. Ничего лучше, чем черный список токенов в бд я для этого не придумал. Должны же быть какие-то стандартные методы для сброса токенов. Для того, чтобы токены можно было отключать, например, реализовывать логаут, их действительно придётся хранить. Мы для этого используем не БД, а кэш Redis, где указываем время хранения. И работает быстрее, и старые записи чистить не надо. Порядок действий: При создании токена генерируете уникальный идентификатор токена "jti" (JWT ID). Создаёте токен доступа (access token). В Redis (ну или в БД) заносите access_token с ключом, основанном на "jti", указав время жизни токена доступа (30 минут). При аутентификации проверяете подпись JWT и время жизни, хранящееся в самом JWT. Если всё нормально, извлекаете значение "jti" и проверяете, есть ли такой ключ в Redis. Если есть, всё нормально, если нет, значит, был логаут, или пользователя выкинул администратор. В любом случае считаем, что пользователь аутентификацию не прошёл. Такое решение позволяет реализовать логаут и бан и является достаточно безопасным. Из-за короткого времени жизни токена доступа, необходимо реализовать перелогин через токен обновления. Этот токен тоже нужно хранить в Redis, только время жизни задавать больше (вполне нормальным считается даже месяц, но вы смотрите, что подходит к вашей предметной области). При логауте/бане токен обновления также нужно удалять, как и токен доступа. Дальше идут детали, которые вам придётся учитывать. Если нужен бан, значит, администратору каким-то образом нужно уметь узнавать "jti" по идентификатору пользователя. Можно например "jti" делать равным "sub" (то есть идентификатору пользователя). В этом случае пользователь не сможет одновременно работать с разных устройств, потому что у него может быть только один токен доступа. Если нужно, чтобы мог работать, можно например "jti" формировать в виде $"{sub}:{random}". Тут возможны разные решения в зависимости от предметной области, но в целом ничего сложного нет.

Ответ 2



Сервер знает о клиенте только то, что клиент прислал ему в http-запросе. По сути, это только: Client IP, который может меняться. UserAgent, который неуникален. Так что ответ - провалидировать привязку токена к девайсу не получится.

Комментариев нет:

Отправить комментарий