Chapter 03

Authorization

Phân quyền truy cập trong ASP.NET Core

Mô hình Authorization

ASP.NET Core authorization được xây dựng từ 4 thành phần chính, xếp theo thứ bậc. Mỗi tầng phụ thuộc vào tầng bên dưới để ra quyết định cuối cùng.

[Authorize] Attribute
Authorization Policy
Requirement 1
Handler A → Succeed() ✓
Handler B → (no decision)
Requirement 2
Handler C → Succeed() ✓
Kết quả: Tất cả requirements thỏa mãn → 200 OK ✓

Khi endpoint có [Authorize(Policy = "X")], hệ thống tìm policy X, lấy danh sách requirements, chạy tất cả handlers cho từng requirement. Nếu MỌI requirement đều có ít nhất 1 handler gọi Succeed() không handler nào gọi Fail(), authorization thành công.

Role-Based Authorization

Cách đơn giản nhất — kiểm tra user có thuộc role nhất định không.

Admin

Toàn quyền quản lý hệ thống, user và cấu hình

Editor

Tạo, chỉnh sửa và xuất bản nội dung

Viewer

Chỉ xem nội dung, không có quyền chỉnh sửa

C#
// Chỉ Admin mới truy cập được
[Authorize(Roles = "Admin")]
public IActionResult ManageUsers() => View();

// Admin HOẶC Editor (dấu phẩy = OR)
[Authorize(Roles = "Admin,Editor")]
public IActionResult EditPost() => View();

// Phải vừa là Admin VÀ Editor (nhiều attribute = AND)
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Editor")]
public IActionResult SpecialAction() => View();

Decision Flow

Request + User
incoming
Kiểm tra Role claim
evaluate
Có role?
check
YES → 200 OK ✓
allowed
Không có role
no match
403 Forbidden ✗
denied
Khi dùng nhiều [Authorize(Roles)] trên cùng một action, chúng hoạt động theo logic AND — user phải có TẤT CẢ các roles. Còn roles cách nhau bởi dấu phẩy trong CÙNG attribute hoạt động theo logic OR.

Claims-Based Authorization

Linh hoạt hơn role-based. Thay vì hỏi "user có role X không?", ta hỏi "user có claim Y với value Z không?".

Interactive Claims Tree

ClaimsPrincipal
ClaimsIdentity: "Cookies"
Claim: Name = "Nguyen Van A"
Claim: Role = "Admin"
Claim: Email = "a@bug.dev"
Claim: Department = "Engineering"
ClaimsIdentity: "Bearer"
Claim: sub = "12345"
Claim: scope = "api.read api.write"
C#
// Kiểm tra claim trực tiếp trong controller
public IActionResult Profile()
{
    var email = User.FindFirstValue(ClaimTypes.Email);
    var department = User.FindFirstValue("Department");
    var roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value);

    if (User.HasClaim(c => c.Type == "Department" && c.Value == "Engineering"))
    {
        // User thuộc phòng Engineering
    }

    return View();
}

Policy-Based Authorization

Cách mạnh mẽ và linh hoạt nhất. Policy là tập hợp các điều kiện (requirements) mà user phải thỏa mãn.

Đăng ký Policies

C#
// Program.cs — Đăng ký policies
builder.Services.AddAuthorization(options =>
{
    // Policy đơn giản: yêu cầu claim
    options.AddPolicy("EmailVerified", policy =>
        policy.RequireClaim("email_verified", "true"));

    // Policy kết hợp nhiều điều kiện
    options.AddPolicy("CanEditPost", policy =>
    {
        policy.RequireAuthenticatedUser();          // Phải đăng nhập
        policy.RequireRole("Editor", "Admin");      // Phải có role
        policy.RequireClaim("email_verified", "true"); // Email đã xác thực
    });

    // Policy với custom requirement
    options.AddPolicy("MinimumAge", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));
});

Sử dụng Policies

C#
[Authorize(Policy = "CanEditPost")]
public IActionResult Edit(int id) => View();

