WebSurfer's Home

Filter by APML

ASP.NET Core Web API と Swagger(その2)

by WebSurfer 23. October 2024 18:06

Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET 8.0 で ASP.NET Core Web API のプロジェクトを作成し、Visual Studio から実行すると下の画像のようにブラウザ上に swagger/index.html というページが表示され、そこから Web API のアクションメソッドを要求して応答を調べることができます。(追記: 2024/11/25 時点で、ターゲットフレームワーク .NET 9.0 で作成した Web API プロジェクトには Swagger は含まれません)

Swagger

Swagger を使って、(1) ファイルをアップロードする方法、及び (2) ベアラトークンを要求ヘッダに含めて送信する方法を調べましたので、備忘録として残しておくことにしました。

先の記事「ASP.NET Core Web API と Swagger(その1)」では (1) について書きました。この記事では (2) について書きます。

(2) ベアラトークンを要求ヘッダに含める

これについてはネットで検索して見つけた記事 OAuth Bearer Token with Swagger UI — .NET 6.0 が参考になりました。

テンプレートを使って作った ASP.NET Core Web API のプロジェクトの Program.cs に含まれている AddSwaggerGen メソッドを以下のように拡張します。コードに Use baerer token authorization header とコメントしてありますように、Type に SecuritySchemeType.Http を設定するのがポイントです。

//builder.Services.AddSwaggerGen();

builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo 
    { 
        Title = "My Web API", 
        Version = "v1" 
    });

    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Name = "Authorization",

        // Use baerer token authorization header
        Type = SecuritySchemeType.Http,

        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "Please enter token",
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new List<string>()
        }
    });
});

上の OpenApiSecurityRequirement クラスの初期化のコードが見慣れない形になっていますが、それについて説明しておきます。

OpenApiSecurityRequirement クラスは Dictionary<OpenApiSecurityScheme,IList<String>> を継承してます。なので、コレクション初期化子を使用してディクショナリを初期化するのと同様に new Dictionary<TKey, TValue> {{ key1, value 1}, { key2, value2 }} という形でキーと値のペアを Add しながら OpenApiSecurityRequirement クラスを初期化しています。

上のコードを実装すると、この記事の一番上の画像のように、ブラウザに表示される Swagger 画面の右上に赤枠で囲ったように[Authorize]ボタンが表示されるようになります。

それをクリックすると、下の画像のように Available authorizations ダイアログがポップアップ表示されるので、そのテキストボックスにトークンを入力して、ダイアログ上の[Authorize]ボタンをクリックすれば、以降は Swagger からの要求すべてにベアラトークンが要求ヘッダに含まれて送信されるようになります。

Available authorizations ダイアログ

下は Swagger からの要求を Fiddler でキャプチャした画像です。赤枠で示したように要求ヘッダにベアラトークンが含まれて送信されています。

要求ヘッダのベアラトークン

上の操作で設定したベアラトークンを送信しないようにするには、Swagger 画面右上の[Authorize]ボタンをクリックして Available authorizations ダイアログを表示し、[Logout]ボタンをクリックします。

ログアウト

Tags: , , , ,

DevelopmentTools

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

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

About this blog

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

Calendar

<<  January 2025  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar