Страницы

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

Показаны сообщения с ярлыком jwt. Показать все сообщения
Показаны сообщения с ярлыком jwt. Показать все сообщения

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

воскресенье, 8 декабря 2019 г.

Как правильно “готовить” авторизацию в SPA?

#c_sharp #aspnet_core #aspnet_web_api #oauth #jwt


Цель такая: написать бэкенд ASP.Net Core MVC* SPA для работы с ReactJS и дальнейшей
возможностью переиспользовать существующий API для создания, скажем, Android приложения.  
Платформа: .Net Core 2.1.

* - Если такое ещё можно назвать MVC, учитывая, что View не будет, а будет отдельная
директория ClientApp со всем содержимым фронтэнда.

Пошуршав интернеты, наткнулся на то, что ASP.Net Core Identity не актуален. Звучит
логично, учитывая, что тот сильно опирается на куки, а при общении через API куки таскать
неудобно. Хотя многие примеры нижеупомянутого JWT всё же используют IdentityUser.

Много инструкций с использованием JWT. Приличная часть из них слишком зациклена на
фронтенд реализации и практически ничего не говорит о бэкенде. Не нашёл примеров с
OAuth2, везде свой велосипед, причём Demo и не пригодный для реального использования. 
В тех же примерах по JWT используются Issuer, Audience и SecretKey, но ни слова о
том, по каким правилам их надо выбирать и/или генерировать, ну и где безопасно хранить
(если исключить примеры с хардкодом, то их обычно хранили в appsettings.json).

Также в процессе гугления (конкретно: попытке найти инфу об JwtSecurityTokenHandler
из System.IdentityModel.Tokens) MSDN Microsoft выдаёт:


  We’re no longer updating this content regularly. Check the Microsoft Product Lifecycle
for information about how this product, service, or technology is supported.


Что это значит? Microsoft более не поддерживает JWT? Технология в принципе уже неактуальна?
Что же тогда использовать?

В итоге мне теперь не совсем понятно как строить авторизацию в своём приложении:


Нужные ли мне IdentityUser из Microsoft.AspNetCore.Identity?
Нужен ли мне IdentityDbContext из Microsoft.AspNetCore.Identity.EntityFrameworkCore?
Актуален ли JWT?
Как построить авторизацию с OAuth2?


Хотелось бы сохранить доступность авторизованного пользователя из HttpContext.User
и к его Claims, чтобы не мучать БД лишний раз для получения Id/UserName/Avatar/LastOnline/etc. 

А также учесть то, что активно будут использоваться роли пользователей. 

UPD: Немного обновлю конечные цели, чтобы стало понятнее:


Это форум с разделами, постами и комментариями в формате вопрос-ответ 
Реализация фронтенда через SPA
Планируется открытое API
Это же API будет использовать SPA
Поддержка быстрой регистрации/входа через сторонние сервисы (Vk, FB, Google, etc.)

    


Ответы

Ответ 1



