Terapkan otorisasi di API web dengan Microsoft. Identity.Web

Dalam artikel ini, Anda menerapkan otorisasi di API web ASP.NET Core menggunakan Microsoft. Identity.Web. Anda akan memvalidasi cakupan (izin yang didelegasikan) dan izin aplikasi (izin aplikasi) untuk mengontrol akses ke sumber daya yang dilindungi. Contohnya menggunakan Microsoft Entra ID sebagai penyedia identitas.

Memahami konsep otorisasi

Bagian ini mencakup perbedaan utama antara autentikasi dan otorisasi, dan menjelaskan apa yang Microsoft. Identity.Web memvalidasi dalam token akses.

Autentikasi vs otorisasi

Konsep Kegunaan Result
Authentication Verifikasi identitas 401 Tidak Sah jika gagal
Otorisasi Memverifikasi izin 403 Dilarang jika tidak memadai

Apa yang divalidasi

Saat API web menerima token akses, Microsoft. Identity.Web memvalidasi:

  1. Tanda tangan token - Apakah dari otoritas tepercaya?
  2. Audiens token - Apakah ditujukan untuk API ini?
  3. Kedaluwarsa token - Apakah masih valid?
  4. Cakupan/Peran - Apakah aplikasi klien dan subjek (pengguna) memiliki izin yang tepat?

Panduan ini berfokus pada #4 - memvalidasi cakupan dan izin aplikasi.

Cakupan (izin yang didelegasikan)

Cakupan berlaku saat pengguna mendelegasikan izin ke aplikasi untuk bertindak atas nama mereka (misalnya, API web yang disebut atas nama pengguna yang masuk).

Detail Nilai
Klaim token scp atau scope (aplikasi klien); roles (pengguna)
Contoh nilai "access_as_user", "User.Read", "Files.ReadWrite"

Izin aplikasi (izin aplikasi)

Izin aplikasi berlaku saat aplikasi memanggil API web dengan sendirinya tanpa konteks pengguna, seperti daemon atau layanan latar belakang menggunakan kredensial klien.

Detail Nilai
Klaim token roles
Contoh nilai "Mail.Read.All", "User.Read.All"

Memvalidasi cakupan dengan RequiredScope

Atribut RequiredScope memeriksa bahwa token akses berisi setidaknya salah satu cakupan yang ditentukan. Gunakan atribut ini saat API Anda hanya melayani permintaan yang didelegasikan pengguna.

Menyiapkan validasi cakupan

Ikuti langkah-langkah ini untuk mengaktifkan validasi cakupan di API Anda.

1. Aktifkan otorisasi di API Anda:

Tambahkan layanan autentikasi dan otorisasi ke alur aplikasi Anda:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(); // Required for authorization

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization(); // Must be after UseAuthentication
app.MapControllers();

app.Run();

2. Lindungi pengontrol atau tindakan:

Terapkan [Authorize] atribut dan [RequiredScope] ke pengontrol atau tindakan individual Anda:

using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Web.Resource;

[Authorize]
[RequiredScope("access_as_user")]
public class TodoListController : ControllerBase
{
    [HttpGet]
    public IActionResult GetTodos()
    {
        // Only accessible if token has "access_as_user" scope
        return Ok(new[] { "Todo 1", "Todo 2" });
    }
}

Menerapkan pola ruang lingkup

Pilih pola yang paling sesuai dengan cara Anda mengelola cakupan dalam aplikasi Anda.

Pola 1: Cakupan yang dikodekan secara permanen

Gunakan pola ini saat cakupan tetap dan diketahui pada waktu pengembangan.

[Authorize]
[RequiredScope("access_as_user")]
public class TodoListController : ControllerBase
{
    // All actions require "access_as_user" scope
}

Untuk menerima salah satu dari beberapa cakupan, cantumkan sebagai parameter:

[Authorize]
[RequiredScope("read", "write", "admin")]
public class TodoListController : ControllerBase
{
    // Token must have "read" OR "write" OR "admin"
}

Pola 2: Lingkup dari konfigurasi

Gunakan pola ini ketika cakupan harus dapat dikonfigurasi per lingkungan. Tentukan cakupan dalam file konfigurasi Anda:

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-api-client-id",
    "Scopes": "access_as_user read write"
  }
}

Referensikan kunci konfigurasi di pengontrol Anda:

[Authorize]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class TodoListController : ControllerBase
{
    // Scopes read from configuration
}

Pendekatan ini memungkinkan Anda mengubah cakupan tanpa kompilasi ulang.

Pola 3: Cakupan tingkat tindakan

