たまにアプリからのfacebookログイン失敗してるけどなんなの
cakePHP(v2.2.2)にFacebook SDK for PHP(v.3.2.2)を入れてFacebookへのログインを実装しているのですが、1度目Facebookへのログインボタンを押してもログインせず、2回目押すとログインするという現象が起きましたので原因を調査しました。
問題のソースはこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
App::uses('Controller', 'Controller'); App::import('Vendor','facebook',array('file' => 'facebook'.DS.'facebook.php')); class AppController extends Controller { var $facebook; var $components = array('Auth'); var $uses = array('User'); function beforeFilter() { parent::beforeFilter(); // ログイン判定 $this->Auth->fields = array( 'Username' => 'facebook_code', 'password' => 'password', ); $this->facebook = new Facebook(array( 'appId' => Configure::read('facebook_appId'), 'secret' => Configure::read('facebook_secret'), )); try { // Facebookの持つcookieによりログアウトしてもgetUserがとれることがあるので/meをとることでエラーになるか判定 $facebook_me = $this->facebook->api('/me', 'GET'); $user = $this->User->find('first', array( 'conditions' => array('facebook_code'=>$uid) )); } catch (Exception $e) { $loginUrl = $this->facebook->getLoginUrl(array( 'scope' => 'user_birthday,user_relationships,user_relationship_details,email', 'display' => 'touch', )); $this->set('loginUrl', $loginUrl); } } } |
Apacheのエラーログをみると
1 |
CSRF state token does not match one provided. |
と記録されています。これはSDKの中にあるbase_facebook.phpの
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
protected function getCode() { if (isset($_REQUEST['code'])) { if ($this->state !== null && isset($_REQUEST['state']) && $this->state === $_REQUEST['state']) { // CSRF state has done its job, so clear it $this->state = null; $this->clearPersistentData('state'); return $_REQUEST['code']; } else { self::errorLog('CSRF state token does not match one provided.'); return false; } } return false; } |
によるものでした。stateがいつの間にか消失してしまったようです。stateは単純にSDKの中でログインURLを取得する際に
1 |
$this->state = md5(uniqid(mt_rand(), true)); |
と生成するもので、セッションに入れておいてリクエストを受け取ったクライアントがリクエストを送ったクライアントと一致するかのチェックに使われています。このstateがどこで消えたのか追っていきました。
犯人はfacebook->api()
結果、Facebookへのログイン状態を確かめるfacebook->api()で消えていました。
facebook->api()を使うのはこことかの質問サイトでみつけたやり方で、facebook->getUserだとfacebookからログアウトした後もクッキーからログインし続ける、または逆にfacebookにログインしていてもユーザーIDがクッキーのために返ってこないことを回避するためにfacebook->api()を使うんだとか。(実際はfacebook->getUserで問題なかった)
facebook->api()のソースを追うと、OAuthで例外エラーがでたときにセッションを破棄しています。ここまで追っていったところで流れが追いきれなくなってきました・・・。
getLoinUrlの前にapi(‘me’)でログインチェックするのはやめよう
整理しますとfacebookでログイン状態、自分の作ったアプリでは未ログイン状態で画面を表示すると、アプリ認証がまだなのでfacebook->getLoginUrl()が実行され、stateがセッションに保存されます。このままログインすればいいのですが、問題の起きるパターンでは、ここで同じページを再度読み込みます。すると再度ログイン認証facebook->api()が行われ、セッションを破棄したうえでログインURLを生成するため、認証先でエラーとなるというわけです。
認証チェックはgetUserを使う、てかそれが普通だった
facebook->api()を使ってこの現象を回避する方法が思いつかなかったので、結局facebook->api()はやめてfacebook->getUser()を使うことにしました。テストしたところ、ちゃんとfacebookのログイン状態と連動した値を返していました。
そしてSDKのreadme.mdをみますと、ちゃんとfacebook->getUser()を使って0以外が返ってきたときだけapi(‘me’)を使うようになっているじゃありませんか。これみて作ってれば最初からこんなエラーはぶち当たらなかったはず・・・。
そんなわけで、普通はこのエラーには当たらないと思います。ただ解決までけっこう時間がかかってしまったのでドキュメントちゃんとみないとだめですよという教訓としてブログに載せてみました。