Вот проверенный код, который работает. Итак, настройка аутентификации в Startup public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); services.AddDbContext(options => options.UseMySql(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity() .AddEntityFrameworkStores(); services.Configure(options => { //........ }); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { // укзывает, будет ли валидироваться издатель при валидации токена ValidateIssuer = false, // будет ли валидироваться потребитель токена ValidateAudience = false, // будет ли валидироваться время существования ValidateLifetime = true, // установка ключа безопасности IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(), // валидация ключа безопасности ValidateIssuerSigningKey = true, }; }); services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); }); // ......... } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ............... app.UseAuthentication(); // ................ } } Класс AuthOptions public class AuthOptions { const string KEY = "mysupersecret_secretkey!"; // ключ для шифрации public const int LIFETIME = 300; // время жизни токена - 5 часов public static SymmetricSecurityKey GetSymmetricSecurityKey() { return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY)); } } Моделька для запроса токена public class TokenRequest { public string UserName { get; set; } public string Password { get; set; } } Контроллер для получения токена [Route("api/[controller]")] public class AccountController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager; public AccountController(UserManager userManager, SignInManager signInManager) { _userManager = userManager; _signInManager = signInManager; } [HttpPost("[action]")] public async Task Auth([FromBody] TokenRequest tokenRequest) { var username = tokenRequest.UserName; var password = tokenRequest.Password; var principal = await GetPrincipal(username, password); if (principal == null) { return StatusCode(400, "Invalid username or password."); } var now = DateTime.UtcNow; // создаем JWT-токен var jwt = new JwtSecurityToken( notBefore: now, claims: principal.Claims, expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)), signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256)); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); var response = new { token = encodedJwt, username = principal.Identity.Name }; return Json(response); } private async Task GetPrincipal(string username, string password) { var user = await _userManager.FindByNameAsync(username); if (user != null) { var check = await _userManager.CheckPasswordAsync(user, password); if (check) { var principal = await _signInManager.CreateUserPrincipalAsync(user); return principal; } } return null; } } И, по моему, это все. С этим все остальные вещи типа атрибутов авторизации, роли и прочее работает из коробки. Насколько я помню, ничего дополнительно делать не надо.

Ответ 2



Для начала, ASP.Net Core Identity никаким образом не опирается на куки! ASP.Net Core Identity - это прежде всего система для хранения и обработки информации о пользователях. Все что делает Identity во время аутентификации - это загружает из БД информацию о пользователе, заполняя набор утверждений о нем, после чего передает его ASP.NET Core. Соответственно, нет никакого смысла искать как использовать ASP.Net Core Identity вместе с Jwt - это попросту не связанные друг с другом вещи. Теперь про OAuth. Технология OAuth предназначена для того, чтобы сторонний разработчик мог создать приложение которое бы работало с вашим сайтом, но при этом не имел доступа к паролям ваших пользователей. Если же вы собрались создавать приложение на Андроиде самостоятельно - использование OAuth будет для вас избыточно! Все что вам нужно - это action в контроллере, который принимает логин с паролем и выдает токен. Ну а если вы решили дать доступ к своему API именно для сторонних разработчиков - Identity Server вам в помощь. Наконец, про JWT. Audience - это параметр токена, который означает целевую систему куда его можно предъявлять. Иными словами, это URL вашего сайта. Однако, нет никакого смысла в его использовании в ситуации когда у вас только один сайт: он нужен когда есть несколько сайтов со своими API и эти сайты не доверяют друг другу. Issuer - это URI провайдера, который выдал JWT. То есть, опять-таки, URL вашего сайта. Это поле используется только в тех случаях когда доступ к API можно получить при помощи токенов от разных поставщиков. В остальных случаях это поле вообще-то не обязательное. SecretKey - это, скорее всего, ключ, которым вы шифруете или подписываете JWT. Хранить его можно в appsettings.json или в переменных окружения. Главное - не публикуйте его на github. Вообще, желательно использовать разные ключи для разработки и на "боевом" сервере.

Ответ 3



Отталкивался я от ответа @tym32167 (поэтому и пометил его как правильный), но на всякий случай добавлю конкретно свою реализацию. Вдруг будет кому-то полезна. Startup.cs public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { // подключение DbContext'ов // подключение Identity services.Configure(options => { // ... }); // игнорирование регистра (url без заглавных букв) services.Configure(options => options.LowercaseUrls = true); // подключение репозиториев services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, //ValidIssuer = "", ValidateAudience = false, //ValidAudience = "", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["Jwt:Key"])), ValidateIssuerSigningKey = true, }; }); services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); }); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/build"; }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); //ToDo: do it better app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseSpaStaticFiles(); app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller}/{action=Index}/{id?}"); }); app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { // актуально для ReactJS spa.UseReactDevelopmentServer(npmScript: "start"); } }); } } appsettings.json { "Jwt": { "Key": "my_very_secret_key", "LifeTime": "300" } } Jwt:Key должен быть больше 16 символов (128 бит) иначе выбрасывает исключение при генерации токена. По крайней мере, при использовании шифрования HmacSha256. TokenRequest.cs public class TokenRequest { [Required(ErrorMessage = "")] [JsonProperty("login")] public string Login { get; set; } [Required(ErrorMessage = "")] [DataType(DataType.Password)] [JsonProperty("password")] public string Password { get; set; } } RegisterModel.cs public class RegisterModel { [Required(ErrorMessage = "")] [JsonProperty("username")] public string UserName { get; set; } [Required(ErrorMessage = "")] [EmailAddress] [DataType(DataType.EmailAddress)] [JsonProperty("email")] public string Email { get; set; } [Required(ErrorMessage = "")] [DataType(DataType.Password)] [JsonProperty("password")] public string Password { get; set; } [DataType(DataType.Password)] [Compare("Password", ErrorMessage = "")] [JsonProperty("confirm_password")] public string ConfirmPassword { get; set; } } AccountController.cs [Route("api/[controller]")] [ApiController] public class AccountController : ControllerBase { private readonly UserManager userManager; private readonly SignInManager signInManager; private readonly IConfiguration configuration; public AccountController( UserManager userManager, SignInManager signInManager, IConfiguration configuration) { this.userManager = userManager; this.signInManager = signInManager; this.configuration = configuration; } [HttpPost("[action]")] public async Task Register([FromBody] RegisterModel model) { if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)); return BadRequest(errors); } var userAccount = new UserAccount { Email = model.Email, UserName = model.UserName }; var result = await userManager.CreateAsync(userAccount, model.Password); if (result.Succeeded) { var token = await GetTokenAsync(userAccount); return Ok(new { token }); } return BadRequest(result.Errors.Select(e => e.Description)); } [HttpPost("[action]")] public async Task Auth([FromBody] TokenRequest tokenRequest) { var userAccount = tokenRequest.Login.Contains('@') ? await userManager.FindByEmailAsync(tokenRequest.Login) : await userManager.FindByNameAsync(tokenRequest.Login); if (userAccount == null) { return NotFound(new { message = $"User with login {tokenRequest.Login} not found!" }); } var passValided = await userManager.CheckPasswordAsync(userAccount, tokenRequest.Password); if (!passValided) { return UnprocessableEntity(new { message = "Invalid username or password." }); } var token = await GetTokenAsync(userAccount); return Ok(token); } private async Task GetTokenAsync(UserAccount userAccount) { var principal = await signInManager.CreateUserPrincipalAsync(userAccount); var identity = (ClaimsIdentity)principal.Identity; if (identity == null) { return null; } var now = DateTime.UtcNow; // created JWT-token var jwt = new JwtSecurityToken( notBefore: DateTime.UtcNow, claims: identity.Claims, expires: now.Add(TimeSpan.FromMinutes(int.Parse(configuration["Jwt:LifeTime"]))), signingCredentials: new SigningCredentials( new SymmetricSecurityKey( Encoding.ASCII.GetBytes(configuration["Jwt:Key"])), SecurityAlgorithms.HmacSha256)); var token = new JwtSecurityTokenHandler().WriteToken(jwt); return token; } } UserAccount наследник IdentityUser, а UserRole наследник IdentityRole

среда, 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}". Тут возможны разные решения в зависимости от предметной области, но в целом ничего сложного нет.

четверг, 18 октября 2018 г.

Как правильно “готовить” авторизацию в SPA?

Цель такая: написать бэкенд ASP.Net Core MVC* SPA для работы с ReactJS и дальнейшей возможностью переиспользовать существующий API для создания, скажем, Android приложения. Платформа: .Net Core 2.1
* - Если такое ещё можно назвать MVC, учитывая, что View не будет, а будет отдельная директория ClientApp со всем содержимым фронтэнда.
Пошуршав интернеты, наткнулся на то, что ASP.Net Core Identity не актуален. Звучит логично, учитывая, что тот сильно опирается на куки, а при общении через API куки таскать неудобно. Хотя многие примеры нижеупомянутого JWT всё же используют IdentityUser
Много инструкций с использованием JWT. Приличная часть из них слишком зациклена на фронтенд реализации и практически ничего не говорит о бэкенде. Не нашёл примеров с OAuth2, везде свой велосипед, причём Demo и не пригодный для реального использования. В тех же примерах по JWT используются Issuer, Audience и SecretKey, но ни слова о том, по каким правилам их надо выбирать и/или генерировать, ну и где безопасно хранить (если исключить примеры с хардкодом, то их обычно хранили в appsettings.json).
Также в процессе гугления (конкретно: попытке найти инфу об JwtSecurityTokenHandler из System.IdentityModel.Tokens) MSDN Microsoft выдаёт:
We’re no longer updating this content regularly. Check the Microsoft Product Lifecycle for information about how this product, service, or technology is supported.
Что это значит? Microsoft более не поддерживает JWT? Технология в принципе уже неактуальна? Что же тогда использовать?
В итоге мне теперь не совсем понятно как строить авторизацию в своём приложении:
Нужные ли мне IdentityUser из Microsoft.AspNetCore.Identity? Нужен ли мне IdentityDbContext из Microsoft.AspNetCore.Identity.EntityFrameworkCore? Актуален ли JWT? Как построить авторизацию с OAuth2?
Хотелось бы сохранить доступность авторизованного пользователя из HttpContext.User и к его Claims, чтобы не мучать БД лишний раз для получения Id/UserName/Avatar/LastOnline/etc.
А также учесть то, что активно будут использоваться роли пользователей.
UPD: Немного обновлю конечные цели, чтобы стало понятнее:
Это форум с разделами, постами и комментариями в формате вопрос-ответ Реализация фронтенда через SPA Планируется открытое API Это же API будет использовать SPA Поддержка быстрой регистрации/входа через сторонние сервисы (Vk, FB, Google, etc.)


Ответ

Вот проверенный код, который работает. Итак, настройка аутентификации в Startup
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; }
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
services.AddDbContext(options => options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity() .AddEntityFrameworkStores();
services.Configure(options => { //........ });
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { // укзывает, будет ли валидироваться издатель при валидации токена ValidateIssuer = false, // будет ли валидироваться потребитель токена ValidateAudience = false, // будет ли валидироваться время существования ValidateLifetime = true,
// установка ключа безопасности IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(), // валидация ключа безопасности ValidateIssuerSigningKey = true, }; });
services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); });
// ......... }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ............... app.UseAuthentication(); // ................ } }
Класс AuthOptions
public class AuthOptions { const string KEY = "mysupersecret_secretkey!"; // ключ для шифрации public const int LIFETIME = 300; // время жизни токена - 5 часов public static SymmetricSecurityKey GetSymmetricSecurityKey() { return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY)); } }
Моделька для запроса токена
public class TokenRequest { public string UserName { get; set; } public string Password { get; set; } }
Контроллер для получения токена
[Route("api/[controller]")] public class AccountController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager;
public AccountController(UserManager userManager, SignInManager signInManager) { _userManager = userManager; _signInManager = signInManager; }
[HttpPost("[action]")] public async Task Auth([FromBody] TokenRequest tokenRequest) { var username = tokenRequest.UserName; var password = tokenRequest.Password;
var principal = await GetPrincipal(username, password); if (principal == null) { return StatusCode(400, "Invalid username or password."); }
var now = DateTime.UtcNow; // создаем JWT-токен var jwt = new JwtSecurityToken( notBefore: now, claims: principal.Claims, expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)), signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256)); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new { token = encodedJwt, username = principal.Identity.Name };
return Json(response); }
private async Task GetPrincipal(string username, string password) { var user = await _userManager.FindByNameAsync(username); if (user != null) { var check = await _userManager.CheckPasswordAsync(user, password); if (check) { var principal = await _signInManager.CreateUserPrincipalAsync(user); return principal; } } return null; } }
И, по моему, это все. С этим все остальные вещи типа атрибутов авторизации, роли и прочее работает из коробки. Насколько я помню, ничего дополнительно делать не надо.