Gunakan pola ini ketika tindakan yang berbeda memerlukan izin yang berbeda. Terapkan [RequiredScope] ke metode tindakan individual:

[Authorize]
public class TodoListController : ControllerBase
{
    [HttpGet]
    [RequiredScope("read")]
    public IActionResult GetTodos()
    {
        return Ok(todos);
    }

    [HttpPost]
    [RequiredScope("write")]
    public IActionResult CreateTodo([FromBody] Todo todo)
    {
        // Only tokens with "write" scope can create
        return CreatedAtAction(nameof(GetTodos), todo);
    }

    [HttpDelete("{id}")]
    [RequiredScope("admin")]
    public IActionResult DeleteTodo(int id)
    {
        // Only tokens with "admin" scope can delete
        return NoContent();
    }
}

Memahami alur validasi

Ketika permintaan tiba, middleware memprosesnya dalam urutan berikut:

  1. ASP.NET Core middleware autentikasi memvalidasi token
  2. RequiredScope pemeriksaan atribut untuk klaim scp atau scope
  3. Jika token berisi setidaknya satu cakupan yang cocok, permintaan akan dilanjutkan.
  4. Jika tidak ada cakupan yang cocok yang ditemukan, API mengembalikan respons Terlarang 403.

Contoh berikut menunjukkan respons kesalahan umum:

{
  "error": "insufficient_scope",
  "error_description": "The token does not have the required scope 'access_as_user'."
}

Memvalidasi izin aplikasi dengan RequiredScopeOrAppPermission

Atribut RequiredScopeOrAppPermission memvalidasi cakupan (didelegasikan) atau izin aplikasi (aplikasi). Gunakan atribut ini saat API Anda melayani aplikasi yang didelegasikan pengguna dan aplikasi daemon/layanan dari titik akhir yang sama.

Jika API Anda hanya melayani permintaan yang didelegasikan pengguna, gunakan RequiredScope sebagai gantinya.

Menyiapkan cakupan atau validasi izin aplikasi

Terapkan atribut untuk menerima salah satu jenis token:

using Microsoft.Identity.Web.Resource;

[Authorize]
[RequiredScopeOrAppPermission(
    AcceptedScope = new[] { "access_as_user" },
    AcceptedAppPermission = new[] { "TodoList.ReadWrite.All" }
)]
public class TodoListController : ControllerBase
{
    [HttpGet]
    public IActionResult GetTodos()
    {
        // Accessible with EITHER:
        // - User-delegated token with "access_as_user" scope, OR
        // - App-only token with "TodoList.ReadWrite.All" app permission
        return Ok(todos);
    }
}

Mengonfigurasi izin aplikasi dari pengaturan

Simpan cakupan dan izin aplikasi dalam konfigurasi untuk mengubahnya tanpa kompilasi ulang.

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-api-client-id",
    "Scopes": "access_as_user",
    "AppPermissions": "TodoList.ReadWrite.All TodoList.Admin"
  }
}

Referensikan kunci konfigurasi di pengontrol Anda:

[Authorize]
[RequiredScopeOrAppPermission(
    RequiredScopesConfigurationKey = "AzureAd:Scopes",
    RequiredAppPermissionsConfigurationKey = "AzureAd:AppPermissions"
)]
public class TodoListController : ControllerBase
{
    // Scopes and app permissions from configuration
}

Membandingkan klaim token yang berbeda

Tabel berikut menunjukkan perbedaan klaim antara token yang didelegasikan pengguna dan hanya aplikasi:

Jenis Token Tuntutan Contoh Nilai
Didelegasikan pengguna scp atau scope "access_as_user User.Read"
Hanya aplikasi roles ["TodoList.ReadWrite.All"]

Contoh berikut menunjukkan token yang didelegasikan pengguna:

{
  "aud": "api://your-api-client-id",
  "iss": "https://login.microsoftonline.com/.../v2.0",
  "scp": "access_as_user",
  "sub": "user-object-id",
  ...
}

Contoh berikut menunjukkan token khusus aplikasi:

{
  "aud": "api://your-api-client-id",
  "iss": "https://login.microsoftonline.com/.../v2.0",
  "roles": ["TodoList.ReadWrite.All"],
  "sub": "app-object-id",
  ...
}

Membuat kebijakan otorisasi

Untuk skenario otorisasi yang kompleks, gunakan kebijakan otorisasi ASP.NET Core. Kebijakan memungkinkan Anda mempusatkan aturan, menggabungkan beberapa persyaratan, dan menulis logika otorisasi yang dapat diuji.

