RBAC と JWT 検証で ASP.NET Core API を保護する
このガイドでは、Logto が発行する ロールベースのアクセス制御 (RBAC) と JSON Web Token (JWT) を使用して、ASP.NET Core API に認可 (Authorization) を実装し、セキュリティを強化する方法を説明します。
始める前に
クライアントアプリケーションは Logto から アクセス トークン (Access token) を取得する必要があります。まだクライアント統合を設定していない場合は、React、Vue、Angular などのクライアントフレームワーク向け クイックスタート や、サーバー間アクセス用の マシン間通信 (M2M) ガイド をご覧ください。
このガイドは、ASP.NET Core アプリケーションにおけるこれらのトークンの サーバーサイド検証 に焦点を当てています。
学べること
- JWT 検証:アクセス トークン (Access token) を検証し、認証 (Authentication) 情報を抽出する方法
 - ミドルウェア実装:API 保護のための再利用可能なミドルウェアの作成
 - 権限モデル:さまざまな認可 (Authorization) パターンの理解と実装
- アプリケーション全体のエンドポイント向けグローバル API リソース
 - テナント固有の機能制御のための組織 (Organization) 権限
 - マルチテナントデータアクセスのための組織レベル API リソース
 
 - RBAC 統合:API エンドポイントでロールベースの権限 (Permission) とスコープ (Scope) を強制する方法
 
前提条件
- .NET の最新安定版がインストールされていること
 - ASP.NET Core および Web API 開発の基礎知識
 - Logto アプリケーションが設定済み(必要に応じて クイックスタート を参照)
 
権限モデルの概要
保護を実装する前に、アプリケーションアーキテクチャに適した権限モデルを選択してください。これは Logto の 3 つの主要な 認可 (Authorization) シナリオ に対応しています:
- グローバル API リソース
 - 組織(非 API)権限
 - 組織レベル API リソース
 

- ユースケース: アプリケーション全体で共有される API リソースを保護する(組織固有ではない)
 - トークンタイプ: グローバルオーディエンスを持つアクセス トークン
 - 例: パブリック API、コアプロダクトサービス、管理エンドポイント
 - 最適: すべての顧客が利用する API を持つ SaaS プロダクト、テナント分離のないマイクロサービス
 - 詳細: グローバル API リソースの保護
 

- ユースケース: 組織固有のアクション、UI 機能、ビジネスロジックを制御する(API ではない)
 - トークンタイプ: 組織固有オーディエンスを持つ組織トークン
 - 例: 機能ゲーティング、ダッシュボード権限、メンバー招待コントロール
 - 最適: 組織固有の機能やワークフローを持つマルチテナント SaaS
 - 詳細: 組織(非 API)権限の保護
 

- ユースケース: 特定の組織コンテキスト内でアクセス可能な API リソースを保護する
 - トークンタイプ: API リソースオーディエンス + 組織コンテキストを持つ組織トークン
 - 例: マルチテナント API、組織スコープのデータエンドポイント、テナント固有のマイクロサービス
 - 最適: API データが組織スコープとなるマルチテナント SaaS
 - 詳細: 組織レベル API リソースの保護
 
💡 進める前にモデルを選択してください — このガイド全体で選択したアプローチを参照します。
クイック準備手順
Logto リソースと権限の設定
- グローバル API リソース
 - 組織(非 API)権限
 - 組織レベル API リソース
 
- API リソースの作成: コンソール → API リソース にアクセスし、API を登録します(例: 
https://api.yourapp.com) - 権限の定義: 
read:productsやwrite:ordersなどのスコープを追加します – 権限付き API リソースの定義 を参照 - グローバルロールの作成: コンソール → ロール にアクセスし、API 権限を含むロールを作成します – グローバルロールの設定 を参照
 - ロールの割り当て: API アクセスが必要なユーザーまたは M2M アプリケーションにロールを割り当てます
 
- 組織権限の定義: 組織テンプレートで 
invite:memberやmanage:billingなどの非 API 組織権限を作成します - 組織ロールの設定: 組織テンプレートで組織固有のロールを設定し、それらに権限を割り当てます
 - 組織ロールの割り当て: 各組織コンテキスト内でユーザーに組織ロールを割り当てます
 
- API リソースの作成: 上記と同様に API リソースを登録しますが、組織コンテキストで使用します
 - 権限の定義: 組織コンテキストにスコープされた 
read:dataやwrite:settingsなどのスコープを追加します - 組織テンプレートの設定: API リソース権限を含む組織ロールを設定します
 - 組織ロールの割り当て: API 権限を含む組織ロールにユーザーまたは M2M アプリケーションを割り当てます
 - マルチテナント設定: API が組織スコープのデータとバリデーションを処理できることを確認します
 
ロールベースのアクセス制御ガイド からステップバイステップのセットアップ手順を始めましょう。
クライアントアプリケーションの更新
クライアントで適切なスコープをリクエストする:
- ユーザー認証 (Authentication): アプリの更新 → で API スコープや組織コンテキストをリクエスト
 - マシン間通信: M2M スコープの設定 → でサーバー間アクセスを設定
 
通常、クライアント設定を次のいずれか、または複数を含めるように更新します:
- OAuth フローでの 
scopeパラメーター - API リソースアクセス用の 
resourceパラメーター - 組織コンテキスト用の 
organization_id 
テストするユーザーまたは M2M アプリが、API に必要な権限を含む適切なロールまたは組織ロールに割り当てられていることを確認してください。
API プロジェクトの初期化
新しい .NET Web API プロジェクトを初期化するには、.NET CLI を使用できます:
dotnet new webapi -n YourApiName
cd YourApiName
JWT 認証 (Authentication) 用に必要な NuGet パッケージを追加します:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
基本的な API コントローラーを作成します:
using Microsoft.AspNetCore.Mvc;
namespace YourApiName.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ApiController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(new { message = "Hello from .NET API" });
        }
    }
}
開発サーバーを起動します:
dotnet run
コントローラーやミドルウェア、その他の機能のセットアップ方法については、 ASP.NET Core のドキュメント を参照してください。
定数とユーティリティの初期化
トークンの抽出と検証を処理するために、コード内で必要な定数やユーティリティを定義してください。有効なリクエストには、Authorization ヘッダーが Bearer <アクセス トークン (Access token)> の形式で含まれている必要があります。
namespace YourApiNamespace
{
    public static class AuthConstants
    {
        public const string Issuer = "https://your-tenant.logto.app/oidc";
    }
}
namespace YourApiNamespace.Exceptions
{
    public class AuthorizationException : Exception
    {
        public int StatusCode { get; }
        public AuthorizationException(string message, int statusCode = 403) : base(message)
        {
            StatusCode = statusCode;
        }
    }
}
Logto テナント情報の取得
Logto が発行したトークンを検証するには、次の値が必要です:
- JSON Web Key Set (JWKS) URI:JWT 署名を検証するために使用される Logto の公開鍵の URL。
 - 発行者 (Issuer):期待される発行者値(Logto の OIDC URL)。
 
まず、Logto テナントのエンドポイントを見つけます。これはさまざまな場所で確認できます:
- Logto コンソールの 設定 → ドメイン で確認できます。
 - Logto で設定した任意のアプリケーション設定内の 設定 → エンドポイント & 資格情報 で確認できます。
 
OpenID Connect ディスカバリーエンドポイントから取得する
これらの値は、Logto の OpenID Connect ディスカバリーエンドポイントから取得できます:
https://<your-logto-endpoint>/oidc/.well-known/openid-configuration
以下はレスポンス例です(他のフィールドは省略しています):
{
  "jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
  "issuer": "https://your-tenant.logto.app/oidc"
}
コード内にハードコーディングする(推奨されません)
Logto では JWKS URI や発行者 (Issuer) のカスタマイズができないため、これらの値をコード内にハードコーディングすることも可能です。ただし、将来的に設定が変更された場合のメンテナンス負荷が増加する可能性があるため、本番アプリケーションでは推奨されません。
- JWKS URI:
https://<your-logto-endpoint>/oidc/jwks - 発行者 (Issuer):
https://<your-logto-endpoint>/oidc 
トークンと権限の検証
トークンを抽出し OIDC 設定を取得した後、次の点を検証してください:
- 署名:JWT は有効であり、Logto(JWKS 経由)によって署名されている必要があります。
 - 発行者 (Issuer):Logto テナントの発行者 (Issuer) と一致している必要があります。
 - オーディエンス (Audience):Logto に登録された API のリソースインジケーター、または該当する場合は組織コンテキストと一致している必要があります。
 - 有効期限:トークンが有効期限切れでないこと。
 - 権限 (スコープ) (Permissions (scopes)):トークンに API / アクションに必要なスコープが含まれている必要があります。スコープは 
scopeクレーム内のスペース区切り文字列です。 - 組織コンテキスト:組織レベルの API リソースを保護する場合、
organization_idクレームを検証してください。 
JWT の構造やクレームについて詳しくは JSON Web Token を参照してください。
各権限モデルで確認すべきこと
クレームや検証ルールは権限モデルによって異なります:
- グローバル API リソース
 - 組織(非 API)権限
 - 組織レベル API リソース
 
- オーディエンスクレーム (
aud): API リソースインジケーター - 組織クレーム (
organization_id): なし - チェックするスコープ(権限) (
scope): API リソース権限 
- オーディエンスクレーム (
aud):urn:logto:organization:<id>(組織コンテキストがaudクレームに含まれる) - 組織クレーム (
organization_id): なし - チェックするスコープ(権限) (
scope): 組織権限 
- オーディエンスクレーム (
aud): API リソースインジケーター - 組織クレーム (
organization_id): 組織 ID(リクエストと一致する必要あり) - チェックするスコープ(権限) (
scope): API リソース権限 
非 API 組織権限の場合、組織コンテキストは aud
クレーム(例:urn:logto:organization:abc123)で表されます。organization_id
クレームは組織レベル API リソーストークンにのみ存在します。
セキュアなマルチテナント API のため、必ず権限(スコープ)とコンテキスト(オーディエンス、組織)の両方を検証してください。
検証ロジックの追加
JWT 認証 (Authentication) に必要な NuGet パッケージを追加します:
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
トークン検証を処理するためのバリデーションサービスを作成します:
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using YourApiNamespace.Exceptions;
namespace YourApiNamespace.Services
{
    public interface IJwtValidationService
    {
        Task ValidateTokenAsync(TokenValidatedContext context);
    }
    public class JwtValidationService : IJwtValidationService
    {
        public async Task ValidateTokenAsync(TokenValidatedContext context)
        {
            var principal = context.Principal!;
            try
            {
                // 権限 (Permission) モデルに基づくバリデーションロジックをここに追加
                ValidatePayload(principal);
            }
            catch (AuthorizationException)
            {
                throw; // 認可 (Authorization) 例外を再スロー
            }
            catch (Exception ex)
            {
                throw new AuthorizationException($"Token validation failed: {ex.Message}", 401);
            }
        }
        private void ValidatePayload(ClaimsPrincipal principal)
        {
            // 権限 (Permission) モデルに基づく検証ロジックをここに実装
            // この内容は下記の権限 (Permission) モデルセクションで示します
        }
    }
}
Program.cs で JWT 認証 (Authentication) を設定します:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using YourApiNamespace.Services;
using YourApiNamespace.Exceptions;
var builder = WebApplication.CreateBuilder(args);
// サービスをコンテナに追加
builder.Services.AddControllers();
builder.Services.AddScoped<IJwtValidationService, JwtValidationService>();
// JWT 認証 (Authentication) を設定
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = AuthConstants.Issuer;
        options.MetadataAddress = $"{AuthConstants.Issuer}/.well-known/openid-configuration";
        options.RequireHttpsMetadata = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = AuthConstants.Issuer,
            ValidateAudience = false, // 権限 (Permission) モデルに基づき手動でオーディエンスを検証
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ClockSkew = TimeSpan.FromMinutes(5)
        };
        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = async context =>
            {
                var validationService = context.HttpContext.RequestServices
                    .GetRequiredService<IJwtValidationService>();
                await validationService.ValidateTokenAsync(context);
            },
            OnAuthenticationFailed = context =>
            {
                // JWT ライブラリエラーを 401 として処理
                context.Response.StatusCode = 401;
                context.Response.ContentType = "application/json";
                context.Response.WriteAsync($"{{\"error\": \"Invalid token\"}}");
                context.HandleResponse();
                return Task.CompletedTask;
            }
        };
    });
builder.Services.AddAuthorization();
var app = builder.Build();
// 認証 (Authentication) / 認可 (Authorization) 失敗時のグローバルエラーハンドリング
app.Use(async (context, next) =>
{
    try
    {
        await next();
    }
    catch (AuthorizationException ex)
    {
        context.Response.StatusCode = ex.StatusCode;
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync($"{{\"error\": \"{ex.Message}\"}}");
    }
});
// HTTP リクエストパイプラインの設定
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
権限 (Permission) モデルに応じて、JwtValidationService で適切なバリデーションロジックを実装します:
- グローバル API リソース
 - 組織 (非 API) 権限 (Permission)
 - 組織レベル API リソース
 
private void ValidatePayload(ClaimsPrincipal principal)
{
    // オーディエンス (Audience) クレームが API リソースインジケーターと一致するか確認
    var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
    if (!audiences.Contains("https://your-api-resource-indicator"))
    {
        throw new AuthorizationException("Invalid audience");
    }
    // グローバル API リソースに必要なスコープ (Scope) を確認
    var requiredScopes = new[] { "api:read", "api:write" }; // 実際の必要スコープに置き換えてください
    var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
    if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
    {
        throw new AuthorizationException("Insufficient scope");
    }
}
private void ValidatePayload(ClaimsPrincipal principal)
{
    // オーディエンス (Audience) クレームが組織フォーマットと一致するか確認
    var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
    var hasOrgAudience = audiences.Any(aud => aud.StartsWith("urn:logto:organization:"));
    if (!hasOrgAudience)
    {
        throw new AuthorizationException("Invalid audience for organization permissions");
    }
    // 組織 ID がコンテキストと一致するか確認(リクエストコンテキストから抽出が必要な場合あり)
    var expectedOrgId = "your-organization-id"; // リクエストコンテキストから抽出
    var expectedAud = $"urn:logto:organization:{expectedOrgId}";
    if (!audiences.Contains(expectedAud))
    {
        throw new AuthorizationException("Organization ID mismatch");
    }
    // 必要な組織スコープ (Scope) を確認
    var requiredScopes = new[] { "invite:users", "manage:settings" }; // 実際の必要スコープに置き換えてください
    var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
    if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
    {
        throw new AuthorizationException("Insufficient organization scope");
    }
}
private void ValidatePayload(ClaimsPrincipal principal)
{
    // オーディエンス (Audience) クレームが API リソースインジケーターと一致するか確認
    var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
    if (!audiences.Contains("https://your-api-resource-indicator"))
    {
        throw new AuthorizationException("Invalid audience for organization-level API resources");
    }
    // 組織 ID がコンテキストと一致するか確認(リクエストコンテキストから抽出が必要な場合あり)
    var expectedOrgId = "your-organization-id"; // リクエストコンテキストから抽出
    var orgId = principal.FindFirst("organization_id")?.Value;
    if (!expectedOrgId.Equals(orgId))
    {
        throw new AuthorizationException("Organization ID mismatch");
    }
    // 組織レベル API リソースに必要なスコープ (Scope) を確認
    var requiredScopes = new[] { "api:read", "api:write" }; // 実際の必要スコープに置き換えてください
    var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
    if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
    {
        throw new AuthorizationException("Insufficient organization-level API scopes");
    }
}
ミドルウェアを API に適用する
これで、保護された API ルートにミドルウェアを適用します。
前のセクションですでに認証 (Authentication) および認可 (Authorization) のミドルウェアを設定しました。これで、アクセス トークンを検証し、認証済みリクエストからクレーム (Claims) を抽出する保護されたコントローラーを作成できます。
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace YourApiNamespace.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    [Authorize] // このコントローラー内のすべてのアクションに認証 (Authentication) を要求
    public class ProtectedController : ControllerBase
    {
        [HttpGet]
        public IActionResult GetProtectedData()
        {
            // アクセス トークン情報を User クレーム (Claims) から直接取得
            var sub = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? User.FindFirst("sub")?.Value;
            var clientId = User.FindFirst("client_id")?.Value;
            var organizationId = User.FindFirst("organization_id")?.Value;
            var scopes = User.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
            var audience = User.FindAll("aud").Select(c => c.Value).ToArray();
            return Ok(new {
                sub,
                client_id = clientId,
                organization_id = organizationId,
                scopes,
                audience
            });
        }
        [HttpGet("claims")]
        public IActionResult GetAllClaims()
        {
            // デバッグや確認用にすべてのクレーム (Claims) を返す
            var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToList();
            return Ok(new { claims });
        }
    }
}
保護された API のテスト
アクセス トークン (Access tokens) の取得
クライアントアプリケーションから: クライアント統合を設定している場合、アプリは自動的にトークンを取得できます。アクセス トークン (Access token) を抽出し、API リクエストで使用してください。
curl / Postman でのテスト用:
- 
ユーザートークン: クライアントアプリの開発者ツールを使い、localStorage またはネットワークタブからアクセス トークン (Access token) をコピーします。
 - 
マシン間通信トークン: クライアントクレデンシャルフローを使用します。以下は curl を使った非公式な例です:
curl -X POST https://your-tenant.logto.app/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=your-m2m-client-id" \
-d "client_secret=your-m2m-client-secret" \
-d "resource=https://your-api-resource-indicator" \
-d "scope=api:read api:write"resourceやscopeパラメーターは API リソースや権限に応じて調整が必要です。API が組織スコープの場合はorganization_idパラメーターも必要になる場合があります。 
トークンの内容を確認したい場合は、 JWT デコーダー を使って JWT をデコード・検証できます。
保護されたエンドポイントのテスト
有効なトークンリクエスト
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  http://localhost:3000/api/protected
期待されるレスポンス:
{
  "auth": {
    "sub": "user123",
    "clientId": "app456",
    "organizationId": "org789",
    "scopes": ["api:read", "api:write"],
    "audience": ["https://your-api-resource-indicator"]
  }
}
トークンなし
curl http://localhost:3000/api/protected
期待されるレスポンス (401):
{
  "error": "Authorization header is missing"
}
無効なトークン
curl -H "Authorization: Bearer invalid-token" \
  http://localhost:3000/api/protected
期待されるレスポンス (401):
{
  "error": "Invalid token"
}
権限モデルごとのテスト
- グローバル API リソース
 - 組織 (非 API) 権限
 - 組織レベル API リソース
 
グローバルスコープで保護された API のテストシナリオ:
- 有効なスコープ: 必要な API スコープ(例: 
api:read,api:write)を含むトークンでテスト - スコープ不足: 必要なスコープがない場合は 403 Forbidden を期待
 - 誤ったオーディエンス: オーディエンスが API リソースと一致しない場合は 403 Forbidden を期待
 
# 必要なスコープがないトークン - 403 を期待
curl -H "Authorization: Bearer token-without-required-scopes" \
  http://localhost:3000/api/protected
組織固有のアクセス制御のテストシナリオ:
- 有効な組織トークン: 正しい組織コンテキスト(組織 ID とスコープ)を含むトークンでテスト
 - スコープ不足: ユーザーが要求された操作の権限を持たない場合は 403 Forbidden を期待
 - 誤った組織: オーディエンスが組織コンテキスト(
urn:logto:organization:<organization_id>)と一致しない場合は 403 Forbidden を期待 
# 別の組織用トークン - 403 を期待
curl -H "Authorization: Bearer token-for-different-organization" \
  http://localhost:3000/api/protected
API リソース検証と組織コンテキストを組み合わせたテストシナリオ:
- 有効な組織 + API スコープ: 組織コンテキストと必要な API スコープの両方を持つトークンでテスト
 - API スコープ不足: 組織トークンに必要な API 権限がない場合は 403 Forbidden を期待
 - 誤った組織: 別の組織のトークンで API にアクセスした場合は 403 Forbidden を期待
 - 誤ったオーディエンス: オーディエンスが組織レベルの API リソースと一致しない場合は 403 Forbidden を期待
 
# API スコープがない組織トークン - 403 を期待
curl -H "Authorization: Bearer organization-token-without-api-scopes" \
  http://localhost:3000/api/protected
さらに詳しく
実践でのロールベースのアクセス制御 (RBAC):アプリケーションのための安全な認可 (Authorization) の実装
マルチテナント SaaS アプリケーションの構築:設計から実装までの完全ガイド