ASP.NET Identity : External Login カスタマイズ (claim, scope, access token などの活用)

環境 : ASP.NET Identity 2.0 (Visual Studio 2013 Update 2), Facebook API 2.0

ASP.NET Identity に関する補足

こんにちは。

もう次期版が見えている時期なんですが (今更なんですが)、ASP.NET Identity について Microsoft Virtual Academy (通称 MVA, 日本版) を収録したのでご参照ください。今回、大輔さん から「ASP.NET Identity を 5 分から 10 分程度でまとめてほしい」という無茶なお題を受け、なんとか 20 分程度に納めました。「以前のメンバーシップの仕組みからどのように変化したのか」、「どんなカスタマイズができるのか」など、全体像を短時間でご理解いただけます。
MVA では、さまざまな最新技術を、動画でわかりやすくご紹介していますので、どうぞ ご活用ください。

なお、ご存じの通り、既に Visual Studio 14 の CTP (ASP.NET vNext) が出ていますが、ここでも (まだ主要な機能しか入ってませんが) ASP.NET Identity は使われています。(ASP.NET Identity のバージョンは新しくなります。EF との DI 統一など、内部の仕組みは結構変わる予定です。。。)

さて、MVA では実際のコードを使った紹介はできなかったので、本ブログで補足します。
まず今回は、MVA でも少し話した External Login (Google Account, Microsoft Account, Facebook, twitter などの外部の IdP との連携) 部分のカスタマイズについてです。後半では、Access Token を使ったサービス (GMail, OneDrive, Facebook など) との連携方法も紹介します。

 

External Login のカスタマイズ (Claim 追加など)

以前、「Build Insider : .NETで使えるアイデンティティ連携のためのライブラリまとめ」で DotNetOpenAuth (ライブラリー) を紹介した際、Google Account を例に追加の Claim を取得するサンプルを紹介しました。ASP.NET Identity においても、上級者向けに、こうした Authentication のパイプラインを細かくカスタマイズ可能です。

例えば、ASP.NET Identity の  External Login で Facebook 認証をする場合、実は、内部では Facebook Graph を使ってログインしているユーザーの付帯情報を取得していますが、ASP.NET Identity の既定の動作では、UserManager を使って Claim 情報を参照しても空になっています。(ログイン ユーザーの Facebook ID や E-Mail などの主要な情報は取得できます。)
ASP.NET Identity の既定の動作では、これら取得した付帯情報を Calim として設定しておらず、開発者は、必要な属性があれば追加で設定する必要があります。

これら付帯情報を Claim として追加するには、App_Start/Startup.Auth.cs を下記の通り編集します。
実は普段は見えていませんが、下記の通り、FacebookAuthenticationOptions、FacebookAuthenticationProvider などの用意されたオブジェクトを使って、細かな Property 設定や、フック処理 (Action) などを実装できます。(同様に、MicrosoftAccountAuthenticationOptions, TwitterAuthenticationOptions, GoogleOAuth2AuthenticationOptions などが使えます。)

補足 : 同様に、NuGet から Microsoft.Owin.Security.OpenIdConnect をインストールして、汎用的な OpenID Connect のフローを扱うこともできます。(app.UseOpenIdConnectAuthentication() 拡張メソッドと OpenIdConnectAuthenticationOptions を使用します。) OpenID に対応した他の IdP なども ASP.NET Identity と連携してみてください。

. . .
using Microsoft.Owin.Security.Facebook;
. . .

public void ConfigureAuth(IAppBuilder app)
{
  . . .

  //// Delete this (when using Facebook AuthN)
  //app.UseFacebookAuthentication(
  //  appId: "478919275571941",
  //  appSecret: "05b0f73dbb...");

  //// Instead, we use like this
  var options = new FacebookAuthenticationOptions()
  {
    AppId = "478919275571941",
    AppSecret = "05b0f73dbb...",
    Provider = new FacebookAuthenticationProvider()
    {
      OnAuthenticated = async (context) =>
        {
          var first_name = context.User.Value<string>("first_name");
          context.Identity.AddClaim(
            new System.Security.Claims.Claim("urn:facebook:first_name", first_name));
          var last_name = context.User.Value<string>("last_name");
          context.Identity.AddClaim(
            new System.Security.Claims.Claim("urn:facebook:last_name", last_name));
        }
    }
  };
  app.UseFacebookAuthentication(options);
  . . .

}
. . .

