チュートリアル: 外部テナントに登録されている ASP.NET Core Web API をセキュリティで保護する
このチュートリアル シリーズでは、外部テナントに登録されている Web API をセキュリティで保護する方法を見ていきます。 このチュートリアルでは、委任されたアクセス許可 (スコープ) とアプリケーションのアクセス許可 (アプリ ロール) の両方を発行する ASP.NET Core Web API を構築します。
このチュートリアルでは、
- アプリの登録の詳細を使用するように Web API を構成する
- アプリの登録に登録されている委任されたアクセス許可とアプリケーションのアクセス許可を使用するように Web API を構成する
- Web API のエンドポイントを保護する
前提条件
少なくとも 1 つのスコープ (委任されたアクセス許可) と 1 つのアプリ ロール (アプリケーション アクセス許可) を発行する API 登録 (ToDoList.Read など)。 まだ行っていない場合は、登録手順に従って、Microsoft Entra 管理センターで API を登録してください。 次の情報を確認します。
- Web API のアプリケーション (クライアント) ID
- Web API が登録されている場所のディレクトリ (テナント) ID
- Web API が登録されている場所のディレクトリ (テナント) サブドメイン。 たとえば、プライマリ ドメインが contoso.onmicrosoft.com である場合、ディレクトリ (テナント) サブドメインは contoso です。
- Web API によって発行される委任されたアクセス許可 (スコープ) としての ToDoList.Read および ToDoList.ReadWrite。
- Web API によって発行されるアプリケーション アクセス許可 (アプリ ロール) としての ToDoList.Read.All と ToDoList.ReadWrite.All。
.NET 7.0 SDK 以降。
Visual Studio Code、または別のコード エディター。
ASP.NET Core Web API を作成する
ターミナルを開いてから、プロジェクトを配置したいフォルダーに移動します。
次のコマンドを実行します。
dotnet new webapi -o ToDoListAPI cd ToDoListAPI
ダイアログ ボックスで、プロジェクトに必要な資産を追加するかどうかを確認されたら、 [はい] を選択します。
パッケージをインストールする
次のパッケージをインストールします。
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
をディレクトリ (テナント) サブドメインに置き換えます。
カスタム URL ドメインを使用する (省略可能)
カスタム ドメインを使用して、認証 URL を完全にブランド化します。 ユーザーの視点から見ると、認証プロセスの間、ユーザーは ciamlogin.com ドメイン名にリダイレクトされず、あなたのドメインにとどまります。
カスタム ドメインを使用するには、次の手順に従います。
「外部テナントのアプリに対してカスタム URL ドメインを有効にする 」の手順を使用して、外部テナントに対してカスタム URL ドメインを有効にします。
appsettings.json ファイルを開きます。
Instance
プロパティの値を https://Enter_the_Custom_Domain_Here/Enter_the_Tenant_ID_Here に更新します。Enter_the_Custom_Domain_Here
を実際のカスタム URL ドメインに、Enter_the_Tenant_ID_Here
を実際のテナント ID に置き換えます。 テナント ID がわからない場合は、テナントの詳細を読み取る方法を確認してください。- [Enter_the_Custom_Domain_Here] という値を持つ
knownAuthorities
プロパティを追加します。
カスタム URL ドメインが login.contoso.com、テナント ID が aaaabbbb-0000-cccc-1111-dddd2222eeee の場合、appsettings.json ファイルに変更を加えた後、ファイルは次のスニペットのようになります。
{
"AzureAd": {
"Instance": "https://login.contoso.com/aaaabbbb-0000-cccc-1111-dddd2222eeee",
"TenantId": "Enter_the_Tenant_Id_Here",
"ClientId": "Enter_the_Application_Id_Here",
"KnownAuthorities": ["login.contoso.com"]
},
"Logging": {...},
"AllowedHosts": "*"
}
アプリのロールとスコープを追加する
クライアント アプリがユーザーのアクセス トークンを正常に取得するために、すべての API は少なくとも 1 つのスコープ (委任されたアクセス許可とも呼ばれます) を発行する必要があります。 また、API は、クライアント アプリがそれ自体としてアクセス トークンを取得するため (つまりユーザーをサインインしていない場合に)、少なくとも 1 つのアプリケーションのアプリ ロール (アプリケーション アクセス許可とも呼ばれます) を発行する必要があります。
これらのアクセス許可は、appsettings.json ファイルで指定します。 このチュートリアルでは、4 つのアクセス許可を登録しました。 委任されたアクセス許可として ToDoList.ReadWrite と ToDoList.Read を、アプリケーションのアクセス許可として ToDoList.ReadWrite.All と ToDoList.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 クラスから派生することによって作成されます。 このチュートリアルでは、テスト目的でメモリ内データベースを使用します。
プロジェクトのルート フォルダーに、DbContext という名前のフォルダーを作成します。
そのフォルダーに移動し、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; } }
アプリのルート フォルダーにある 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 つのアクション 項目のみを作成します。 エンドポイントを保護する方法を示す、"すべてを読み取り" アクション項目と "作成" アクション項目。
プロジェクトのルート フォルダーにある [コントローラー] フォルダーに移動します。
このフォルダー内に 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 のビルドではなく保護です。
必要なパッケージをインポートします。 Microsoft.Identity.Web パッケージは、たとえばトークンの検証を処理することで、認証ロジックを簡単に処理できるようにする MSAL ラッパーです。 エンドポイントに確実に認可が必要になるように、組み込みの Microsoft.AspNetCore.Authorization パッケージを使用します。
この 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"); } }
行われている要求に、目的のアクションを実行するのに十分なアクセス許可が含まれているかどうかを決定するヘルパー関数を追加します。 アプリが自身のための要求を行っているのか、またはアプリが特定のリソースを所有するユーザーに代わって呼び出しを行っているのかをユーザー 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; }
アクセス許可の定義を組み込んでルートを保護します。
[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 コードの完全な例については、サンプル ファイルを参照してください。