#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, который неуникален.
Так что ответ - провалидировать привязку токена к девайсу не получится.