MiniAPI（九）：ロールベースの認証と認可

認証とは、ユーザーが資格情報を提供し、それをオペレーティングシステム、データベース、アプリケーション、またはリソースに格納されている資格情報と比較するプロセスです。認可プロセスでは、資格情報が一致した場合、ユーザーの認証が成功し、権限を与えられた操作を実行できます。認可とは、ユーザーが実行できる操作を決定するプロセスです。認証は、サーバー、データベース、アプリケーション、またはリソースなどの空間への入場方法と考えることができ、認可はその空間（サーバー、データベース、アプリケーション）内で、ユーザーがどのオブジェクトに対してどのような操作を行うことができるかを指します。
マイクロソフト公式ドキュメント
asp.net coreは、多くの認可方式をサポートしています。この記事では、JWTに基づくロールベースの認可方式について説明します。
JWTに基づくロール認証と認可のアイデアは、ログイン時に暗号化されたTokenを配布し、リソースへのアクセス時にこのTokenを持っていることです。サーバーはこのTokenが自分で配布したものかを検証し、次にアクセス範囲が正しいかどうかを検証します。本記事の範囲は、どのリソースがどのロールでアクセスできるか、Tokenを受け取ったユーザーが現在どのロールであるか、すなわちロールとリソースのマッチングです。
asp.net coreでの実装手順：
1. appsettings.jsonにJWTパラメータを設定
2. 認証と認可サービスおよびミドルウェアを追加
3. Token生成メソッドとTokenパラメータ検証メソッドを定義
4. ログイン時に身分を検証し、Tokenを配布

```csharp
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

var builder = WebApplication.CreateBuilder();
// JWTパラメータを取得しサービスコンテナに注入
var jwtConfig = new JWTConfig();
builder.Configuration.GetSection("JWTConfig").Bind(jwtConfig);
builder.Services.AddSingleton(jwtConfig);
// JWT方式の認証と認可を追加
builder.Services
    .AddAuthorization()
    .AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, opt =>
    {
        opt.RequireHttpsMetadata = false;
        opt.TokenValidationParameters = JwtToken.CreateTokenValidationParameters(jwtConfig);
    });

var app = builder.Build();
// 認証と認可のミドルウェアを使用
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/hellosystem", (ILogger<Program> logger, HttpContext context) =>
{
    var message = $"hello,system,{context.User?.Identity?.Name}";
    logger.LogInformation(message);

    return message;
}).RequireAuthorization(new RoleData { Roles = "system" });

app.MapGet("/helloadmin", (ILogger<Program> logger, HttpContext context) =>
{
    var message = $"hello,admin,{context.User?.Identity?.Name}";
    logger.LogInformation(message);
    return message;
}).RequireAuthorization(new RoleData { Roles = "admin" });

app.MapGet("/helloall", (ILogger<Program> logger, HttpContext context) =>
{
    var message = $"hello,all roles,{context.User?.Identity?.Name}";
    logger.LogInformation(message);
    return message;
}).RequireAuthorization(new RoleData { Roles = "admin,system" });

// ログイン成功し、Tokenを配布
app.MapPost("/login", [AllowAnonymous] (ILogger<Program> logger, LoginModel login, JWTConfig jwtConfig) =>
{
    logger.LogInformation("login");
    if (login.UserName == "gsw" && login.Password == "111111")
    {
        var now = DateTime.UtcNow;
        var claims = new Claim[] {
            new Claim(ClaimTypes.Role, "admin"),
            new Claim(ClaimTypes.Name, "桂素伟"),
            new Claim(ClaimTypes.Sid, login.UserName),
            new Claim(ClaimTypes.Expiration, now.AddSeconds(jwtConfig.Expires).ToString())
        };
        var token = JwtToken.BuildJwtToken(claims, jwtConfig);
        return token;
    }
    else
    {
        return "username or password is error";
    }
});

app.Run();
```
// ログインエンティティ
```csharp
public class LoginModel
{
    public string? UserName { get; set; }
    public string? Password { get; set; }
}
```
// JWT設定
```csharp
public class JWTConfig
{
    public string? Secret { get; set; }
    public string? Issuer { get; set; }
    public string? Audience { get; set; }
    public int Expires { get; set; }
}
```
// JWTの操作型
```csharp
public class JwtToken
{
    // Tokenを取得
    public static dynamic BuildJwtToken(Claim[] claims, JWTConfig jwtConfig)
    {
        var now = DateTime.UtcNow;
        var jwt = new JwtSecurityToken(
            issuer: jwtConfig.Issuer,
            audience: jwtConfig.Audience,
            claims: claims,
            notBefore: now,
            expires: now.AddSeconds(jwtConfig.Expires),
            signingCredentials: GetSigningCredentials(jwtConfig)
        );
        var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
        var response = new
        {
            Status = true,
            AccessToken = encodedJwt,
            ExpiresIn = now.AddSeconds(jwtConfig.Expires),
            TokenType = "Bearer"
        };
        return response;
    }

    static SigningCredentials GetSigningCredentials(JWTConfig jwtConfig)
    {
        var keyByteArray = Encoding.ASCII.GetBytes(jwtConfig?.Secret!);
        var signingKey = new SymmetricSecurityKey(keyByteArray);
        return new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
    }
    // Tokenのパラメータを検証
    public static TokenValidationParameters CreateTokenValidationParameters(JWTConfig jwtConfig)
    {
        var keyByteArray = Encoding.ASCII.GetBytes(jwtConfig?.Secret!);
        var signingKey = new SymmetricSecurityKey(keyByteArray);
        return new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = signingKey,
            ValidateIssuer = true,
            ValidIssuer = jwtConfig?.Issuer,
            ValidateAudience = true,
            ValidAudience = jwtConfig?.Audience,
            ClockSkew = TimeSpan.Zero,
            RequireExpirationTime = true,
        };
    }
}
```
// 認証権限のパラメータ型を追加するmini api
```csharp
public class RoleData : IAuthorizeData
{
    public string? Policy { get; set; }
    public string? Roles { get; set; }
    public string? AuthenticationSchemes { get; set; }
}
```
検証結果：
1. ログインせずに401を返す
![alt 画像](https://mmbiz.qpic.cn/mmbiz_png/4CAA63nvHRAlojKvgR3l5Hd63JHW74GI73LF0EXI8Y9Hodgz0px18jPcvFdxdXicq1eZWHMyibHnq77pxOojAcxg/640?wx_fmt=png)
2. ログインしてtokenを取得
![alt 画像](https://mmbiz.qpic.cn/mmbiz_png/4CAA63nvHRAlojKvgR3l5Hd63JHW74GIbnY9sJ800z2S1DTH3pEoAdOdQO8r24nLibmWw43t2Zo4jX5IJe5faMA/640?wx_fmt=png)
3. 正しいアクセス
![alt 画像](https://mmbiz.qpic.cn/mmbiz_png/4CAA63nvHRAlojKvgR3l5Hd63JHW74GItbp3vGdz5icAGSG555T0re1XD9oiclZFlEXrKKTCrKozyjBaVDoX6W7A/640?wx_fmt=png)
4. 認可されていないアクセスで403を返す
![alt 画像](https://mmbiz.qpic.cn/mmbiz_png/4CAA63nvHRAlojKvgR3l5Hd63JHW74GIicoVgYDyvTgpCQpIOYaXUV6ZoFf4aDVEhRWIZSicF8hMD8dUOsceFiaOQ/640?wx_fmt=png)

(Translated by GPT)

元のリンク：https://mp.weixin.qq.com/s?__biz=MzA3NDM1MzIyMQ==&mid=2247484878&idx=1&sn=3a51bed090a5c33a784300d5d8d80f87&chksm=9f005ae4a877d3f27616a637a95688efc3699eb33ed8440dca17c80f08aa9baa08569813b72c&token=1806044876&lang=zh_CN#rd