WebSurfer's Home

Filter by APML

Minimal API で JWT を使った認証

by WebSurfer 8. July 2025 11:38

先の記事「ASP.NET Core Minimal API」の続きです。先の記事では Microsoft のチュートリアルに従って基本的な REST API を構築し、CORS の機能を追加しました。今回、それに JWT を使ったユーザー認証の機能を追加しましたので、その方法を備忘録として書いておきます。

JWT (デコード結果)

上の画像はこの記事に書いた方法で発行された JWT を JSON Web Token (JWT) Debugger というサイトでデコードした結果です。ロールによるアクセス制限の検証を行うため Admin ロールもクレームとして追加しています。

基本的には、先の記事「ASP.NET Web API と JWT (CORE)」と「ASP.NET Core Web API に Role ベースの承認を追加」に書いた、普通に Controller を使う Web API の場合と同じ方法で可能です。

Microsoft のドキュメント「Minimal API での認証と認可」にも説明がありますので、そちらにも目を通しておくことをお勧めします。

以下に JWT を使った認証の機能、JWT の発行機能、ロールによる承認の機能をどのように実装したかを書きます。

(1) NuGet パッケージのインストール

NuGet パッケージ Microsoft.AspNetCore.Authentication.JwtBearer をインストールします。

NuGet パッケージのインストール

(2) JWT 認証サービスの登録とミドルウェアを追加

既存の Program.cs に AddAuthentication メソッドを追加して JWT 認証サービスを登録します。加えて、AddAuthorization メソッドで承認サービスを登録します。

承認サービスは、Visual Studio 2022 のテンプレート ASP.NET Core Web API を使って作成した Controller を使う Web API ではデフォルトで登録されていますが、参考にした Microsoft のチュートリアルに従って ASP.NET Core (空) を使って作成したプロジェクトには含まれていませんので、AddAuthorization メソッドで登録する必要があることに注意してください。

さらに、認証・承認が働くようにするためのミドルウエアを、UseAuthentication メソッドおよび UseAuthorization メソッドにより追加します。ミドルウェアの順序に注意してください。先の記事で CORS を追加していますので、認証・承認のミドルウェアはその後に追加します。

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace MinimalAPI
{
    public class Program
    {
        const string MyAllowAnyOrigins = "_myAllowAnyOrigins";

        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // ・・・中略・・・

            // JWT ベースの認証サービスを登録
            builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = builder.Configuration["Jwt:Issuer"],
                        ValidAudience = builder.Configuration["Jwt:Issuer"],
                        IssuerSigningKey = new SymmetricSecurityKey(
                            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
                    };
                });

            // 承認サービスを登録
            builder.Services.AddAuthorization();

            // ・・・中略・・・

            // 先の記事で追加した CORS のミドルウェア
            app.UseCors(MyAllowAnyOrigins);

            // 認証・承認を行うミドルウェアを追加
            app.UseAuthentication();
            app.UseAuthorization();

            // ・・・中略・・・
        }
    }
}

(3) Key と Issuer を appsettings.json に登録

上の (2) コードでは Key と Issuer を appsettings.json ファイルより取得するようにしていますので、以下のように appsettings.json に "Jwt" 要素を追加します。

{

  ・・・中略・・・

  "AllowedHosts": "*",
  "Jwt": {
    "Key": "veryVerySecretKeyWhichMustBeLongerThan32",
    "Issuer": "https://localhost:44374"
  }
}

Key はパスワードのようなもので任意の文字列を設定できます。ASP.NET Core 3.1 のころは 16 文字以上ということだったのですが、いつの間にかそれが変わったらしく 32 文字以上にしないとエラーになります。Issuer はサービスを行う URL にします。

(4) エンドポイントに [Authorize] 属性を付与

Program.cs の中のエンドポイントに [Authorize] 属性を付与します。この記事では /todoitems/auth というエンドポイントを新たに追加しました。そのコードは以下の通りです。

// 認証が必要なエンドポイント
app.MapGet("/todoitems/auth", [Authorize] async (TodoDb db) =>
    await db.Todos.ToListAsync());

ここまでの設定で JWT トークンベースのアクセス制限の実装は完了しており、トークンなしで上のエンドポイント /todoitems/auth を呼び出すと HTTP 401 Unauthorized 応答が返ってきます。

(5) JWT を発行するエンドポイントを実装

クライアントからユーザー ID とパスワードを受信し、JWT を発行するエンドポイント /createtoken を Program.cs に追加します。appsettings.json に登録した Issuer と Key が必要ですが、それらは自動的に DI される IConfiguration から取得できます。

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;

namespace MinimalAPI
{
    public class Program
    {
        const string MyAllowAnyOrigins = "_myAllowAnyOrigins";

        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // ・・・中略・・・

            // id と password を受け取って検証し JWT を発行するエンドポイント
            app.MapPost("/createtoken", async ([FromBody] LoginModel login, 
                                               IConfiguration config) => 
            {
                string? id = login.Username;
                string? pw = login.Password;
                IResult result = Results.Unauthorized();

                if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(pw))
                {
                    // 受け取った id と password を検証する
                    if (await VerifyUserAsync(id, pw))
                    {
                        // JWT を生成する
                        var tokenString = BuildToken(config);
                        result = Results.Ok(new { token = tokenString });
                    }
                }

                return result;
            });

            app.Run();
        }

        // 受け取った id と password を検証するヘルパメソッド
        // 内部で UserManager.CheckPasswordAsync(id, pw) を使うことを
        // 想定して非同期メソッドにした
        private static Task<bool> VerifyUserAsync(string id, string pw)
        {
            // ここでは全て検証結果 OK として true を返す
            return Task.FromResult(true);
        }


        // JWT を生成するヘルパメソッド
        private static string BuildToken(IConfiguration config)
        {
            var key = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(config["Jwt:Key"]!));

            var creds = new SigningCredentials(
                key, SecurityAlgorithms.HmacSha256);

            var token = new JwtSecurityToken(
                issuer: config["Jwt:Issuer"],
                audience: config["Jwt:Issuer"],
                claims: null,
                notBefore: null,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: creds);

            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }

    // クライアントから送信されてきた id と password を受け取る
    // ための View Model
    public class LoginModel
    {
        public string? Username { get; set; }
        public string? Password { get; set; }
    }
}

(6) 試験用のクライアント側のコード

試験用に、id と password を上の (5) のエンドポイントに送信して JWT を受け取り、受け取った JWT をベアラトークンに含めて上の (4) のエンドポイントに要求をかけるコードを、先の記事で試験用に作った別の MVC アプリの View のコードに追加します。

const url = "https://localhost:44374/todoitems"; // IIS Express
const elem = document.querySelector("#heroes");

const minimalApiGetAuth = async () => {
    const tokenUrl = "https://localhost:44374/createtoken";
    let token;

    // 送信する ID とパスワード
    const credentials = {
        Username: "oz@mail.example.com",
        Password: "myPassword"
    };

    const params = {
        method: "POST",
        body: JSON.stringify(credentials),
        headers: { 'Content-Type': 'application/json' }
    };

     // ID とパスワードを POST 送信して JWT を取得
    const responseToken = await fetch(tokenUrl, params);
    if (responseToken.ok) {
        const data = await responseToken.json();
        token = data.token;
    }

    // 受け取った JWT をベアラトークンに含めて認証を受ける
    const response = await fetch(url + "/auth",
        { headers: { 'Authorization': `Bearer ${token}` } });
    if (response.ok) {
        const data = await response.json();
        elem.innerHTML = "";
        for (let i = 0; i < data.length; i++) {
            elem.insertAdjacentHTML("beforeend",
                `<li>${data[i].id}: ${data[i].name}, ${data[i].isComplete}</li>`);
        }
    } else {
        elem.innerHTML = "失敗";
    }
};

ボタンクリックなどのイベントハンドラで上の minimalApiGetAuth を起動すると JWT が取得され、取得した JWT をベアラトークンとして送信するので認証に成功し、期待通り応答が返ってきます。

(7) Role によるアクセス制限を追加

上の (4) で作成したエンドポイントが Admin ロールを必要とするよう変更します。 [Authorize] 属性を [Authorize(Roles="Admin")] に変更するだけでアクセスには Admin ロールが必要になります。

その上で上の (6) の minimalApiGetAuth メソッドを起動すると、JWT を取得してそれをベアラトークンとして送信するところまでは動きますが、JWT に Admin ロールが含まれてないので 403 Forbidden 応答が返ってきます。(401 Unauthorized 応答ではないことに注意)

(8) Admin ロールを JWT の claims に含める

発行する JWT に Admin ロールを claims に含めるよう、上の (5) の BuildToken メソッドを変更します。具体的には、JwtSecurityToken コンストラクタのパラメータ claims に以下の通り設定します。

private static string BuildToken(IConfiguration config)
{
    var key = new SymmetricSecurityKey(
        Encoding.UTF8.GetBytes(config["Jwt:Key"]!));

    var creds = new SigningCredentials(
        key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: config["Jwt:Issuer"],
        audience: config["Jwt:Issuer"],

        // Admin ロール情報を Claims に追加
        claims: [new Claim(ClaimTypes.Role, "Admin")],

        notBefore: null,
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: creds);

    return new JwtSecurityTokenHandler().WriteToken(token);
}

その上で(6) の minimalApiGetAuth メソッドを起動すると、取得した JWT にはこの記事の一番上の画像のように Admin ロールもクレームに追加されるので 認証・承認が成功し、期待通りデータが返ってきます。

下の画像はその時の要求・応答を Fiddler を使ってキャプチャしたものです。#3 は JWT 発行エンドポイントへの Preflight リクエスト、#4 は JWT の取得、#5 は上の (4) のエンドポイントへの Preflight リクエスト、#6 は (4) のエンドポイントからのデータの取得です。

要求・応答を Fiddler を使ってキャプチャ

Tags: , , , ,

Web API

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  November 2025  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar