FuelPHPでのFacebookログインの実装

# はじめに

わりと今さらですが、FuelPHP で Facebook ログインを実装した時のメモです。
FuelPHP では、Opauth というパッケージを使うことにより容易にソーシャルログインを実装することが可能です。

# Facebookアプリを作成

これは FuelPHP に限った話ではないですが、Facebook の使ったソーシャルログインを実装するためには、まず Facebook 側でソーシャルログイン用の Facebook アプリを作成しておく必要があります。
Facebook アプリを作成するには Facebook の開発者登録が必要ですので、まずは開発者登録を行います。

## 開発者登録
開発者登録をしたいアカウントでFacebookにログインした状態で、下記のページにアクセスします。

https://developers.facebook.com/

ページの右上の「スタートガイド」をクリックすると登録用のウィンドウが出てきます。
あとは画面の案内通りに進めれば開発者登録が終わります。
ちなみに、開発者登録には電話番号認証もしくはクレジットカード認証が必要になりますので、どちらかを用意しておきます。

##アプリ作成

開発者登録を行うと、https://developers.facebook.com/ のスタートガイドの部分が「マイアプリ」に変わります。
「マイアプリ」をクリックすると、Facebook アプリの作成画面に行くことができます。

ここで、作成したいアプリを選択して作成すると、アプリIDと secret が作成されます。
この２つが Facebook アプリを作成する上で必要となるので、どこかに記録しておきます。
（アプリの設定についても適宜行いますが、ここでは省きます）

# FuelPHP 側の設定

Facebook アプリの作成が終われば、あとは FuelPHP側での対応を行えばOKです。
以降、FuelPHP-1.8.2 の例になります。

## FuelPHP インストール

まずは、FuelPHP をインストールします。
やり方はなんでもいいですが、例えば下記のような感じで。
詳細は、各種 FuelPHP のインストールを解説しているページを参照ください。

```
% git clone --recursive git://github.com/fuel/fuel.git
% cd fuel
% php composer.phar update
% php oil refine install
```

以降、FuelPHP を `/project/test1` 以下にインストールしたこととします。

## パッケージインストール

次に、Facebook ログインの実装に必要なパッケージをインストールします。
パッケージは Composer を使ってインストール可能なので、まずは下記のように composer.json に追記します。

```composer.json
"opauth/opauth": "0.4.*",
"opauth/facebook": "dev-master",
```

下記のように Composer を一度 self-update しつつ、パッケージをインストールします。

```
% php composer.phar self-update
% php composer.phar update
```

## 設定

まず、config.php を下記のように設定して、auth パッケージを有効化します。

```config.php
'always_load' => array(
		'packages' => array(
			'orm',
			'auth',
		),
),
```

次に、`packages/auth/config/opauth.php` を `app/config/` 以下にコピーし、下記のように記録しておいた Facebook のアプリIDとsecret を記載します。
また、email 情報も取得したい場合には、デフォルトでは email 情報は得られないので、下記のように scope と fields も設定します。

```opauth.php
'Strategy' => array(
		'Facebook' => array(
	       'app_id' => '1111111111111111',
	       'app_secret' => 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
         'scope' => 'email',
         'fields' => 'email,first_name,last_name,name',
	    ),
),
```

## DB作成

通常の FuelPHP 使用時と同様に、MySQL などの DB 環境を整備し、FuelPHP 側の設定を行います。
FuelPHPからDB接続ができる状態となったら、Opauth に必要なDBテーブルを作成するために、下記を実行します。

```
% php /project/test1/oil refine migrate --packages=auth
```

ここまでで設定関係は完了となります。

## 実装

ここまでで設定した内容を使って、Facebook ログインを実装します。
ここでは非常に簡単な例として、下記のようなサイトを作成します。

|URL path|概要|
|:-----------|:------------|
|/sample/index|	ログイン済みの時のみ表示可能とする（未ログインなら /user/login にリダイレクト）
|/user/login	|Facebook ログインするためのリンクを表示
|/user/auth/facebook|	Facebook ログイン

### /sample/index

まずは、ログイン済みの時だけ表示可能な /sample/index を実装します。
手っ取り早く下記で generate します。

```
% php /project/test1/oil g controller sample index
Creating view: /project/test1/fuel/app/views/template.php
Creating view: /project/test1/fuel/app/views/sample/index.php
Creating controller: /project/test1/fuel/app/classes/controller/sample.php
```

`app/views/sample/index.php` はログイン済みの時に表示される内容なので、適当に表示したい内容に修正します。
`app/classes/contoller/sample.php` は、生成時の状態に `before()` を追加して、未ログイン時にログインページにリダイレクトするようにします。

```app/classes/contoller/sample.php
<?php

class Controller_Sample extends Controller_Template
{
	public function before()
    {
        parent::before();
        if ( ! Auth::check())
        {
            Session::set_flash('error', 'no login');
            Response::redirect('user/login');
        }
	}

	public function action_index()
	{
		$data["subnav"] = array('index'=> 'active' );
		$this->template->title = 'Sample » Index';
		$this->template->content = View::forge('sample/index', $data);
	}

}
```

### /user/login

次に、ログインするためのリンクを表示する /user/login を実装します。
未ログインの場合には、このページにリダイレクトされてきます。
こちらも、手っ取り早く下記のように generate します。

```
% php /project/test1/oil g controller user login
Creating view: /project/test1/fuel/app/views/user/login.php
Creating controller: /project/test1/fuel/app/classes/controller/user.php
```

`app/views/user/login.php` は、Facebook ログインする際には /user/auth/facebook に遷移させるようにします。
下記は、単にリンクだけを置く例です。

