Страницы

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

среда, 17 апреля 2019 г.

ASP.NET 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 токеном в каждом запросе, что позволяет определить какое устройство получает доступ. Но этот ключ тоже могут украсть, как и сам токен :(
Также необходимо сделать так, чтобы если параметры пользователя изменились, ранее выданные токены стали невалидными. Ничего лучше, чем черный список токенов в бд я для этого не придумал. Должны же быть какие-то стандартные методы для сброса токенов.


Ответ

Получив строку токена, можно авторизоваться с любого браузера или устройства.
Да, эта проблема, которую токены аутентификации не решают. Поэтому важно реализовать следующие возможности:
Использовать 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}". Тут возможны разные решения в зависимости от предметной области, но в целом ничего сложного нет.

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

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