FacebookのOAuthを実装する
1 Facebookテンプレートの認証UIはちょっとVS2012 SP2 以降で提供されているMVC5用のFacebookテンプレートは、超簡単にFacebookアプリが作成できる優れものです。同テンプレートでは、Actionに[FacebookAuthorize]属性を指定するだけで、必要な認証処理を全てバックグランドでやってくれるため、プログラマは受けとったFacebookContextを使うだけです。
public class HomeController : Controller{
[FacebookAuthorize("email", "user_photos")]
public async Task<ActionResult> Index(FacebookContext context){
var user = await ontext.Client.GetCurrentUserAsync<MyAppUser>();
return View(user);
}
しかし、このアプリ認証のUIにちょっと問題があります。
(1)問題点1 認証ダイアログが2回表示される
1つの目の問題は、認証ダイアログが2回表示されて、ユーザに違和感を与えるというものです。テンプレートに付属されている上記のコードでも、認証ダイアログが次のように2回に表示されます。
1回目に表示されるダイアログ(左側)は、全てのFacebookユーザにデフォルトで与えられている許可である「公開プロファイル」と「友達リスト」に関する承認です。そして2回目に表示されるダイアログ(右側)は[FacebookAuthorize]属性で指定した、「"email", "user_photos"」「メールアドレス、写真」に関する追加承認です。
許可を求める権限は、oauth/dialogのscopeパラメータで指定するだけなのですが、なぜ、2回に分けているのでしょう。
以下は、Fiddler確認したoauth/dialogへの2回のリダイレクト先です。
GET https://www.facebook.com/dialog/oauth?redirect_uri={url}&client_id={appId} HTTP/1.1 GET https://www.facebook.com/dialog/oauth?redirect_uri={url}&client_id={appId}&scope=email%2Cuser_photos HTTP/1.1
当初scopeの指定のない認証を要求し、改めてscopeを指定した認証を行っているのが分かります。
デフォルトの権限を確認し、新たに、追加の権限を確認しているます。
気持ちはよく分かるのですが、この分け方が果たしてユーザに伝わっているのか・・・・
(2)問題点2 キャンセルできない
2回に分けて表示される認証ダイアログですが、1回目のダイアログで「OK」を選択すると、2回目に「キャンセル」を選択しても、延々と、2つ目のダイアログが表示されます。そう「OK」を選択するまで永遠と許可を求めてくるのです。逃れるためにはブラウザをKILLするしか無いのです。1つ目のダイアログでOKしても、途中で気が変わることは十分にありますので、この無現ループは、比較的よく発生するのではないでしょうか。
問題1は、まだ少し許せるところもありますが、この問題2は、ちょっと許されないような気がしています。
2 OAuthの実装という事で、OAuth部分を実装してみることにします。
(1)FbOAuthクラスの作成
最初に必要な設定値を保持したFbOAuthクラスを作成します。必要な値は下記の4つですが、1,2は「Facebookテンプレート」でWeb.configに記載されているので、それをそのまま使用することにしました。
1 アプリケーションID
2 アプリのシークレットキー
3 キャンパスページ
4 キャンパスページのURL
public class FbOAuth{ static readonly string AppId = ConfigurationManager.AppSettings["Facebook:AppId"]; static readonly string AppSecret = ConfigurationManager.AppSettings["Facebook:AppSecret"]; const string CanvasPage = "https://apps.facebook.com/test_abcde/"; const string CanvasUrl = "https://localhost:44302/"; }
(2)oauth/dialogへのリダイレクト
認証の最初のアクションは https://www.facebook.com/dialog/oauth へのリダイレクトです。このリダイレクト先(パラメータをセットしたもの)を返すメソッド Dialog() をFbOAuthクラスに実装することにします。public class FbOAuth{ public static string Dialog(string scope, string callback){ var fb = new FacebookClient(); var loginUrl = fb.GetLoginUrl(new{ client_id = AppId, client_secret = AppSecret, redirect_uri = CanvasUrl + callback, //callback URL response_type = "code", scope = scope}); //Canvasの場合、iframe内からなので、この方法でないとリダイレクトできない return string.Format("<html><script>window.top.location.href ='{0}'; </script></html>",loginUrl.AbsoluteUri); }
ここで使用している FacebookClientのGetLoginUrl()メソッドは、/oauth/dialogへのurl(パラメータつき)を生成するメソッドです。
redirect_uriには、このリダイレクト先で認証作業が完了した際に、このアプリに戻ってくる時のURLを指定しています。
なお、このリダイレクトは return Redirect(new_url)という風にしてもブラウザは真っ白になってしまいます。
これは、Facebookアプリ(Canvasアプリ)がiframeの中で動作しているため、iframeの外でリダイレクトさせる必要があるからです。
ここでは、window.top.location.hrefにリダイレクト先を指定したScriptを含んだHTMLを生成し、return Context(html)と処理することで対処しています。
下記は、このDialogメソッドを呼び出しているAction側のコードになります。
public class HomeController : Controller{ public async Task<ActionResult> Index(){ //...... return Content(FbOAuth.Dialog("email,user_photos","/Home/CallBack")); //...... }
(3)コールバック
リダイレクト先で認証処理を終了すると、「認可コード」を添えて、指定したコールバックに戻されます。public class HomeController : Controller{ public ActionResult Callback(string code){ var accessToken = FbOAuth.GetAccessToken(code, Request.Url.AbsoluteUri); if (accessToken == null){ return View("Error");//キャンセルされた } Session["AccessToken"] = accessToken; return Redirect(FbOAuth.Canvas()); }
受け取った「認証コード」を使用して「アクセストークン」を取得するメソッド GetAccessToken() をFbOAuthクラスに実装します。
具体的には、https://graph.facebook.com/oauth/access_tokenへアクセスしてトークンを取得しています。
public class FbOAuth{ public static string GetAccessToken(string code, string requestUrl){ if (code == null){ return null;//認証をキャンセルされた場合、code=nullになっている } //アクセストークンの取得 try{ var fb = new FacebookClient(); //2014.02.28 修正 dynamic result = fb.Post("oauth/access_token",new{ dynamic result = fb.Get("oauth/access_token",new{ client_id = AppId, client_secret = AppSecret, redirect_uri = requestUrl, //これがoauth/dialogへのリクエストと一致していないとトークンは取得できない code = code }); return result.access_token; } catch (Exception){ ; //coedがタイムアウトしている場合などは、失敗する } return null; }
※2014.02.28追記
こちらのリクエストはPostではなくGetでした。修正させて頂きました
最後に、Coallbackメソッドで、元のURLにリダイレクトさせるためのURL取得用メソッド Canvas()ですが、これは、単純に、「キャンパスページ」を返しているだけです。
public class FbOAuth{ public static string Canvas(){ return CanvasPage; }
3 FbOAuthクラスの使用最後に、今回作成したFbOAuthクラスを使用して書き換えたIndexアクションを記載します。
仕様としては、FbOAothクラスを使用してアクセストークン取得に成功した場合、Session["AccessToken"]に保持するようにしています。
なお、AccessTokenを使用してFasebookClientを生成する場合、ClientProviderを使用しないと、内部のSerializerなどが初期化されないので注意(※1)が必要です。
public class HomeController : Controller{ //[FacebookAuthorize("email", "user_photos")] public async Task<ActionResult> Index(){ var accessToken = (string)Session["AccessToken"]; if (accessToken == null){ return Content(FbOAuth.Dialog("email,user_photos","/Home/CallBack")); } //Privider経由で作成しないとSerializer等が初期化されない var client = GlobalFacebookConfiguration.Configuration.ClientProvider.CreateClient(); client.AccessToken = accessToken; var user = await client.GetCurrentUserAsync<MyAppUser>(); return View(user); }
上記で、見た目は当初のサンプルとまったく同じですが、認証ダイアログの表示は1回となり、ちゃんとキャンセルもできるようになりました。めでたしめでたし。