Аутентификация и авторизация в ASP.NET Core. Авторизация клиента с помощью JWT

В предыдущей части мы разработали небольшое приложение, позволяющее аутентифицировать пользователя с использованием токенов JWT. Также мы протестировали работу приложения с использованием сервиса ReqBin и убедились, что токен работает — получили доступ к конечной точке, требующей авторизации пользователя. Однако, при разработке приложений (если это не обычный API для получения доступа к данным от сторонних клиентов) необходимо также предусмотреть и авторизацию пользователей с использованием полученного JWT-токена. И сегодня мы рассмотрим один из возможных вариантов того как происходит авторизация клиента с помощью JWT в ASP.NET Core

Как известно, любую задачу в программировании можно решить несколькими способами. Поле получения JWT-токена мы можем поступать с ним как угодно — например, написать клиент на JavaScript для передачи токена в заголовке Authorization. Также, мы можем воспользоваться возможностями ASP.NET Core и, например, сохранять полученный токен в cookies или использовать сессии. Рассмотрим один из вариантов работы с токеном через cookies.

Передача токена клиенту через Cookies

На данный момент, в нашем приложении определена конечная точка для входа пользователя в систему:

app.MapGet("/login", (string? login, string? password, HttpContext context) =>
{
    if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password))
        return Results.BadRequest(new { Error = "Не задан логин или пароль" });
    var user = UserDatabase.FirstOrDefault(u => (u.Login == login) && (u.Password == password));
    if (user == null)
        return Results.BadRequest(new { Error = "Не верно введен логин или пароль" });
    JwtSecurityToken token = new(
        issuer: authOptions?.Issuer,
        audience: authOptions?.Audience,
        expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)),
        signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
    );
    
    JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
    string tokenString = handler.WriteToken(token);

    return Results.Ok(new { token = tokenString, user = login });
});

Здесь мы можем сделать следующее — передать cookie с токеном клиенту и, затем, получать значение токена при каждом запросе. Допишем код конечной точки следующим образом:

app.MapGet("/login", (string? login, string? password, HttpContext context) =>
{
    //здесь код формирования токена
    
    JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
    string tokenString = handler.WriteToken(token);

    context.Response.Cookies.Append("token", tokenString);//отпраляем клиенту куку с токеном

    return Results.Ok(new { token = tokenString, user = login });
});

Теперь, как только пользователь введет верный логин и пароль, он получит токен в виде куки.

Авторизация клиента с помощью JWT

Чтобы клиент смог авторизоваться по этому токену необходимо каким-либо образом значение куки передать в заголовок авторизации. Сделать это можно, например, написав свой компонент middleware, который будет встраиваться в конвейер перед всеми конечными точками для которых необходима авторизация.

Компонент будет выглядеть достаточно просто:

app.Use(async (context, next) =>
{
    if (context.Request.Cookies.TryGetValue("token", out string? token))
        context.Request.Headers.Authorization = $"Bearer {token}";
    await next();
});

по сути, всё, что мы здесь делаем — это пробуем получить куку с именем «token» и, если таковая находится, то её значение передается в заголовок авторизации:

context.Request.Headers.Authorization = $"Bearer {token}";

Встраиваем этот middleware в начало конвейера запросов (сразу после строки var app = builder.Build();):

var app = builder.Build();

app.Use(async (context, next) =>
{
    if (context.Request.Cookies.TryGetValue("token", out string? token))
        context.Request.Headers.Authorization = $"Bearer {token}";
    await next();
});

Протестируем наше приложение

получение токена по логину и паролю

получаем доступ к ресурсу для которого требуется авторизация:

так как мы ничего не меняли в приложении с прошлого раза, то в ответ нам, как и предполагалось, вернулись настройки генерации токена, что говорит нам о том, что наш способ авторизации клиента работает. Кроме этого, если подождать пару минут и повторить запрос, то получим ошибку доступа так как токен «живет» две минуты

Исходный код проекта

Ниже представлен весь исходный код проекта на данный момент

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;

using System.IdentityModel.Tokens.Jwt;
using System.Text;
namespace AspAuth
{
    public class AuthOptions
    {
        public string Issuer { get; set; } //издатель токена
        public string Audience { get; set; } //потребитель токена
        public string SecretKey { get; set; } //секретный ключ для подписи
        public bool ValidateAudience { get; set; } //проверять потребителя
        public bool ValidateIssuer { get; set; } //проверять издателя
        public bool ValidateLifetime { get; set; } //проверять время жизни токена
        public bool ValidateIssuerSigningKey { get; set; } //проверять подпись
        public int TokenLifetime { get; set; } //время жизни токена в минутах
    }
    public class User
    {
        public string Login { get; set; }
        public string Password { get; set; }
    }
    public class Program
    {
        public static void Main(string[] args)
        {

            //условная база данных с учетными записями пользователей
            List<User> UserDatabase = new()
            {
                new User()
                {
                    Login = "user",
                    Password ="12345"
                },
                new User()
                {
                    Login = "admin",
                    Password = "root"
                },
            };

            var builder = WebApplication.CreateBuilder(args);
            var authOptions = builder.Configuration.GetSection("Jwt").Get<AuthOptions>();
            SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authOptions.SecretKey));
            builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
                {
                    options.RequireHttpsMetadata = true;
                    options.SaveToken = true;
                    options.IncludeErrorDetails = true;
                    
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = authOptions.ValidateIssuer,
                        ValidateAudience = authOptions.ValidateAudience,
                        ValidateLifetime = authOptions.ValidateLifetime,
                        ValidateIssuerSigningKey = authOptions.ValidateIssuerSigningKey,
                        ValidIssuer = authOptions.Issuer,
                        ValidAudience = authOptions.Audience,
                        IssuerSigningKey = key
                    };
                });
            builder.Services.AddAuthorization();
            var app = builder.Build();

            app.Use(async (context, next) =>
            {
                if (context.Request.Cookies.TryGetValue("token", out string? token))
                    context.Request.Headers.Authorization = $"Bearer {token}";
                await next();
            });


            app.UseAuthentication();
            app.UseAuthorization();

            app.MapGet("/", () => "Hello World!");
            app.MapGet("/auth", [Authorize] () =>
            {
                return Results.Ok(authOptions);
            });
            app.MapGet("/login", (string? login, string? password, HttpContext context) =>
            {
                if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password))
                    return Results.BadRequest(new { Error = "Не задан логин или пароль" });
                var user = UserDatabase.FirstOrDefault(u => (u.Login == login) && (u.Password == password));
                if (user == null)
                    return Results.BadRequest(new { Error = "Не верно введен логин или пароль" });
                JwtSecurityToken token = new(
                    issuer: authOptions?.Issuer,
                    audience: authOptions?.Audience,
                    expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)),
                    signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
                );
                
                JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
                string tokenString = handler.WriteToken(token);

                context.Response.Cookies.Append("token", tokenString);//отпраляем клиенту куку с токеном

                return Results.Ok(new { token = tokenString, user = login });
            });
            app.Run();
        }
    }
}

Итого

Сегодня мы рассмотрели один из возможных вариантов авторизации клиента по JWT-токену. Для сохранения и передачи токена на сервер мы использовали Cookies, а для формирования заголовка авторизации — написали свой компонент middleware, который встроили в самое начало конвейера запросов.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии