Аутентификация и авторизация в ASP.NET Core. JWT и объект HttpContext.User

Одной из задач аутентификации в приложении ASP.NET Core является установка личности пользователя, который представляется в приложении свойством User класса HttpContext. В этом свойстве могут передаваться различные сведения о пользователе — имя, логин, принадлежность к определенной группе пользователей и т.д., которые, в дальнейшем, могут использоваться в приложении для авторизации. В этой части мы продолжим работу над приложением, демонстрирующем аутентификацию и авторизацию с использованием JWT и разберемся, как можно использовать свойство HttpContext.User

Свойство HttpContext.User и класс ClaimsPrincipal

Свойство HttpContext.User определено следующим образом:

public abstract ClaimsPrincipal User { get; set; }

Класс ClaimsPrincipal, в свою очередь, содержит следующие свойства, относящиеся к личности пользователя

Claims Коллекция всех утверждений (свойств пользователя) из всех удостоверений, связанных с пользователем.
Identities Коллекция удостоверений, связанных с пользователем.
Identity Основное удостоверение, связанное с этим пользователем.

Таким образом, с одним пользователем могут быть связаны различные удостоверения (Identity). Каждое удостоверение представляет собой объект, реализующий интерфейс IIdentity со следующими свойствами:

AuthenticationType Тип используемой аутентификации.
IsAuthenticated Значение, определяющее, прошел ли пользователь проверку подлинности.
Name Имя текущего пользователя.

 

Для каждого удостоверения (Identity) могут задаваться различные утверждения (Claims), позволяющие более полно идентифицировать пользователя.

Работа с HttpContext.User при использовании JWT

До этого момента мы никак не касались свойства User при разработке нашего приложения, демонстрирующего аутентификацию и авторизацию пользователя. Теперь, немного разобравшись с этим свойством, мы можем его использовать. Код приложения, с которым мы будем сейчас работать, можно посмотреть здесь.

Допишем класс пользователя следующим образом:

public class User
{
    public string Login { get; set; }
    public string Password { get; set; }
    public string Name { get; set; }   
    public string Email { get; set; }
    public int Age { get; set; }

    public User(string login, string password, string name, string email, int age)
    {
        Login = login;
        Password = password;
        Name = name;
        Email = email;
        Age = age;
    }

    public IEnumerable<Claim> GetUserClaims()
    {
        return new List<Claim>
        {
            new Claim(ClaimTypes.Name, Name, ClaimValueTypes.String),
            new Claim(ClaimTypes.Email, Email, ClaimValueTypes.Email),
            new Claim("Age", Age.ToString(), ClaimValueTypes.Integer)
        };
    }
}

Здесь мы добавили в класс новые свойства — имя, адрес электронной почты и возраст, добавили новый конструктор, а также метод GetUserClaims, который возвращает список утверждений (Claim) для текущего пользователя. Для демонстрации, мы формируем этот список на основании трех свойств класса User. Теперь передадим данные о пользователе в токен.

Добавим пользователей в условную БД

//условная база данных с учетными записями пользователей
List<User> UserDatabase = new()
{
    new User("user","12345","Иванов Иван Иванович", "user@mail.ru", 17),
    new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 25),
};

В нашем приложении токен генерируется с использованием следующего конструктора:

public JwtSecurityToken(string issuer = null, string audience = null, IEnumerable<Claim> claims = null, DateTime? notBefore = null, DateTime? expires = null, SigningCredentials signingCredentials = null)

используя при этом только несколько параметров:

JwtSecurityToken token = new(
                   issuer: authOptions?.Issuer,
                   audience: authOptions?.Audience,
                   expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)),
                   signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
               );

Добавим в конструктор список, который мы получаем из метода GetUserClaims:

JwtSecurityToken token = new(
    issuer: authOptions?.Issuer,
    audience: authOptions?.Audience,
    claims: user.GetUserClaims(), //добавляем список Claim в токен
    expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)),
    signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);

Теперь, если мы передаем токен в заголовке входящего запроса, то ASP.NET Core получит из токена все свойства пользователя и вернет их в свойстве HttpContext.User. Изменим конечную точку «/» следующим образом:

app.MapGet("/", (HttpContext context) => 
{
    if ((context.User != null) && (context.User.Identity.IsAuthenticated))
    {
        var claims = context.User.Claims;
        var name = claims.FirstOrDefault(f => f.Type == ClaimTypes.Name);
        return name != null ? $"Привет, {name.Value}" : $"Привет, аноним";
    }
    else
        return "Вы не аутентифицированы";
});

здесь мы проверяем, определено ли свойство HttpContext.User и аутентифицирован ли пользователь. Если пользователь аутентифицирован, то мы получаем список всех утверждений:

var claims = context.User.Claims;

и пытаемся найти в этом списке имя пользователя:

var name = claims.FirstOrDefault(f => f.Type == ClaimTypes.Name);

если имя найдено, то здороваемся с пользователем по имени, иначе — как с анонимом:

return name != null ? $"Привет, {name.Value}" : $"Привет, аноним";

если же свойство HttpContext.User не определено или пользователь не аутентифицирован, то сообщаем об этом пользователю:

return "Вы не аутентифицированы";

Проверим работу нашего приложения. Так выглядит главная страница приложения после запуска:

Теперь аутентифицируем пользователя:

Перейдем снова на главную страницу приложения

страница доступа только для авторизованных пользователей:

 

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

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

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

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Xml.Linq;

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 string Name { get; set; }   
        public string Email { get; set; }
        public int Age { get; set; }

        public User(string login, string password, string name, string email, int age)
        {
            Login = login;
            Password = password;
            Name = name;
            Email = email;
            Age = age;
        }

        public IEnumerable<Claim> GetUserClaims()
        {
            return new List<Claim>
            {
                new Claim(ClaimTypes.Name, Name, ClaimValueTypes.String),
                new Claim(ClaimTypes.Email, Email, ClaimValueTypes.Email),
                new Claim("Age", Age.ToString(), ClaimValueTypes.Integer)
            };
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {

            //условная база данных с учетными записями пользователей
            List<User> UserDatabase = new()
            {
                new User("user","12345","Иванов Иван Иванович", "user@mail.ru", 17),
                new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 25),
            };

            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(auth =>
            {
                auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                auth.DefaultChallengeScheme = 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("/", (HttpContext context) => 
            {
                if ((context.User != null) && (context.User.Identity.IsAuthenticated))
                {
                    var claims = context.User.Claims;
                    var name = claims.FirstOrDefault(f => f.Type == ClaimTypes.Name);
                    return name != null ? $"Привет, {name.Value}" : $"Привет, аноним";
                }
                else
                    return "Вы не аутентифицированы";
            });
            
            
            app.MapGet("/auth", [Authorize] (HttpContext context) =>
            {
                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,
                    claims: user.GetUserClaims(), //добавляем список Claim в токен
                    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();
        }
    }
}

Итого

В этой части мы разобрались с некоторыми свойствами объекта HttpContext.User и научились устанавливать и получать утверждения (Claim), относящиеся к конкретному пользователю. Тема использования авторизации с использованием классов Claim, ClaimsPrincipal и ClaimIdentity довольно обширная, поэтому в следующих частях мы продолжим знакомиться с этими классами и их использованием для авторизации.

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