ASP.NET MVC で OpenID、OAuth とバリバリ連携 (DotNetOpenAuth)

DotNetOpenAuth があまりに使いやすいので、ASP.NET MVC と組み合わせたプログラミングについて書こうと思う。

やりたいこと

ASP.NET MVC のプロジェクト テンプレートが作成するフォーム認証では、下図のような「Log On」リンクが表示され、 ここをクリックすると独自のログイン画面を使ったフォーム認証がおこなわれる。 これを Google などの OpenID や OAuth (Live ID など) でログインするようにアプリケーションを改造したいことがあるが、面倒なのは、OpenID や OAuth の処理だ。

そこで、あの DotNetOpenAuth を使って、この面倒な処理を簡単に作ってみようと思う。
知らない人は「怪しいライブラリーを使うのは心配」と思うかもしれないが、sourceforge.jp magazine でも書かれている通り、結構ちゃんとした経緯を持つ由緒あるライブラリーだ。
なお、DotNetOpenAuth に掲載されているサンプル コード は上記の良いサンプルとなっているが、このサンプルでは、独自の画面を提供して、ここに接続先の Provider のアドレスを入力する仕様となっているため、 今回は、このコードをちょっとカスタマイズする感じになる。

まず、知らない人のために、ASP.NET MVC のプロジェクトが既定で作成する一般的なフォーム認証の流れを復習しておく。(これ分からないと、このあとのコードの流れがわからないので)

  • ユーザーが「Log On」(上図) をクリックすると、ログイン画面 (/Account/LogOn のビュー) が表示される
  • ここに必要な情報を入力してログインを実行すると、Web.config に設定されているメンバーシップ データベース (SQL Server のデータベース) にユーザーの確認をおこなう (これも、/Account/LogOn アクションの中でおこなわれている)
  • 認証に成功したら、もと居たページに戻っていく。(ASP.NET のフレームワークが、Query String を使って戻り先の URL を取得し、リダイレクトする。)
  • さらに、Cookie に「認証成功」の情報 (フォーム認証用のチケット) を書き込む。各ページでは、このクッキー情報を見て、そのユーザーが認証済みかどうかを判断する。
  • なお、Web.config には、こうしたフォーム認証用の設定がおこなわれている。(で、フレームワークが、この主要な流れを処理している。)

他にも、ASP.NET MVC の既定のプロジェクトでは、ユーザー情報を登録・管理するための実装もおこなわれているが、今回は、この辺の細かなことは無視する。

で、上記をどう改造するかというと、フォーム認証の仕組み全体を無くしてしまうのはもったいない (mottainai) ので、 上記で記載したフォーム認証の基本的な仕組み自体はそのまま利用して、 ログイン画面での処理を Google、Live ID、Facebook などの外の ID Provider を使って認証させる。

まずは、OpenID の例で確認し、つぎに OAuth 2 を使う方法で確認してみる。

OpenID の場合 (プログラミング)

では、早速。
今回は、Google Account を使って実験してみる。

まずは、Visual Studio 2010 で ASP.NET MVC 3 のプロジェクトを新規作成する。

DotNetOpenAuth は NuGet で取得できるが、DotNetOpenAuth そのものを NuGet で取得すると、OAuth 用の dll や、OpenID Provider 用の dll など、余計な dll まで持ってきてしまう。

PM> get-package -filter DotNetOpenAuth -l
Id                             Version
—                             ——-
DCCreative.DNOA4Glimpse        0.9.4
DotNetOpenAuth                 4.0.2.12119
DotNetOpenAuth.AspNet          4.0.2.12119
DotNetOpenAuth.InfoCard        4.0.2.12119
DotNetOpenAuth.InfoCard.UI     4.0.2.12119
DotNetOpenAuth.OAuth.Common    4.0.2.12119
DotNetOpenAuth.OAuth.Consumer  4.0.2.12119
DotNetOpenAuth.OAuth.Servic… 4.0.2.12119
DotNetOpenAuth.OpenId.Provider 4.0.2.12119
DotNetOpenAuth.OpenId.Provi… 4.0.2.12119
DotNetOpenAuth.OpenId.Relyi… 4.0.2.12119
DotNetOpenAuth.OpenId.Relyi… 4.0.2.12119
DotNetOpenAuth.OpenIdInfoCa… 4.0.2.12119
DotNetOpenAuth.OpenIdOAuth     4.0.2.12119
DotNetOpenAuth.Ultimate        4.0.2.12119
MVCRaven                       1.0.2
SimpleID                       1.0

このため、今回は、DotNetOpenAuth.OpenId.RelyingParty だけを NuGet でインストールする。(依存する必要な dll が参照設定される。)
なお、DotNetOpenAuth.OpenId.RelyingParty には、コードを使うものと、ASP.NET control を使うものの 2 種類があるが、今回は、前者を使う。

install-package DotNetOpenAuth.OpenId.RelyingParty

あと、ASP.NET 関連の dll も入れておく。

install-package DotNetOpenAuth.AspNet

さて、これで準備完了。あとは、コードを作成していく。
まず、ASP.NET MVC が作る AccountController.cs を開くと LogOn アクションは下記の通り 2 つあるが、 後者は、ログイン画面で情報を入力した場合に呼ばれる POST メソッド用のアクションなので、 今回は前者のみを使用する。

public class AccountController : Controller
{
   public ActionResult LogOn()
   {
     ...
   }
   [HttpPost]
   public ActionResult LogOn(LogOnModel model, string returnUrl)
   {
     ...
   }

あと、この LogOn を実装 (改良) する際は、いろいろなケースを想定しておく必要がある。 まず、右上の「Log On」のリンク (上図) をクリックして飛んでくる場合は、 引数 (Query String) には何も設定されないが、MVC の Authorize 属性 (フィルター) があるアクションが呼ばれ、その結果、この LogOn アクションにリダイレクトされる場合は、 ReturnUrl という Query String の値が入って飛んでくる。こうした場合でも、ちゃんと OpenID Provider で処理した後に、 この ReturnUrl に戻す必要がある。

こうしたことを考慮すると、下記のような実装になるだろう。

