在 Microsoft.Identity.Web 的網頁 API 中實作授權功能。

本文介紹如何使用 Microsoft.Identity.Web 實作 ASP.NET Core 中的網頁 API 授權。 你會驗證 範圍 (委派權限)和 應用程式權限 (應用程式權限),以控制對受保護資源的存取。 範例中使用 Microsoft Entra ID 作為身份提供者。

了解授權概念

本節涵蓋認證與授權的主要差異,並說明 Microsoft.Identity.Web 如何驗證存取權杖中的內容。

認證與授權

概念 Purpose Result
驗證 驗證身份 失敗時返回 401 未授權
授權 驗證權限 403 若不足夠則禁止

什麼會被驗證

當網頁 API 收到存取權杖時,Microsoft。Identity.Web 驗證:

  1. 令牌簽名 ——是來自可信機構嗎?
  2. Token 受眾 ——它是為這個 API 設計的嗎?
  3. 代幣到期 ——它還有效嗎?
  4. 範圍/角色 ——客戶端應用程式和主體(使用者)是否擁有正確的權限?

本指南聚焦於 #4 — 驗證範圍與應用程式權限

範圍(授權權限)

當使用者將權限委派給應用程式以代表其行動時,適用於此範圍(例如,代表已登入使用者呼叫的網頁 API)。

詳細資料 價值
象徵性主張 scpscope (用戶端應用程式); roles (使用者)
範例數值 "access_as_user""User.Read""Files.ReadWrite"

應用程式權限(應用程式權限)

當應用程式以自身身份呼叫 Web API 並且沒有使用者上下文時,應用程式許可權會生效,例如使用用戶端憑證的背景程序或背景服務。

詳細資料 價值
象徵性主張 roles
範例數值 "Mail.Read.All""User.Read.All"

使用 RequiredScope 驗證範圍

屬性 RequiredScope 檢查存取權杖是否至少包含其中一個指定的範圍。 當 API 只提供使用者委派請求時,請使用此屬性。

設定範圍驗證

請依照以下步驟在你的 API 中啟用範圍驗證。

1. 在您的 API 中啟用授權:

在您的應用程式管線中加入認證與授權服務:

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. 保護控制者或行動:

[Authorize][RequiredScope] 屬性套用到您的控制器或個別操作上:

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" });
    }
}

套用範圍樣式

選擇最適合你管理應用範圍的模式。

模式一:硬編碼瞄準鏡

當瞄準鏡固定且開發時已知時,請使用此模式。

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

要接受多個作用域中的任一個,請將它們列為參數:

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

模式二:從配置開始的示波器

當範圍應該可依環境設定時,請使用此模式。 請在你的設定檔中定義作用域:

appsettings.json:

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

參考你控制器裡的設定金鑰:

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

這種方法讓你可以在不重新編譯的情況下更改範圍。

模式三:動作層級範圍

當不同動作需要不同權限時,請使用此模式。 應用 [RequiredScope] 於個別行動方法:

[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();
    }
}

了解驗證流程

當請求到達時,中介軟體會依以下順序處理:

  1. ASP.NET Core 認證中介軟體會驗證該憑證
  2. RequiredScope 屬性檢查用於 scpscope 聲明
  3. 如果該標記至少包含一個匹配的範圍,請求就會繼續進行。
  4. 若未找到匹配的範圍,API 會回傳 403 禁止回應。

以下範例展示了典型的誤差響應:

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

用 RequiredScopeOrAppPermission 驗證應用程式權限

屬性 RequiredScopeOrAppPermission 用來驗證 範圍 (委派)或 應用程式權限 (應用程式)。 當你的 API 同時從同一端點服務使用者委派的應用程式和守護程序/服務應用程式時,請使用這個屬性。

如果你的 API 只處理使用者委派的請求,請改用 RequiredScope

設定範圍或應用程式權限驗證

套用屬性以接受任一標記類型:

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);
    }
}

從設定中設定應用程式權限

在設定中儲存範圍和應用程式權限,方便在不重新編譯的情況下更改它們。

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"
  }
}

參考你控制器裡的設定鍵:

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

比較代幣權利主張的差異

下表顯示用戶委派與僅應用程式代幣間申訴的差異:

記號類型 索賠 範例值
使用者委派 scpscope "access_as_user User.Read"
僅限應用程式 roles ["TodoList.ReadWrite.All"]

以下範例展示了一個由使用者委派的標記:

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

以下範例展示了一個僅供應用程式使用的權杖:

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

建立授權政策

對於複雜的授權情境,請使用 ASP.NET Core 授權政策。 政策讓你能集中管理規則、結合多項需求,並撰寫可測試的授權邏輯。

優點 Description
集中式邏輯 定義一次授權規則,然後在各處重複使用
可組合 結合多項需求(範圍 + 理賠 + 自訂邏輯)
可測試 更易於單元測試授權邏輯
Flexible 範圍驗證之外的自訂需求

模式一:用 RequireScope 定義政策

定義需要特定範圍的具名政策,接著在控制器中引用它們。

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();

將政策套用到控制器的操作:

[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);
    }
}

模式二:定義具有 ScopeAuthorizationRequirement 的政策

使用 ScopeAuthorizationRequirement 以更明確地要求範圍:

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" })
        );
    });
});

模式三:設定預設政策

設定一個自動套用於所有 [Authorize] 屬性的預設政策:

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

    options.DefaultPolicy = defaultPolicy;
});

每個[Authorize]屬性現在都需要作用域:access_as_user

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

模式四:結合多項需求

將範圍、角色與認證需求合併於單一政策中:

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

模式五:從設定建立政策

從設定載入範圍以確保政策專屬於特定環境。

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

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

依租戶篩選請求

限制 API 存取權杖,僅限於特定 Microsoft Entra 租戶。 當你的多租戶 API 只接受已核准的客戶租戶請求時,這很有用。

限制允許的租戶進入

請定義一份將租戶ID申索與允許清單進行比對的政策:

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");
});

從設定中設定租戶過濾

將允許的租戶 ID 儲存在設定中,以便在不更改程式碼的情況下進行管理。

appsettings.json:

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

請閱讀租戶名單並在啟動時建立政策:

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>()
        );
    });
});

將範圍與租戶過濾結合使用

建立一份同時要求有效範圍與核准租戶的政策:

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
        );
    });
});

遵循最佳做法

應用這些建議來建立安全且可維護的授權邏輯。

應該做的事

1. 務必與範圍驗證配對 [Authorize]

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

2. 使用適用於特定環境的範圍設定:

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

3. 適用最低特權:

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

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

4. 使用政策進行複雜授權:

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

5. 在開發過程中啟用詳細的錯誤回應:

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

禁忌事项

1. 使用[Authorize]時不要跳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. 不要在生產環境中硬編碼租戶 ID:

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

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

3. 不要將範圍與角色混淆:

//  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. 避免在生產錯誤訊息中暴露敏感範圍資訊:

為生產環境設定適當的日誌等級與錯誤處理。


排除授權問題

請依照以下指引診斷常見的授權問題。

403 禁忌 - 缺少瞄準鏡

錯誤: API 即使有有效憑證,也會回傳 403。

诊断:

  1. jwt.ms 解碼代幣。
  2. 請檢查scpscope理賠。
  3. 確認數值符合你的 RequiredScope 屬性。

Solution:

  • 確保客戶端應用程式在取得令牌時請求正確的授權範圍。
  • 確認 Scope 是否在 Microsoft Entra 的 API 應用程式註冊中公開。
  • 如有需要,請給予管理員同意。

RequiredScope 無法運作

症狀: 這個屬性似乎被忽略了。

檢查:

  1. 你有加屬性嗎 [Authorize]
  2. app.UseAuthorization() 之後有呼叫 app.UseAuthentication() 嗎?
  3. services.AddAuthorization() 是否已註冊?

未找到設定鍵

錯誤: 範圍驗證無聲地失敗。

檢查:

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

確保設定路徑完全一致。