2012年3月2日金曜日

ApiControllerで認証する際にログインページにリダイレクトしたくない

@jsakamotoさんへ

そりゃそーですね!Apiなんだから。FormsAuthenticationの認証チケットを利用してWCFでもForm認証を使う、っていう話が前にありましたが、それはそれで王道なやり方じゃない、ですよね。APIとして認証するならOAuthとかなんでしょうかね。その場合の受けはHttpMessageHandler使ってやればいいのかな。

とはいえ今回は受けじゃなくてレスポンスのですしたね。

普通にAuthorize属性使うと401になるから問題無さそうだけど、MVC(WebFormsでも一緒)アプリケーションに組み込んだ場合、Form認証も入ってたりするはずなので、FormsAuthenticationModuleに途中でレスポンスコードを横取りされた結果、ログインページへのリダイレクト(302)になってしまう。そうなると、クライアントがブラウザならいいけど、APIを呼び出してるプログラムだったら、そんな~、ログインページとか困ります~、です。

Web APIの標準テンプレートを使った場合に、以下のようにHomeControllerとValuesControllerを書き換えたとします。

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult Login()
        {
            return Content("ログインしてね!");
        }

        [Authorize]
        public ActionResult AccessDenied()
        {
            return Content("About");
        }
    }

 

[Authorize]
public class ValuesController : ApiController
{
	// GET /api/values
	public IEnumerable<string> Get()
	{
		return new string[] { "value1", "value2" };
	}
	:
	:
}

ログインページは以下のように変更。

<forms loginUrl="~/Home/Login" timeout="2880" />

そうするとApiControllerにAuthorizeつけてブラウザでアクセスすると「ログインしてね!」が表示されます。ステータスコードは302のあとページ表示で200。

api1

コラー!

なので、FormsAuthenticationModuleに書き換えられたステータスをカスタムモジュールで再度書き換えました。

using System;
using System.Web;
using System.Web.Http.WebHost;
using System.Web.Security;

namespace Mvc4BApi
{
    public class CustomAuthenticationModule : IHttpModule
    {
        public void Dispose()
        {
        }

        public void Init(HttpApplication context)
        {
            context.EndRequest += new EventHandler(context_EndRequest);
        }

        void context_EndRequest(object sender, EventArgs e)
        {
            var application = sender as HttpApplication;
            var response = application.Response;

            if (!(application.Context.CurrentHandler is HttpControllerHandler))
                return;

            if(response.StatusCode == 302 && response.RedirectLocation.StartsWith(FormsAuthentication.LoginUrl))
            {
                response.ClearHeaders();
                response.ClearContent();
                response.StatusCode = 401;
                application.CompleteRequest();
            }
        }
    }
}

うぬ。Web.configもモジュール使うようにしましょう。

  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules runAllManagedModulesForAllRequests="true">
      <add name="customauth" type="Mvc4BApi.CustomAuthenticationModule" />
    </modules>
  </system.webServer>

そうすると、どーなるかっていうと。

api2

ステータスは401で中身なしになりました。

予め用意しといたAuthorize属性のついた/home/accessdeniedにアクセスすると。

api3

ちゃんとForm認証と同じ挙動ですね。ログインページにリダイレクト。

FormsAuthenticationModuleが仕込まれてると、これはもうHttpHandlerの実行タイミングじゃどうにもならないのtで、こういうやり方になると思います。標準で用意されてる認証モジュールって共存出来ないし。

ApiControllerはHttpControllerHandlerから実行されるので、実質HttpHandlerですよね。途中でMessageをいじるっていうのはそこに対するAOP的な動作。

それほどコードを書くわけでも無いので、コレでいいかなーって思いますが、いかがでしょーか?後は、素直に別サイトにしてしまう、とか...。