【画像解説】ASP.NET Core MVCに認証機能Identityを実装する～ログインからメール検証機能まで～

# 前書き
<details><summary><b>ASP.NET Core Identityってなんなん</b></summary>

ASP.NET Core Identityはユーザー認証機能をサポートするAPIです。

もっと雑に言うと、
「ログイン機能の既製品」という感じです。

・アカウント登録
・ログイン
・ログアウト
・閲覧制限

などの単純な機能だけでなく、

・アカウントを有効化する機能
・パスワードを忘れたら再設定する機能
・Twitter、Google、FaceBookのアカウントでログインできる機能

などなど本格的な機能も用意してくれています。

さらに中身的なことだけでなく、UIテンプレートまで用意していくれているので
イチからHTMLを書いて画面をデザインする手間も省けます。

Identityはテンプレートをいじれるようになっています。
特に、本記事で紹介するメール検証機能を実装できたあかつきには
多少は自由にカスタマイズできるレベルになると思います。

</details>
<details><summary><b>既存プロジェクトにパパッとIdentity実装したいのだが</b></summary>

既存プロジェクトに後からIdentityを実装する方法ですが...
現在まだ楽な方法を見つけられておりません…。

私はネット先人たちの見よう見まねで
・Identityを半分ぐらい使いつつ半分ぐらい自前で実装する
・Identityを全く使わずに完全に自前実装する
・新規のIdentityプロジェクトに既存プロジェクトのデータをマージする

などを試しましたが、
ピヨグラマ的に一番マシなのは3つ目の方法でしたね…。

開発は計画的に。
</details>

#### 環境
Windows10
Visual Studio 2022 Community
Microsoft SQL Server 2019
.NET 6.0
C# 10.0

# 目次
この記事では
Identityのテンプレートを利用し、

[**【STEP1】アカウント登録/ログイン/ログアウト機能を実装する**](#step1アカウント登録ログインログアウト機能を実装する)
─最も基本の認証機能を作る
[**【STEP2】ページに閲覧制限をかける**](#step2ページに閲覧制限をかける)
─ログインしてからでないと閲覧できないページを作る
[**【STEP3】メール検証機能を実装する**](#step3メール検証機能を実装する)
─登録したメールアドレスに検証リンクを送信する機能を作る

というところまで、手を動かせば初心者でもできるように説明します。

:::note info
時間のない方や、仕組みが理解できなくていいからとりあえず手を動かして作ってみたい方は
これ以降、この緑の枠内は読み飛ばしてください。
:::

では早速実装していきましょう。

## 【STEP1】アカウント登録/ログイン/ログアウト機能を実装する
### [1/3] 新規プロジェクト作成時に"認証の種類"を選択する
Identityをフル実装するには最初が肝心です。
とはいえプロジェクト作成時に1か所設定を変更するだけ。

まず、ASP.NET Core Webアプリ プロジェクトを作成します。
![1プロジェクト作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/e2e31cde-3e2d-9c1e-dbe2-3360dcb8e692.png)

認証の種類で「個別のアカウント」を選択します。
![2認証の種類を選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/295bfd31-8f12-335d-5d3c-b174ddbc7ced.png)
普段スルーしてたけどこんなところでIdentityを適用できたのかー。
### [2/3] マイグレーションする
:::note info
.NETには<B>EntityFramework</B>(略称:<B>EF</B>)という、オブジェクトをDBに反映してくれる便利なライブラリが存在します。
EFのおかげで生SQLを書くことなく、専用メソッドを組み合わせてCRUD操作できたり、クラス定義がそのままテーブル設計になってくれたりします。
ソースコードだけでDB設計が行える機能(技術)を<B>コードファースト</B>といい、DB内のデータを保ったままテーブル設計に変更を加える機能(技術)を<B>マイグレーション</B>といいます。
:::

ここではEFのコードファストでマイグレーションをし、Identityに必要なDBを生成します。
[1/3]で既にEFのパッケージは入っているので何もしなくてOKです。

まずはパッケージマネージャーコンソールを表示しておきましょう。
![PMCの出し方.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/a3ed1260-19b1-7428-0285-29dd2dba54ae.png)

:::note info
<b>パッケージマネージャーコンソール</b>(略称:<b>PMC</B>)は、VisualStudio内のPowerShellコンソールです。
名前の通り、主に.NET FrameworkのパッケージマネージャーであるNuGetの代替手段として、パッケージのインストール、アンインストール、アップデートに使われます。
しかしそれだけでなく、多くのパッケージがPMCコマンドを持っており、例えば今回のようにEntityFrameworkの機能を利用するときにもPMCを使います。
:::

PMCに以下のコマンドを入力し実行します。
```text:PMC
update-database
```
この時PMCにブワーーーっとCREATE TABLE文等のSQLが流れていきます。
これは、Data>Migrationsフォルダ内の00000000000000_CreateIdentitySchema.cs内に定義されているCreateIdentitySchemaクラスから発行されています。
![maigure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/e0ed4653-88aa-f0d4-42c5-f524809e89b6.png)


これでDBが生成されました。
デフォルトではSQL Serverのローカルに作成されるようです。
接続文字列はsettings.jsonにあります。

生成されたDBを確認すると、下の画像の赤枠内と同様のテーブルができているはずです。
AspNetから始まるものはIdentityで使うテーブルです。
__EFMigrationHistoryはマイグレーションの反映履歴を記録するテーブルです。
![DB中身確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/e071f037-2e3f-c8e1-0b17-7d9f8cd1617f.png)

これでSTEP1でやることは終了です！
### [3/3] 動作確認
アプリケーションをデバッグ実行し、アカウント登録、ログイン、ログアウト機能が正常に動作することを確認しましょう。
#### アカウント登録
登録すると、Register confirmation画面 に遷移します。
![RegisterConfirm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/247969d6-513e-3da5-0ee4-328c5a922289.png)
"Click here to confirm your account"をクリックして検証を完了させます。
![ConfirmEmail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/3b93093f-5f18-966b-f8ce-8f276514f436.png)
※本来はRegister Confirmationの画面に飛んだ時点で、登録したメールアドレスに検証メールが届きます。		
届いたメールのリンク先に飛ぶことで検証が完了し、ログインができるようになる仕組み…なのですが		このメール検証機能は、自分で実装しないといけません。(【STEP3】で実装します)		
代わりに先ほどの"Click here to confirm your account"をクリックすることでメール検証機能が未実装の状態でもアカウント登録～ログインの流れを試せるようになっています。


#### ログイン
ログインに成功するとナビゲーションバーに"Hello [登録したメールアドレス]!"と表示されます。
![ログイン成功.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/9b7b6218-b1f6-4e09-682f-c93119e66028.png)

#### ログアウト
ナビゲーションバーの"Logout"からログアウトします。

以上でアカウント登録・ログイン・ログアウトの一連の流れを試すことができました。
でも、現状はログインしていなくてもすべての画面を閲覧できるのであまり実感が湧きませんね。
ということで次のSTEPです。
## 【STEP2】ページに閲覧制限をかける
### [1/1]アノテーションを追記する
コントローラークラスまたはコントローラー内のメソッドに以下のように[Authorize]アノテーションを追記するだけで、任意のページに閲覧制限をかけることができます。

▼コントローラー単位で制限をかける例（HomeとPrivacyに閲覧制限がかかります）
```csharp:HomeController
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MyIdentityApp.Models;
using System.Diagnostics;

namespace MyIdentityApp.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
       //以下省略
```

▼メソッド単位で閲覧制限をかける例（Privacyにのみ閲覧制限がかかります）
```csharp:HomeController
 [Authorize]
        public IActionResult Privacy()
        {
            return View();
        }
```
デバッグ実行すると、今度はログイン画面に転送されるようになりました。
ナビゲーションバーから各ページへ飛ぼうとしても同様にログインを求められます。

## 【STEP3】メール検証機能を実装する
このステップでは、アカウント初回登録時やパスワード再設定時などに、登録したメールアドレスに検証リンクを送信する機能を作ります！

手順が多い…と思われるかもしれませんが、とりあえず脳死状態でコピペとテンプレの微修正すれば初心者でも完成するのでご安心ください。

### [1/8] SendGridに無料登録しAPIKeyを取得する
今回は<B>SendGrid</B>というメールプロバイダーのメール送信機能を使ってメールを送信します。
MicroSoft様いわくSMTPを使いこなすのは難しいからこういう外部サービス使うほうがオススメのようです。
SendGridは顧客とのメールのやり取りを通じてマーケティングまでできちゃうクラウドサービスとのこと。
まあ今回は送信機能しか使いませんが。

:::note warn
SendGridではフリーメールが使えないようなので、独自ドメインのメールアドレスを用意してください。
:::

アプリケーションからSendGridを利用するために**APIKey**を取得します。

まずはSendGridの指示に従い、アカウント登録・初期設定を済ませておいてください。
なんやかんやで私は20分ぐらいかかりました。

次に、画面左側のメニューバーからSettings→APIKeysをクリックします。
![yokoMenu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/769c48fa-0ef2-00ea-bc2e-ca8931def916.png)

Create API Key をクリックします。
![createApiKey.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/a6d56d0f-a783-fcf1-2d5c-8d8e0f2bd178.png)

APIKeyに任意の名前をつけて Create & View をクリックします。
![createApiKey2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/16390f2e-5c0c-be98-b315-d0dc288426c6.png)

APIKeyが作成されました。（下画像のモザイク箇所）
画面にも記載のある通り、セキュリティの観点から二度と表示されないのでこの場でメモしてください。後で使います。
もし紛失した場合、APIKeyを新たに作り直すことは可能です。
![createApiKey3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/d881f0a8-906c-c0ee-1a2c-95f7f54eee32.png)

### [2/8] SendGridパッケージのインストール
プログラムとSendGridが連携できるようにします。
PMCで以下のコマンドを実行します。
```text:PMC
install-package SendGrid
```

### [3/8] ユーザーシークレットの設定
:::note info
<B>ユーザーシークレット</B>とは、"開発用コンピューターで ASP.NET Core アプリ用の<B>機密データを管理</B>できるヤツ"としてMSが推奨しているヤツです。
機密データをソースコードに直接書いちゃったら（ハードコーディング）ヤバイよ！ってことでローカルのある場所に格納しておき、プログラムが取ってきて使う仕組みです。
:::

今回は<B>SendGridのAPIKey</B>が機密データにあたるので、これをユーザーシークレットに入れます。

まずはユーザーシークレットを表示します。
ソリューションエクスプローラーのプロジェクト上で右クリック→ユーザーシークレットの管理(G) を選択します。
![ユーザーシークレットの管理.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/cd24a77d-6ccb-c5d3-ec6b-7d109a42f740.png)
secret.jsonが表示されるので、[1/8]で取得したAPIKeyを格納します。
```json:secret.json
{
  "SendGridKey": "ここをご自身のAPIKeyに書き換えてください"
}

```
:::note info
ちなみにユーザーシークレットが格納される場所は以下です。
%APPDATA%\Microsoft\UserSecrets

:::

### [4/8] AuthMessageSenderOptionsクラス実装
[3/8]でユーザーシークレットに格納したデータを保持するクラスを実装します。

プロジェクト直下にServiceフォルダを作成し、AuthMessageSenderOptions.csを追加します。
以下の通りにコードを記述します。
```csharp:AuthMessageSenderOptions.cs
namespace MyIdentityApp.Services
{
    public class AuthMessageSenderOptions
    {
        public string SendGridKey { get; set; }
    }
}
```

### [5/8] EmailSenderクラス実装
このSTEPのメインパート。
実際にメール送信を行うクラスです。
以下のようにコードを記述します。
※メールアドレスと送信者名はご自身のものに書き換えてください。
```csharp:
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;

namespace MyIdentityApp.Services;

public class EmailSender : IEmailSender
{
    private readonly ILogger _logger;

    public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
                       ILogger<EmailSender> logger)
    {
        Options = optionsAccessor.Value;
        _logger = logger;
    }

    public AuthMessageSenderOptions Options { get; } //Set with Secret Manager.

    public async Task SendEmailAsync(string toEmail, string subject, string message)
    {
        if (string.IsNullOrEmpty(Options.SendGridKey))
        {
            throw new Exception("Null SendGridKey");
        }
        await Execute(Options.SendGridKey, subject, message, toEmail);
    }

    public async Task Execute(string apiKey, string subject, string message, string toEmail)
    {
        var client = new SendGridClient(apiKey);
        var msg = new SendGridMessage()
        {
            From = new EmailAddress("an-nin@dooofuuu.com", "送信者名"), // 引数はご自身のものに書き換えてください。
            Subject = subject,
            PlainTextContent = message,
            HtmlContent = message
        };
        msg.AddTo(new EmailAddress(toEmail));

        // Disable click tracking.
        // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
        msg.SetClickTracking(false, false);
        var response = await client.SendEmailAsync(msg);
        _logger.LogInformation(response.IsSuccessStatusCode
                               ? $"Email to {toEmail} queued successfully!"
                               : $"Failure Email to {toEmail}");
    }
}
```


### [6/8] Program.csでDIする

まず、Program.cs内に以下のusingを追記します。
```csharp:Program.cs
using Microsoft.AspNetCore.Identity.UI.Services;
using MyIdentityApp.Services;
```

続いて、var app = builder.Build(); の手前に以下のコードを追記します。
これでクライアントにIEmailSender型のオブジェクトを渡せるようになりました。
```csharp:Program.cs
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

// var app = builder.Build();
```

:::note info
<B>Dependency Injection</B>（略称:**DI**）とは、和訳すると"<B>依存性の注入</B>"です。（日本語なのに意味がわからん）

あるオブジェクトAが別のオブジェクトBを使うとき（AがBに**依存**するとき）、呼び出されるBのプログラムに問題があれば呼び出し元のAの動作にも影響を及ぼす可能性があります。このとき、何も考えなければAのプログラム内でBをインスタンス化すると思いますが、問題はAだけの動作確認（**単体テスト**）をしたいときです。Aを呼ぶと必ずBも一緒に呼ばれるため、正しく動いているように見えてもそれは偶然Bが暗躍したからかもしれないし、誤動作が起こっても犯人がAなのかBなのか特定しづらいです。

**DI**とやらをしていれば単体テストがしやすくなります。
概要としては、Aとは別の場所（**外部**）でBオブジェクトを生成し、Aに渡します。
Aは渡されたBオブジェクトを使うだけになります。

Aの単体テスト時は、テストクラスで**モック**というBのハリボテを生成し、Aに渡します。
ハリボテは中身がすっからかんなので、もしテストに失敗すれば必ず原因はAと特定できます。

また、Aのような使う側を**クライアント**、Bのような使われる側を**サービス**といいます。

まとめると、DIとは<B>依存性を外部から注入するデザインパターン</B>です。（雀の涙ぐらいマシになったか）
:::

:::note info
DIでサービスを受け渡す方法はいくつかありますが、今回はProgram.cs（旧Startup.cs）内でサービスオブジェクトを生成し、これをクライアントがコンストラクタの引数で受け取るという**コンストラクタ・インジェクション**が採用されています。

Identityが定義しているRegister.cshtml.cs等の複数クラスで、メール送信を行うためにIEmailSender型オブジェクトをコンストラクタの引数で受け取るようにコードが書かれています。（このコードを見たい方は後に説明するスキャフォールドをしてください）

今回、各プログラムの関係性は以下のようになっています。
- **クライアント(使う側)**：Register.cshtml.cs等、メール送信指示を出す複数のクラス
- **サービス(使われる側)**：IEmailSenderの実装である、実際にメール送信を行うEmailSenderクラス
- **外部(サービスオブジェクトを用意する場所)**：Program.cs内
:::


以上で中身は完成しました！
残りは見た目に微修正を加えるだけです。

### [7/8] スキャフォールドしてUIテンプレートのソースコードを取得する
:::note info
スキャフォールド機能とは、テンプレソースを自動生成してくれる便利機能のことです。
イチからコードを書く必要がなく、自動生成されたテンプレをいじってカスタマイズしていけば効率良く開発ができる。まるで全自動食洗器やドラム式洗濯機みたいなマジでナイスなヤツです。
:::

今回は[8/8]のための準備でスキャフォールドします。

【STEP1】で新規プロジェクトを作成した時点で、ユーザー認証に関係するUIや基本機能は既に完成していましたが、メール検証機能を実装するにあたってUIに変更を加えます。
Identityが隠し持っているソース自体をいじることはできないのですが、代わりにスキャフォールディングによってそっくりさんたち（オーバーライド）を生成し、それらをいじることでIdentityをカスタマイズできる仕組みです。

以下の手順でスキャフォールドを行います。

ソリューションエクスプローラーのプロジェクト上で右クリック→追加(D)→新規スキャフォールディングアイテム(F) を選択します。
![スキャ１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/c45353e3-eed4-6820-0e1c-edb1076b0816.png)

画面左側のIDを選択→画面中央のIDが選択されていることを確認→画面右下の追加をクリックします。
![スキャ２.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/c3d4720d-b53b-0453-6919-a34c277da15d.png)


スキャフォールディングするファイルを選択します。
最低限は今回編集するAccount¥RegisterConfirmにチェックを入れればOKです。
また、[5/8]で実装したEmailSenderを実際に使っているコードを見てみたければ、Account¥Registerにもチェックを入れてみてください(※他にも数クラスあります)。そこではコンストラクタの引数でIEmail型オブジェクトを受け取り、SendEmailAsyncメソッドを呼び出しています。

![スキャ３.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/2111df1e-804a-7ef7-2ec3-6c61f2f9fe55.png)
Areas>Identity>Pages>Accountに選択したファイルが現れました。
![スキャ４.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/2304bdc6-fb20-aef9-6c41-393b79f3a094.png)





### [8/8] RegisterConfirm.cshtml.csの編集
【STEP1】[3/3]の注意書きの通り、メール送信機能が実装されていない状態でもアカウント登録機能を試せるように、Register confirmation画面には検証用リンクが表示されていました。
[6/8]の時点で機能自体は完成済なので、これを非表示にしましょう。
![RegisterConfirm - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/672d93a7-71c5-bff6-4f5e-cac49320a973.png)



RegisterConfirm.cshtml.csファイルを開き、47行目あたりのOnGetAsync()メソッド内を編集します。
 "// Once you add a real email sender, you should remove this code that lets you confirm the account"
というコメントに従い、62行目から一連のコードを削除します。
```csharp:RegisterConfirmation.cshtml.cs 削除箇所抜粋
 //Once you add a real email sender, you should remove this code that lets you confirm the account
           DisplayConfirmAccountLink = true;
            if (DisplayConfirmAccountLink)
            {
                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                EmailConfirmationUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                    protocol: Request.Scheme);
            }
```

代わりに以下のコードに書き換えます。
```csharp:RegisterConfirmation.cshtml.cs
DisplayConfirmAccountLink = false;
```


これで検証リンクが非表示になりました。
![RegisterConfirmation後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/60eec05d-c39c-44ba-174f-efc5f0a5831b.png)

新しいアカウントを作ったり、パスワードを忘れたふりして再発行してみましょう。
アカウント登録したメールアドレスに検証メールが飛べば成功です！

#### 新規アカウント登録時
リンク先に飛ぶと検証フラグが立ちます。
(AspNetUserテーブルのEmailConfirmedがtrueになる)
これをもってアカウントが有効化され、ログイン可能な状態になります。
![登録時メール.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/74fb62e1-4d62-f21c-db8a-127ada8eb10f.jpeg)

#### パスワード再設定時
リンク先に飛ぶと新しいパスワードを設定できます。
![pw再設定時メール.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2271094/b283f210-a14a-22eb-f7a3-2953790c0dd4.jpeg)


# 終わりに
手を動かすだけならそれほど苦労なく実装できたかと思いますが、
多少の仕組みまで理解するとなれば初心者には勉強になることがたくさんあったはずです。

また、今後Identityをカスタマイズしたい方にとって、
【STEP3】のメール検証機能の実装ではいろいろ収穫があったのではないでしょうか。

今後もC#.NETでの開発を楽しみましょう！
※そのうち本記事の情報量を増やしたり減らしたりする可能性があります。