ASP.NET Identity では、初回、ログインをおこなうと、以下の ExternalLoginConfirmation の画面が表示され、ログインしたユーザー情報 (ID, Mail アドレス, など) をデータベース (IUserStore) に保持します。(既定では、Microsoft.AspNet.Identity.EntityFramework パッケージを使って、Entity Framework 経由で SQL Server に保存します。カスタマイズ次第で、MongoDB や Azure Table など、他のデータストアに保存することもできます。)
そして、以降は、この設定された属性情報をアプリケーションで参照できます。(次回以降、同じ Id でログインした際にも、このデータベースの内容を参照します。)
なお、この際、下図の「出身地」(HomeTown) のように、ログイン情報にアプリ固有の追加のユーザー属性を設定することもできます。

さて、上記の Startup.Auth.cs で追加した External Login の Claim も、同様にデータベースに保持しておかないと以降の処理で参照できません。
この設定をおこなうには、例えば、Controllers/AccountController.cs の ExternalLoginConfirmation に下記太字の通り追記して、External Login の Claim も保存します。ここでは、強制的に External Login で取得した Claim をデータベースに (Claim として) 保持していますが、上図のように UI で確認してから登録させるような実装も可能でしょう。
なお、ここで登場する External Cookie は、この ExternalLoginConfirmation と同時に消されてしまうので、この永続化の処理は、必ずこのタイミングで実行してください。(以降の処理で External Login の Claim を参照することはできません。)

public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
{
  if (User.Identity.IsAuthenticated)
  {
    return RedirectToAction("Manage");
  }

  if (ModelState.IsValid)
  {
    var info = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
      return View("ExternalLoginFailure");
    }
    var user = new ApplicationUser()
    {
      UserName = model.Email,
      Email = model.Email,
      Hometown = model.Hometown
    };
    IdentityResult result = await UserManager.CreateAsync(user);
    if (result.Succeeded)
    {
      result = await UserManager.AddLoginAsync(user.Id, info.Login);
      if (result.Succeeded)
      {
        var claimsIdentity =
          await AuthenticationManager.GetExternalIdentityAsync(
            DefaultAuthenticationTypes.ExternalCookie);
        var claims = claimsIdentity.Claims;
        foreach(var claim in claims)
        {
          if ((claim.Type == "urn:facebook:first_name") ||
            (claim.Type == "urn:facebook:last_name"))
            await UserManager.AddClaimAsync(user.Id, claim);
        }

        await SignInAsync(user, isPersistent: false);
        return RedirectToLocal(returnUrl);
      }
    }
    . . .

この Claim の内容は、アプリケーションでは、IdentityUser.Claims の Collection に入ります。(なお、ASP.NET Identity 2 では、IdentityUser から継承された ApplicationUser が使用されています。)
このため、アプリケーションからは、例えば、以下の方法で、この Claim を参照できます。

. . .
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Owin;
. . .

public class HomeController : Controller
{
  public async Task<ActionResult> Index()
  {
    var usermgr = HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    var claims = await usermgr.GetClaimsAsync(User.Identity.GetUserId());
    var firstName, lastName;
    foreach (var claim in claims)
    {
      if(claim.Type == urn:facebook:first_name)
        firstName = claim.Value;
      else if(claim.Type == urn:facebook:last_name)
        lastName = claim.Value;
      . . .
    }
    . . .

    return View();
  }
}
. . .

実際に動作をさせた後、Server Explorer などでデータベース (LocalDb) の内容を確認すると、下図の通り Claim 情報が登録されているのが確認できます。

 

Access Token の取得と外部サービス (Facebook SDK, etc) との連携

上記を応用すると、OAuth の scope 設定や、access token などを使って、各種サービスと連携した処理も構築できます。(例えば、Microsoft Account を使用して OneDrive と連携したり、Google Account を使用して GMail と連携可能です。)
今回は、上記の続きとして、Facebook Graph を使って友達リスト (Friend list) を取得するサンプルを実装します。

OAuth では、特定の操作が可能な access token を取得するには、内容に応じた scope を設定します。(例えば、Microsoft Account から取得する access token を使って OneDrive へファイル登録を行う場合は、scope に wl.signin, wl.skydrive_update を設定します。)
今回は、Facebook で Friend list を取得するので、scope として user_friends を追加します。
まず、App_Start/Startup.Auth.cs に下記太字の通り追記します。

public void ConfigureAuth(IAppBuilder app)
{
  . . .

  var options = new FacebookAuthenticationOptions();
  options.AppId = "478919275571941";
  options.AppSecret = "05b0f73dbb...";
  options.Scope.Add("user_friends");
  . . .

External システム (Facebook など) の Access Token は、下記の通り、認証後に返ってくる context の context.AccessToken で参照できます。
下記の通り、今回は、上記の first_name, last_name と同様、この取得した Access Token を Claim として保持しておきます。

public void ConfigureAuth(IAppBuilder app)
{
  . . .

  var options = new FacebookAuthenticationOptions();
  options.AppId = "478919275571941";
  options.AppSecret = "05b0f73dbb...";
  options.Scope.Add("user_friends");
  options.Provider = new FacebookAuthenticationProvider()
  {
    OnAuthenticated = async (context) =>
      {
        var first_name = context.User.Value<string>("first_name");
        context.Identity.AddClaim(
          new System.Security.Claims.Claim("urn:facebook:first_name", first_name));
        var last_name = context.User.Value<string>("last_name");
        context.Identity.AddClaim(
          new System.Security.Claims.Claim("urn:facebook:last_name", last_name));
        context.Identity.AddClaim(
          new System.Security.Claims.Claim("ExternalAccessToken", context.AccessToken));
      }
  };
  app.UseFacebookAuthentication(options);
  . . .
}
. . .

また、前述の通り、この access token も Claim の一部として永続化しておくため、Controllers/AccountController.cs の ExternalLoginConfirmation メソッドに下記太字の通り追記しておきます。

public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
{
  if (User.Identity.IsAuthenticated)
  {
    return RedirectToAction("Manage");
  }

  if (ModelState.IsValid)
  {
    var info = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
      return View("ExternalLoginFailure");
    }
    var user = new ApplicationUser() { UserName = model.Email, Email = model.Email, Hometown = model.Hometown };
    IdentityResult result = await UserManager.CreateAsync(user);
    if (result.Succeeded)
    {
      result = await UserManager.AddLoginAsync(user.Id, info.Login);
      if (result.Succeeded)
      {
        var claimsIdentity =
          await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
        var claims = claimsIdentity.Claims;
        foreach(var claim in claims)
        {
          if ((claim.Type == "urn:facebook:first_name") ||
            (claim.Type == "urn:facebook:last_name") ||
            (claim.Type == "ExternalAccessToken"))
            await UserManager.AddClaimAsync(user.Id, claim);
        }
        . . .

補足 : 今回、access token を Claim の一部としてデータベースに保持していますが、access token には有効期限があるので注意してください。(永続的ではありません。)

以上で準備完了です。
あとは、保持された Facebook の access token を使って Friend list を取得する処理をプログラミングします。
今回は、.NET 用の Facebook SDK を使用してみましょう。NuGet から .NET 用の Facebook SDK (下図) をインストールします。

例えば、以下の通り記述すると、SignIn した User の友達 (Friend) の名前を取得できます。

. . .
using Facebook;
using Newtonsoft.Json.Linq;
. . .

public async Task<ActionResult> Index()
{
  var usermgr = HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
  var claims = await usermgr.GetClaimsAsync(User.Identity.GetUserId());
  var accessToken = (from c in claims
            where c.Type == "ExternalAccessToken"
            select c.Value).FirstOrDefault();
  var client = new FacebookClient(accessToken);
  var frindObj = client.Get("/me/taggable_friends");
  JObject jfriend = JObject.Parse(frindObj.ToString());
  foreach (var f in jfriend["data"].Children())
  {
    var friendname = f["name"].ToString();
    . . .
  }
  . . .

  return View();
}
. . .

なお、この Web アプリを実行すると、Facebook の SignIn 後に、下図の通り「友達リスト」(Friend List) の取得を要求する Consent UI が表示されます。(普段は「公開プロフィール」のみを要求します。)
このアプリケーションを実行するには、利用者が、この Consent UI で承諾をおこなう必要があります。

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s