WebSurfer's Home

Filter by APML

Minimal API で JWT からクレーム情報を取得

by WebSurfer 12. July 2025 13:07

ASP.NET Core Minimal API アプリで、クライアントから送信されてきた JWT からユーザー名やトークンの有効期限などの Payload 情報(ASP.NET ではそれらがクレーム情報になる)を取得する方法を書きます。

JWT (デコード結果)

先の記事「Minimal API で JWT を使った認証」に書いた JWT による認証を実装した ASP.NET Core Minimal API アプリでは、普通に Controller を使用した Web API と同様に、上の画像の JWT の Payload 部分は ASP.NET Core で言うクレームのコレクションとして取り扱われます。

先の記事に従って JWT の発行機能を実装すれば、上の画像通り JWT の Payload に exp (Expiration Time), iss (Issuer), aud (Audience) はデフォルトで含まれます。

任意のクレーム情報を追加することも可能です。例えば Admin ロールとユーザーの id を追加する場合は、先の記事に書いた JWT を生成するヘルパメソッド BuildToken で JwtSecurityToken コンストラクタのパラメータ claims に追加する情報を設定します。具体例は以下の通りです。

// JWT を生成するヘルパメソッド
private static string BuildToken(IConfiguration config, string id)
{
    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 ロールと id を Claims に追加
        claims: [
            new Claim(ClaimTypes.Role, "Admin"),
            new Claim("UserId", id)
        ],
        
        notBefore: null,
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: creds);

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

上の BuildToken メソッドで生成した JWT をデコードしたものがこの記事の一番上の画像で、Payload に Admin ロールと UserId が追加されています。

エンドポイントにおいて JWT からクレーム情報を取得するのは先の記事「JWT からクレーム情報を取得」に書いた方法と同様にして可能です。

ただし、先の記事のアプリでは ControllerBase.User プロパティから ClaimsPrincipal オブジェクトを取得していましたが、Minimal API は Controller を使わないので、そこのところのみ異なります。

Minimal API では、Microsoft のドキュメント「Minimal API クイック リファレンス」の「特殊な型」のセクションに書いてありますように、エンドポイントに設定したデリゲートの引数に ClaimsPrincipal を含めることで取得します。具体例は以下の通りです。

// 認証が必要なエンドポイント
app.MapGet("/todoitems/auth", [Authorize(Roles = "Admin")] 
                    async (TodoDb db, ClaimsPrincipal user) =>
{
    // JWT の Payload の情報は以下のようにして取得できる
    string? role = user.FindFirst(ClaimTypes.Role)?.Value;
    string? userId = user.FindFirst("UserId")?.Value;
    string? exp = user.FindFirst("exp")?.Value;  // UNIX 時間
    if (exp != null)
    {
        var ticks = long.Parse(exp) * 1000L * 1000L * 10L +
                    DateTime.UnixEpoch.Ticks;
        var expDateTime = new DateTime(ticks, DateTimeKind.Utc);
        var dateTimeUtcNow = DateTime.UtcNow;
        bool isBeforeExp = expDateTime > dateTimeUtcNow;
    }

    return Results.Ok(await db.Todos.ToListAsync());
});

クライアントから JWT が送信されてくると、その Payload の情報から ClaimsPrincipal オブジェクトが生成され、上のコードのデリゲートの引数 ClaimsPrincipal user に渡されます。それから FindFirst(String) メソッドを使ってJWT の Payload の情報を取得できます。

下の画像は Visual Studio 2022 のデバッガを使って上のコードのローカル変数の値を表示したものです。JWT はこの記事の一番上の画像のものです。JWT の Payload の role, userId, exp の情報が正しく取得できていることが分かりますでしょうか?

クレーム情報の取得

Tags: , , , , ,

CORE

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: , , , ,

CORE

ASP.NET Core Minimal API

by WebSurfer 6. July 2025 15:56

今さらながらですが、ASP.NET Core に Minimal API という軽量・高速 HTTP API を構築する簡単な手法があるということを知ったので、少しだけですが試してみました。以下に備忘録として試したことを書いておきます。

Minimal API からデータ取得

Minimal API というのは、Microsoft のドキュメント「Minimal API の概要」に書いてありますように、Controller を使わないで最小のコードと構成で REST API を構築するもので、Controller のアクションメソッドに代えて Program.cs のミドルウェアで要求を処理して応答を返します。

基本的な作り方は Microsoft のチュートリアル「チュートリアル: ASP.NET Core を使って最小 API を作成する」にありましたので、それを参考に「API コードを追加する」のセクションのところまで作ってみました。

