ASP.NET Core 3.1でPostgreSQLを利用してIdentityで認証を使えるようにする

#初めに
　以前、同様の内容をASP.Net Core 2.2の環境で書いてみたのですが、最近3.1の環境に移行するにあたって、気づいた点を書いておきます。元ネタは[ASp.Net Core でPostgreSQLを利用してIdentityで認証を使えるようにする（１）](https://qiita.com/nosa67/items/49ac036a15a4f7deed8b)にありますので、参照してください。

#環境
　今回の環境は以下の通りです。
- VisualStudio2019 Ver.16.6.4
- ASP.NET Core 3.1
- PostgreSQL 12 インストール済み、接続用のアカウント作成済み

##プロジェクトの作成からデータベースの設定まで。
###プロジェクト作成
　ASP.NET Core 2.2の時と同じです。作成時にASP.NET Core 3.1を選んでください。

###Postgres用EntityFrameworkの設定
　「appsettings.json」はASP.NET Core 2.2の時と同じように変更します。
　「Startup.cs」のメソッド「ConfigureServices」は内容が変更されていますが、変更箇所は同じで、「options.UseSqlServer」の部分を「options.UseNpgsql」に変更します。「services.AddMvc()」が「services.AddRazorPages()」とレーザーページ専用のメソッドができているのが面白いです。

####PostgreSQLが古いとエラーになる
　Postgresqlのバージョンには注意が必要です。以前の記事の時のバージョン9.6で「Update-Database」を実行すると、エラーになります（ SqlState: 42601、MessageText: "GENERATED"またはその近辺で構文エラー）。どうやらSQLコマンドに変更があり新しいバージョンでないと対応できない様です。ネットで調べると「SetCompatibilityVersion」メソッドで対応できそうな記事がありましたが、ダメでした。作成されているDB構築のSQLを修正すればいけるでしょうが、後々DBの変更で大変なのでしません。
　また、試しに「Npgsql.EntityFrameworkCorePostgreSQL」を当時のバージョンに落としてみると、今度は「Add-Migration」で失敗しました。
　結論としてはPostgreSQLは新しいもの（おそらく10以上と思うけど試してません。今回は12を使いました）を使ってください。

###でもこのまま「Regist」でユーザーを作っても「Login」でログインできない
　この状態で実行して画面右上の「Regist」で表示された画面でユーザーを登録し、「Login」でログインしようとすると失敗します。
　基本的にこの認証をそのまま使う気はないので対応する必要もないのですが、気になるので調べてみました。「Startup.cs」の「ConfigureServices()」で「services.AddDefaultIdentity<IdentityUser>()」の処理にオプション「options => options.SignIn.RequireConfirmedAccount = true」が設定されていますが、これを消してしまえばいいです。
　このオプションはメールによってIDの確認を行うようで、メール送信の設定をしていないと確認が失敗してログインできません。
　マイクロソフトの「ASP.NET Core でのアカウントの確認とパスワードの回復」には「SendGrid」というメールサービスを利用する方法が書かれており、「SMTP」を利用する方法は推奨しない旨の記述があります。ASP.NET CoreにあったSMTPも使えなくなっているようです（確認してませんが）。
　それでもSMTPを使いたい人向けに別途記事を[「ASP.NET Core 3.1のIdentityで認証の確認メールをSMTPで行う」](https://qiita.com/nosa67/items/f745140b26551c605353)に書いておきます。

###ロールの利用と認証必須の設定
　ロールの追加はASP.NET Core 2.2の時と同じように「services.AddDefaultIdentity()」に対して「AddRoles<IdentityRole>()」で行います。
　承認が未指定のページを全てデフォルトで承認が必要にするためのオプションは、ASP.NET Core 2.2では「services.AddMvc()」の中にオプションとして組み込んでいたのですが、ASP.NET Core 3.1では追加された「services.AddAuthorization」メソッドとそのオプションで行うようです。

```C#:Startup.cs
public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));

            // 認証を有効にする
            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            services.AddDefaultIdentity<IdentityUser>()
                .AddRoles<IdentityRole>() // <<== ロールを利用するためにの追加
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddRazorPages();

            // 基本的に指定のないページはすべて認証が必要になるように設定
            services.AddAuthorization(options =>
            {
                options.FallbackPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
            });
        }
```

　これを設定して実行すると、最初に「WELCOME」のページが表示されず、ログイン画面が表示されるようになります。ログインできると「Privacy」画面が表示され、「Home」をクリックすると「WELCOME」のページが表示されます。

###初期のロールとシステム管理ユーザーの作成
　システムの初期設定として、管理ユーザーと一般ユーザー、システム管理のロールを作成するクラスを以下のように作りました。（ASP.NET Core 2.2と少し変わってます。）

```C#:IdentityUserInitializer.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

namespace WebApplication1.Areas.Identity
{
    public class IdentityUserInitializer
    {
        // 初期化時のロール
        public static readonly string SystemManagerRole = "SystemManager";      // システム管理権限

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

        /// <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));    // システム管理ロール

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

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

                // テスト用の一般ユーザー作成
                var normalUser = new IdentityUser { UserName = NormalUserEmail, Email = NormalUserEmail, EmailConfirmed = true };
                await userManager.CreateAsync(normalUser, NormalUserPassword);
            }
        }
    }
}

```

　これを実行するために「Program.cs」の「Main()」を以下のように変更します。

```C#:Program.cs
        public static void Main(string[] args)
        {
            // これが元の1行
            //CreateHostBuilder(args).Build().Run();

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

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

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

            host.Run();
        }
```

###ロールの変更とロール動作の確認
　ここで一度実行して、ユーザー「user@test.com」/パスワード「!User01」でログインして「Privacy」が表示されることを確認します。
　次に、テンプレートで作成されている「Page/Privacy.cshtml.cs」ファイルを開き、以下のようにクラスにロールアトリビュートを設定します。

```C#:Privacy.cshtml.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace WebApplication1.Pages
{
    [Authorize(Roles = "SystemManager")] <= ロールを追加
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }
}
```

　再度実行して「user@test.com」でログインし、「Privacy」を開いてみると、拒否されます。ユーザー「system@test.com」/パスワード「!initialPassword01」でログインすると「Privacy」が表示されます。

###自動マイグレーション
　システムが変更されてDBが変更された場合に、常に手動で「Update-Database」を実行するのは、リリース時に作業忘れを発生しそうなので、自動マイグレーションを設定します。常に手動でしたい場合は、ここは無視してください。（自動マイグレーションでも「Add-Migration <キー>」はする必要がありますが、これは開発環境だけです。自動マイグレーションを設定しておくと、実環境にリリースした際にその環境で実施する「Update-Database」を省けます）

　「Program.cs」を以下の様に変更します。

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

namespace WebApplication1
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // これが元の1行
            //CreateHostBuilder(args).Build().Run();

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

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

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

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

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

　実施後、データベースをいったん削除して自動的にDBが作成されることを確認してください。

###デフォルトのページを利用しないように設定する
　ASP.NET Core 2.2の時と同じように「AddDefaultIdentity」をやめるのですが、3.0からマイクロソフトのページに「完全な Identity UI ソースの作成」というタイトルで記述されていました（AddMVC()のパターンでしたが）。
　それをもとにして変更したのが以下の部分です。

```C#:Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));

            //// メール送信用のパラメータを「appsettings.json」から抜きしてDIで利用できるようにしている
            //services.Configure<SendMailParams>(Configuration.GetSection("SendMailParams"));

            //// メール送信用のクラスを認証時に利用するメール送信サービスとして登録。これで認証時に確認メールが送信できるようになる
            //services.AddScoped<IEmailSender, MailSender.MailSender>();

            // 認証を有効にする
            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //services.AddDefaultIdentity<IdentityUser>()
            services.AddIdentity<IdentityUser, IdentityRole>()
                //.AddRoles<IdentityRole>() // <<== ロールを利用するためにの追加 <= AddIdentityでは不要みたい
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            // AddMvc()の場合はオプションを設定する必要がありそうだが...
            services.AddRazorPages();

            // これを追加する
            services.ConfigureApplicationCookie(options =>
            {
                options.LoginPath = $"/Identity/Account/Login";
                options.LogoutPath = $"/Identity/Account/Logout";
                options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
            });

            // 基本的に指定のないページはすべて認証が必要になるように設定
            services.AddAuthorization(options =>
            {
                options.FallbackPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
            });
        }
```

ここで、ログインとログアウトとアクセス拒否のページは必要なので、スキャフォールディング（[「ASp.Net Core でPostgreSQLを利用してIdentityで認証を使えるようにする（３）」](https://qiita.com/nosa67/items/5893fc7782c3436235a8)の「Identityをスキャフォールディングしてみる」参照）で、そのページのみ（「Account￥AccessDenied」「Account￥Login」「Account￥Logout」）取り込む。

　これで実行すると、同じようにログイン、ログアウト、アクセス拒否は表示されますが、それ以外の「Regist」は実行されなくなります。URLを直接入力しても、存在しない旨の表示がされます。

　以上、3.1でのidentityのカスタマイズでした。