Share via


チュートリアル: 外部テナントに登録されている ASP.NET Core Web API をセキュリティで保護する

このチュートリアル シリーズでは、外部テナントに登録されている Web API をセキュリティで保護する方法を見ていきます。 このチュートリアルでは、委任されたアクセス許可 (スコープ) とアプリケーションのアクセス許可 (アプリ ロール) の両方を発行する ASP.NET Core Web API を構築します。

このチュートリアルでは、

  • アプリの登録の詳細を使用するように Web API を構成する
  • アプリの登録に登録されている委任されたアクセス許可とアプリケーションのアクセス許可を使用するように Web API を構成する
  • Web API のエンドポイントを保護する

前提条件

ASP.NET Core Web API を作成する

  1. ターミナルを開いてから、プロジェクトを配置したいフォルダーに移動します。

  2. 次のコマンドを実行します。

    dotnet new webapi -o ToDoListAPI
    cd ToDoListAPI
    
  3. ダイアログ ボックスで、プロジェクトに必要な資産を追加するかどうかを確認されたら、 [はい] を選択します。

パッケージをインストールする

次のパッケージをインストールします。

  • Microsoft.EntityFrameworkCore.InMemory により、メモリ内のデータベースで Entity Framework Core を使用すことが許可されます。 運用環境で使用するようには設計されていません。
  • Microsoft.Identity.Web により、Microsoft ID プラットフォームと統合される Web アプリおよび Web API への認証および認可サポートの追加が簡略化されます。
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Identity.Web

アプリ登録の詳細を構成する

アプリ フォルダーで appsettings.json ファイルを開き、Web API の登録後に記録したアプリ登録の詳細に追加します。

{
    "AzureAd": {
        "Instance": "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/",
        "TenantId": "Enter_the_Tenant_Id_Here",
        "ClientId": "Enter_the_Application_Id_Here",
    },
    "Logging": {...},
  "AllowedHosts": "*"
}

次のプレースホルダーを置き換えます。

  • Enter_the_Application_Id_Here をアプリケーション (クライアント) ID に置き換えます。
  • Enter_the_Tenant_Id_Here をディレクトリ (テナント) ID に置き換えます。
  • Enter_the_Tenant_Subdomain_Here をディレクトリ (テナント) サブドメインに置き換えます。

アプリのロールとスコープを追加する

クライアント アプリがユーザーのアクセス トークンを正常に取得するために、すべての API は少なくとも 1 つのスコープ (委任されたアクセス許可とも呼ばれます) を発行する必要があります。 また、API は、クライアント アプリがそれ自体としてアクセス トークンを取得するため (つまりユーザーをサインインしていない場合に)、少なくとも 1 つのアプリケーションのアプリ ロール (アプリケーション アクセス許可とも呼ばれます) を発行する必要があります。

これらのアクセス許可は、appsettings.json ファイルで指定します。 このチュートリアルでは、4 つのアクセス許可を登録しました。 委任されたアクセス許可として ToDoList.ReadWriteToDoList.Read を、アプリケーションのアクセス許可として ToDoList.ReadWrite.AllToDoList.Read.All を指定します。

{
  "AzureAd": {
    "Instance": "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/",
    "TenantId": "Enter_the_Tenant_Id_Here",
    "ClientId": "Enter_the_Application_Id_Here",
    "Scopes": {
      "Read": ["ToDoList.Read", "ToDoList.ReadWrite"],
      "Write": ["ToDoList.ReadWrite"]
    },
    "AppPermissions": {
      "Read": ["ToDoList.Read.All", "ToDoList.ReadWrite.All"],
      "Write": ["ToDoList.ReadWrite.All"]
    }
  },
  "Logging": {...},
  "AllowedHosts": "*"
}

認証スキームを追加する

認証スキームは、認証中に認証サービスを構成するときに名前が付けられます。 この記事では、JWT ベアラー認証スキームを使用します。 Programs.cs ファイルに次のコードを追加して、認証スキームを追加します。

// Add the following to your imports
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

// Add authentication scheme
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration);

モデルを作成する

プロジェクトのルート フォルダーに Models という名前のフォルダーを作成します。 そのフォルダーに移動し、ToDo.cs という名前のファイルを作成してから、次のコードを追加します。 このコードでは、ToDo という名前のモデルを作成します。