  • LogOn アクションでは、毎回、OpenID に関する情報を確認する。
    OpenID の認証前は、この情報が空 (null) なので、 OpenID Provider (今回は、Google を使用) にリダイレクトし、処理を要求する (下記コードの「1st Stage」のコメントの箇所)
  • OpenID Provider でログイン処理が完了すると、OpenID Provider が情報を付与して LogOn に戻す。(また、LogOn アクションに戻ってくる。この際にも、ちゃんと、上述の ReturnUrl の Query String が付与された状態で戻ってくるはず。)
  • 再度、LogOn での処理になるが、この OpenID Provider が返してきた情報を取得して、ASP.NET のフォーム認証の完了 (成功) 処理をおこなう。(下記コードの「2nd Stage」のコメントの箇所)
  • 以降、どのアクション (ビュー) に接続する場合にも、フォーム認証の仕組みによって、「認証された」という扱いで処理がおこなわれる。 また、Google Account のサイト側でも認証 Cookie が書き込まれているため、同じブラウザーを使って、Google Account を使用する別のサイト (GMail など) に接続すると、ログイン ID とパスワードの入力をおこなうことなく利用できる。(いわゆる、SSO が実現できる。)

コードで書くと、下記のようになる。

using DotNetOpenAuth.OpenId.RelyingParty; using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;

[HandleError(ExceptionType = typeof(Exception))]
public ActionResult LogOn(string returnUrl)
{
   var openid = new OpenIdRelyingParty();
   IAuthenticationResponse response = openid.GetResponse();

   //
   // 1st Stage : When logging first (redirect to provider)
   //
   if (response == null)
   {
     UriBuilder realmUriBld =
       new UriBuilder(Request.Url) { Query = "" };
     IAuthenticationRequest request = openid.CreateRequest(
       "https://www.google.com/accounts/o8/id",
       realmUriBld.Uri,
       Request.Url);

     // This app needs email address !
     request.AddExtension(new ClaimsRequest
     {
       Email = DemandLevel.Require
     });
     return request.RedirectingResponse.AsActionResult();
   }

   //
   // 2nd Stage : When logged-in to provider (resirect to ReturnUrl)
   //
   if (response.Status == AuthenticationStatus.Authenticated)
   {
     // if email exists, we set email to app's identifier.
     //   else, we set claime identifier to app's identifier.
     string identifier = string.Empty,
       email = string.Empty;
     FetchResponse fetchRes = response.GetExtension<FetchResponse>();
     if ((fetchRes != null) &&
       (fetchRes.Attributes[WellKnownAttributes.Contact.Email] != null))
       email = fetchRes.Attributes[
         WellKnownAttributes.Contact.Email].Values[0];
     if (string.IsNullOrEmpty(email))
       identifier = response.ClaimedIdentifier;
     else
       identifier = email;

     // Setting Form Authentication cookie and redirect !
     FormsAuthentication.SetAuthCookie(identifier, false);
     if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1
       && returnUrl.StartsWith("/")
       && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
       return Redirect(returnUrl);
     else
       return RedirectToAction("Index", "Home");
   }

   throw new Exception("some kind of error ...");
}

上記のコードでは、Google Account に入っている E メール アドレスの情報を要求し、これを取得している。(リダイレクトの際、Google 側で、EMail の情報を渡すことをユーザーに確認する画面が表示される。)

なお、今回、使わないゴミ (使っていない Login.cshtml など) はそのまま残しているが、必要なければこうした使わないファイルやアクションは削除しておこう。

OAuth 2.0 の場合 (プログラミング)

OAuth 1.0/1.0a (twitter など)、OAuth 2.0 (Live ID, Facebook, など) を使った認証も、このライブラリーを使うと楽に処理できる。(OAuth2 は、スクラッチでのプログラミングも大丈夫なくらい仕様がすっきりしてるが、DotNetOpenAuth を使うと、さらに楽に組める。)

今回は、Live ID (OAuth2) を例にプログラミングしてみる。

まず、現時点の DotNetOpenAuth (バージョン 4.0.1) で OAuth 2 のプログラミングをするための注意点として、DotNetOpenAuth が、OAuth 2 に最近 対応したという経緯などもあり、NuGet ではなく、DotNetOpenAuth のオフィシャル サイト からダウンロード可能なモジュールを使わないとダメみたい。
ダウンロードしたら、Bin フォルダーにある DotNetOpenAuth.dll をプロジェクトに参照追加する。(ここに、すべてのクラスが入っている。)

OAuth2 では、Provider 側で許可されたアプリケーションのみが接続を許される。許可されたアプリケーションには、Client ID, Client Secret が提供され、アプリケーション (Relying Party) は、 この情報を使って、Provider を使用することができる。 Live ID の場合は、下記のサイトでアプリケーションの登録をおこなうことで、アプリケーションの Client ID と Client Secret を取得できる。(あらかじめ、サインアップと、アプリケーションの登録をおこなっておく。)

Live Connect デベロッパー センター – マイ アプリケーション https://manage.dev.live.com/Applications/Index

さらに、上記のサイトで、リダイレクト ドメインと呼ばれるものを設定しておく (下図)。アプリケーションは、Live ID の認証後、下記以外のリダイレクト先に戻ることは許可されない。このため、開発時なども、どの URL を使うか、ちゃんと計画しておく必要がある。

以上で準備完了。あとはプログラミングするだけだ。

今回は、下記の通りプログラミングすれば良い。(「あれ、ReturnUrl は、どこに行っちゃったの ?」と思うかもしれないが、理由は、あとで説明します。。。)

using System.IO; using System.Net;
using System.Json;
using System.Configuration;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth2;

public class LiveAuthorizationTracker : IClientAuthorizationTracker
{
   public IAuthorizationState GetAuthorizationState(
     Uri callbackUrl, string clientState)
   {
     return new AuthorizationState()
     {
       Callback = callbackUrl
     };
   }
}

public class AccountController : Controller
{
   private static readonly AuthorizationServerDescription desc =
     new AuthorizationServerDescription
   {
     AuthorizationEndpoint = new Uri("https://oauth.live.com/authorize"),
     TokenEndpoint = new Uri(https://oauth.live.com/token),
     ProtocolVersion = ProtocolVersion.V20
   };

   private static readonly WebServerClient oauth2Cl =
     new WebServerClient(
       desc, "0000000000000000", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
     {
       AuthorizationTracker = new LiveAuthorizationTracker()
     };

   public ActionResult LogOn(string returnUrl)
   {
     IAuthorizationState oauthStat =
       oauth2Cl.ProcessUserAuthorization();

     //
     // 1st Stage : When logging first (redirect to provider)
     //
     if (oauthStat == null)
     {
       UriBuilder returnUriBld =
         new UriBuilder(Request.Url)
         {
           Query = ""
         };
       return oauth2Cl.PrepareRequestUserAuthorization(
         new string[]
         {
           "wl.signin",
           "wl.basic",
           "wl.emails",
           "wl.skydrive_update"
         },
         returnUriBld.Uri).AsActionResult();
     }

     //
     // 2nd Stage : When logged-in to provider,
     //   get user profile and resirect to ReturnUrl
     //
     string userStr = string.Empty;
     var req = WebRequest.Create(
       https://apis.live.net/v5.0/me?access_token=
         + Uri.EscapeDataString(oauthStat.AccessToken));
     using (var res = req.GetResponse())
     {
       using (var st = res.GetResponseStream())
       {
         StreamReader reader = new StreamReader(st);
         userStr = reader.ReadToEnd();
       }
     }
     JsonObject userJson = (JsonObject)JsonValue.Parse(userStr);
     JsonObject emailJson = (JsonObject)userJson["emails"];
     JsonPrimitive accountJson = (JsonPrimitive)emailJson["account"];

     FormsAuthentication.SetAuthCookie(
       (string) accountJson.Value, false);
     return RedirectToAction("Index", "Home");
   }
   ...

}

上記は、以下のような流れで処理される。(基本的には OpenID と類似。もちろん、使用されているプロトコルなどは、まったく別物だけど。。。)

  • ユーザーが「Log On」(上図) をクリックすると、上記の LogOn アクションが実行される。
  • もちろん、最初は、まだ Live ID の認証がおこなわれていないため (認証情報はないため)、 上記の oauthStat は null となり、上記の「1st Stage」の箇所が処理される。
  • 「1st Stage」の PrepareRequestUserAuthorization の箇所で、必要な情報が付与され、Windows Live の認証サイトへリダイレクトされる。(上記の wl.signin、wl.basic、・・・ とかの内容については、あとで解説。。。)
  • Live ID の認証が完了すると、再度、このアクション (/Account/LogOn) に戻ってくる。
    今度は、Live ID から必要な情報が Query String などで送られてくるため、ProcessUserAuthorization で必要な情報が取得され、oauthStat に設定される。(その結果、上記の「2nd Stage」が処理される。)
  • 上記の「2nd Stage」では、EMail Address などのユーザー情報を取得して、その情報を使って ASP.NET フォーム認証用の設定 (Cookie への認証情報の設定) をおこなう。
    さいごに、/Home/Index へリダイレクトする。

OpenID のときと同様、ログインし、同じブラウザーを使って、SkyDrive など Live ID を使う他のアプリケーションを起動すると、Live ID の認証サイト側で「認証されてるよ」という Cookie が書き込まれているため、ログイン画面は表示されず、認証済みの状態でアプリケーション (SkyDrive) にリダイレクトされる。つまり、SSO (シングル サインオン) が実現されている。

OAuth2 独自のポイントもある。
まず、上記で、WebServerClient のコンストラクタの引数(“0000000000000000” と “xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx”) には、上述で Windows Live から取得した Client ID と Client Secret を設定する。上述の通り、OAuth2 では、許可されたアプリケーションのみが Provider (Windows Live) に接続でき、決められたドメインのみへリダイレクトが許される。
つぎのポイントとして、OAuth 2 では、最終的に、Access Token と呼ばれる「許可証」のようなものが発行され、すべて、この Access Token を使って処理する。上記で、ユーザー情報を取得している箇所 (「2nd Stage」の箇所) でも Access Token を使用しているのがわかる。そして、上記の wl.xxx (wl.signin、wl.basic など) は、この Access Token にとって重要な役割を果たしている。この wl.xxx は、scope と呼ばれるもので、OAuth 2 では、この scope を使って、 必要な操作が可能な専用の Access Token が発行される。上記の wl.xxx は、すべて Live ID (Windows Live) 独自の scope で、 それぞれ以下を意味している。

  • wl.signin では、サインイン (ログイン) が許可される
  • wl.basic では、ユーザー情報などの基本情報の取得が許可される
  • wl.emails では、情報として EMail address (機微な情報) の取得が許可される。もちろん、この際、リダイレクト時に、確認(eメールをRPに渡すよ!という確認)は ちゃんと表示されるので、安心してほしい。(まあ Live だから、そこはちゃんとしてるでしょう)
  • wl.skydrive_update では、Windows Live SkyDrive の利用が許可される (SkyDrive へのデータの更新も許可される)

この他に、今回は使っていないが、wl.offline を指定すると、Refresh Token というものを使って、あとから Access Token を取り直すこともできる。(オフライン作業をおこなうアプリケーションにとっては、うってつけの方法。)

上記の「2nd Stage」では、取得した Access Token を使用してユーザー情報を取得しているが、このように RESTful なサービス (Web Api) を呼び出すことで、 SkyDrive にアクセスするなど、その他の Live 関連の操作もおこなうことができる。
ただ、OAuth2 の場合、上記のように、ユーザー情報 (Claim) を取得するために、Provider 独自なコード (上記の場合、Live ID 独自のコード) が混ざってしまうという点は、ちょっと嫌なところ。。。

上記の「2nd Stage」で、変数 userStr には、以下のフォーマットの文字列 (要は、Json) が返ってくるので、以降の処理で、この文字列を Parse して、ここから E-Mail Address を取得している。(.NET Framework 4 以前の場合は、NuGet で System.Json のパッケージを入れておくこと。)

{
  "id": "xxxxxxxxxxxxxxxx",
  "name": "ほげ 太郎",
  "first_name": "太郎",
  "last_name": "ほげ",
  "link": http://profile.live.com/cid-xxxxxxxxxxxxxxxx/,
  "gender": null,
  "emails":
  {
    "preferred": "hogehoge@live.com",
    "account": "hogehoge@live.com",
    "personal": null,
    "business": null
  },
  "locale": "ja_JP",
  "updated_time": "2011-08-24T02:55:15+0000"
}

ちなみに、現在のバージョン (DotNetOpenAuth 4.0.1) では、2 つほど注意点 (バグ ?) がある。

まず、今回のコードでは、上記の通り、AuthorizationTracker という少し面倒なものを使用しているが、わざわざ AuthorizationTracker (上記コードの LiveAuthorizationTracker) を使用する必要はなく、関数の引数への設定などで処理をおこなうことができる。しかし、AuthorizationTracker を指定しない場合、DotNetOpenAuth は、ID Provider (今回の場合、Live ID) から受け取った値の検証時に Session ID を使うので、今回のように Session を使っていない場合に mismatch エラーとなってしまう。このため、上記の通り、やや無理矢理 AuthorizationTracker を使っている。(逆の言い方をすると、Session を使っている場合は、わざわざ AuthorizationTracker を使用する必要はない。)

2 つ目は、ReturnUrl について。現在の DotNetOpenAuth 4.0.1 では、ReturnUrl のような Query String が入っていると、Access Token を取得する際の HTTP 要求 (上記コードの ProcessUserAuthorization の箇所) で POST データに正しい値を設定しないようで、要求に失敗する。(Live ID が Status Code 400 を返す。) このため、上記では、Query String で渡される ReturnUrl を使わず、 認証完了後は、必ず /Home/Index に飛ばすように実装している。
実際の開発では、例えば、最初に受け取った ReturnUrl を Session に入れておくなど (そして、認証後に、その Session の値を見て飛ばす) 回避可能なので、 必要なら頑張ってみてください。(あるいは、ここ から落とせるサンプル コードのように、自力で Access Token を取得する処理を書く !)

まあ、こんな感じで、ちょっとバグっぽいところはあるけど、なかなか使えるライブラリーだ。
いまだに、「.NET = ビジネスだけ」と思い込んでいる人をたまに見かけるが、最近の ASP.NET (特に ASP.NET MVC とその仲間達) をおいかけてる方はご存じの通り、いまや、ASP.NET は、オープンソース ライブラリーと組み合わせて柔軟に活用していかないと損だ。(ASP.NET MVC では、jqueryui, knockout.js などは、もう当たり前のように使ってるでしょ)

広告

ASP.NET MVC で OpenID、OAuth とバリバリ連携 (DotNetOpenAuth)」への1件のフィードバック

  1. ピンバック: SkyDrive REST API を使った Web アプリケーション開発 (Sample Code) - 松崎 剛 Blog - Site Home - MSDN Blogs

コメントは受け付けていません。