しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET Core で Azure AD B2C を使った際に謎のエラーが多発する問題

ASP.NET Core の OpenID Connect Middleware を使って Azure AD B2C なログインを GitHub で公開されているサンプルとほぼ同じように実装したら、よくわからないエラーが多発して困ったという話です。

実際に Application Insights からデータを引っ張ってきました。本番のデータなので URL は隠しました。

f:id:shiba-yan:20171226003014p:plain

エラーメッセージの内容自体は Remote Authentication Handler が自動的に使っている CSRF 対策のクッキー検証に失敗したというだけなんですが、正直なところこのエラーの発生率は異常でした。

よくわからないし、実害あまりなさそうなので放っておいたのですが、件数多いし何とか対応しないといけない機運になったので、詳細に調べてみました。GitHub にも Issue がいくつか上がっているようでしたが、決定的な原因と言えるものは見当たらず。

再現しないなーと思っていたら、ブラウザで戻った時に発生するという話が Stack Overflow で見つかったので、あーこれが原因なんだろうなとほぼ確信しました。

signin-oidc に戻ってしまった場合にエラー画面に飛ぶのは、明らかに問題が多いです。理想的な挙動としては Correlation failed の場合はもう一度サインイン画面に飛ばすべきでしょう。

エラー時の処理は OnRemoteFailure イベントを使えばよいので簡単です。AAD B2C のサンプルでは以下のようなイベントハンドラが登録されています。

public Task OnRemoteFailure(RemoteFailureContext context)
{
    context.HandleResponse();
    // Handle the error code that Azure AD B2C throws when trying to reset a password from the login page 
    // because password reset is not supported by a "sign-up or sign-in policy"
    if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118"))
    {
        // If the user clicked the reset password link, redirect to the reset password route
        context.Response.Redirect("/Session/ResetPassword");
    }
    else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied"))
    {
        context.Response.Redirect("/");
    }
    else
    {
        context.Response.Redirect("/Home/Error?message=" + context.Failure.Message);
    }
    return Task.FromResult(0);
}

context.Failure.Message に Correlation failed が含まれていたら、サインイン画面にリダイレクトするように条件を追加すればよい感じですね。

Correlation failed とは別に、AAD B2C 固有っぽいエラーも発生していました。これもまた Application Insights から本番のエラーデータを引っ張ってきました。

f:id:shiba-yan:20171226004832p:plain

何故か改行コードがヘッダーに混ざっていると言ってます。まあ、普通はあり得ないです。

調べてみるとどうやら AAD B2C が返すエラーメッセージは改行されているものがあるらしく、Redirect で URL エンコードしていないサンプルコードのせいで発生していたみたいです。

public Task OnRemoteFailure(RemoteFailureContext context)
{
    context.HandleResponse();
    // Handle the error code that Azure AD B2C throws when trying to reset a password from the login page 
    // because password reset is not supported by a "sign-up or sign-in policy"
    if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118"))
    {
        // If the user clicked the reset password link, redirect to the reset password route
        context.Response.Redirect("/Session/ResetPassword");
    }
    else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied"))
    {
        context.Response.Redirect("/");
    }
    else if (context.Failure.Message.Contains("Correlation failed"))
    {
        context.Response.Redirect("/Session/SignIn");
    }
    else
    {
        context.Response.Redirect("/Home/Error?message=" + WebUtility.UrlEncode(context.Failure.Message));
    }
    return Task.FromResult(0);
}

ちゃんと URL エンコードしてから渡すことでエラーは発生しなくなりました。

そもそもこの処理が必要かどうかは別の話なので、実際の場合は汎用的なエラー画面を出しておきつつ、内部では Application Insights にテレメトリを送っておけば良いです。