BlazorとIdentity Coreで確認メールのURLからエスケープ文字を取り除く

# はじめに
BlazorとIdentity Coreで認証画面を作成した時に、確認メールのURLでパラメーターの一部が正しく処理されず、エラーになりました。

確認メールのURLをクリックすると、ConfirmEmail.cshtml.csのOnGetAsyncがコールされますが、userIdは値が必ず入り**code**の引数が必ずnullとなってしまいIndexに戻ってしまう、という現象になってしまいます。

```ConfirmEmail.cshtml.cs
public async Task<IActionResult> OnGetAsync(string userId, string code)
{
    if (userId == null || code == null) // codeが常にnullなのでIndexに飛ばされる。
    {
        return RedirectToPage("/Index");
    }
    ...
}
```

メールで送られてきたURLパラメーターを見ると **\&amp;code=** になっていました。amp;部分を削除すると正しく処理されたので、文字列をエンコードしている箇所に原因があるのだと思われます。

```実際に届いたメールのURL.txt
https://ほげほげ/Identity/Account/ConfirmEmail?userId=ユーザーID&amp;code=コード値&amp;returnUrl=%2F
```

今回はメールに送られてきたURLから **\&amp;** を取り除き正しく処理されるようにします。

## 環境
* Windows 11
* C#
* Visual Studio 2022
* .Net Core SDK 6.0.30
* ASP.Net Core SDK 6.0.30

## 前提条件
以下の事を事前に行っています。

* メールが送信できるよう設定済
* .NET IdentityでLogin、Register、ConfirmEmailを作成

#### 以下、IDからスキャッフォールディングした内容
Areaフォルダ内にあるIdentityフォルダを右クリックし、追加→新規スキャフォールディングアイテムを選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119455/d1802cfa-ae87-bb39-6101-7eaba4e13eeb.png)

IDを選択し
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119455/733ecd2c-368f-845d-661a-077d3a274ef3.png)

IDの追加でLogin、Register、ConfirmEmailを追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119455/6d5ab0ee-6ca8-d7a1-67d3-3e9d9a6a33cb.png)

# 確認メールを行っている箇所の画面部分

実際にデバッグを行い、ユーザーの新規作成時にメールアドレス、パスワードを入力後のRegisterボタン実行後に確認メールが送られます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119455/e160707d-1f70-507e-93bb-ce22a94e6f8d.png)

ボタン入力後、OnPostAsyncに遷移し**await _emailSender.SendEmailAsync**でメール送信をおこないます。
ここで
```
HtmlEncoder.Default.Encode(callbackUrl)
```
となっている箇所を
```
callbackUrl
```
に修正します。

実際に修正したソースです。
※ 修正前のソース箇所をコメントアウトしています。

```Register.cshtml.cs
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
        var result = await _userManager.CreateAsync(user, Input.Password);

        if (result.Succeeded)
        {
            _logger.LogInformation("User created a new account with password.");

            var userId = await _userManager.GetUserIdAsync(user);
            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            var callbackUrl = Url.Page(
                "/Account/ConfirmEmail",
                pageHandler: null,
                values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                protocol: Request.Scheme);

            //await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
            //    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

            await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                $"Please confirm your account by <a href='{callbackUrl}'>clicking here</a>.");  // ここでHtmlEncoderを使わない

            if (_userManager.Options.SignIn.RequireConfirmedAccount)
            {
                return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
            }
            else
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }
)
```

修正後、再度同じ手順で実行すると、確認メールのURLに **amp;** が消えている事が確認できます。このURLからConfirmEmail.cshtml.csのOnGetAsyncの引数にcodeの値が入りメール登録できるようになります。

# なぜ \&amp; が問題になるのか
\&amp;はHTMLエスケープシーケンスであり、HTML文書内では問題なく使用できますが、URLパラメータとして使う場合、特定の状況で問題が発生することがあります。

#### 1.HTMLエスケープシーケンスの意味
\&amp; はHTMLエスケープシーケンスで、HTML文書中の&文字を表します。ブラウザがHTML文書を解析するときに、\&amp;は&に変換されます。

#### 2.メールクライアントの処理
メールクライアントはHTMLをレンダリングする際に\&amp;を&として表示しますが、リンクとして認識する場合に問題が発生することがあります。メールクライアントやウェブベースのメールサービスはリンクを正しく解釈しないことがあります（これは設定等にも影響がありそうですが）

#### 3.URLのエンコードとデコードの処理
URLにエスケープシーケンスが含まれている場合、サーバーサイドでそのURLを受け取った際に正しくデコードされないことがあります。このため、パラメータの分離に失敗し、結果としてcodeがnullになることがあります。