はじめに
BlazorとIdentity Coreで認証画面を作成した時に、確認メールのURLでパラメーターの一部が正しく処理されず、エラーになりました。
確認メールのURLをクリックすると、ConfirmEmail.cshtml.csのOnGetAsyncがコールされますが、userIdは値が必ず入りcodeの引数が必ずnullとなってしまいIndexに戻ってしまう、という現象になってしまいます。
public async Task<IActionResult> OnGetAsync(string userId, string code)
{
if (userId == null || code == null) // codeが常にnullなのでIndexに飛ばされる。
{
return RedirectToPage("/Index");
}
...
}
メールで送られてきたURLパラメーターを見ると &code= になっていました。amp;部分を削除すると正しく処理されたので、文字列をエンコードしている箇所に原因があるのだと思われます。
https://ほげほげ/Identity/Account/ConfirmEmail?userId=ユーザーID&code=コード値&returnUrl=%2F
今回はメールに送られてきたURLから & を取り除き正しく処理されるようにします。
環境
- 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フォルダを右クリックし、追加→新規スキャフォールディングアイテムを選択。
IDの追加でLogin、Register、ConfirmEmailを追加
確認メールを行っている箇所の画面部分
実際にデバッグを行い、ユーザーの新規作成時にメールアドレス、パスワードを入力後のRegisterボタン実行後に確認メールが送られます。
ボタン入力後、OnPostAsyncに遷移しawait _emailSender.SendEmailAsyncでメール送信をおこないます。
ここで
HtmlEncoder.Default.Encode(callbackUrl)
となっている箇所を
callbackUrl
に修正します。
実際に修正したソースです。
※ 修正前のソース箇所をコメントアウトしています。
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の値が入りメール登録できるようになります。
なぜ & が問題になるのか
&はHTMLエスケープシーケンスであり、HTML文書内では問題なく使用できますが、URLパラメータとして使う場合、特定の状況で問題が発生することがあります。
1.HTMLエスケープシーケンスの意味
& はHTMLエスケープシーケンスで、HTML文書中の&文字を表します。ブラウザがHTML文書を解析するときに、&は&に変換されます。
2.メールクライアントの処理
メールクライアントはHTMLをレンダリングする際に&を&として表示しますが、リンクとして認識する場合に問題が発生することがあります。メールクライアントやウェブベースのメールサービスはリンクを正しく解釈しないことがあります(これは設定等にも影響がありそうですが)
3.URLのエンコードとデコードの処理
URLにエスケープシーケンスが含まれている場合、サーバーサイドでそのURLを受け取った際に正しくデコードされないことがあります。このため、パラメータの分離に失敗し、結果としてcodeがnullになることがあります。