ASp.Net Core でPostgreSQLを利用してIdentityで認証を使えるようにする（２）

　前回Identityを組み込んでDBをPostgreSQLにしたプロジェクトに、オートマイグレーションを設定し初期のロールと管理ユーザーを登録します。

# 全体
　・[プロジェクトの作成からデータベースの設定まで（前回）](https://qiita.com/nosa67/items/49ac036a15a4f7deed8b)
　・オートマイグレーションとロールと管理ユーザーの登録（本稿）
　・[不要な認証用ページの削除](https://qiita.com/nosa67/items/5893fc7782c3436235a8)

## -オートマイグレーションとロールと管理ユーザーの登録-

### 環境
- VisualStudio2019 Ver.16.2.3
- ASP.NET Core 2.2
- PostgreSQL 9.6 インストール済み、接続用のアカウント作成済み
- EntityFramework

### データベースの削除
　前の回で「Update-Database」を実行してデータベースができている場合は今回の自動マイグレーションの為に削除しておきます。作成していない場合は何もする必要はありません。

>**【補足１】認証(~~Identity~~ Authentication)と承認(~~Authentication~~Authorization)についての整理**
　これは、私の頭の整理ですので「当たり前えじゃん！」という人は飛ばしてください。読んで「間違ってるデー」という人は教えてください。
　マイクロソフトのページを見ていると、~~Identity~~ Authenticationを日本語では「認証」としていて、~~Authentication~~Authorizationを「承認」としているようです。なんか、似ているので読んでいると混乱してしまうのですが...。
　認証(~~Identity~~ Authentication)は、利用者を特定する、すなわちログインを行うための機能のことを指しており、特定したことを利用して表示するページに制限をかける機能を承認(~~Authentication~~Authorization)としているようです。そう考えて、メソッドを見ているとすっきりとすると思います。とにかくIdentityの中身は自動で行われていることが多く、改造しようとすると目的の機能がどこにあるのかわかりにくいのでこのことを抑えていると、見つけやすいかと思います。
※認証と承認の英語が間違ってました。でも、マイクロソフトのページも間違ってる（「承認」が「認可」になってたりするけど自動翻訳だから仕方ないか）ので、最後はきっちり英語で読む必要がありそう。

### ロール機能の追加
　「Startup.cs」でIdentitiyサービスにロールの機能を追加します。
　さらに、「 services.AddMvc()」の部分を変更して基本的なページは全て認証しないと表示できないようにしてしまいます。

```C#:Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));

            services.AddDefaultIdentity<IdentityUser>()
                .AddDefaultUI(UIFramework.Bootstrap4)
                .AddRoles<IdentityRole>()                                   <<== ロールを利用するためにの追加
                .AddEntityFrameworkStores<ApplicationDbContext>();

            // 認証機能を追加する
            services.AddMvc(config =>
            {
                // このフィルターを追加することで、すべてのページが原則認証されているないと表示できなくなる。
                // ログインページなどは認証されていなくても表示できるようにするため、ページモデルに
                //  [AllowAnonymous]が必要となるが、デフォルトで設定されている。
                var policy = new AuthorizationPolicyBuilder()
                                 .RequireAuthenticatedUser()
                                 .Build();
                config.Filters.Add(new AuthorizeFilter(policy));       
            }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }
```

### 初期のロールとシステム管理ユーザーの作成
　初期のロールとユーザーを作るために以下のクラスを作成します。
　フォルダ構成をどうしようか悩みました（Identityの下にするか、DataやMigrationの下かなど）が、新たに「Authorization」フォルダを作成して以下のクラスを作りました。ここでは２つのロールと１つのシステムユーザーを作っていますが、自身の環境に合わせて変更してください。

```C#:UserRollInitialize.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

namespace WebApplication1.Authorization
{
    public static class UserRollInitialize
    {
        // 初期化時のロール
        public static readonly string SystemManagerRole = "SystemManager";      // システム管理権限
        public static readonly string GroupManagerRole = "GroupManager";        // グループ管理権限

        // 初期化時のシステム管理ユーザーID
        public static readonly string StstemManageEmail = "system@test.com";    // 最初のシステム管理ユーザーのメールアドレス
        public static readonly string StstemManagePassword = "!initialPassword01";    // 最初のシステム管理ユーザーの初期パスワード

        /// <summary>
        /// ユーザーとロールの初期化
        /// 　初期のシステムユーザーあが存在しない場合のみ内容が実行される。存在する場合は何もせずに終了
        /// </summary>
        /// <param name="serviceProvider"></param>
        public static async Task Initialize(IServiceProvider serviceProvider)
        {
            // ユーザー管理を取得(using Microsoft.Extensions.DependencyInjectionがないとエラーになる)
            var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

            // 初期のユーザーマネージャーが存在しなければロールの作成と初期システムユーザーを作成する
            var systemManager = await userManager.FindByNameAsync(StstemManageEmail);
            if (systemManager == null)
            {
                // ロール管理を取得
                var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

                // ロールの追加
                await roleManager.CreateAsync(new IdentityRole(SystemManagerRole));    // システム管理ロール
                await roleManager.CreateAsync(new IdentityRole(GroupManagerRole));    // グループ管理ロール

                // 初期システム管理者の作成
                // なぜか知らないが、デフォルトのログイン画面はユーザーIDではなくメールアドレスを要求し、バリデーションもメールで設定されている。
                // ところが、ログイン処理自体は「Email」ではなく「UserName」で行われるので両方に設定せざるを得ない。
                // なんでこんなことのなっているのか？　変更するにはログイン画面を変えればいい
                systemManager = new IdentityUser { UserName = StstemManageEmail, Email = StstemManageEmail };
                await userManager.CreateAsync(systemManager, StstemManagePassword);

                // システム管理ユーザーにシステム管理ロールを追加
                systemManager = await userManager.FindByNameAsync(StstemManageEmail);
                await userManager.AddToRoleAsync(systemManager, SystemManagerRole);
            }
        }
    }
}

```

さらにプロジェクト直下の「Program.cs」を以下のように変更して、自動マイグレーションと初期データの作成を実施しています。

```C#:Program.cs
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using WebApplication1.Authorization;
using WebApplication1.Data;

namespace WebApplication1
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // CreateWebHostBuilder(args).Build().Run();　<= もともとこの１行だけのはず
            // 上の１行のコードを以下のように変更

            var host = CreateWebHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                // サービスプロバイダーの取得
                var services = scope.ServiceProvider;

                // データベースの自動マイグレーション
                var context = services.GetRequiredService<ApplicationDbContext>();
                context.Database.Migrate();

                // 初期のユーザーとロールの作成
                UserRollInitialize.Initialize(services).Wait();
            }

            host.Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
}

```

　ここでは
    // データベースの自動マイグレーション
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
の部分で自動マイグレーションを実行しています。自動マイグレーションはデータベースの状態と最新の舞グレード状態を判定して自動的にデータベースを更新してくれます。データベース項目にかかわるエンティティーを変更した場合はパッケージマネージャコンソールから「Add-Migration 」を実行しておくだけになります。
　また、初期データの登録として
    // 初期のユーザーとロールの作成
    UserRollInitialize.Initialize(services).Wait();
を実行することで、初期のロールとユーザーを作成しています。初期ユーザーが登録されているかどうかで判定して実行しているの、初期ユーザーを削除することがあるならロジックを変更する必要があります（初回だけ動作するように条件付けが必要。例えばDB項目に判定用のテーブルを作成し、初期化済みフラグを用いるなど）。さらにデータベースのバージョンが更新された時に実施する必要のあるDBの変更処理も、条件を付けて実行するようにしていくことで、DBの状態を適切に保つ必要があります。

### ロールの変更とロール動作の確認
　ロールの確認のために一旦このままコンパイルしてデバッグを実行します。
デバッグを実行すると、データベースが自動で作成され初期のロールとユーザーが作成され、以下の画面が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482873/cc4f5790-ccd3-e5f7-e8ef-df6cdb9d26cf.png" width="300p">

　先に作ったユーザーのメールアドレスとパスワードでログインすると、home画面が表示されます。
　画面上部の「Privacy」をクリックして画面が表示されることを確認しておいてください。

　一旦デバッグを終了し、「/Pages/PrivacyModel.html.cs」のモデルクラスの前にロールの承認「[Authorize(Roles = "GroupManager")]」を追加します。

```C#:PrivacyModel.html.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApplication1.Pages
{
    [Authorize(Roles = "GroupManager")]    
    public class PrivacyModel : PageModel
    {
        public void OnGet()
        {
        }
    }
}
```

　コンパイルしてデバッグを実行し、先ほどと同じように「system@test.com」でログインし画面上の「Privacy」をクリッすると「Access Denied!」のページが表示されます。これは先ほど設定したロールをこのユーザーが持っていないからです。
　先ほどの「PrivacyModel.html.cs」の変更で「[Authorize(Roles = "SystemManager")] 」とするか、「GroupManager」のロールを与えると表示できるようになります。

>**【補足２】２つの承認方法「ロール」「ポリシー」と「クレーム」**
　今回はわかりやすいロールを承認に利用しました。
　「ポリシー」を利用した承認は、ルールや保管の条件を複数足し合わせたような条件での承認を利用する場合に用い、あらかじめ作成したポリシーの名前を利用して「[Authorize(Policy = "MyPolicy")]」と記述して利用でします。機会が有ればその記述方法を書こうかと思います。
　マイクロソフトのページではさらに同じような立ち位置で「クレーム」という言葉が出てきますが、これは利用者情報の情報に指定した項目が存在するかどうか、またはその値が指定した値のリストなどに含まれているかどうかなどの条件を記述するもので、「ロール」や「ポリシー」のように直接記述するものではなく、「ポリシー」の一つの条件として設定できるもののようです。
　ちなみに[Authorize]は単純に認証されていれば誰でも表示できる承認であう。
　認証していなくても見ていいページは[AllowAnonymous]とします。

>**【補足３】その他の承認方法**
　今回は全体の承認の設定と、承認の除外（Loginページが除外されていること）、ロールによる承認について記述しました。
　基本的にはこれでも十分できるのですが、フォルダーやエリア単位でのポリシーの設定、ビューでの承認などほかにもいろいろ有るようなので、おいおい記述していける機会が有ればと思います。

[私のブログページ(https://nosa67.hatenablog.com/)](https://nosa67.hatenablog.com/)