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

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

Политики авторизации

Политика авторизации — это некий набор критериев, которыми должен обладать клиент для доступа к ресурсу. Как мы уже знаем, для каждого пользователя можно задавать различные свойства (утверждения, требования) с использованием коллекции объектов типа Claim. Именно с использованием Claim и реализуются в ASP.NET Core различные политики авторизации. И так как коллекция объектов Claim для пользователя может быть самой разнообразной, это позволяет организовать гибкую систему авторизации в вашем приложении.

Рассмотрим применение политик авторизации на примере нашего приложения, использующего для аутентификации JWT-токены. Последнюю версию исходного кода приложения можно посмотреть здесь.

Настройка политик авторизации в ASP.NET Core

Все настройки политик авторизации в ASP.NET Core осуществляются при добавлении в проект сервиса авторизации методом AddAuthorization(), например, следующим образом:

builder.Services.AddAuthorization((options) => 
{
    options.AddPolicy("MinimumAgePolicy", 
        policy => 
        { 
            policy.RequireClaim("Age","18"); 
        });
});

здесь options представляет собой объект типа AuthorizationOptions с помощью которого мы управляем политиками авторизации с помощью следующих свойств и методов

Свойство DefaultPolicy Возвращает или задает политику авторизации по умолчанию, которая будет использоваться, если атрибут Authorize задается без параметров
Метод AddPolicy() Добавьте политику с указанным именем, созданную на основе делегата .
Метод GetPolicy() Возвращает политику для указанного имени или значение NULL, если политика с этим именем не существует.

Для нас, на данный момент, важным является метод AddPolicy()

public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy)

первый параметр метода — имя новой политики авторизации, а второй — делегат, в котором мы, используя AuthorizationPolicyBuilder настраиваем политику. Класс AuthorizationPolicyBuilder содержит довольно много свойств и методов, позволяющих настраивать и объединять различные политики авторизации. Приведем основные из них:

Requirements Возвращает или задает список требований, которые должны выполняться чтобы пользователь был авторизован
AddRequirements() Добавляет новое требование в список Requirements.
RequireAuthenticatedUser() Пользователь должен быть аутентифицирован, чтобы чтобы соответствовать политике
RequireClaim(type) У пользователя должен быть определенный Claim, чтобы соответствовать политике. При этом, значение Claim не проверяется — только наличие
RequireClaim(type, values) у пользователя должен быть определен Claim с типом type и одним из значений, перечисленных в values
RequireRole(values) у пользователя должны быть определена хотя бы одна роль из списка values
RequireUserName(name) пользователь должен иметь определенное имя, чтобы соответствовать политике

Таким образом, выше мы определили политику авторизации с именем MinimumAgePolicy в которой установили ограничение — пользователь должен иметь Claim с именем «Age» и значением «18».

Применение политик авторизации

Для применения политик авторизации в ASP.NET Core мы можем либо воспользоваться атрибутом Authorize, указав в нем имя политики:

[Authorize(Policy = "MinimumAgePolicy")]

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

app.MapGet("/roles/{user}/add", (string user, string role) => 
           {
               //здесь код для конечной точки
           }).RequireAuthorization(policyNames: new[] { "MinimumAgePolicy"});

В метод RequireAuthorization необходимо передать массив содержащий имена политик авторизации, которые будут применены к конечной точке.

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

Вернемся к нашему приложению для аутентификации и авторизации пользователей и настроим свою политику авторизации пользователя. Исходный код проекта можно посмотреть здесь. На данный момент класс пользователя системы имеет следующее описание:

public static class UserRoles
{
    public const string USER = "USER";
    public const string MANAGER = "MANAGER";
    public const string ADMIN = "ADMIN";
}

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 List<string> Roles { get; set; } = new();

    public User(string login, string password, string name, string email, int age)
    {
        Login = login;
        Password = password;
        Name = name;
        Email = email;
        Age = age;
        Roles.Add(UserRoles.USER);//по умолчанию все пользователи имеют роль USER
    }

    public User(string login, string password, string name, string email, int age, string[] roles) : this(login, password, name, email, age)
    {
        Roles.AddRange(roles);
    }

    public IEnumerable<Claim> GetUserClaims()
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, Name, ClaimValueTypes.String),
            new Claim(ClaimTypes.Email, Email, ClaimValueTypes.Email),
            new Claim("Age", Age.ToString(), ClaimValueTypes.Integer)
        };
        //добавляем роли пользователя в список
        foreach (var role in Roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }
        return claims;
    }
}

Добавим к этому классу ещё два свойства — организацию и отдел в котором работает пользователь и будем передавать эти свойства в коллекции Claim

public class User
{
    //...//
    public string Organization { get; set; }
    public string Department { get; set; } 
    //...//

    public User(string login, string password, string name, string email, int age, string organization, string department)
    {
        //...//
        Organization = organization;
        Department = department;
    }

    public User(string login, string password, string name, string email, int age, string[] roles, string organization, string department) : this(login, password, name, email, age, organization, department)
    {
        Roles.AddRange(roles);
    }

    public IEnumerable<Claim> GetUserClaims()
    {
        var claims = new List<Claim>
        {
            //...//
            new Claim("Organization", Organization),
            new Claim("Department", Department),
        };
        //...//
    }
}

Соответственно, список пользователей, представляющий собой условную БД в методе Main() проекта будет теперь формироваться следующим образом:

//условная база данных с учетными записями пользователей
List<User> UserDatabase = new()
{
    new User("user","12345","Иванов Иван Иванович", "user@mail.ru", 17, new[]{ UserRoles.MANAGER }, "MyCompany", "Administration"),
    new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 18, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }, "Microsoft", "Development"),
    new User("john","12345","Сноу Джон", "snow@mail.ru", 35, "Cisco", "Security"), //новый пользователь
};

