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

Web API

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

by WebSurfer 20. August 2024 16:47

トークンベース (JWT) の認証を実装した ASP.NET Core Web API アプリで、クライアントから送信されてきた JWT から、ユーザー名やトークンの有効期限などの情報を取得する方法を書きます。

デコードされた JWT

実は最近知ったのですが、先の記事「ASP.NET Core Web API に Role ベースの承認を追加」に書いた JWT による認証を実装した ASP.NET Core Web API アプリでは、上の画像の JWT の Payload 部分は ASP.NET Core で言うクレームのコレクションとして取り扱われるようです。

具体的に言うと、JWT の Payload の項目から ClaimsIdentity オブジェクトが作られ、コントローラーに使われる ControllerBase クラスの User プロパティで取得できる ClaimsPrincipal オブジェクトに ClaimsIdentity が含まれるようになります。

なので、上の画像のように Payload に Role を追加すれば、先の記事に書いたように [Authorize(Roles = "Admin")] 属性によって Web API 側でアクセス制限ができるようになります。

さらに、ClaimsIdentity オブジェクトの各クレームの値は ClaimsPrincipal クラスの FindFirst(String) メソッドを使って取得できますので、必要があればサーバー側でユーザー名とかトークンの有効期限などを取得して、その内容に応じて何らかの処置を行うというようなことも可能になります。

以下に具体例を書いておきます。

まず、上の画像のように JWT の Payload に Role、UserId、exp を含める方法ですが、先の記事「Blazor WASM から ASP.NET Core Web API を呼び出し」で紹介したトークンを発行する API の BuildToken メソッドで、JwtSecurityToken コンストラクタの引数の claims に以下のようにロール情報と UserId を追加します。上の画像の JWT の Payload の "exp" は引数 expires に設定した UNIX 時間となります。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace WebApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TokenController : ControllerBase
    {
        private readonly IConfiguration _config;
        private readonly UserManager<IdentityUser> _userManager;

        public TokenController(IConfiguration config,
                               UserManager<IdentityUser> userManager)
        {
            _config = config;
            _userManager = userManager;
        }

        [AllowAnonymous]
        [HttpPost]
        public async Task<IActionResult> CreateToken(LoginModel login)
        {
            string? id = login.Username;
            string? pw = login.Password;
            IActionResult response = Unauthorized();

            if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(pw))
            {
                var user = await _userManager.FindByNameAsync(id);
                if (user != null && 
                    await _userManager.CheckPasswordAsync(user, pw))
                {
                    // クライアントから送信されてきた id を UserId 
                    // として JWT の Payload に含める
                    var tokenString = BuildToken(id);

                    response = Ok(new { token = tokenString });
                }
            }

            return response;
        }

        private string BuildToken(string userId)
        {
            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"],

                // Role 情報と UserID を追加
                claims: [ 
                    new Claim(ClaimTypes.Role, "Admin"),
                    new Claim("UserId", userId)
                ],

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

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

    public class LoginModel
    {
        public string? Username { get; set; }
        public string? Password { get; set; }
    }
}

Web API のコントローラーで、ControllerBase.User プロパティから取得できる ClaimsPrincipal オブジェクトの FindFirst(String) メソッドを使って、JWT の Payload の情報を取得できます。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace WebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        // ・・・中略・・・

        [Authorize(Roles = "Admin")]
        [HttpGet(Name = "GetWeatherForecast")]
        public IEnumerable<WeatherForecast> Get()
        {
            // 2024/8/20
            // 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 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();
        }
    }
}

クライアントから上の画像の JWT を送信した結果は下の画像の通りとなります。Web API のコントローラーで上の画像の Payload の情報を取得できていることが分かりますでしょうか?

JWT の Payload 情報の取得結果



【オマケ】

本題とは直接関係ないことですが、Microsoft のドキュメント「ASP.NET Core でのクレーム ベースの承認」に ClaimsIdentity というのは何かを、運転免許証を例にとって説明してあって分かりやすかったので、忘れないように以下に抜粋を貼っておきます。

"運転免許証にはあなたの生年月日が記載されています。 この場合、クレーム名は DateOfBirth になり、クレームの値は、たとえば 8th June 1970 となります。そして発行者は、運転免許証機関になります。クレーム ベースの承認では、簡単に言うと、クレームの値がチェックされ、その値に基づいてリソースへのアクセスが許可されます。たとえば、ナイト クラブへの入場 (アクセス) であれば、承認プロセスは次のようになる可能性があります。"

"出入口のセキュリティ責任者が、あなたの生年月日クレームの値と、発行者 (運転免許機関) を信頼するかどうかを評価します。"

ちなみに、ClaimsIdentity クラスの解説は以下のようになっています。

"ClaimsIdentity クラスは、クレームベースの ID の具体的な実装です。つまり、クレームのコレクションによって記述される ID です。クレームは発行者によるエンティティに関する宣言で、プロパティ、権利、またはその他の品質が記述されます。このようなエンティティは、クレームの対象と言われます。クレームは Claim クラスによって表されます。ClaimsIdentity に含まれるクレームは、対応する ID が表すエンティティに記述し、承認と認証の決定を行うために使用できます。"

免許証の例を読んでからでないと分かりにくいかも。何を隠そう、自分はさっぱり分かりませんでした。(笑)

Tags: , , ,

Authentication

プロファイル情報の追加 (.NET 版)

by WebSurfer 26. May 2020 16:23

.NET Framework 版の ASP.NET MVC5 アプリに ASP.NET Identity を利用したユーザー認証を実装して、プロファイル情報としてハンドル名を追加し、ログイン時にページのヘッダ右上にハンドル名(デフォルトはメールアドレス)を表示する方法を書きます。

ハンドル名を表示

先の記事「プロファイル情報の追加 (CORE 版)」の .NET Framework 版です。CORE 版とは基本的なところでの大きな差はありませんが、細かいところでいろいろ違うので備忘録として残しておくことにしました。

(1) プロジェクトの作成

以下の説明は、Visual Studio 2019 を使って Mvc5ProfileInfo という名前で作成した ASP.NET MVC5 アプリのプロジェクトをベースしています。

プロジェクトの作成

デフォルトでは認証は「認証なし」になっていますが、それを「個別のユーザーアカウント」に変更します。それによって ASP.NET Identity を利用したユーザー認証のためのソースコードが一式自動生成されます。

(2) HandleName プロパティの追加

Models/IdentityModels.cs ファイルに IdentityUser を継承した ApplicationUser クラスが定義されています。それに HandleName プロパティを追加します。以下の画像の赤枠部分がそれです。

HandleName プロパティの追加

そのあと Migration 操作を行うと Entity Framework Code First の機能によりデータベースには HandleName フィールドが作られます。HandleName フィールドはデータアノテーション属性の Required により NULL 不可に、StringLength の引数 256 により nvarchar(256) になります。

(3) Enable-Migrations / Add-Migration

Visual Studio のパッケージマネージャーコンソールで Enable-Migrations コマンドを実行します。成功するとプロジェクトのルート直下に Migrations フォルダが生成され、その中に Configuration.cs という名前のクラスファイルが生成されます。

次に、Add-Migration Initial コマンドを実行します(Initial という名前は任意です)。成功すると Migrations フォルダに xxxxx_Initial.cs という名前のクラスファイルが生成されます。(xxxxx は作成日時)

Initial.cs

その中に、dbo.AspNetUsers テーブルを SQL Server に生成するコードがあります。上の画像の赤枠のコードを見てください。ステップ (2) で HandleName プロパティに付与した属性に従って HandleName というフィールドを NULL 不可、nvarchar(256) で生成するようになっています。

(注: この記事ではプロジェクト作成直後のデータベースには何もない状態から Migration 関係の操作を行っています。データーベースがすでに生成済みで、それに HandleName を追加する場合とは手順が違うので注意してください)

