Авторизация на основе Claims

Последнее обновление: 12.01.2022

Атрибут Authorize легко позволяет разграничить доступ в зависимости от роли, однако для создания авторизации функциональности ролей бывает недостаточно. Например, что если мы хотим разграничить доступ на основе возраста пользователя или каких-то других признаков. Для этого применяется авторизация на основе claims. Собственно авторизация на основе ролей фактически представляет частный случай авторизации на основе claims, так как роль это тот же объект Claim, имеющий тип ClaimsIdentity.DefaultRoleClaimType.

Для авторизации на основе claims используются политики (policy). Политика представляет набор ограничений, которым должен соответствовать пользователь для доступа к ресурсу.

Все применяемые политики добавляются в приложение с помощью метода builder.Services.AddAuthorization(). Этот метод устанавливает политики с помощью объекта AuthorizationOptions. Например:

builder.Services.AddAuthorization(opts => {
    
    opts.AddPolicy("OnlyForMicrosoft", policy => {
        policy.RequireClaim("company", "Microsoft");
    });
});

В данном случае добавляется политика с именем "OnlyForMicrosoft". И она требует обязательной установки для текущего пользователя объекта Claim с типом "company" и значением "Microsoft". Если для пользователя не будет установлено подобного объекта Claim, то такой пользователь не будет соответствовать политике.

Для управления политиками в классе AuthorizationOptions определены следующие свойства и методы:

  • DefaultPolicy: возвращает политику по умолчанию, которая используется, когда атрибут Authorize применяется без параметров

  • AddPolicy(name, policyBuilder): добавляет политику

  • GetPolicy(name): возвращает политику по имени

Ключевым методом здесь является AddPolicy(). Первый параметр метода представляет название политики, а второй - делегат, который с помощью объекта AuthorizationPolicyBuilder позволяет создать политику по определенным условиям. Для создания политики могут применяться следующие методы класса AuthorizationPolicyBuilder:

  • RequireAuthenticatedUser(): пользователь обязательно должен быть аутентифицирован для соответствия политике

  • RequireClaim(type): для пользователя должен быть установлен claim с типом type. Причем не важно, какое значение будет иметь этот claim, главное, его наличие

  • RequireClaim(type, values): для пользователя должен быть установлен claim с типом type. Но теперь claim должен в качестве значения иметь одно из значений из массива values.

  • RequireRole(roles): пользователь должен принадлежать к одной из ролей из массива roles

  • RequireUserName(name): для соответствия политике пользователь должен иметь ник (логин) name

  • RequireAssertion(handler): запрос должен соответствовать условию, которое устанавливается с помощью делегата handler

  • AddRequirements(requirement): позволяет добавить кастомное ограничение requirement, если имеющихся недостаточно

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

[Authorize(Policy = "OnlyForMicrosoft")]

Для установки политики у атрибута AuthorizeAttribute применяется свойство Policy. Оно указывает на название политики, которой должны соответствовать пользователи.

Применение авторизации на основе Claims

Допустим, у нас есть следующий класс, который представляет пользователя:

record class Person(string Email, string Password, string City, string Company);

У класса Person кроме свойств для хранения email и пароля также определено свойство City для хранения города и свойство Company для хранения компании пользователя.

Определим в приложении авторизацию на основе свойств City и Company. Для этого изменим код файла Program.cs следующим образом:

using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;

var people = new List<Person> 
{ 
    new Person("tom@gmail.com", "12345", "London", "Microsoft"),
    new Person("bob@gmail.com", "55555", "Лондон", "Google"),
    new Person("sam@gmail.com", "11111", "Berlin", "Microsoft")
};

var builder = WebApplication.CreateBuilder();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/login";
        options.AccessDeniedPath = "/login";
    });
builder.Services.AddAuthorization(opts => {

    opts.AddPolicy("OnlyForLondon", policy => {
        policy.RequireClaim(ClaimTypes.Locality, "Лондон", "London");
    });
    opts.AddPolicy("OnlyForMicrosoft", policy => {
        policy.RequireClaim("company", "Microsoft");
    });
});

var app = builder.Build();

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

app.MapGet("/login", async (HttpContext context) =>
{
    context.Response.ContentType = "text/html; charset=utf-8";
    // html-форма для ввода логина/пароля
    string loginForm = @"<!DOCTYPE html>
    <html>
    <head>
        <meta charset='utf-8' />
        <title>METANIT.COM</title>
    </head>
    <body>
        <h2>Login Form</h2>
        <form method='post'>
            <p>
                <label>Email</label><br />
                <input name='email' />
            </p>
            <p>
                <label>Password</label><br />
                <input type='password' name='password' />
            </p>
            <input type='submit' value='Login' />
        </form>
    </body>
    </html>";
await context.Response.WriteAsync(loginForm);
});

