Cutting Edge

ASP.NET Identity による外部認証

Dino Esposito

Dino EspositoVisual Studio 2013 には、アプリケーションのビルド用に、新しく ASP.NET MVC 5 プロジェクト テンプレートと Web フォーム プロジェクト テンプレートが含まれています。サンプル テンプレートの中には、オプションで ASP.NET Identity に基づく既定の認証層を含むテンプレートもあります。生成されるコードにはたくさんの機能が含まれますが、そのしくみを理解するのは簡単なことではありません。今回は、ソーシャル ネットワークなど、OAuth ベースや OpenID ベースの外部サーバーを使って認証を行う方法と、認証が完了し、ダウンロードされたすべての要求にアクセスできるようになった後に行う手順について説明します。

まず、空の ASP.NET MVC 5 アプリケーションを作成し、必要最低限の認証層を追加します。新しい ASP.NET Identity フレームワークにより、認証スタック全体の複雑さや能力は増しますが、新しいパッケージや API について学習する必要が一切なく、外部ユーザー認証を迅速かつ簡単に調整できるようになります。

アカウント コントローラーの要素を取り出す

ASP.NET MVC のリリース当初から、フレームワークの概要を新しいユーザーに紹介するサンプル アプリケーションは、認証のすべての側面を管理するために、かなり大きなコントローラー クラスを提供していました。2014 年 3 月号のコラム「ASP.NET Identity の概要」(msdn.microsoft.com/magazine/dn605872) では、Visual Studio に既定で用意されているコードから開始して ASP.NET Identity フレームワークの概要を示しました。今回は、空の ASP.NET MVC 5 プロジェクトから開始してさまざまな部分を 1 つにまとめます。その過程では、単一責任の原則 (bit.ly/1gGjFtx、英語) に注意深く従います。

空のプロジェクトから開始し、ASP.NET MVC 5 スキャフォールディングを追加します。次に、LoginController と AccountController という 2 つの新しいコントローラー クラスを追加します。LoginController は、サイト固有のローカル メンバーシップ システム、または Twitter など外部 OAuth プロバイダーのどちらで実現するかにかかわらず、サインインとサインアウトの操作のみを対象とします。名前からもわかるように、AccountController クラスにはユーザー アカウントの管理に関連するほぼすべての機能が含まれます。説明を簡単にするために、現時点では新規ユーザーの登録機能のみを含めます。図 1 に、Visual Studio 2013 で表示される 2 つのコントローラー クラスのスケルトンを示します。

Skeleton of LoginController and AccountController Classes in Visual Studio 2013
図 1 Visual Studio 2013 の LoginController クラスと AccountController クラスのスケルトン

UI はログインしたユーザー、ローカル ログイン フォーム、およびソーシャル ログイン フォームの 3 つのビューから構成します (図 2 参照)。

The Three Partial Views Used in the Authentication Stack
図 2 認証スタックで使用する 3 分割したビュー

IdentityController クラス

図 1 に示したスケルトンでは、LoginController クラスと AccountController クラスはどちらも ASP.NET MVC 5 Controller 基本クラスから継承しています。ただし、ASP.NET Identity フレームワークを使用する場合は、この方法ではコードが重複する可能性があるため、必ずしも最善策とはいえません。そこで、Visual Studio 2013 で一時プロジェクトを作成し、何にでも対応できる包括的な AccountController クラスに格納された既定のコードを見てみることをお勧めします。ASP.NET Identity では、UserManager<TUser> クラスとストレージ メカニズムをコントローラーに挿入する必要があります。また、ユーザーのサインインとリダイレクトのプロセスを簡略化するため、いくつかヘルパー メソッドを追加します。UserManager ルート オブジェクトを参照するためのプロパティと、基盤となる Open Web Interface for .NET (OWIN) ミドルウェアとの接続点を確立するためにヘルパー プロパティも必要です。

これらすべての定型コードを、重複することなく 1 回で配置できる中間クラスを導入するのがお勧めです。このクラスを IdentityController とし、図 3 で示すように定義します。

図 3 ASP.NET Identity 認証の新しい基本クラス

using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using AuthSocial.ViewModels.Account.Identity;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin.Security;
namespace AuthSocial.Controllers
{
  public class IdentityController<TUser, TDbContext> : Controller
  where TUser : IdentityUser
  where TDbContext : IdentityDbContext, new()
  {
    public IdentityController()
    : this(new UserManager<TUser>(
    new UserStore<TUser>(new TDbContext())))
    {
    }
    public IdentityController(UserManager<TUser> userManager)
    {
      UserManager = userManager;
    }
    protected UserManager<TUser> UserManager { get; set; }
    protected IAuthenticationManager AuthenticationManager
    {
      get { return HttpContext.GetOwinContext().Authentication; }
    }
    protected async Task SignInAsync(
      TUser user, bool isPersistent)
    {
    AuthenticationManager.SignOut(
    defaultAuthenticationTypes.ExternalCookie);
      var identity = await UserManager.CreateIdentityAsync(user,
        DefaultAuthenticationTypes.ApplicationCookie);
      AuthenticationManager.SignIn(
        new AuthenticationProperties {
          IsPersistent = isPersistent },
          identity);
        }
      protected ActionResult RedirectToLocal(string returnUrl)
      {
        if (Url.IsLocalUrl(returnUrl))
        {
          return Redirect(returnUrl);
        }
        return RedirectToAction("Index", "Home");
      }
      protected ClaimsIdentity GetBasicUserIdentity(string name)
      {
        var claims = new List<Claim> {
          new Claim(ClaimTypes.Name, name) };
        return new ClaimsIdentity(
          claims, DefaultAuthenticationTypes.ApplicationCookie);
      }
  }
}

ジェネリック (および関連する制約) の使用により、IdentityUser クラスに基づくユーザーの定義と IdentityDbContext に基づくストレージ コンテキストの定義を使用するように IdentityController クラスを制限します。

IdentityUser は、具体的には、ユーザーを表し、プロパティの最小限のセットを提供する基本クラスにすぎません。プレーンな継承を使用してより具体的な新しいクラスを作成してもかまいません。これを行うには、IdentityController から派生するクラスの宣言でカスタム クラスを宣言するだけです。以下に示すのは、IdentityController<TUser, TDbContext> に基づく LoginController と AccountController の宣言です。

public class LoginController :
  IdentityController<IdentityUser, IdentityDbContext>
{
  ...
}
public class AccountController :
  IdentityController<IdentityUser, IdentityDbContext>
{
  ...
}

このようにして、認証の複雑さとユーザー管理コードの大部分を 3 つのクラスに分割する基礎作業を行います。さらに、共通のサービスを提供する 1 つのクラスとログイン タスク専用の 1 つのクラスを用意します。最後に、ユーザー アカウントを管理するために別のクラスを用意します。アカウント クラスから始めましょう。説明を簡単にするため、新しいユーザーを登録するためのエンドポイントのみを公開します。

AccountController クラス

基本的に、コントローラー メソッドが必要になるのは、エンド ユーザーが操作するために表示される UI 部分がある場合のみです。たとえば、ユーザーがクリックする送信ボタンがある場合、POST に対応するコントローラー メソッドが必要です。このメソッドを定義する場合は、ビュー モデルから着手します。ビュー モデル クラスは、表示した UI を使ってやり取りするすべてのデータ (文字列、日付、数値、ブール値、コレクション) を集めたプレーンなデータ転送オブジェクト (DTO) です。ローカル メンバーシップ システムに新規ユーザーを追加するメソッドには、以下のビュー モデル クラスを用意します (ソース コードは、既定の Visual Studio 2013 ASP.NET MVC 5 テンプレートから取得できるコードと同じです)。

public class RegisterViewModel
{
  public string UserName { get; set; }
  public string Password { get; set; }
  public string ConfirmPassword { get; set; }
}

検証と表示を目的として、このクラスにデータの注釈を付けてもかまいません。ただし、設計レベルから考えれば、重要なのは実際の UI の入力コントロールごとに 1 つのプロパティをクラスに備えることだけです。図 4 に、新規ユーザーを登録する POST 要求を処理するコントローラー メソッドの実装を示します。

図 4 新規ユーザーを登録する

public async Task<ActionResult> Register(
  RegisterViewModel model)
{
  if (ModelState.IsValid) {
    var user = new IdentityUser { UserName = model.UserName };
    var result = await UserManager.CreateAsync(
      user, model.Password);
    if (result.Succeeded) {
      await SignInAsync(user, false);
      return RedirectToAction("Index", "Home");
    }
  Helpers.AddErrors(ModelState, result);
  }
  return View(model);
}

UserManager クラスの CreateAsync メソッドは、基盤となるストレージ メカニズムを使って、指定されたユーザーの新しいエントリを作成するだけです。この AccountController クラスは、ユーザー アカウントを編集または削除するメソッドや、パスワードをリセットするメソッドを定義するのにも適しています。

LoginController クラス

ログイン コントローラーには、アプリケーションが許容できる方法でユーザーをサインインまたはサインアウトするメソッドを配置します。アカウント コントローラーの説明と同様、SignIn メソッドは証明書、認証を保存するためのフラグ、戻り先の URL といったプロパティを備えた DTO を管理します。既定のコードでは、戻り先の URL は ViewBag で管理し、同様にビュー モデル クラスに配置できます。

public class LoginViewModel
{
  public string UserName { get; set; }
  public string Password { get; set; }
  public bool RememberMe { get; set; }
  public string ReturnUrl { get; set; }
}

アプリケーション サーバーで完全に管理されるローカル メンバーシップ システムを操作するコードは、既定のプロジェクトからコピーして貼り付けます。以下に示すのは、新しい SignIn メソッドの POST 実装の抜粋です。

var user = await UserManager.FindAsync(
  model.UserName, model.Password);
if (user != null)
{
  await SignInAsync(user, model.RememberMe);
  return RedirectToLocal(returnUrl);
}

最終的にユーザーをサインインするコードは、既定の実装から借用し、IdentityController 基本クラスでプロテクト メソッドとして書き直した SignInAsync メソッドです。

protected async Task SignInAsync(TUser user, bool isPersistent)
{
  AuthenticationManager.SignOut(
  DefaultAuthenticationTypes.ExternalCookie);
  var identity = await UserManager.CreateIdentityAsync(user,
    DefaultAuthenticationTypes.ApplicationCookie);
  AuthenticationManager.SignIn(
    new AuthenticationProperties { IsPersistent = isPersistent },  
  identity);
}

ASP.NET Identity フレームワークを使用しているときは、ASP.NET の機能に明示的に関連付けられるクラスは一切使用しません。ASP.NET MVC ホストというコンテキストでは、FormsAuthentication ASP.NET クラスを直接使用しないことを除けば、依然としてログイン フォームと ASP.NET 認証クッキーを使ってユーザー認証を管理します。従来の ASP.NET 開発者が取り組まなけらばならない 2 つのコード層があります。1 つは UserManager や IdentityUser などのクラスで表されるプレーンな ASP.NET Identity API です。もう 1 つは OWIN クッキー ミドルウェアで、ログインしたユーザーの情報を格納および保持する方法といった細かい処理からファサード認証 API を単純に切り離します。フォーム認証を引き続き使用し、おなじみの .ASPXAUTH クッキーも作成します。ただし、すべての処理が新しいメソッドの背後で行われるようになります。これをしっかりと理解するには、Brock Allen のブログ記事「A Primer on OWIN Cookie Authentication Middleware for the ASP.NET Developer」(ASP.NET 開発者のための OWIN クッキー認証ミドルウェアの概要、英語) を参照してください (bit.ly/1fKG0G9)。

OWIN ミドルウェアのエントリ ポイントである Startup­Auth.cs から最初に呼び出されている以下のコードを見てみましょう。

app.UseCookieAuthentication(
  new CookieAuthenticationOptions
{
  AuthenticationType =
    DefaultAuthenticationTypes.ApplicationCookie,
  LoginPath = new PathString("/Login/SignIn")
});
app.UseExternalSignInCookie(
  DefaultAuthenticationTypes.ExternalCookie);
app.UseTwitterAuthentication(
  consumerKey: "45c6...iQ",
  consumerSecret: "pRcoXT...kdnU");

このコードは、旧式の web.config 認証エントリの代わりに使用する、ある種なめらかなインターフェイスです。また、Twitter に基づく認証にもリンクし、サード パーティのログイン プロバイダーを通じてユーザー ログイン情報を一時的に保存するため、ExternalSignInCookie を使用するように OWIN ミドルウェアを構成します。

AuthenticationManager オブジェクトは OWIN ミドルウェアのエントリポイントと、FormsAuthentication に基づく基盤となる ASP.NET 認証 API を呼び出すファサードを表します。

その結果、統一された API を使って、ユーザーを認証し、資格情報を検証してメンバーシップ システムが Web サーバーに対してローカルか、外部のソーシャル ネットワークのログイン プロバイダーにデリゲートされるかを判断することになります。以下のコードは、サイトのユーザーが Twitter アカウントを使って認証できるようにします。

[AllowAnonymous]
public ActionResult Twitter(String returnUrl)
{
  var provider = "Twitter";
  return new ChallengeResult(provider,
    Url.Action("ExternalLoginCallback", "Login",
    new { ReturnUrl = returnUrl }));
}

なによりもまず、Twitter 開発者アカウントに関連付けられた構成済みの Twitter アプリケーションが必要です。詳細については、dev.twitter.com (英語) を参照してください。Twitter アプリケーションは、コンシューマー キーとシークレットという英数字文字列のペアで一意に識別されます。

