ASP.NET MVCにてBasic認証を行う

ASP.NET MVCにて単純なXMLをやり取りするインターフェイスに、Basic認証が必要となった。その程度の仕組みは既にフレームワーク側に用意されているのかと思ったけど、実は含まれていないようだ。今どき、Basic認証なんて使うな!というご信託かも知れないが、必要なものは必要なので仕方ない。IISを使わない方法として、自分で認証フィルタを用意してみた。

開発環境は下記の通り。

public void OnAuthorization(AuthorizationContext filterContext)
{
    if (filterContext == null)
    {
        return;
    }

    string auth = filterContext.HttpContext.Request.Headers["Authorization"];
    if (String.IsNullOrEmpty(auth))
    {
        filterContext.Result = new HttpUnauthorizedResult();
        return;
    }

    string[] scheme = auth.Split(' ');
    if (scheme.Length != 2 || !scheme[0].ToLower().Equals("basic"))
    {
        filterContext.Result = new HttpUnauthorizedResult();
        return;
    }

    byte[] encoded = Convert.FromBase64String(scheme[1]);
    string decoded = Encoding.ASCII.GetString(encoded);
    string username = decoded.Substring(0, decoded.IndexOf(':'));
    string password = decoded.Substring(decoded.IndexOf(':') + 1);

    if (!ValidateUser(username, password))
    {
        filterContext.Result = new HttpUnauthorizedResult();
        return;
    }
}

動作は良好で特に問題はない。ところが、認証失敗時にHTTPレスポンス302(Found)とログインページへのパスが返されてしまうことに気がついた。確かにユーザが使うウェブページも別に用意されていて、そちらへのアクセス時にログインページが返るのは分かるのだけど、XMLインターフェイスではそのような処理は不要だ。単純にHTTPレスポンス401(Unauthorized)を返して欲しい。

しかしながら、幾らフィルタでエラーを設定しても、いつの間にか302に置き換えられてしまうようで上手くいかない。同様の疑問を持つ人は他にもいるようで、掲示板には類似の質問が載っていた。

Even though this works, my question is there something in Asp.net MVC 2 that would prevent me from having to do this? Or, in general is there a better way? I would think this would come up a lot for anyone doing REST api's or just people that do ajax requests in their controllers.

rest - In Asp.Net MVC 2 is there a better way to return 401 status codes without getting an auth redirect - Stack Overflow

上記の情報を参考にして、Application_EndRequest()にてレスポンスコードを無理やり置き換えるようにしたのがこちら。無事に401が返るようになり、当初の目的はこれで達せられた。あまりスマートな方法とは思えないけど。

protected void Application_EndRequest()
{
    if (Context.Response.StatusCode == 302 && Context.Request.Path.StartsWith("/path/to/api")))
    {
        Context.Response.Clear();
        Context.Response.StatusCode = 401;
    }
}