Keuntungan Deskripsi
Logika terpusat Tentukan aturan otorisasi sekali, gunakan kembali di mana saja
Komposabel Menggabungkan beberapa persyaratan (ruang lingkup + klaim + logika khusus)
Dapat diuji Lebih mudah untuk menyatukan logika otorisasi pengujian
Flexibel Persyaratan kustom di luar validasi ruang lingkup

Pola 1: Tentukan kebijakan dengan RequireScope

Tentukan kebijakan bernama yang memerlukan cakupan tertentu, lalu referensikan pada pengontrol Anda:

using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("TodoReadPolicy", policyBuilder =>
    {
        policyBuilder.RequireScope("read", "access_as_user");
    });

    options.AddPolicy("TodoWritePolicy", policyBuilder =>
    {
        policyBuilder.RequireScope("write", "admin");
    });
});

var app = builder.Build();

Terapkan kebijakan ke tindakan pengontrol:

[Authorize]
public class TodoListController : ControllerBase
{
    [HttpGet]
    [Authorize(Policy = "TodoReadPolicy")]
    public IActionResult GetTodos()
    {
        return Ok(todos);
    }

    [HttpPost]
    [Authorize(Policy = "TodoWritePolicy")]
    public IActionResult CreateTodo([FromBody] Todo todo)
    {
        return CreatedAtAction(nameof(GetTodos), todo);
    }
}

Pola 2: Tentukan kebijakan dengan ScopeAuthorizationRequirement

Gunakan ScopeAuthorizationRequirement untuk persyaratan cakupan yang lebih eksplisit:

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CustomPolicy", policyBuilder =>
    {
        policyBuilder.AddRequirements(
            new ScopeAuthorizationRequirement(new[] { "access_as_user" })
        );
    });
});

Pola 3: Mengatur kebijakan default

Tetapkan kebijakan default yang berlaku untuk semua [Authorize] atribut secara otomatis:

builder.Services.AddAuthorization(options =>
{
    var defaultPolicy = new AuthorizationPolicyBuilder()
        .RequireScope("access_as_user")
        .Build();

    options.DefaultPolicy = defaultPolicy;
});

Setiap [Authorize] atribut sekarang memerlukan access_as_user cakupan:

[Authorize] // Automatically requires "access_as_user" scope
public class TodoListController : ControllerBase
{
    // All actions protected by default policy
}

Pola 4: Menggabungkan beberapa persyaratan

Gabungkan persyaratan cakupan, peran, dan autentikasi dalam satu kebijakan:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policyBuilder =>
    {
        policyBuilder.RequireScope("admin");
        policyBuilder.RequireRole("Admin"); // Also check role claim
        policyBuilder.RequireAuthenticatedUser();
    });
});

Pola 5: Membangun kebijakan dari konfigurasi

Memuat cakupan dari konfigurasi untuk memastikan kebijakan tetap khusus untuk lingkungan tertentu.

var requiredScopes = builder.Configuration["AzureAd:Scopes"]?.Split(' ');

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiAccessPolicy", policyBuilder =>
    {
        if (requiredScopes != null)
        {
            policyBuilder.RequireScope(requiredScopes);
        }
    });
});

Memfilter permintaan menurut penyewa

Batasi akses API ke token dari penyewa Microsoft Entra tertentu. Ini berfungsi ketika API multi-penyewa Anda hanya boleh menerima permintaan dari penyewa pelanggan yang telah disetujui.

Membatasi akses ke penyewa yang diizinkan

Tentukan kebijakan yang memeriksa klaim ID penyewa terhadap daftar yang diizinkan:

builder.Services.AddAuthorization(options =>
{
    string[] allowedTenants =
    {
        "14c2f153-90a7-4689-9db7-9543bf084dad", // Contoso tenant
        "af8cc1a0-d2aa-4ca7-b829-00d361edb652", // Fabrikam tenant
        "979f4440-75dc-4664-b2e1-2cafa0ac67d1"  // Northwind tenant
    };

    options.AddPolicy("AllowedTenantsOnly", policyBuilder =>
    {
        policyBuilder.RequireClaim(
            "http://schemas.microsoft.com/identity/claims/tenantid",
            allowedTenants
        );
    });

    // Apply to all endpoints by default
    options.DefaultPolicy = options.GetPolicy("AllowedTenantsOnly");
});

Mengatur pemfilteran penyewa melalui pengaturan

Simpan ID tenant yang diizinkan dalam konfigurasi untuk mengelolanya tanpa perubahan kode.

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "your-api-client-id",
    "AllowedTenants": [
      "14c2f153-90a7-4689-9db7-9543bf084dad",
      "af8cc1a0-d2aa-4ca7-b829-00d361edb652"
    ]
  }
}

Baca daftar penyewa dan buat kebijakan saat startup:

var allowedTenants = builder.Configuration.GetSection("AzureAd:AllowedTenants")
    .Get<string[]>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AllowedTenantsOnly", policyBuilder =>
    {
        policyBuilder.RequireClaim(
            "http://schemas.microsoft.com/identity/claims/tenantid",
            allowedTenants ?? Array.Empty<string>()
        );
    });
});

Menggabungkan cakupan dengan pemfilteran penyewa

Buat kebijakan yang memerlukan cakupan yang valid dan penyewa yang disetujui:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("SecureApiAccess", policyBuilder =>
    {
        // Require specific scope
        policyBuilder.RequireScope("access_as_user");

        // AND require specific tenant
        policyBuilder.RequireClaim(
            "http://schemas.microsoft.com/identity/claims/tenantid",
            allowedTenants
        );
    });
});

Ikuti praktik terbaik

Terapkan rekomendasi ini untuk membangun logika otorisasi yang aman dan dapat dipertahankan.

Yang Harus Dilakukan

1. Selalu pasangkan [Authorize] dengan validasi cakupan:

[Authorize] // Authentication
[RequiredScope("access_as_user")] // Authorization
public class MyController : ControllerBase { }

2. Gunakan konfigurasi untuk cakupan khusus lingkungan:

[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]

3. Terapkan hak istimewa paling sedikit:

[HttpGet]
[RequiredScope("read")] // Only read permission needed

[HttpPost]
[RequiredScope("write")] // Write permission for modifications

4. Gunakan kebijakan untuk otorisasi kompleks:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
    {
        policy.RequireScope("admin");
        policy.RequireClaim("department", "IT");
    });
});

5. Aktifkan respons kesalahan terperinci dalam pengembangan:

if (builder.Environment.IsDevelopment())
{
    Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
}

Hal-hal yang Tidak Boleh Dilakukan

1. Jangan lewati [Authorize] saat menggunakan RequiredScope:

//  Wrong - RequiredScope won't work without [Authorize]
[RequiredScope("access_as_user")]
public class MyController : ControllerBase { }

//  Correct
[Authorize]
[RequiredScope("access_as_user")]
public class MyController : ControllerBase { }

2. Jangan hardcode ID penyewa di lingkungan produksi:

//  Wrong
policyBuilder.RequireClaim("tid", "14c2f153-90a7-4689-9db7-9543bf084dad");

//  Better - use configuration
var tenants = Configuration.GetSection("AllowedTenants").Get<string[]>();
policyBuilder.RequireClaim("tid", tenants);

3. Jangan membingungkan cakupan dengan peran:

//  Wrong - This checks roles claim, not scopes
[RequiredScope("Admin")] // "Admin" is typically a role, not a scope

//  Correct
[RequiredScope("access_as_user")] // Scope
[Authorize(Roles = "Admin")] // Role

4. Jangan mengekspos informasi cakupan sensitif dalam pesan kesalahan produksi:

Konfigurasikan tingkat pengelogan yang sesuai dan penanganan kesalahan untuk lingkungan produksi.


Memecahkan masalah otorisasi

Gunakan panduan berikut untuk mendiagnosis masalah otorisasi umum.

403 Terlarang - cakupan hilang

Kesalahan: API mengembalikan 403 bahkan dengan token yang valid.

Diagnosis:

  1. Dekode token pada jwt.ms.
  2. Periksa klaim scp atau scope.
  3. Verifikasi bahwa nilai cocok dengan atribut Anda RequiredScope .

Solution:

  • Pastikan aplikasi klien meminta cakupan yang benar saat memperoleh token.
  • Verifikasi ruang lingkup yang diekspos dalam pendaftaran aplikasi API di Microsoft Entra.
  • Berikan persetujuan admin jika diperlukan.

RequiredScope tidak berfungsi

Gejala: Atribut tampaknya diabaikan.

Check:

  1. Apakah Anda menambahkan [Authorize] atribut?
  2. Apakah app.UseAuthorization() dipanggil setelah app.UseAuthentication()?
  3. Apakah services.AddAuthorization() terdaftar?

Kunci konfigurasi tidak ditemukan

Kesalahan: Validasi cakupan gagal secara diam-diam.

Check:

{
  "AzureAd": {
    "Scopes": "access_as_user" // Matches RequiredScopesConfigurationKey
  }
}

Pastikan jalur konfigurasi cocok dengan tepat.