Теперь создадим новую политику авторизации, в которой будут установлены следующие требования к пользователю — он должен работать в организации Microsoft в отделе Development и иметь роль администратора (ADMIN):

builder.Services.AddAuthorization((options) => 
{
    options.AddPolicy("OnlyForMicrosoftAdmin", 
        policy => 
        { 
            policy.RequireClaim("Organization","Microsoft").//должен работать в Microsoft
                   RequireClaim("Department","Development").//должен работать в отделе Development
                   RequireRole(new[] { "ADMIN"}); //должен быть админом
        });
});

Теперь применим эту политику авторизации к конечной точке в которой мы управляли ролями пользователей:

app.MapGet("/roles/{user}/add", [Authorize(Policy = "OnlyForMicrosoftAdmin")] (string user, string role) =>
{
    var userData = UserDatabase.FirstOrDefault(f => f.Login == user);
    if (userData != null)
    {
        userData.Roles.Add(role);
        return $"Для пользователя {user} добавлена роль {role} \n Все роли пользователя: {string.Join(",", userData.Roles)}";
    }
    return $"Пользователь {user} не найден в БД";
});

Этой политике у нас соответствует только один пользователь с логином admin. Проверим работу приложения. Авторизуемся с логином john 

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

В доступе отказано, так как пользователь не соответствует политике. Теперь авторизуемся с логином admin и попробуем повторить операцию:

Как можно увидеть, пользователь admin полностью соответствовал политике авторизации, поэтому его запрос на добавление новой роли прошел успешно.

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

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;

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 static class UserRoles
    {
        public const string USER = "USER";
        public const string MANAGER = "MANAGER";
        public const string ADMIN = "ADMIN";
    }

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

        public List<string> Roles { get; set; } = new();

        public User(string login, string password, string name, string email, int age, string organization, string department)
        {
            Login = login;
            Password = password;
            Name = name;
            Email = email;
            Age = age;
            Roles.Add(UserRoles.USER);//по умолчанию все пользователи имеют роль USER
            Organization = organization;
            Department = department;
        }

        public User(string login, string password, string name, string email, int age, string[] roles, string organization, string department) : this(login, password, name, email, age, organization, department)
        {
            Roles.AddRange(roles);
        }

        public IEnumerable<Claim> GetUserClaims()
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, Name, ClaimValueTypes.String),
                new Claim(ClaimTypes.Email, Email, ClaimValueTypes.Email),
                new Claim("Age", Age.ToString(), ClaimValueTypes.Integer),
                new Claim("Organization", Organization),
                new Claim("Department", Department),
            };
            //добавляем роли пользователя в список
            foreach (var role in Roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }
            return claims;
        }
    }

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

            //условная база данных с учетными записями пользователей
            List<User> UserDatabase = new()
            {
                new User("user","12345","Иванов Иван Иванович", "user@mail.ru", 17, new[]{ UserRoles.MANAGER }, "MyCompany", "Administration"),
                new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 18, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }, "Microsoft", "Development"),
                new User("john","12345","Сноу Джон", "snow@mail.ru", 35, "Cisco", "Security"), //новый пользователь
            };

            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((options) => 
            {
                options.AddPolicy("OnlyForMicrosoftAdmin", 
                    policy => 
                    { 
                        policy.RequireClaim("Organization","Microsoft").//должен работать в Microsoft
                               RequireClaim("Department","Development").//долден работать в отделе Development
                               RequireRole(new[] { "ADMIN"}); //должен быть админом
                    });
            });
            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) =>
            {
                string greetings = "Вы не аутентифицированы";
                string roleDesc = "Вы НЕ можете управлять приложением";

                if ((context.User != null) && (context.User.Identity.IsAuthenticated))
                {
                    var claims = context.User.Claims;
                    var name = claims.FirstOrDefault(f => f.Type == ClaimTypes.Name);
                    if (name != null)
                        greetings = $"Привет, {name.Value}";

                    //проверяем пользователя на принадлежность роли ADMIN
                    if (context.User.IsInRole("ADMIN"))
                        roleDesc = "Вы можете управлять приложением";
                }

                return Results.Content("<html><body>" +
                    $"<p>{greetings}</p>" +
                    $"<p>{roleDesc}</p>" +
                    "</body></html>", "text/html; charset=utf-8");
            });


            app.MapGet("/roles/{user}/add", [Authorize(Policy = "OnlyForMicrosoftAdmin")] (string user, string role) =>
            {
                var userData = UserDatabase.FirstOrDefault(f => f.Login == user);
                if (userData != null)
                {
                    userData.Roles.Add(role);
                    return $"Для пользователя {user} добавлена роль {role} \n Все роли пользователя: {string.Join(",", userData.Roles)}";
                }
                return $"Пользователь {user} не найден в БД";
            });


            app.MapGet("/auth", [Authorize(Roles ="ADMIN")] (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();
        }
    }
}

Итого

Сегодня мы познакомились с ещё одним способом авторизации пользователей — на основе политик. Политики позволяют сделать систему авторизации более гибкой и предъявлять к пользователю самые различные требования для успешной авторизации. Несмотря на то, что ASP.NET Core предоставляет нам довольно широкие возможности по формированию политик, всё же их бывает недостаточно. Например, политика авторизации, рассмотренная в самом начале этой части с названием MinimumAgePolicy на самом деле будет ограничивать всех пользователей возраст которых не равен 18, что не соответствует названию. Поэтому в следующей части мы рассмотрим вопрос создания собственных требований к авторизации в которых и затронем вопрос об использовании требований типа «не менее», «содержит что-либо» и т.д.

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