```app/views/user/login.php
<ul class="nav nav-pills">
	<li class='<?php echo Arr::get($subnav, "login" ); ?>'><?php echo Html::anchor('user/auth/facebook','Facebookでログインする');?></li>
</ul>
```

`app/classes/controller/user.php` は、とりあえず /user/login については特に何も手を入れなくてもOKです。


### /user/auth/facebook

今回の本題となる Facebook ログインを実装します。
基本的には、下記の公式ドキュメントの通りに実装すれば問題ありません。
http://fuelphp.jp/docs/1.8/packages/auth/examples/opauth.html

今回は、/user/auth/facebook という path なので、`app/classes/controller/user.php` に Facebook ログインのコードを実装していきます。

#### action_auth()

まず、/usr/auth/facebook の path に直接関係する `action_auth()` メソッドを実装する必要がありますが、このメソッドはドキュメントの通り下記の内容でOKです。

```app/classes/controller/user.php
public function action_oauth($provider = null)
{
	// 呼び出すための OAuth プロバイダを持っていない場合は出て行く
	if ($provider === null)
	{
		\Messages::error(__('login-no-provider-specified'));
		\Response::redirect_back();
	}

	// Opauth の読み込み、プロバイダのストラテジーを読み込みプロバイダにリダイレクトするでしょう
	\Auth_Opauth::forge();
}
```

コメントに書かれている通り、有効な provider （今回であれば facebook）が指定された場合には、`\Auth_Opauth::forge()` により、指定された provider のページにリダイレクトされます。


#### action_callback()

`action_callback()` は、Facebook のページでログイン後にコールバックされるメソッドです。
ここで、Facebook での認証結果を取得します。

```app/classes/controller/user.php
public function action_callback() 
{
    try {
        $opauth = Auth_Opauth::forge(false);
        $status = $opauth->login_or_register();
        $url = 'sample/index';
        Log::warning("login_or_register:" . $status);
        switch ($status) {
            case 'linked':
                // ログイン済みでプロバイダはこのユーザーと関連付けられている
                break;
            case 'logged_in':
                // 既知のプロバイダへ関連付けられ、そのアカウントでログイン
                break;
            case 'register':
                // このプロバイダでのログインは関連付けられていないので、ローカルアカウントを作成する必要あり
                $url = "user/register";
                break;
            case 'registered':
                // このプロバイダでのログインは関連付けられていないが十分な情報が返されたので、ローカルアカウントを自動的に登録した
                break;
            default:
                // 状態を判定できず
	  	  }
	      Response::redirect($url);
    }catch(OpauthException $e) {
        // 認証失敗
    }catch(OpauthCancelException $e) {
        // 認証失敗（キャンセル）
    }
}
```

`login_or_register()` で認証結果を取得することができるので、これを使って判定します。
ここでは、**linked** / **logged_in** / **registered** の時にはログインが完了したことを示しているので、/sample/index にリダイレクトするようにしています。

**register** の時には、Facebook ログインは成功しているが、まだローカルアカウントと関連付けられていない（ローカルアカウントが作成されていない）状態なので、ローカルアカウントと関連付けるために /user/register にリダイレクトするようにしています。


#### action_register()

`action_register()` では、Facebook ログインの情報を関連付けつつローカルアカウントを作成する処理を行います。

```app/classes/controller/user.php
public function action_register()
{
    if ($authentication = Session::get('auth-strategy.authentication', array()))
    {
        try
        {
            $user = Session::get('auth-strategy.user');
            $user_id = Auth::create_user(
                $user['email'],
                'dummy',
                $user['email'],
                Config::get('application.user.default_group', 1),
                array(
                    'fullname' => $user['name'],
                )
            );

            if ($user_id)
            {
                $opauth = Auth_Opauth::forge(false);

                // call Opauth to link the provider login with the local user
                $insert_id = $opauth->link_provider(array(
                    'parent_id' => $user_id,
                    'provider' => $authentication['provider'],
                    'uid' => $authentication['uid'],
                    'access_token' => $authentication['access_token'],
                    'secret' => $authentication['secret'],
                    'refresh_token' => $authentication['refresh_token'],
                    'expires' => $authentication['expires'],
                    'created_at' => time(),
                ));
                Auth::instance()->force_login((int) $user_id);

                Session::set_flash('success', __('login.new-account-created'));
                Response::redirect_back('calendar');
            }
            else
            {
                \Session::set_flash('error', __('login.account-creation-failed'));
            }
        }

        // catch exceptions from the create_user() call
        catch (SimpleUserUpdateException $e)
        {
            // メールアドレスが重複
            if ($e->getCode() == 2)
            {
                Session::set_flash('error', __('login.email-already-exists'));
            }

            // ユーザー名が重複
            elseif ($e->getCode() == 3)
            {
                Session::set_flash('error', __('login.username-already-exists'));
            }

            // これは起こり得ないが、ずっとそうとは限らない...
            else
            {
                Session::set_flash('error', $e->getMessage());
            }
        }
    }
    else
    {
        Session::set_flash('error', 'Failed to retrieve a user information from the provider.');
    }

    Response::redirect_back('user/login');
}
```

`Session::get('auth-strategy.authentication')` で認証情報が、`Session::get('auth-strategy.user')` でそれに紐づくユーザー情報が取得できます。
これらを使ってローカルユーザーを作成し、provider login と紐付けることをしています。

# おわりに
ここまでの実装で、一通り Facebook ログインが機能することが確認できます。
一度流れが分かってしまえばかなり簡単に実装することができるので、いろいろと使えそうですね。
Opauth は存在は知っていましたが、面倒くさくてずっと放置していたのですが、もっと早く使っておけばよかったと思いました。
なお、Opauth は Facebook 以外にも Twitter や Google あたりには少なくとも対応しているようです。