(4) Update-Database

Visual Studio のパッケージマネージャーコンソールで Update-Database コマンドを実行します。成功すると ASP.NET Identity 用のデータベースが Entity Framework Code First の機能によって追加されます。

ASP.NET Identity 用のデータベース

この記事では Visual Studio でプロジェクトを作成したときに自動生成された web.config をそのまま使っています。その中に "DefaultConnection" という名前で LocalDB に接続する接続文字列が定義されており、aspnet-Mvc5ProfileInfo-20200526103410.mdf という名前(数字はプロジェクトの作成日時)の .mdf ファイルを動的に LocalDB にアタッチして使うように設定されています。

Models/IdentityModels.cs ファイルには IdentityDbContext<ApplicationUser> を継承したコンテキストクラス ApplicationDbContext が定義されており、web.config の接続文字列 "DefaultConnection" を使うように設定されています。

Update-Database コマンドによって、web.config の接続文字列で LocalDB に接続し、指定された名前のデータベースを LocalDB に作成し、ステップ (3) で生成されたクラスファイル xxxxx_Initial.cs に従って ASP.NET Identity 用のテーブルを生成します。

その結果が上の画像です。aspnet-Mvc5ProfileInfo-20200526103410 という名前のデータベースが作成され、その中の dbo.AspNetUsers テーブルにステップ (2) で HandleName プロパティを追加したとおり、HandleName フィールドが NULL 不可 nvarchar(256) で生成されています。

(5) Register アクションメソッドの修正

Controllers/AccountController.cs の Register アクションメソッドに以下の画像の赤枠のコードを追加します。登録時にハンドル名を自動的に Email と同じになるようにするものです。

Register アクションメソッドの修正

ステップ (4) の画像の通り dbo.AspNetUsers テーブルの HandleName フィールドは NULL 不可になっています。そのため、新規ユーザーの登録を行う際 HandleName を入力しないと CreateAsync でエラーとなります。なので、初回登録時はとりあえずハンドル名は Email と同じになるようにしました。

登録時にユーザーにハンドル名を決めて入力してもらうようにもできますが、そういう手間を増やすと嫌がられそうですし、ユーザーによってはハンドル名は Email と同じでよいという人もいるでしょうから。

ここまで実装できれば、アプリケーションを実行してユーザー登録が可能になり、登録操作を行うと dbo.AspNetUsers テーブルの HandleName フィールドにはメールアドレスと同じ文字列が設定されているはずです。

(6) ハンドル名の変更機能を実装

ログイン後、ページのヘッダの右上のユーザー名(デフォルトで Email)をクリックすると Manage/Index ページに遷移しますが、そこからさらにリンクをたどって別ページに飛んで、そこでユーザーがハンドル名を任意に変更できるようにします。

ハンドル名の変更は、Manage コントローラーに ChangeHandleName という名前のアクションメソッドを追加しそこで行うようにします。

まず、Manage/Index ページのビュー Views/Manage/Index.cshtml に ChangeHandleName アクションメソッドへのリンクを追加します。

ChangeHandleName へのリンク追加

変更が完了した際に Manage/Index ページに表示するためのメッセージを追加します。Controllers/ManageController.cs ファイルを開いて以下の画像の赤枠のコードを追加します(2 箇所)。

メッセージを追加

メッセージを追加

Models/ManageViewModels.cs に ChangeHandleName アクションメソッドとビューの間でデータをやり取りするための ChangeHandleNameViewModel クラスを追加します。入力できるハンドル名の長さはとりあえず 30 文字に制限してみました。

// HandleName 変更用に追加
public class ChangeHandleNameViewModel
{
    [Required]
    [Display(Name = "旧ハンドル名")]
    public string OldHandleName { get; set; }

    [Required(ErrorMessage = "{0}は必須")]
    [StringLength(30, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "新ハンドル名")]
    public string NewHandleName { get; set; }
}