using System;

namespace ToDoListAPI.Models;

public class ToDo
{
    public int Id { get; set; }
    public Guid Owner { get; set; }
    public string Description { get; set; } = string.Empty;
}

データベース コンテキストの追加

データベース コンテキストは、データ モデルに対して Entity Framework 機能を調整するメイン クラスです。 このクラスは、Microsoft.EntityFrameworkCore.DbContext クラスから派生することによって作成されます。 このチュートリアルでは、テスト目的でメモリ内データベースを使用します。

  1. プロジェクトのルート フォルダーに、DbContext という名前のフォルダーを作成します。

  2. そのフォルダーに移動し、ToDoContext.cs という名前のファイルを作成してから、そのファイルに次の内容を追加します。

    using Microsoft.EntityFrameworkCore;
    using ToDoListAPI.Models;
    
    namespace ToDoListAPI.Context;
    
    public class ToDoContext : DbContext
    {
        public ToDoContext(DbContextOptions<ToDoContext> options) : base(options)
        {
        }
    
        public DbSet<ToDo> ToDos { get; set; }
    }
    
  3. アプリのルート フォルダーにある Program.cs ファイルを開いてから、ファイルに次のコードを追加します。 このコードは、ToDoContext と呼ばれる DbContext サブクラスをスコープ サービスとして ASP.NET Core アプリケーション サービス プロバイダー (依存関係注入コンテナーとも呼ばれます) に登録します。 コンテキストは、メモリ内データベースを使用するように構成されています。

    // Add the following to your imports
    using ToDoListAPI.Context;
    using Microsoft.EntityFrameworkCore;
    
    builder.Services.AddDbContext<ToDoContext>(opt =>
        opt.UseInMemoryDatabase("ToDos"));
    

コントローラーを追加する

ほとんどの場合、コントローラーには複数のアクションがあります。 通常は、"作成"、"読み取り"、"更新"、"削除" (CRUD) アクションです。 このチュートリアルでは、2 つのアクション 項目のみを作成します。 エンドポイントを保護する方法を示す、"すべてを読み取り" アクション項目と "作成" アクション項目。

  1. プロジェクトのルート フォルダーにある [コントローラー] フォルダーに移動します。

  2. このフォルダー内に ToDoListController.cs という名前のファイルを作成します。 ファイルを開いてから、次のボイラー プレート コードを追加します。

    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Identity.Web;
    using Microsoft.Identity.Web.Resource;
    using ToDoListAPI.Models;
    using ToDoListAPI.Context;
    
    namespace ToDoListAPI.Controllers;
    
    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class ToDoListController : ControllerBase
    {
        private readonly ToDoContext _toDoContext;
    
        public ToDoListController(ToDoContext toDoContext)
        {
            _toDoContext = toDoContext;
        }
    
        [HttpGet()]
        [RequiredScopeOrAppPermission()]
        public async Task<IActionResult> GetAsync(){...}
    
        [HttpPost]
        [RequiredScopeOrAppPermission()]
        public async Task<IActionResult> PostAsync([FromBody] ToDo toDo){...}
    
        private bool RequestCanAccessToDo(Guid userId){...}
    
        private Guid GetUserId(){...}
    
        private bool IsAppMakingRequest(){...}
    }
    

コントローラーにコードを追加する

このセクションでは、作成したプレースホルダーにコードを追加します。 ここでの焦点は、API のビルドではなく保護です。

  1. 必要なパッケージをインポートします。 Microsoft.Identity.Web パッケージは、たとえばトークンの検証を処理することで、認証ロジックを簡単に処理できるようにする MSAL ラッパーです。 エンドポイントに確実に認可が必要になるように、組み込みの Microsoft.AspNetCore.Authorization パッケージを使用します。

  2. この API を呼び出すアクセス許可は、ユーザーの代わりに委任されたアクセス許可か、ユーザーの代わりではなくクライアントからそれ自体として呼び出すアプリケーションのアクセス許可を使用して付与したので、呼び出しがアプリによってアプリのために行われているかどうかを把握することが重要です。 これを行う最も簡単な方法は、アクセス トークンに idtyp オプション要求が含まれているかどうかを確認する要求です。 この idtyp 要求が、API がトークンがアプリ トークンかアプリ + ユーザー トークンかを判断するための最も簡単な方法です。 idtyp オプション要求を有効にすることをお勧めします。

    idtyp 要求が有効になっていない場合は、roles および scp 要求を使用して、アクセス トークンがアプリ トークンかアプリ + ユーザー トークンかを判断できます。 Microsoft Entra External ID により発行されたトークンは、2 つのクレームのうち少なくとも 1 つを持ちます。 ユーザーに発行されたアクセス トークンは、scp 要求を含みます。 アプリケーションに発行されたアクセス トークンは、roles 要求を含みます。 両方の要求を含むアクセス トークンはユーザーに対してのみ発行され、scp 要求は委任されたアクセス許可を指定し、roles 要求はユーザーのロールを指定します。 どちらも持たないアクセス トークンは受け入れられません。

    private bool IsAppMakingRequest()
    {
        if (HttpContext.User.Claims.Any(c => c.Type == "idtyp"))
        {
            return HttpContext.User.Claims.Any(c => c.Type == "idtyp" && c.Value == "app");
        }
        else
        {
            return HttpContext.User.Claims.Any(c => c.Type == "roles") && !HttpContext.User.Claims.Any(c => c.Type == "scp");
        }
    }
    
  3. 行われている要求に、目的のアクションを実行するのに十分なアクセス許可が含まれているかどうかを決定するヘルパー関数を追加します。 アプリが自身のための要求を行っているのか、またはアプリが特定のリソースを所有するユーザーに代わって呼び出しを行っているのかをユーザー ID を検証することで確認します。

    private bool RequestCanAccessToDo(Guid userId)
        {
            return IsAppMakingRequest() || (userId == GetUserId());
        }
    
    private Guid GetUserId()
        {
            Guid userId;
            if (!Guid.TryParse(HttpContext.User.GetObjectId(), out userId))
            {
                throw new Exception("User ID is not valid.");
            }
            return userId;
        }
    
  4. アクセス許可の定義を組み込んでルートを保護します。 [Authorize] 属性をコントローラー クラスに追加することで、API を保護します。 これによって、認可されている ID で API が呼び出された場合にのみコントローラー アクションを呼び出すことができるようになります。 アクセス許可の定義では、これらのアクションを実行するために必要なアクセス許可の種類を定義します。

    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class ToDoListController: ControllerBase{...}
    

    すべてのエンドポイントの GET とエンドポイントの POST にアクセス許可を追加します。 Microsoft.Identity.Web.Resource 名前空間の一部である RequiredScopeOrAppPermission メソッドを使用してこれを行います。 次に、RequiredScopesConfigurationKey 属性および RequiredAppPermissionsConfigurationKey 属性を使用して、このメソッドにスコープとアクセス許可を渡します。

    [HttpGet]
    [RequiredScopeOrAppPermission(
        RequiredScopesConfigurationKey = "AzureAD:Scopes:Read",
        RequiredAppPermissionsConfigurationKey = "AzureAD:AppPermissions:Read"
    )]
    public async Task<IActionResult> GetAsync()
    {
        var toDos = await _toDoContext.ToDos!
            .Where(td => RequestCanAccessToDo(td.Owner))
            .ToListAsync();
    
        return Ok(toDos);
    }
    
    [HttpPost]
    [RequiredScopeOrAppPermission(
        RequiredScopesConfigurationKey = "AzureAD:Scopes:Write",
        RequiredAppPermissionsConfigurationKey = "AzureAD:AppPermissions:Write"
    )]
    public async Task<IActionResult> PostAsync([FromBody] ToDo toDo)
    {
        // Only let applications with global to-do access set the user ID or to-do's
        var ownerIdOfTodo = IsAppMakingRequest() ? toDo.Owner : GetUserId();
    
        var newToDo = new ToDo()
        {
            Owner = ownerIdOfTodo,
            Description = toDo.Description
        };
    
        await _toDoContext.ToDos!.AddAsync(newToDo);
        await _toDoContext.SaveChangesAsync();
    
        return Created($"/todo/{newToDo!.Id}", newToDo);
    }
    

API を実行する

dotnet run コマンドを使用して、API を実行して、エラーなしで正しく実行されていることを確認します。 テスト中でも HTTPS プロトコルを使用する場合は、.NET の開発証明書を信頼する必要があります。

この API コードの完全な例については、サンプル ファイルを参照してください。

次のステップ