app.MapPost("/login", async (string? returnUrl, HttpContext context) =>
{
    // получаем из формы email и пароль
    var form = context.Request.Form;
    // если email и/или пароль не установлены, посылаем статусный код ошибки 400
    if (!form.ContainsKey("email") || !form.ContainsKey("password"))
        return Results.BadRequest("Email и/или пароль не установлены");
    string email = form["email"];
    string password = form["password"];

    // находим пользователя 
    Person? person = people.FirstOrDefault(p => p.Email == email && p.Password == password);
    // если пользователь не найден, отправляем статусный код 401
    if (person is null) return Results.Unauthorized();
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, person.Email),
        new Claim(ClaimTypes.Locality, person.City),
        new Claim("company", person.Company)
    };
    var claimsIdentity = new ClaimsIdentity(claims, "Cookies");
    var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
    await context.SignInAsync(claimsPrincipal);
    return Results.Redirect(returnUrl ?? "/");
});
// доступ только для City = London
app.Map("/london", [Authorize(Policy = "OnlyForLondon")]() => "You are living in London");

// доступ только для Company = Microsoft
app.Map("/microsoft", [Authorize(Policy = "OnlyForMicrosoft")]() => "You are working in Microsoft");

app.Map("/", [Authorize](HttpContext context) =>
{
    var login = context.User.FindFirst(ClaimTypes.Name);
    var city = context.User.FindFirst(ClaimTypes.Locality);
    var company = context.User.FindFirst("company");
    return $"Name: {login?.Value}\nCity: {city?.Value}\nCompany: {company?.Value}";
});
app.MapGet("/logout", async (HttpContext context) =>
{
    await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return "Данные удалены";
});

app.Run();

record class Person(string Email, string Password, string City, string Company);

Здесь для тестирования механизма авторизации на основе Claims определена условная бд - список пользователей people:

var people = new List<Person> 
{ 
    new Person("tom@gmail.com", "12345", "London", "Microsoft"),
    new Person("bob@gmail.com", "55555", "Лондон", "Google"),
    new Person("sam@gmail.com", "11111", "Berlin", "Microsoft")
};

Для настройки авторизации в зависимости от данных пользователя в делегате в методе AddAuthorization устанавливаются две политики доступа - "OnlyForLondon" и "OnlyForMicrosoft":

builder.Services.AddAuthorization(opts => {

    opts.AddPolicy("OnlyForLondon", policy => {
        policy.RequireClaim(ClaimTypes.Locality, "Лондон", "London");
    });
    opts.AddPolicy("OnlyForMicrosoft", policy => {
        policy.RequireClaim("company", "Microsoft");
    });
});

Политика "OnlyForLondon" требует, чтобы claim с типом ClaimTypes.Locality имел значение "London" или "Лондон". Если значений много, то мы их можем перечислить через запятую. Вторая политика - "OnlyForMicrosoft" требует наличия Claim с типом "company" и значением "Microsoft".

Для входа пользователей в приложение определена конечная точка app.MapGet("/login"), которая обрабатывает GET-запросы по пути "/login" и отправляет пользователям форму для ввода логина и пароля.

После заполнения и отправки формы логина данные в POST-запросе получает конечная точка app.MapPost("/login"), которая получает логин и пароль и по них находит пользователя в списке people. Значения свойств найденного пользователя добавляются в список claims:

var claims = new List<Claim>
{
	new Claim(ClaimTypes.Name, person.Email),
    new Claim(ClaimTypes.Locality, person.City),
    new Claim("company", person.Company)
};
var claimsIdentity = new ClaimsIdentity(claims, "Cookies");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

Благодаря этому инфраструктура ASP.NET Core сможет получить значения claims с типами ClaimTypes.Locality и "company" (то есть соответственно город и компанию пользователей) и на их основе решить, предоставлять ли доступ пользователю к ресурсам приложения, которые используют политику доступа на основе этих Claim.

Для тестирования политик доступа определены две конечных точки:

// доступ только для City = London
app.Map("/london", [Authorize(Policy = "OnlyForLondon")]() => "You are living in London");

// доступ только для Company = Microsoft
app.Map("/microsoft", [Authorize(Policy = "OnlyForMicrosoft")]() => "You are working in Microsoft");

Здесь доступ по пути "/london" имеют только те пользователи, которые удовлетворяют политике "OnlyForLondon". А ресурс "/microsoft" доступен только для пользователей, соответствующих политике "OnlyForMicrosoft".

Запустим проект и залогинимся, используя данные одного из пользователей из списка people:

Авторизация на основе Claims в ASP.NET Core и C#

И если пользователь живет в Лондоне, то он имеет доступ по пути "/london". Аналогично если пользователь работает в Microsoft, он имеет доступ по пути "/microsoft":

Claims based authorization in ASP.NET Core и C#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850