先の記事「ASP.NET Core Minimal API」の続きです。先の記事では Microsoft のチュートリアルに従って基本的な REST API を構築し、CORS の機能を追加しました。今回、それに 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 をインストールします。

(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) のエンドポイントからのデータの取得です。
