🔐

MSAL JWT Bearer認証認可を使用したClient-WebAPI(.NET)構築でゼロトラスト環境へ対応

に公開

はじめに

皆様はアプリ間通信のセキュリティについてどのような設計、実装をされてますでしょうか?
正直に申しますと、私がこれまで担当したシステムは、オンプレミスの閉じたローカル環境にシステム構築をすることが多かったため、セキュリティについてはそこまでこだわっておりませんでした。なんなら「外部インターネットに繋がらない環境だから、セキュリティはそこそこで、クライアントとサーバーが通信できれば、へーきへーき!」とさえ思ってました。

といっても、時代はゼロトラスト志向、あまり乱暴なことは言ってられません。どこの馬の骨かもわからないソフトウェアや通信は、監視、遮断しておきたいものです。

そんなこんなで、代表的なOAuth2.0準拠のClient-WebAPI認証認可の仕組みを整理したく、本記事作成に至ります。

Bearer認証とは

Bearer認証とは、HTTPリクエストのAuthorizationヘッダーに、Bearer <アクセストークン>の形式で認証情報(トークン)を含めてサーバーに送信する認証方法です。トークンを「持参している人(bearer)」だけがアクセスできるという仕組みで、OAuth 2.0で広く使われています。

特徴
  • HTTPヘッダーにトークンを含める: クライアントがサーバーにリクエストを送る際、Authorization: Bearer <アクセストークン>というヘッダーを付けて送信します。
  • 「Bearer」という意味: 「Bearer」という言葉は「持参人」や「保持者」を意味し、そのトークンを持っている人なら誰でもアクセスできる、という仕組みです。
  • ステートレス: サーバーはセッション情報を保持する必要がなく、各リクエストごとにトークンを検証するだけでよいため、ステートレスな設計が可能です。
  • セキュリティ向上: 有効期限を設定でき、安全な通信(HTTPS)を利用することで、Basic認証よりも安全性が高いとされています。
  • OAuth 2.0の標準: OAuth 2.0の仕様の一部として定義されていますが、それ以外でも利用可能です。

以下は簡単なBearer認証のシーケンスイメージ図です。

JWT (Json Web Token) とは

JWT(JSON Web Token)は、ウェブアプリケーションやAPI通信で、ユーザー認証や情報の安全なやり取りに使う、コンパクトで自己完結型のJSON形式のトークンです。
トークン自体に必要な情報が含まれているため、サーバー側で状態を保持する必要がなく、スケーラビリティやパフォーマンスの向上に役立ちます。デジタル署名が付与されているため、改ざんを検知してデータの完全性を保証します。

Microsoft ID プラットフォームと OAuth 2.0 認証コード フロー

OAuth 2.0 認可コード付与タイプ ("認可コード フロー") を使用すると、クライアント アプリケーションは Web API などの保護されたリソースへの認可されたアクセスを取得できます。 認可コード フローには、認可サーバー (Microsoft ID プラットフォーム) からアプリケーションへのリダイレクトをサポートするユーザー エージェントが必要です。 たとえば、ユーザーがアプリにサインインしてデータにアクセスするために操作する Web ブラウザー、デスクトップ、モバイル アプリケーションなどです。
OAuth 2.0 認可コード フローを使用するアプリは、Microsoft ID プラットフォームによって保護されたリソース (通常は API) への要求に含める access_token を取得します。 アプリでは、更新メカニズムを使用して、以前に認証されたエンティティの新しい ID とアクセス トークンを要求することもできます。
次の図は、認証フローの概要を示しています。

要するに、ユーザーやアプリは、OAuth 2.0 の認可コードフローと Microsoft ID プラットフォームを通じてアクセストークンを取得することで、 Web API などの保護されたリソースに安全にアクセスできますよ、という理解です。

Microsoft Authentication Library (MSAL) とは

Microsoft Authentication Library (MSAL) を使用すると、ユーザーを認証し、セキュリティで保護された Web API にアクセスするため、開発者は Microsoft ID プラットフォームからセキュリティ トークンを取得できます。 これは、Microsoft Graph、その他の Microsoft API、サード パーティの Web API、または、独自の Web API へのセキュリティで保護されたアクセスを提供するために使用できます。

MSALは、Microsoftのサービスや独自APIへの安全なアクセスを実現するための基盤となるライブラリ、という認識です。

やりたいこと

本題ですが、
.NET で作成したクライアントアプリとWebAPIのHttp通信において、Microsoft ID プラットフォームと OAuth 2.0 認証コード フローを用いたMSAL JWT Bearer認証認可の機能を実装したいです。

前提

Microsoft Entra ID

  • Microsoft Entra テナント
    テナントに対してEntra ID アプリ登録などを行える権限を持つアカウントを使用できること。

開発環境

  • Visual Studio 2026
  • .NET 10 SDK

ソースコード構成

以下のようにクライアントアプリ(コンソールアプリ)とサーバーアプリ(WebAPI)のプロジェクトが存在するとします。

クライアントアプリがサーバーアプリのエンドポイントに向けて、Http通信で通信を行う想定です。このクライアントアプリ <--> サーバーアプリの通信においてMSAL JWT Bearer認証認可を次項以降で実装していきます。

実装

いよいよ実装を行います。
必要な準備と実装は大きく以下3点です。

  1. Microsoft Entra IDアプリ登録
  2. クライアント側実装(MSAL JWT Bearerトークン取得)
  3. WebAPIサーバー側実装(認証認可処理)

Microsoft Entra IDでClient Credentials Flowを利用できるようにアプリ登録を行った後、クライアントアプリ(コンソールアプリ)がサーバーアプリ(Web API)へHTTP通信を行う際に、MSALで取得したJWT BearerトークンをHTTPヘッダーのAuthorizationに付与します。これにより、サーバーアプリ(Web API)が認証・認可を実施できるように実装を進めます。

全体像を図示すると、以下を想定しています。

1. Microsoft Entra IDアプリ登録

Azure Portalにて、Microsoft Entra ID アプリ登録を行い、Client Credentials Flowを利用できるように準備をします。

WebAPIサーバー用アプリ登録

まずは、Azure Portal内のMicrosoft Entra ID サービス内からアプリ登録を選択し、新規作成を行います。

WebAPI用のアプリ登録を行うので、名称は「WebAPI_Auth」として作成します。

APIの公開設定

続いて、作成したWebAPIサーバー用アプリ登録の「APIの公開」ページを選択しアプリケーションID URIを作成します。

アプリロール設定

さらに、「アプリロール」ページを選択し、名称は「WebAPI.Access」としてアプリロールを作成します。このとき、許可されたメンバーの種類は「アプリケーション」を選択します。

WebAPIサーバー用のMicrosoft Entra ID アプリ登録は以上です。

クライアントアプリ用アプリ登録

次に、クライアント側のMicrosoft Entra ID アプリ登録を行います。
WebAPIサーバー用アプリ登録と同様に、まずは、Azure Portal内のMicrosoft Entra ID サービス内からアプリ登録を選択し、新規作成を行います。

クライアント用のアプリ登録を行うので、名称は「Get_MSAL_JWT」として作成します。

アクセス許可設定

続いて、作成したクライアント用アプリ登録の「APIのアクセス許可」ページを選択し、WebAPIサーバー用アプリ登録時に設定を行った「アプリロール」を参照するようにします。
ページ内の「アクセス許可の追加」-->「自分のAPI」より、作成したWebAPI_Authを選択します。

APIアクセス許可の要求ダイアログ内で、WebAPI.Accessを選択し、「アクセス許可の追加」を押下実行します。

続いて、構成されたアクセス許可WebAPI.Accessにテナントの「管理者の同意」を付与します。
「<テナント名>に管理者の同意を与えます」を押下し、ポップアップ内「管理者の同意の確認を与えます。」を「はい」で実行します。

「<テナント名>に付与されました」と表示されればOKです。

クライアントシークレット設定

本記事では、クライアントアプリから接続する際のクライアントシークレットを設定します。
作成したクライアント用アプリ登録の「証明書とシークレット」ページを選択し、「クライアントシークレット」-->「新しいクライアントシークレット」を選択し、「追加」を押下実行にてクライアントシークレットを作成します。

作成されたクライアントシークレットの「値」は、クライアントアプリ実装時に使用します。

クライアント用のMicrosoft Entra ID アプリ登録は以上です。

MSAL JWTトークン取得時のエンドポイントバージョン指定

MSAL JWTトークン取得時のエンドポイントバージョンを指定する場合は、マニュフェストのページにて、accessTokenAcceptedVersionの項目を変更します。
初期値はnull(v1)ですが、サーバー側の都合でv2を指定する必要がある場合は、accessTokenAcceptedVersionを「2」とします。

2. クライアント側実装(MSAL JWT Bearerトークン取得)

クライアント側実装の実装を行います。いろいろ試したいことはあるのですが、、説明上で面倒でしたので、コンソールアプリにて、MSAL JWT Bearerトークン取得処理、Httpリクエスト処理を実装するとします。

使用するNugetパッケージ

MSALライブラリ。NETは、開発者向けのMicrosoft IDプラットフォーム(旧称:Azure AD)v2.0の一部です。セキュリティトークンを取得し、保護されたAPIを呼び出すことができます。業界標準のOAuth2およびOpenID Connectを使用しています。このライブラリはAzure AD B2Cもサポートしています。

処理

クライアント側処理では、以下3点の処理が必要です。

  1. MSAL クライアント ConfidentialClientApplication の構築
  2. AcquireTokenForClient でアクセストークン取得
  3. Authorization ヘッダーに Bearer トークンを設定し、Web APIを呼び出し

以下はConsoleApp_Clientプロジェクトのappsettings.json例とProgram.csです。
本記事では、appsettings.jsonにIDなどを定義、使用してますが、実際は、Azure Key Vaultなどに配置する運用が適切かと考えます。

appsettings.json(ConsoleApp_Client)
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "TTT",
    "GetMSAL_ClientId": "XXX",
    "GetMSAL_ClientSecret": "YYY",
    "WebAPI_Scopes": [ "api://ZZZ/.default" ]
  },
  "WebApi": {
    "BaseUrl": "https://localhost:5001",
    "SecureEndpoint": "/api/secure"
  }
}

Program.cs(ConsoleApp_Client)
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Client;
using System.Net.Http.Headers;

// 構成ファイル(appsettings.json)から設定を読み込む
IConfigurationRoot configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .Build();

// Azure AD 関連の設定を取得
string tenantId = configuration["AzureAd:TenantId"]!;
string clientId = configuration["AzureAd:GetMSAL_ClientId"]!;
string clientSecret = configuration["AzureAd:GetMSAL_ClientSecret"]!;
string instance = configuration["AzureAd:Instance"]!;
string[] scopes = configuration.GetSection("AzureAd:WebAPI_Scopes").Get<string[]>()!;

// Web API 関連の設定を取得
string webApiBaseUrl = configuration["WebApi:BaseUrl"]!;
string secureEndpoint = configuration["WebApi:SecureEndpoint"]!;

Console.WriteLine("== MSAL JWT Bearer 認証サンプル クライアント ==");
Console.WriteLine("開始するには何かキーを押してください...");
Console.ReadKey();
Console.WriteLine();

try
{
    // 1. MSAL クライアント ConfidentialClientApplication の構築
    Console.WriteLine($"1. MSAL クライアント ConfidentialClientApplication を構築");

    // ConfidentialClientApplication は Client Credentials Flow 用のクライアント
    string authority = $"{instance}{tenantId}";

    // アプリケーションの認証情報でクライアントを構築
    IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(clientId)
        .WithClientSecret(clientSecret)  // クライアントシークレットで認証
        .WithAuthority(new Uri(authority))  // Azure AD のテナント指定
        .Build();

    Console.WriteLine($"1. 完了");

    // 2. AcquireTokenForClientでアクセストークン取得
    // Client Credentials Flow を使用してアプリケーション自身の権限でトークンを取得
    Console.WriteLine();
    Console.WriteLine($"2. AcquireTokenForClientでアクセストークン取得");
    Console.WriteLine($"   認証方式: Client Credentials Flow");

    // トークンリクエストの実行
    AuthenticationResult result = await app.AcquireTokenForClient(scopes)
        .ExecuteAsync();

    Console.WriteLine($"   トークン有効期限: {result.ExpiresOn.ToLocalTime():yyyy/MM/dd HH:mm:ss}");
    Console.WriteLine($"2. 完了");

    // HTTPクライアントの作成
    using HttpClient httpClient = new();

    // 3. Authorization ヘッダーに Bearer トークンを設定し、Web APIを呼び出し
    // 取得したトークンを Bearer トークンとして HTTP リクエストに付与
    Console.WriteLine();
    Console.WriteLine("3. Authorization ヘッダーに Bearer トークンを設定し、Web APIを呼び出し");
    Console.WriteLine($"   エンドポイント: {webApiBaseUrl}{secureEndpoint}");
    Console.WriteLine($"   認証ヘッダー: Bearer <token>");

    // Authorization ヘッダーに Bearer トークンを設定
    httpClient.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", result.AccessToken);

    // Web API への GET リクエスト実行
    HttpResponseMessage response = await httpClient.GetAsync($"{webApiBaseUrl}{secureEndpoint}");

    Console.WriteLine($"   HTTP ステータス: {(int)response.StatusCode} {response.StatusCode}");
    Console.WriteLine();

    // レスポンスの処理
    if (response.IsSuccessStatusCode)
    {
        // 認証成功
        string content = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"   認証成功!!");
        Console.WriteLine($"   サーバーレスポンス: {content}");
    }
    else
    {
        // 認証失敗
        Console.WriteLine($"   認証失敗: {response.StatusCode}");

        // エラー詳細がある場合は表示
        string errorContent = await response.Content.ReadAsStringAsync();
        if (!string.IsNullOrWhiteSpace(errorContent))
        {
            Console.WriteLine($"   エラー詳細: {errorContent}");
        }
    }
    Console.WriteLine($"3. 完了");
}
catch (Exception ex)
{
    Console.WriteLine();
    Console.WriteLine($"予期しないエラー");
    Console.WriteLine($"   メッセージ: {ex.Message}");
}

Console.WriteLine();
Console.WriteLine("== 処理完了 ==");
Console.WriteLine("終了するには何かキーを押してください...");
Console.ReadKey();

クライアント側の実装は以上です。
実際の実装では、トークン有効期限が切れた場合は取得しなおすような処理が必要と考えます。

3. WebAPIサーバー側実装(認証認可)

WebAPIサーバー側の実装を行います。便宜上、MinimalAPIを使用して認証認可の実装を行うとします。...決して、コントローラーを準備することが面倒だったわけではありません。

使用するNugetパッケージ

Microsoft.AspNetCore.Authentication.JwtBearer は、ASP.NET Core アプリケーション向けに設計されたミドルウェアコンポーネントです。JSON Web Token(JWT)認証を容易にし、APIおよびWebサービスにおける安全な認証を可能にします。このパッケージを使用すると、認証サーバーが発行するJWTトークンを認証することで、アプリケーションのリソースへの安全なアクセスを確保できます。

処理

WebAPIサーバー側処理での認証認可には、以下5点の処理が必要です。

  1. Azure AD JWT Bearer 認証の設定 AddAuthentication()
  2. 認可サービスの登録 AddAuthorization()
  3. 認証ミドルウェア UseAuthentication()
  4. 認可ミドルウェア UseAuthorization()
  5. 認証が必要な Minimal API エンドポイント準備

以下はWebAPI_MSAL_Sampleプロジェクトのappsettings.json例とProgram.csです。
クライアントと同様に、本記事では、appsettings.jsonにIDなどを定義、使用してますが、実際は、Azure Key Vaultなどに配置する運用が適切かと考えます。

appsettings.json(WebAPI_MSAL_Sample)
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "Https": {
        "Url": "https://localhost:5001"
      }
    }
  },
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "TTT",
    "WebAPI_ClientId": "ZZZ",
    "WebAPI_Audience": "api://ZZZ"
  }
}

Program.cs(WebAPI_MSAL_Sample)
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// サービスの登録
builder.Services.AddControllers();
builder.Services.AddOpenApi();

// Kestrel の設定を appsettings.json から読み込む
// これにより、HTTP/HTTPS のポート設定などが適用される
builder.WebHost.ConfigureKestrel((context, options) =>
{
    // appsettings.json の Kestrel セクションが自動的に適用される
});

// Azure AD 設定を構成ファイルから取得
IConfigurationSection azureAdSection = builder.Configuration.GetSection("AzureAd");
string? tenantId = azureAdSection["TenantId"];
string? clientId = azureAdSection["WebAPI_ClientId"];
string? instance = azureAdSection["Instance"];
string? audience = azureAdSection["WebAPI_Audience"];

// 1. Azure AD JWT Bearer 認証の設定 AddAuthentication()
// Client Credentials Flow で取得したトークンを検証する
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // トークン発行元(Authority)の設定
        // Azure AD のメタデータエンドポイントから公開鍵を取得する
        options.Authority = $"{instance}{tenantId}";

        // このAPIが期待する Audience(対象者)
        // トークンの aud クレームがこの値と一致する必要がある
        options.Audience = audience;

        // トークン検証のパラメータ設定
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // トークン発行者(Issuer)を検証する
            ValidateIssuer = true,

            // トークンの対象者(Audience)を検証する
            ValidateAudience = true,

            // トークンの有効期限を検証する
            ValidateLifetime = true,

            // トークンの署名鍵を検証する
            ValidateIssuerSigningKey = true,

            // 有効な Audience のリスト
            // Application ID URI と ClientId の両方を許可
            ValidAudiences = new[] { audience, clientId },

            // 有効な Issuer のリスト
            // v1.0 と v2.0 両方のトークン形式に対応
            ValidIssuers = new[]
            {
                // v1.0 エンドポイント形式
                $"https://sts.windows.net/{tenantId}/",
                // v2.0 エンドポイント形式
                $"https://login.microsoftonline.com/{tenantId}/v2.0"
            }
        };

        // 認証イベントのログ設定
        // 本番環境では機密情報を含まない最小限のログのみ出力
        options.Events = new JwtBearerEvents
        {
            // トークン検証成功時
            OnTokenValidated = context =>
            {
                // 機密情報を含まない認証成功のログ
                Console.WriteLine("[認証成功] トークンが検証されました");
                return Task.CompletedTask;
            },

            // 認証失敗時
            OnAuthenticationFailed = context =>
            {
                // エラーの種類のみログ出力(詳細は含めない)
                string errorType = context.Exception.GetType().Name;
                Console.WriteLine($"[認証失敗] エラータイプ: {errorType}");
                return Task.CompletedTask;
            }
        };
    });

// 2. 認可サービスの登録 AddAuthorization();
builder.Services.AddAuthorization();

WebApplication app = builder.Build();

// HTTPS リダイレクトミドルウェア
// HTTP リクエストを HTTPS にリダイレクト
app.UseHttpsRedirection();

// 3. 認証ミドルウェア UseAuthentication();
// JWT トークンの検証を実行
app.UseAuthentication();

// 4. 認可ミドルウェア UseAuthorization();
// ユーザーのアクセス権限をチェック
app.UseAuthorization();

// コントローラーのエンドポイントをマップ
app.MapControllers();

// 5. 認証が必要な Minimal API エンドポイント準備
// Client Credentials Flow で取得したトークンでアクセス可能
app.MapGet("/api/secure", () =>
{
    Console.WriteLine("[API呼び出し] /api/secure 認証が必要な Minimal API エンドポイント");

    // 認証成功時のレスポンス
    return Results.Ok(new
    {
        message = "認証に成功しました!!",
        timestamp = DateTime.UtcNow
    });
})
.RequireAuthorization(); // このエンドポイントは認証が必須

// 起動時の情報表示
Console.WriteLine("== WebAPI 起動 ==");
Console.WriteLine($"認証方式: Azure AD JWT Bearer");
Console.WriteLine($"エンドポイント: /api/secure (認証必須)");
Console.WriteLine();

// アプリケーションの実行
app.Run();


WebAPIサーバー側の実装は以上です。
今回はMinimalAPIで実装していますが、実際はコントローラークラスで実装することが多いと思います。その場合は、コントローラーメソッドのアノテーションに[Authorize] 付与することが必要です。

動作確認

いざいざ動作確認です。
左側がWebAPIサーバー、右側がクライアント(コンソールアプリ)です。
gif

無事、コンソールアプリでJWT Bearerトークンを取得し、WebAPIへのリクエストも認証認可されていることがわかります。はじめて認証が成功したときは、神にも許されたような全能感を実感しましたね。ええ、自分で設定したんですけども。

まとめ

とうわけで、MSAL JWT Bearer認証認可を使用した簡単なWebAPI(.NET)構築と動作確認を行いました。

要点を整理しますと、以下3点かと考えます。

  • 認証はMicrosoft Entra IDに委譲し、WebAPI側はJWTの基本検証(Issuer / Audience / 署名 / 有効期限)に集中する構成で実装できる。
  • Client Credentials Flow+MSAL.NETでアプリ間(ユーザーなし)の安全なトークン取得が比較的容易に実装できる。
  • App Roleを使うことで権限付与をコード外(ディレクトリ側)に寄せ、疎結合で管理者同意を伴う明確なアクセス制御が可能。

本記事の内容が少しでも何かのお役に立てれば幸いです。

参考

  • Microsoft Learn Microsoft ID プラットフォームのドキュメント
  • Microsoft Learn Microsoft ID プラットフォームのアプリの種類と認証フロー
  • Microsoft Learn Microsoft ID プラットフォームと OAuth 2.0 クライアント資格情報フロー
  • Zenn Basic Auth vs. Bearer Token: API認証方式の最適な選択
  • Qiita Bearer認証とJWT認証
  • The OAuth 2.0 Authorization Framework: Bearer Token Usage

Discussion