.NET 6.0でIdentityフレームワークを使用してJWT認証と許可を実装する

.NET 6.0でIdentityフレームワークを使用してJWT認証と許可を実装する

ASP.NET Core 6.0 Web APIを使用して複数のファイルをアップロードおよびダウンロードする簡単なプロセスを紹介します。

最終更新 2022/07/25 20:43
Sarathlal Saseendran
読了目安 8 分
カテゴリ
.NET
タグ
.NET C# ASP.NET Core Web API 認証

原文作者:Sarathlal Saseendran

原文链接:https://www.c-sharpcorner.com/article/jwt-authentication-and-authorization-in-net-6-0-with-identity-framework/

翻訳:沙漠尽头的狼(Google翻訳による補助)

はじめに

Microsoft は 2021 年 11 月に .NET 6.0 をリリースしました。私はすでに C# CornerJWT 認証に関する記事をいくつか執筆しています。.NET 6.0 ではいくつかの大きな変更が行われたため、.NET 6.0 バージョンを使用した JWT 認証 に関する記事を書くことにしました。ユーザーとロール情報の保存には Microsoft Identity フレームワークを使用します。

Authentication(認証)はユーザーの資格情報を検証するプロセスであり、Authorization(承認)はアプリケーション内の特定のモジュールにアクセスするためのユーザーの権限を確認するプロセスです。この記事では、JWT 認証を実装して ASP.NET Core Web API アプリケーションを保護する方法について説明します。また、ASP.NET Core で承認を使用してアプリケーションのさまざまな機能へのアクセスを提供する方法についても説明します。ユーザー資格情報は SQL Server データベースに保存します(注:MySQL、PostgreSQL などの他のリレーショナルデータベースも使用できます)。データベース操作には EF Core フレームワークと Identity フレームワークを使用します。

JSON Web Token (JWT) は、JSON オブジェクトを使用して関係者間で情報を安全に伝送するための、コンパクトで自己完結型の方法を定義するオープンスタンダード (RFC 7519) です。この情報はデジタル署名されているため、検証および信頼できます。JWT は、秘密鍵(HMAC アルゴリズムを使用)または RSAECDSA の公開鍵/秘密鍵ペアを使用して署名できます。

コンパクトな形式では、JSON Web Tokens はドット (.) で区切られた 3 つの部分で構成されます。

  • Header(ヘッダー)
  • Payload(ペイロード)
  • Signature(署名)

したがって、JWT 形式は通常次のようになります。

xxxx.yyyy.zzzz

JSON Web Token の詳細については、以下のリンクを参照してください。

https://jwt.io/introduction/

Visual Studio 2022 を使用した ASP.NET Core Web API の作成

.NET 6.0 アプリケーションを作成するには Visual Studio 2022 が必要です。Visual Studio 2022ASP.NET Core Web API テンプレートを選択できます。

プロジェクトに適切な名前を付け、.NET 6.0 フレームワークを選択します。

新しいプロジェクトが作成されます。

新しいプロジェクトに以下の 4 つのライブラリをインストールする必要があります。

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • Microsoft.AspNetCore.Authentication.JwtBearer

NuGet パッケージマネージャーを使用して上記のパッケージをインストールできます。

appsettings.json を次の値に変更できます。これには、JWT 認証のデータベース接続詳細とその他の詳細が含まれています。

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "ConnStr": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=JWTAuthDB;Integrated Security=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  },
  "JWT": {
    "ValidAudience": "http://localhost:4200",
    "ValidIssuer": "http://localhost:5000",
    "Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr"
  }
}

新しいフォルダー Auth を作成し、Auth フォルダーの下に ApplicationDbContext クラスを作成して、次のコードを追加します。認証関連のクラスはすべて Auth フォルダーに追加します。

ApplicationDbContext.cs

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace IdentityDemo.Auth
{
    public class ApplicationDbContext : IdentityDbContext<IdentityUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }
}

静的クラス UserRoles を作成し、次の値を追加します。

UserRoles.cs

namespace IdentityDemo.Auth
{
    public static class UserRoles
    {
        public const string Admin = "Admin";
        public const string User = "User";
    }
}

2 つの定数値 AdminUser をロールとして追加しました。必要に応じてロールを追加できます。

クラス RegisterModel を作成します。これは新規ユーザー登録時に使用します。

RegisterModel.cs

using System.ComponentModel.DataAnnotations;

namespace IdentityDemo.Auth
{
    public class RegisterModel
    {
        [Required(ErrorMessage = "ユーザー名は必須です")] public string? Username { get; set; }

        [EmailAddress]
        [Required(ErrorMessage = "メールは必須です")]
        public string? Email { get; set; }

        [Required(ErrorMessage = "パスワードは必須です")] public string? Password { get; set; }
    }
}

クラス LoginModel を作成します。これはユーザーログイン時に使用します。

LoginModel.cs

using System.ComponentModel.DataAnnotations;

namespace IdentityDemo.Auth
{
    public class LoginModel
    {
        [Required(ErrorMessage = "ユーザー名は必須です")] public string? Username { get; set; }

        [Required(ErrorMessage = "パスワードは必須です")] public string? Password { get; set; }
    }
}

Response クラスを作成できます。これはユーザー登録とユーザーログイン後に応答値を返すために使用します。リクエストが失敗した場合もエラーメッセージを返します。

Response.cs

namespace IdentityDemo.Auth
{
    public class Response
    {
        public string? Status { get; set; }
        public string? Message { get; set; }
    }
}

Controllers フォルダーに API コントローラー AuthenticateController を作成し、次のコードを追加します。

AuthenticateController.cs

using IdentityDemo.Auth;
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 IdentityDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthenticateController : ControllerBase
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly IConfiguration _configuration;

        public AuthenticateController(
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager,
            IConfiguration configuration)
        {
            _userManager = userManager;
            _roleManager = roleManager;
            _configuration = configuration;
        }

        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login([FromBody] LoginModel model)
        {
            var user = await _userManager.FindByNameAsync(model.Username);
            if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
            {
                var userRoles = await _userManager.GetRolesAsync(user);

                var authClaims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                };

                foreach (var userRole in userRoles)
                {
                    authClaims.Add(new Claim(ClaimTypes.Role, userRole));
                }

                var token = GetToken(authClaims);

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token),
                    expiration = token.ValidTo
                });
            }

            return Unauthorized();
        }

        [HttpPost]
        [Route("register")]
        public async Task<IActionResult> Register([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError,
                    new Response { Status = "Error", Message = "ユーザーは既に存在します!" });

            IdentityUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError,
                    new Response
                    {
                        Status = "Error", Message = "ユーザーの作成に失敗しました!確認してから再試行してください。"
                    });

            return Ok(new Response { Status = "Success", Message = "ユーザーが正常に追加されました!" });
        }

        [HttpPost]
        [Route("register-admin")]
        public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError,
                    new Response { Status = "Error", Message = "ユーザーは既に存在します!" });

            IdentityUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError,
                    new Response
                    {
                        Status = "Error", Message = "ユーザーの作成に失敗しました!確認してから再試行してください。"
                    });

            if (!await _roleManager.RoleExistsAsync(UserRoles.Admin))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));
            if (!await _roleManager.RoleExistsAsync(UserRoles.User))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.User));

            if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.Admin);
            }

            if (await _roleManager.RoleExistsAsync(UserRoles.User))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.User);
            }

            return Ok(new Response { Status = "Success", Message = "ユーザーが正常に追加されました!" });
        }

        private JwtSecurityToken GetToken(List<Claim> authClaims)
        {
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));

            var token = new JwtSecurityToken(
                issuer: _configuration["JWT:ValidIssuer"],
                audience: _configuration["JWT:ValidAudience"],
                expires: DateTime.Now.AddHours(3),
                claims: authClaims,
                signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
            );

            return token;
        }
    }
}

コントローラークラスに loginregisterregister-admin の 3 つのメソッドを追加しました。registerregister-admin はほぼ同じですが、register-admin メソッドは Admin ロールを持つユーザーを作成するために使用されます。login メソッドでは、ログイン成功後に JWT token を返しています。

.NET 6.0 では、Microsoft は Startup クラスを削除し(注:引き続きこの方法を使用することもできます)、Program クラスのみを残しました。すべての依存関係注入とその他の構成は Program クラスで定義する必要があります。

Program.cs

using IdentityDemo.Auth;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;

// Add services to the container.

// For Entity Framework
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("ConnStr")));

// For Identity
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

// Adding Authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})

// Adding Jwt Bearer
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidAudience = configuration["JWT:ValidAudience"],
        ValidIssuer = configuration["JWT:ValidIssuer"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Secret"]))
    };
});

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

アプリケーションを実行する前に、必要なデータベースとテーブルを作成する必要があります。Entity Framework (EF Core) を使用しているため、以下のデータベース移行コマンドとパッケージマネージャーコンソールを使用して移行スクリプトを作成できます。

add-migration Initial

次のコマンドを使用してデータベースとテーブルを作成します。

update-database

SQL Server オブジェクトエクスプローラー を使用してデータベースを確認すると、データベース内に次のデータベーステーブルが作成されていることがわかります。

データベース移行中に、UserRoleClaims 用に 7 つのテーブルが作成されました。これは Identity フレームワーク用です。

ASP.NET Core Identity は、アプリケーションにログイン機能を追加できるメンバーシップシステムです。ユーザーはアカウントを作成し、ユーザー名とパスワードを使用してログインしたり、Facebook、Google、Microsoft アカウント、Twitter などの外部ログインプロバイダーを使用したりできます。

ASP.NET Core Identity は、SQL Server データベースを使用してユーザー名、パスワード、プロファイルデータを保存するように構成できます。または、Azure テーブルストレージなどの他の永続ストレージにデータを保存するために、独自の永続ストレージを使用することもできます。

WeatherForecast コントローラーに Authorize 属性を追加できます。

アプリケーションを実行し、Postman ツールから WeatherForecastControllerget メソッドにアクセスしてみます。

401 認証エラーが発生しました。これは、コントローラー全体に Authorize 属性を追加したためです。このコントローラーとそのメソッドにアクセスするには、リクエストヘッダーで有効な token を提供する必要があります。

AuthenticateController の登録メソッドを使用して新しいユーザーを作成できます。

入力データは生の JSON 形式で提供しました。

上記のユーザー資格情報を使用してログインし、有効な JWT token を取得できます。

上記の資格情報でログインに成功すると、token を受け取りました。

https://jwt.io サイトを使用して token をデコードし、クレームやその他の情報を確認できます。

承認タブで上記の token 値を Bearer token として渡し、WeatherForecastControllerget メソッドを再度呼び出すことができます。

今回は、コントローラーから値を受け取ることに成功しました。

ロールベースの認可を使用して WeatherForecastController を変更できます。

これで、管理者(Admin)ロールを持つユーザーのみがこのコントローラーとメソッドにアクセスできるようになります。

Postman ツールで同じ token を使用して WeatherForecastController に再度アクセスしてみます。

今回は、401 ではなく 403 拒否エラーが発生しました。有効な token を渡していても、コントローラーにアクセスするための十分な権限がありません。このコントローラーにアクセスするには、ユーザーに Admin ロール権限が必要です。現在のユーザーは一般ユーザーであり、Admin ロール権限はありません。

Admin ロール権限を持つ新しいユーザーを作成できます。AuthenticateController には、同じ目的のためのメソッド register-admin が既にあります。

これらの新しいユーザー資格情報を使用してログインし、新しい token を取得できます。token をデコードすると、ロールが token に追加されていることがわかります。

この token を古い token の代わりに使用して WeatherForecastController にアクセスできます。

これで WeatherForecastController からデータを正常に取得できました。

結論

この記事では、.NET 6.0 ASP.NET Core Web API アプリケーションで JSON Web token を作成し、この token を認証と承認に使用する方法について説明しました。ロールなしのユーザーと Admin ロールを持つユーザーの 2 つのユーザーを作成しました。コントローラーレベルで認証と承認を適用し、これら 2 つのユーザーの異なる動作を確認しました。

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2024/01/19

.NET ベースの FluentValidation 検証チュートリアル

FluentValidationは、.NETベースの検証フレームワークで、オープンソースかつ無料、そしてエレガントです。チェーン操作をサポートし、理解しやすく、機能が充実しています。さらに、MVC5、WebApi2、ASP.NET Coreと深く統合でき、コンポーネント内には十数種類の一般的なバリデーターが用意されており、拡張性が高く、カスタムバリデーターをサポートし、ローカライズ多言語にも対応しています。

続きを読む
同じカテゴリ / 同じタグ 2024/06/20

CodeWF.EventBus:軽量イベントバス、コミュニケーションをよりスムーズに

CodeWF.EventBusは、モジュール間の疎結合通信を実現する柔軟なイベントバスライブラリです。WPF、WinForms、ASP.NET Coreなど、さまざまな.NETプロジェクトタイプに対応しています。シンプルな設計で、コマンドのパブリッシュとサブスクライブ、リクエストとレスポンスを簡単に実装できます。順序付けられたイベント処理により、イベントが適切に処理されることを保証します。コードを簡素化し、システムの保守性を向上させます。

続きを読む