作り方については、プロジェクトの作成からサンプル REST API を構築するための詳しい方法までチュートリアルに述べられていますのでそちらを見てください。手抜きでスミマセンが、チュートリアルには十分な情報が提供されており、追加で書くことなどもありませんので。

チュートリアルの「API コードを追加する」のセクションまで進めば、GET, POST, PUT, DELETE 要求を受けて JSON 文字列を返す REST API が完成します。

上の画像は、検証のため別の ASP.NET Core MVC アプリに作成した REST API を呼び出すページを追加して、ボタンクリックで JavaScript の fetch を使って要求を出し、JSON 文字列として返される応答を表示したものです。そのコードはこの記事の下の方に参考に載せておきます。

また、検証に使用した呼び出し側のアプリはクロスオリジンになるので Minimal API では CORS の機能を有効にする必要がありますが、その方法を以下に書いておきます。

(1) CORS の機能を実装

先の記事「Web API に CORS 実装 (CORE)」に書いた Controller を使う普通の ASP.NET Core Web API と同様に、Program.cs に AddCors と UseCors を追加すれば CORS の機能は働くようになります。具体例は以下の通りです。

namespace MinimalAPI
{
    public class Program
    {
        // CORS の機能を追加
        const string MyAllowAnyOrigins = "_myAllowAnyOrigins";

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

            // CORS の機能を追加
            builder.Services.AddCors(options =>
            {
                options.AddPolicy(name: MyAllowAnyOrigins,
                                  policy =>
                                  {
                                      policy.AllowAnyOrigin()
                                            .AllowAnyHeader()
                                            .AllowAnyMethod();
                                  });
            });

            // ・・・中略・・・

            var app = builder.Build();

            // CORS の機能を追加
            app.UseCors(MyAllowAnyOrigins);

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

Microsoft のドキュメント「CORS(異なるオリジン間でのリソース共有) 」にも説明がありますが、そのドキュメントには書いてない AllowAnyHeader() と AllowAnyMethod() は Preflight リクエストに必要なので注意してください。

また、そのドキュメントには "CORS は、[EnableCors] 属性により、または RequireCors メソッドを使用して宣言できます" と書いてありますが、自分が試した限りではそれらは不要でした。

(2) 呼び出し側の View のコード

検証のため、別の ASP.NET Core MVC アプリに作成した View のコードです。ボタンクリックで JavaScript の fetch を使って要求を出し、JSON 文字列として返される応答を表示します。

@{
    ViewData["Title"] = "MinimalApiCors";
}

<h1>MinimalApiCors</h1>

<input type="button" value="READ ALL" onclick="minimalApiGet();" />
<input type="button" value="READ 1" onclick="minimalApiGet1();" />
<input type="button" value="READ COMPLETE" onclick="minimalApiGetCompleted();" />
<input type="button" value="UPDATE 1" onclick="minimalApiPut1();" />
<input type="button" value="DELETE 1" onclick="minimalApiDelete1();" />
<input type="button" value="CREATE" onclick="minimalApiPost();" />

<ul id="heroes"></ul>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[

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

        const minimalApiGet = async () => {
            const response = await fetch(url);
            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 = "失敗";
            }
        };

        const minimalApiGet1 = async () => {
            const response = await fetch(url + "/1");
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}, ${data.isComplete}</li>`);
            } else {
                elem.innerHTML = `失敗 (${response.status})`;
            }
        };

        const minimalApiGetCompleted = async () => {
            const response = await fetch(url + "/complete");
            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 = "失敗";
            }
        };

        const minimalApiPut1 = async () => {
            const params = {
                method: "PUT",
                body: '{"Id":1,"Name":"Updated animal","isComplete":true}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch(url + "/1", params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}, ${data.isComplete} - Updated</li>`);
            } else {
                elem.innerHTML = `失敗 (${response.status})`;
            }
        };

        const minimalApiDelete1 = async () => {
            const params = {
                method: "DELETE"
            }
            const response = await fetch(url + "/1", params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}, ${data.isComplete} - Deleted</li>`);
            } else {
                elem.innerHTML = `失敗 (${response.status})`;
            }
        };

        const minimalApiPost = async () => {
            const params = {
                method: "POST",
                body: '{"name":"Posted animal","isComplete":true}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch(url, params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}, ${data.isComplete} - Inserted</li>`);
            } else {
                elem.innerHTML = "失敗";
            }
        };

        //]]>
    </script>
}

Tags: , , ,

CORE

About this blog

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

Calendar

<<  July 2025  >>
MoTuWeThFrSaSu
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar