ASP.NET Core MVC に Identity で認証機能を追加する

## やること
ASP.NET Core MVC の MVC アーキテクチャに Identity を組み込んで認証機能を追加します。
Visual Studio で提供される MVC + Identity のテンプレートでは MVC に Razor Pages を追加する形になっていて気持ち悪い...全部 MVC に組み込みたい！ということでこのようなことをやります。
開発環境は Visual Studio for Mac です。

## 作るもの
認証機能付きの簡単な ToDo アプリを作ってみます。
仕様は以下のようにします。

- アカウントは名前とパスワードのみで登録
- 各アカウントが登録するタスクは独立している
    - つまり、A さんが登録するタスクは B さんから見ることができない。逆も然り。
- CRD だけ実装して、U は実装しない（別にしてもいいけど、省略）


## やっていくぞ

### 1. 新規プロジェクトを作成
web アプリケーション (MVC), .NET Core3.1, 認証なし, と選択していきます。
![スクリーンショット 2021-04-29 14.57.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/953258/4414234c-93ad-4b80-aa07-dc1a180a7870.png)
![スクリーンショット 2021-04-29 14.57.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/953258/fe29f4a8-70fb-ee88-44bf-b6aeadab6b91.png)

### 2. NuGet パッケージ
必要になるものをあらかじめインストールしてしまいましょう。  
以下の 6 つをインストールします。.NET Core 3.1 でやってるので、各パッケージのバージョンも 3.1.x で設定しておきます。

- AspNetCore.Identity
- AspNetCore.Identity.EntityFramework
- AspNetCore.Identity.UI
- EntityFrameworkCore
- EntityFrameworkCore.Design
- EntityFrameworkCore.Sqlite

![スクリーンショット 2021-05-01 16.14.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/953258/a27973b5-96e0-e1ab-0e16-dc47b8e0ce73.png)

### 3. データベースコンテキストの作成
各ユーザーの認証情報はデータベースに格納されるので、まずはデータベースを使えるようにするところから。
普通にデータベース作るだけなら、`DbContext` を継承したクラスを作ればいいのですが、Identity で使うデータベースコンテキストは `IdentityDbContext` を継承します。このクラスには `Users` とか、認証情報を格納する `DbSet` がすでにプロパティに入っているので、継承さえしてしまえば OK です。

```cs:ToDoContext.cs
namespace ToDoIdentity.Data
{
    public class ToDoContext : IdentityDbContext
    {
        public ToDoContext(DbContextOptions<ToDoContext> options)
                : base(options) { }
    }
}
```

### 4. Identity を使うために Startup.cs を編集
ここがややこしい...~~ぶっちゃけおまじない状態~~

#### Startup.ConfigureServices
やることは 3 つ。

- データベースコンテキストを使えるようにする
- Identity を使えるようにする
- 認証機能の設定をする

`ConfigureServices` に以下のようなコードを追加します。

```cs:Startup.cs
services.AddDbContext<ToDoContext>(options =>
    Configuration.GetConnectionString("DefaultConnection")));

// Identity を追加
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = false)
    .AddEntityFrameworkStores<ToDoContext>();

// Identity を設定
services.Configure<IdentityOptions>(options =>
{
    // Password settings.
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = false;
    options.Password.RequiredLength = 6;
    options.Password.RequiredUniqueChars = 1;

    // Lockout settings.
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;

    // User settings.
    options.User.AllowedUserNameCharacters = null; // UserName に使える文字の制限をなしにする
    options.User.RequireUniqueEmail = false;
});

services.ConfigureApplicationCookie(options =>
{
    // Cookie settings
    options.Cookie.HttpOnly = true;
    options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

    options.LoginPath = "/Identity/Login";
    options.AccessDeniedPath = "/Identity/AccessDenied"; //AccessDenied は未実装
    options.SlidingExpiration = true;
});
```

`services.AddDefaultIdentity<IdentityUser>` で、ユーザー情報として使うクラスを設定します。ここではデフォルトの `IdentityUser` を使っていますが、`IdentityUser` を継承したクラスでも設定できます（詳しくは[このへん](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-5.0#customize-the-model)を見ると良い）
Identity の設定は[これ](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/identity-configuration?view=aspnetcore-5.0)を見れば良い。

#### Startup.Configure
`app.UseAuthentication()`を `app.UseRouting()` と `app.UseAuthorization()` の間に入れます。

```cs:Starup.cs
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();
```

これで、[詳しいこと](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0)はよくわからんが、HTTP リクエストが認証ミドルウェアを通るようになるらしい。

### 5. 認証機能の実装
さてここからバシバシとコードを書いていくわけですが、ここまでくれば普通の MVC の考え方を使っていけばいいです。
つまり、Identity の Model, View, Controller を作っていきます。

#### View
ログイン画面とアカウント登録画面を作って、それぞれにフォームを追加するだけ。  
例えばログイン画面はこんな感じ

```html:Login.cshtml
@model IdentityInputModel 
@{
    ViewData["Title"] = "Login";
}

<h1>ログイン</h1>

<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="UserName" class="control-label"></label>
                <input asp-for="UserName" class="form-control" />
                <span asp-validation-for="UserName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Password" class="control-label"></label>
                <input type="password" asp-for="Password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input formmethod="post" type="submit" value="ログイン" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

#### Model
View からフォームで post されたものを格納できれば良いです。

```cs:IdentityInputModel.cs
namespace ToDoIdentity.Models
{
    public class IdentityInputModel
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
}
```

#### Controller
ここはちょっと普通の Controller と違う部分が出てきます。というのも、ここでログインしたりログアウトしたりという処理を行うからです。
まずはコンストラクタの引数で `UserManager` と `SignInManager` のインスタンスを受け取ります。これらはそれぞれ、リクエスト元のユーザー情報を取得するインスタンス、ログイン関係の処理を行うインスタンスです。

```cs:IdentityController.cs
public IdentityController(UserManager<IdentityUser> _userManager,
SignInManager<IdentityUser> _signInManager)
{
        userManager = _userManager;
        signInManager = _signInManager;
}
```

これらを使って、ログイン、ログアウト、アカウント登録を行っていきましょう。

##### ログイン
```cs:IdentityController.cs
var result = await signInManager.PasswordSignInAsync(model.UserName, model.Password, true, false);
```

これでログイン処理を行い、その結果が `result` に格納されます。ログインの成功・失敗によってリダイレクト先を変えたりできます。

##### ログアウト
```cs:IdentityController.cs
await signInManager.SignOutAsync();
```
これだけ！

##### アカウント登録
```cs:IdentityController
var user = new IdentityUser() { UserName = model.UserName };
var result = await userManager.CreateAsync(user, model.Password);
```

こっちは `userManager` を使って行います。ユーザーインスタンスとパスワードを引数に入れて登録です。これも成功したかどうかが `result` に入れられます。


### 6. 普通に ToDo アプリを作っていく
さて、これにて認証機能は実装できたので、あとは ToDo の CR(U)D を実装していくだけですね。
ところが、仕様をもう一度確認してみると
> 各アカウントが登録するタスクは独立している

とあります。
これは、ToDo モデルにユーザー ID を持たせることで実装できそうです。

```cs:ToDoItem.cs
namespace ToDoIdentity.Models
{
    public class ToDoItem
    {
        [Key]
        public int Id { get; set; }
        public string ToDo { get; set; }
        public string UserId { get; set; }
    }
}
```

本当は `UserId` を外部キーにした方がいいんですけど、ここでは省略。

さて、あとは Controller で ToDo モデルに `UserId` を渡すようにすれば OK です。Controller ではユーザーの情報を以下のようにして取得することができます。

```cs:ToDoItemController.cs
var newItem = new ToDoItem()
{
    UserId = userManager.GetUserId(User),
    ToDo = toDoName
};
```

このように `userManager` からユーザーの情報を取得できます。`UserManager` インスタンスは、`IdentityController` でやったのと同様に、コンストラクタの引数で受け取れます。


## 完成！！
![スクリーンショット 2021-05-01 17.10.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/953258/2b0e0bc8-7795-b9be-b062-ddab7d498082.png)
![スクリーンショット 2021-05-01 17.10.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/953258/fcda20fd-7406-d2ff-c22a-3e7d98743705.png)

できた〜！
ということでコードを[こちら](https://github.com/Saza-ku/ToDoIdentity)にあげています。参考にしてみてください！

## おわり
ぶっちゃけ Identity は高機能すぎて使いこなせてないです...ちゃんと勉強しなきゃー