Microsoft.Identity.Web を使用して Web API に認証を実装します。

この記事では、Microsoft.Identity.Webを使用して、ASP.NET Coreのweb APIで認可を実装します。 スコープ (委任されたアクセス許可) とアプリのアクセス許可 (アプリケーションのアクセス許可) を検証して、保護されたリソースへのアクセスを制御します。 この例では、ID プロバイダーとして Microsoft Entra ID を使用します。

承認の概念を理解する

このセクションでは、認証と承認の主な違いを説明し、Microsoft.Identity.Web がアクセストークンで何を検証するかについて述べます。

認証と承認

概念 Purpose 結果
認証 ID を確認する 401 失敗した場合は未承認
認可 アクセス許可の確認 403 不十分な場合は禁止

検証される内容

Web API がアクセス トークンを受信すると、Microsoft。Identity.Web によって次の検証が行われます。

  1. トークン署名 - 信頼された機関からの署名ですか?
  2. トークンの対象ユーザー - この API を対象としていますか?
  3. トークンの有効期限 - まだ有効ですか?
  4. スコープ/ロール - クライアント アプリとサブジェクト (ユーザー) に適切なアクセス許可がありますか?

このガイドでは 、#4 ( スコープとアプリのアクセス許可の検証) に焦点を当てています。

スコープ (委任アクセス許可)

スコープは、ユーザーが自分の代わりに動作するアクセス許可をアプリに委任する場合に適用されます (たとえば、サインインしているユーザーの代わりに呼び出される Web API)。

Detail 価値
トークン要求 scp または scope (クライアント アプリ)、 roles (ユーザー)
値の例 "access_as_user""User.Read""Files.ReadWrite"

アプリのアクセス許可 (アプリケーションのアクセス許可)

アプリのアクセス許可は、アプリがクライアント資格情報を使用してデーモンやバックグラウンド サービスなどのユーザー コンテキストなしで Web API をそれ自体として呼び出す場合に適用されます。

Detail 価値
トークン要求 roles
値の例 "Mail.Read.All""User.Read.All"

RequiredScope を使用してスコープを検証する

RequiredScope属性は、アクセス トークンに指定されたスコープの少なくとも 1 つが含まれていることを確認します。 この属性は、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" });
    }
}

スコープ パターンを適用する

アプリケーションでスコープを管理する方法に最も適したパターンを選択します。

パターン 1: ハードコーディングされたスコープ

このパターンは、開発時にスコープが固定され既知である場合に使用します。

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

パターン 2: 構成からのスコープ

環境ごとにスコープを構成する必要がある場合は、このパターンを使用します。 構成ファイルでスコープを定義します。

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
}

この方法では、再コンパイルせずにスコープを変更できます。

パターン 3: アクション レベルのスコープ

このパターンは、異なるアクションで異なるアクセス許可が必要な場合に使用します。 個々のアクション メソッドに [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 属性は、 scp または scope 要求をチェックします
  3. トークンに少なくとも 1 つの一致するスコープが含まれている場合、要求は続行されます。
  4. 一致するスコープが見つからない場合、API は 403 Forbidden 応答を返します。

次の例は、一般的なエラー応答を示しています。

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

トークン要求の違いを比較する

次の表は、ユーザー委任トークンとアプリ専用トークンの要求の違いを示しています。

トークンの種類 請求 値の例
ユーザーによる委任 scp または scope "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
一元化されたロジック 承認規則を 1 回定義し、どこでも再利用する
コンポーザブル 複数の要件を組み合わせる (スコープ + 要求 + カスタム ロジック)
テスト可能 簡単な単体テスト承認ロジック
フレキシブル スコープ検証以外のカスタム要件

パターン 1: 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);
    }
}

パターン 2: 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" })
        );
    });
});

パターン 3: 既定のポリシーを設定する

すべての [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
}

パターン 4: 複数の要件を組み合わせる

スコープ、ロール、および認証の要件を 1 つのポリシーで結合します。

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

パターン 5: 構成からポリシーを構築する

ポリシー環境固有の状態を維持するために、構成からスコープを読み込みます。

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

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

テナント別に要求をフィルター処理する

特定のMicrosoft Entra テナントからのトークンへの API アクセスを制限します。 これは、マルチテナント 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. scpまたはscope要求を確認します。
  3. 値が RequiredScope 属性と一致するかどうかを確認します。

Solution:

  • トークンを取得するときに、クライアント アプリが正しいスコープを要求していることを確認します。
  • Microsoft Entraの API アプリ登録でスコープが公開されていることを確認します。
  • 必要に応じて、管理者の同意を付与します。

RequiredScope が機能しない

症状: 属性は無視されるように見えます。

チェック:

  1. [Authorize]属性を追加しましたか?
  2. app.UseAuthorization()app.UseAuthentication()後に呼び出されますか?
  3. services.AddAuthorization()登録されていますか?

構成キーが見つかりません

エラー: スコープの検証はサイレントモードで失敗します。

チェック:

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

構成パスが正確に一致していることを確認します。