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 Web API に Role ベースの承認を追加

by WebSurfer 20. July 2024 13:34

トークンベース (JWT) の認証を実装した ASP.NET Core Web API に Role による承認を追加する話を書きます。(認証と承認は違いますのでご注意ください)

クッキーベースの ASP.NET Identity 認証システムを実装した ASP.NET Web アプリでは、認証に加えて承認によるアクセスコントロールが可能です。承認には Role が使われます。例えばサイト管理者がマネージャー・ゲスト・管理者・メンバーなどといった Role を定義し、ユーザー別に Role をアサインすることにより細かいアクセスコントロールが可能になっています。

例えばアクションメソッドに [Authorize(Roles = "Admin")] という属性を付与したとすると、ユーザーが有効な ID とパスワードでログインして認証は通っていたとしても、Admin という Role を持っていないと承認されず、ユーザーはそのアクションメソッドにはアクセスできなくなります。

それは JWT ベースの認証機能を実装した Web API でも同様です。先の記事「Blazor WASM から ASP.NET Core Web API を呼び出し」で紹介した WeatherForecastController コントローラの Get() メソッドで、以下のように [Authorize] 属性を [Authorize(Roles = "Admin")] に変更してから Blazor アプリからアクセスすると、

// [Authorize]
[Authorize(Roles = "Admin")]
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}

以下の画像のように HTTP 403 Forbidden 応答 (サーバーは要求を理解しましたが、要求の実行を拒否しています・・・と言う意味) が帰ってきてデータの取得に失敗します。ちなみに、認証が通らない場合は HTTP 401 Unauthorized 応答が返ってきます。

HTTP 403 応答

上の画像の #11 では、Blazor アプリからは有効な ID とパスワードが送信されるので認証は通ってトークンが返ってきています。そして、#13 でそのトークンを要求ヘッダに含めて上のアクションメソッド Get に要求をかけています。その結果、サーバー側では認証には成功するものの Admin ロールを持っていないので承認が通らず HTTP 403 応答が返ってきています。

Role による承認が通るようにするには、上の画像の #11 で返されるトークンに Admin ロールを持っているという情報を含める必要があります。それにはトークンを生成する際、Claims 情報として Admin ロールを追加してやります。

具体的には、先の記事「Blazor WASM から ASP.NET Core Web API を呼び出し」で紹介したトークンを発行する API の BuildToken メソッドで、以下のように Admin ロール情報を Claims に追加します。

private string BuildToken()
{
    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);
}

これにより発行されるトークンには Admin ロールを持っているという情報が含まれます。以下の画像は発行されたトークンを JWT というサイトでデコードしたものです。赤枠部分を見てください。

Admin ロール情報を含む JWT

そのトークンを要求ヘッダに含めて要求をかければ承認が通って、下の画像の #6 の通りデータが返され、

200 応答

Blazor アプリには期待通り結果が表示されます。

Blazor アプリの結果の表示

Tags: , , , ,

Web API

ロールに属するユーザー一覧を表示 (CORE)

by WebSurfer 13. February 2021 11:19

ASP.NET Core MVC アプリでユーザー認証・承認を行うのに ASP.NET Core Identity を利用するケースで、ロールにアサインされたユーザー一覧を表示するサンプルを書きます。

ロールに属するユーザー一覧の表示

先の記事「ASP.NET Identity のロール管理 (CORE)」で、管理者がロールの表示・追加・変更・削除を行うためのサンプルを紹介しましたが、それの Details アクションメソッド / ビューに実装してみます。(先の記事では Details は未実装でした)

(1) ビューモデルの追加

コントローラーからビューに渡すモデルとして UsersInRole クラスを追加します。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

// 追加
using Microsoft.AspNetCore.Identity;
using MySQLIdentity.Areas.Identity.Data;

namespace MySQLIdentity.Models
{
    // ・・・中略(先の記事のコードと同じ)・・・

    // 追加
    public class UsersInRole
    {
        public IdentityRole Role { set; get; }
        public IList<MySQLIdentityUser> Users { set; get; }
    }
}

(2) Details アクションメソッドを追加

先の記事のコントローラー RoleController にアクションメソッド Details を追加します。

using System.Collections.Generic;
using System.Linq;
// ・・・中略(先の記事のコードと同じ)・・・

namespace MySQLIdentity.Controllers
{
    public class RoleController : Controller
    {
        // ・・・中略(先の記事のコードと同じ)・・・

        // 以下のアクションメソッドを追加
        public async Task<IActionResult> Details(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var role = await _roleManager.FindByIdAsync(id);
            if (role == null)
            {
                return NotFound();
            }

            var model = new UsersInRole
            {
                Role = role,
                Users = await _userManager.GetUsersInRoleAsync(role.Name)
            };

            return View(model);
        }

        // ・・・中略(先の記事のコードと同じ)・・・
    }
}

(3) ビュー Details.cshtml を追加

@model MySQLIdentity.Models.UsersInRole

@{
    ViewData["Title"] = "Details";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Details</h1>

<h4>Users in Role "@Model.Role.Name"</h4>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Users.First().UserName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Users.First().HandleName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Users.First().PhoneNumber)
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Users)
        {
            <tr>

                <td>
                    @Html.DisplayFor(modelItem => item.UserName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HandleName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.PhoneNumber)
                </td>
            </tr>
        }
    </tbody>
</table>

<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Role.Id }) |
    @Html.ActionLink("Back to List", "Index")
</p>

一覧表示画面 Index の Details リンクをクリックし、Member ロールに属するユーザー一覧を表示したのが上の画像です。

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