ASP.NET MVC 5 プロジェクト ウィザードから取得した既定のコードは、まったく新しいクラス (ChallengeResult) を使って外部ログインを処理することも提案しています (図 5 参照)。

図 5 ChallengeResult クラス

public class ChallengeResult : HttpUnauthorizedResult
{
  public ChallengeResult(string provider, string redirectUri)
  {
    LoginProvider = provider;
    RedirectUri = redirectUri;
  }
  public string LoginProvider { get; set; }
  public string RedirectUri { get; set; }
  public override void ExecuteResult(ControllerContext context)
  {
    var properties = new AuthenticationProperties
    {
      RedirectUri = RedirectUri
    };
    context.HttpContext
           .GetOwinContext()
           .Authentication
           .Challenge(properties, LoginProvider);
  }
}

ChallengeResult クラスは、指定したログイン プロバイダー (スタートアップで登録) に接続して認証を実行するタスクを、Challenge というメソッドで OWIN ミドルウェアにデリゲートします。戻り先の URL は、ログイン コントローラーの ExternalLoginCallback メソッドです。

[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(
  string returnUrl)
{
  var info = await AuthenticationManager.GetExternalLoginInfoAsync();
  if (info == null)
    return RedirectToAction("SignIn");
  var identity = GetBasicUserIdentity(info.DefaultUserName);
  AuthenticationManager.SignIn(
    new AuthenticationProperties { IsPersistent = true },
    identity);
  return RedirectToLocal(returnUrl);
}

GetExternalLoginInfoAsync メソッドは、ログイン プロバイダーが公開している認証済みユーザーに関する情報を受け取ります。GetExternalLoginInfoAsync メソッドから戻ると、アプリケーションはユーザーを実際にサインインするのに十分な情報を得られますが、まだ何も実行しません。ここで、利用可能な選択肢を把握しておくことが重要です。まず、今回の例のように、正規の ASP.NET 認証クッキーを単純に処理および作成できます。IdentityController 基本クラスで定義された GetBasicUserIdentity ヘルパー メソッドは、指定された Twitter アカウント名に関する ClaimsIdentity オブジェクトを作成します。

protected ClaimsIdentity GetBasicUserIdentity(string name)
{
  var claims = new List<Claim> { new Claim(
    ClaimTypes.Name, name) };
  return new ClaimsIdentity(
    claims, DefaultAuthenticationTypes.ApplicationCookie);
}

図 6 に示すように、Twitter のユーザーが Twitter アプリケーションを認証して情報を共有したら、Web ページはユーザー名やおそらくその他のデータを受け取ります (これは、実際に使用しているソーシャル API によって異なります)。

The Classic Twitter-Based Authentication
図 6 Twitter に基づくクラシック認証

ExternalLoginCallback メソッドの前のコードでは、ローカル メンバーシップ システムにユーザーが追加されていないことがわかります。これは、最もシンプルなシナリオです。状況によっては、ローカル ログインと外部認証のユーザー名の間のリンクをプログラムから登録するために Twitter 情報を使用する場合もあります (これは、ウィザードの既定の認証コードで提示されるソリューションです)。最後に、ユーザーを別のページにリダイレクトし、電子メール アドレスを入力できるようにするか、単にサイトに登録できるようにするかを決めることができます。

クラシック認証とソーシャル認証を混在させる

ほぼすべての Web サイトでは、ある程度の認証が必要です。ASP.NET Identity によってコードの記述が容易になり、統一されたファサードを利用してクラシック認証とソーシャル認証を混在させることができます。ただし、ユーザー アカウントの処理と、どのような情報を収集および保存するかについて明確な戦略を立てることが重要です。クラシック認証とソーシャル認証を混在させると、個人ユーザー アカウントを持ち、そのアカウントを Facebook、Twitter などの複数のログイン プロバイダーや、ASP.NET Identity でサポートされる任意のログイン プロバイダーに関連付けることができます。このようにして、ユーザーは複数の方法でサインインできますが、アプリケーションから見ればユーザーは一意のままです。また、多くのログイン プロバイダーを簡単に追加または削除できます。

Dino Esposito は、『Architecting Mobile Solutions for the Enterprise』(Microsoft Press、2012 年)、および近日出版予定の『Programming ASP.NET MVC 5』(Microsoft Press) の著者です。JetBrains の .NET Framework および Android プラットフォームのテクニカル エバンジェリストでもあります。世界各国で開催される業界のイベントで頻繁に講演しており、software2cents.wordpress.com (英語) や Twitter (twitter.com/despos、英語) でソフトウェアに関するビジョンを紹介しています。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Pranav Rastogi に心より感謝いたします。