// Hoặc trong Minimal API
app.MapDelete("/posts/{id}", DeletePost)
   .RequireAuthorization("CanEditPost");

Decision Flow

Request arrives with [Authorize(Policy = "CanEditPost")]
Tìm policy "CanEditPost"
Lấy danh sách requirements:
  • 1. RequireAuthenticatedUser Handler checks → Succeed ✓
  • 2. RequireRole("Editor") Handler checks → Succeed ✓
  • 3. RequireClaim("email_verified") Handler checks → Fail ✗
Có requirement thất bại → 403 Forbidden ✗

Custom Requirements & Handlers

Khi built-in policies không đủ, bạn tự tạo requirement và handler riêng.

Custom Requirement

C#
// Requirement: User phải đủ tuổi tối thiểu
public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }
    public MinimumAgeRequirement(int age) => MinimumAge = age;
}

Custom Handler

C#
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dobClaim = context.User.FindFirst(c => c.Type == "DateOfBirth");

        if (dobClaim == null)
            return Task.CompletedTask; // Không gọi Succeed cũng không Fail
                                        // → Handler khác có thể xử lý

        var dob = DateTime.Parse(dobClaim.Value);
        var age = DateTime.Today.Year - dob.Year;

        if (age >= requirement.MinimumAge)
        {
            context.Succeed(requirement); // ✓ Thỏa mãn!
        }

        return Task.CompletedTask;
        // Lưu ý: KHÔNG gọi Fail() ở đây
        // Nếu không Succeed, requirement vẫn chưa thỏa mãn
        // nhưng handler khác vẫn có cơ hội Succeed
    }
}

Đăng ký Handler

C#
// Program.cs — Đăng ký handler
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

Logic đánh giá Handler

Hiểu rõ cách hệ thống đánh giá kôt quả từ các handlers là chìa khóa để viết authorization logic chính xác.

Succeed() ✓
Bất kỳ handler nào gọi context.Succeed(requirement) → Requirement PASSED
Fail() ✗
Bất kỳ handler nào gọi context.Fail() → Toàn bộ authorization FAILED
No Decision
Không handler nào gọi Succeed() cũng không Fail() → Requirement NOT MET
Tất cả requirements PASSED
→ Authorization thành công → 200 OK
Có requirement FAILED hoặc NOT MET
403 Forbidden
Tránh gọi context.Fail() trừ khi bạn muốn CHẮC CHẮN từ chối, bất kể handler khác nói gì. Thường thì chỉ cần không gọi Succeed() là đủ — điều này cho phép các handler khác có cơ hội xử lý.

Resource-Based Authorization

Đôi khi bạn cần kiểm tra quyền dựa trên resource cụ thể (ví dụ: "user này có phải tác giả của bài viết này không?"). Không thể dùng attribute vì resource chỉ có sau khi load từ database.

Resource Handler

C#
public class PostAuthorizationHandler : AuthorizationHandler<EditRequirement, Post>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        EditRequirement requirement,
        Post post)
    {
        // Tác giả luôn được edit bài của mình
        if (post.AuthorId == context.User.FindFirstValue(ClaimTypes.NameIdentifier))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

Sử dụng trong Controller

C#
// Trong controller — inject IAuthorizationService
public async Task<IActionResult> Edit(int id)
{
    var post = await _db.Posts.FindAsync(id);
    var result = await _authService.AuthorizeAsync(User, post, new EditRequirement());

    if (!result.Succeeded) return Forbid(); // 403
    return View(post);
}
Resource-based authorization không thể dùng qua [Authorize] attribute vì resource chưa tồn tại tại thời điểm filter chạy. Thay vào đó, inject IAuthorizationService và gọi AuthorizeAsync() trực tiếp trong action method sau khi đã load resource từ database.
Nếu bạn cần chạy custom authorization logic tại MVC filter pipeline (với access đến ActionDescriptor, RouteData và các thông tin của action), xem Chapter 06: MVC Filters — bao gồm IAuthorizationFilter, TypeFilterAttribute và ServiceFilterAttribute.