Views/Manage/Index.cshtml に ChangeHandleName アクションメソッドのコードを追加します。旧ハンドル名を入力できるようになっていたり、いきなり例外をスローするところがちょっと乱暴かもしれませんが、検証用ということで・・・

// GET: /Manage/ChangeHandleName
public async Task<ActionResult> ChangeHandleName()
{
    var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user == null)
    {
        throw new InvalidOperationException(
            "ログイン中のユーザー情報が削除されました。");
    }

    var model = new ChangeHandleNameViewModel();
    model.OldHandleName = user.HandleName;
    return View(model);
}

// POST: /Manage/ChangeHandleName
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ChangeHandleName(ChangeHandleNameViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user == null)
    {
        throw new InvalidOperationException(
            "ログイン中のユーザー情報が削除されました。");
    }

    if (model.OldHandleName != user.HandleName)
    {
        throw new InvalidOperationException(
            "DB に既存のハンドル名と旧ハンドル名が一致しません。");
    }

    user.HandleName = model.NewHandleName;
    var result = await UserManager.UpdateAsync(user);
    if (result.Succeeded)
    {
        return RedirectToAction("Index", 
            new { Message = ManageMessageId.ChangeHandleNameSuccess });
    }
    AddErrors(result);
    return View(model);
}

アクションメソッドを追加したら、スキャフォールディングのデザイナで[テンプレート(T)]を Edit とし[モデルクラス(M)]を上に定義した ChangeHandleNameViewModel としてビューを追加します。コードは自動生成されたものがほぼそのまま使えますのでこの記事に書くのは割愛します。

(7) ハンドル名の変更操作

ユーザーがログインするとページのヘッダの右上に Hello xxxxx! と表示され、それが Manage/Index ページへのリンクになります。それをクリックして Manage/Index ページを表示し、その中のリンク[ハンドル名の変更]をクリックするとすると Manage/ChangeHandleName ページに遷移します。

ChangeHandleName ページ

上の[新ハンドル名]テキストボックスに新しいハンドル名を入力し[Save]ボタンをクリックするとデータベースの HandleName フィールドが更新され、その後 Manage/Index ページにリダイレクトされます。

Manage/Index ページにリダイレクト

赤枠で示した部分にステップ (6) で追加したメッセージが表示されているところに注目してください。

(8) ClaimsIdentity へ追加

ページのヘッダの右上に Hello xxxxx! と表示される xxxxx にはデフォルトではメールアドレスが表示されますが(ステップ (7) の画像参照)、それをハンドル名に変更します。結果、この記事の一番上の画像のようになります。

プロファイル情報を Claim として ClaimsIdentity オブジェクトに追加し、拡張メソッドを使って ClaimsIdentity オブジェクトからハンドル名を取得・表示するようにしています。

プロジェクト作成時に自動生成された Models/IdentityModels.cs ファイルの ApplicationUser クラスにそのコードを書く場所がコメントで「ここにカスタム ユーザー クレームを追加します」と示されているので、そこに追加します。以下の画像の上の赤枠部分の通りです。

カスタムユーザークレーム

ClaimsIdentity オブジェクトからハンドル名を取得する拡張メソッドもそこに書いておきます。画像の下の赤枠のコードがそれです。

画像には表示されていませんが、using 句で名前空間 System.Linq と System.Security.Principal を取り込むようにしてください。

拡張メソッドは名前空間をインポートすればスコープの中に取り込むことができます。この記事の一番上の画像のようにレイアウトページの右上に表示する場合は Views/Shared/_LoginPartial.schtml に名前空間 Mvc5ProfileInfo.Models を取り込んで User.Identity.GetHandleName() というコードで取得します。

ClaimsIdentity オブジェクトにハンドル名を追加すると、認証クッキーにハンドル名が含まれるようになります。認証クッキーからハンドル名を取得できれば、毎回データベースにクエリを投げて取得するより負荷は軽い(であろう)というのが ClaimsIdentity を使う理由です。

Tags: , , ,

Authentication

About this blog

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

Calendar

<<  January 2026  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar