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" 要素を追加します。Key

{

  ・・・中略・・・

  "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 (VerifyUser(id, pw))
                    {
                        // JWT を生成する
                        var tokenString = BuildToken(config);
                        result = Results.Ok(new { token = tokenString });
                    }
                }

                return result;
            });

            app.Run();
        }

        // 受け取った id と password を検証するヘルパメソッド
        private static bool VerifyUser(string id, string pw)
        {            
            // ここでは全て検証結果 OK として true を返す
            return 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 応答ではないことに注意)

(7) 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

IIS での IP アドレスによるアクセス制限

by WebSurfer 3. June 2025 10:17

自分のブログに良からぬことをする輩がいるので IP アドレスでアクセス制限をかけました。以下にどのように設定したかを備忘録として書いておきます。

IIS Manager が使える場合は、Microsoft のドキュメント「IP セキュリティ <ipSecurity>」および「IP セキュリティの追加 <add>」に従って設定すればよさそうです。

しかしながら、自分が使っているホスティングサービスでは IIS Manager は利用できず、ホスティングサービスが用意しているコントロールパネル上では GUI による操作ができません。なので、直接 web.config を編集してアクセス制限のための ipSecurity 要素を書き加えなければなりません。

自分の開発マシンは Windows 10 Pro 64-bit ですので IIS 10 と IIS Manager が使えます。それを使って GUI でアクセス制限のための ipSecurity 要素を作成できます。それを運用環境の web.config にコピーすればよさそうです。

注意:
自分の環境では ApplicationHost.config に <section name="ipSecurity" overrideModeDefault="Deny" /> という制約があって、下位のサイト/アプリケーションの web.config には ipSecurity 要素を設定できません。なので、自分の環境では 以下の IIS Manager を使っての操作では ipSecurity 要素は ApplicationHost.config に追加されます。ホスティングサービスの ApplicationHost.config にはそのような制約はなく、作成した ipSecurity 要素はサイト/アプリケーションの web.config にコピーできます。

デフォルトでは IIS 10 には IP セキュリティの機能は含まれません。機能を追加するには、 Windows 10 PC の場合、下の画像のように「Windows の機能の有効化または無効化」のダイアログで [IP セキュリティ] にチェックを入れて機能を有効化します。(Windows Server では [役割と機能の追加] で [IP and Domain Restriction] にチェックを入れて有効化します)

IP セキュリティの機能を追加

IP セキュリティの機能を追加すると、IIS Manager 上に「IP アドレスおよびドメインの制限」という機能が表示されるようになります。下の画像がそれです。

IP アドレスおよびドメインの制限

ローカルの適当なサイトまたはアプリケーションを選び「IP アドレスおよびドメインの制限」機能を開いて作業を行います。上の画像の例では、Default Web Site 下の MvcApp という名前のアプリケーションを選んでいます。そのアプリケーション用の ipSecurity 要素を追加します。

「IP アドレスおよびドメインの制限」のアイコンをダブルクリックして機能を開き、IIS Manager 上の「操作」ウィンドウの [拒否エントリの追加...] をクリックすると下のダイアログが表示されます。問題の IP アドレスは 185.208.8.76 でしたが、今回は 185.208.8.0 から 185.208.8.255 までの帯域をアクセス制限します。

拒否の制限規則の追加

[OK] ボタンをクリックすると下の画面が表示されるので、上の操作で追加した規則を選んで「操作」ウィンドウの [機能設定の編集...] をクリックします。

機能設定の編集

下の画像の「IP およびドメインの制限の編集」ダイアログが表示されますので、画像のように設定し [OK] ボタンをクリックします。

IP およびドメインの制限の編集

上の画像で[特定できないクライアントのアクセス(A):]は[許可](デフォルト) にします。[拒否]にすると制限した IP アドレス以外も拒否されてしまいます。

[拒否アクションの種類(D):]はドロップダウンの 4 項目の中から選べます。今回は[検出されていません](HTTP 404 Not Found 応答を返す) を選びました。

その他の項目についての説明は Microsoft のドキュメント「IIS 8.0 動的 IP アドレス制限」を見てください。

[OK] ボタンをクリックするとルールが作成され、ApplicationHost.config に以下の通り ipSecurity 要素が追加されているはずなので確認してください。(未検証ですが、ApplicationHost.config で ipSecurity の overrideModeDefault が Allow となっていれば当該サイト/アプリケーションの web.config に追加されると思われます)

<location path="Default Web Site/MvcApp">
  <system.webServer>
    <security>
      <ipSecurity denyAction="NotFound">
        <add ipAddress="185.208.8.0" 
             subnetMask="255.255.255.0" 
             allowed="false" />
      </ipSecurity>
    </security>
  </system.webServer>
</location>

上の ipSecurity 要素を web.config にコピーしました。その作業を行ったのが 2025/5/20 です。それ以来、この記事を書いた今日まで 2 週間は問題のアクセスはないです・・・が、アクセス制限の効果があったのか、問題の輩がアクセスをやめただけなのかは分かりません。前者に期待したいですが。(笑)

Tags: , ,

Windows Server

About this blog

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

Calendar

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

View posts in large calendar