facebookアカウントを使ったソーシャルログインを実装してみる。

## Index
* [初めに](#初めに)
* [必要条件](#必要条件)
* [構築手順](#構築手順)
    * [facebook側の設定](#facebook側の設定)
    * [連携アプリ側の設定](#連携アプリ側の設定)
* [動かしてみる](#動かしてみる)

## 初めに
ソーシャルログインが最低限動く環境を作ってみました。
シンプルな造りなので概要は理解しやすいかと思います。

でも、セキュリティを担保できるほどの試験はしてなし、
実際に商用でのサービス提供もしていないので、これをそのまま商用で使うのはちょっと心許ないです。
また、商用でアプリを公開するためにはfacebookにアプリをレビューしてもらう必要があるみたいです。

ちなみに[googleアカウントを使ったソーシャルログイン環境の構築手順](https://qiita.com/yamori4/items/b1763825b64c2b5b3037)もまとめてますので、
興味のある方はそちらもどうぞ。

## 必要条件
* Facebook for Developersを利用できるアカウント(事前に作成済である前提としています。)
* phpが動作するウェブサーバ環境(本記事ではXAMPPでlocalhost環境を使ってますが、LAMPでも動きます。)  
※XAMPP環境の構築をする場合は[こちら](https://github.com/yamori4/objectOriented/blob/master/_readMe/installXampp.md)を参考にしてください。

## 構築手順
基本的に[facebook for Developers](https://developers.facebook.com/)に書かれている通りに順番に構築していけばいいのですが、個人的には分かりにくかったので詳細をこちらに書いておきます。  
### facebook側の設定
* [facebook for Developers](https://developers.facebook.com/)にアクセスして、[マイアプリ](https://developers.facebook.com/apps/)を選択します。
![マイアプリ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/c011dda1-7286-1d6a-03fb-752e3743c80d.jpeg)

* [アプリを作成]をクリック
![アプリを作成.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/cae22824-89ea-f173-ebd7-185691592153.jpeg)

* アプリタイプは「ビジネス」を選択して「次へ」
![ビジネス.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/efafa7a4-7a79-3002-3ce0-8983749b780c.jpeg)

* 「アプリ表示名」に適当な名前を設定して、アプリの目的は「自分自身または自分のビジネス」を選択して「次へ」
![アプリ作成設定.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/ff97225c-e308-ce2a-7944-a6de70b860b1.jpeg)

* アプリの作成時にパスワードを聞かれるので、それを入力すればアプリが作成できます。

* 作成したアプリの「商品を追加」から「Facebookログイン」を追加します。
![facebookログイン_追加.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/a852b388-dca4-ef40-4a58-dc4b8c55a954.jpeg)

* アプリのプラットフォームは「ウェブ」を選択します。
![webを選択.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/c691b25f-9377-9492-55c6-237cf90d2c62.jpeg)

* ウェブサイトのURLを設定します。ここではlocalhost環境を用いていますので、サイトURLは[https://localhost/social-login/facebook/](https://localhost/social-login/facebook/)と、しています。
![ウェブサイトの情報を入力する.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/b350b663-6fac-dbbc-9721-a86dbb840e96.jpeg)

* この後に2～5と説明が続きます。本来ならこれらの情報を元にアプリ側の実装をするのですが、今回は既に作ってありますので、適当へ「次へ」をクリックしていき、設定を終えます。
![手順一覧.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/e32a29ed-2c76-9d80-99d6-89eff3d7b379.jpeg)

* ちなみに、再度これらの設定を変更する場合は左側のサイドバーから「Facebookログイン＞クイックスタート」を選択すればよいです。
![クイックスタート.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/12785659-ee15-d4da-ddb8-9efd37c53e54.jpeg)

<b>以上で[facebook for Developers](https://developers.facebook.com/)での設定は終わりです。</b>

### 連携アプリ側の設定
* Apacheの制御下にあるディレレクトリ(phpが動作する環境)へ、以下の様にファイル配置していきます。

```
C:\xampp\htdocs (←Apacheの制御下のフォルダ、環境の設定によって変わる)
　　　　　　└ social-login
    　　　　　　└ facebook
    　   　　　　 　├ index.php
    　　　   　　 　├ common.php
    　　　　　    　├ verify.php
    　　　　　    　├ myPage.php
    　　　　　    　└ logout.php
```

* index.phpはログインのためのボタンを表示してるだけのシンプルな画面のphpソースコードです。でも内部的にはfacebookから書くように指示されたjavascriptを色々と書いてます。それと多少の独自処理を追加してます。

```index.php
<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>HTML Sample</title>
</head>
<body>
  <?php
  require_once __DIR__ . "/common.php";
  session_start();
  if (array_key_exists("userId", $_SESSION) && $_SESSION["userId"] !== null) {
    // ログイン済ならマイページへリダイレクト
    header("Location: ./myPage.php", true, 307);
    exit;
  }

  ?>
  <script>
    window.fbAsyncInit = function() {
      FB.init({
        appId: '<?php echo $appId; ?>',
        cookie: true,
        xfbml: true,
        version: '<?php echo $apiVersion; ?>'
      });
      FB.AppEvents.logPageView();
    };

    (function(d, s, id) {
      var js, fjs = d.getElementsByTagName(s)[0];
      if (d.getElementById(id)) {
        return;
      }
      js = d.createElement(s);
      js.id = id;
      js.src = "https://connect.facebook.net/en_US/sdk.js";
      fjs.parentNode.insertBefore(js, fjs);
    }(document, 'script', 'facebook-jssdk'));

    function onLogin() {
      console.log("OnLogin");

      FB.getLoginStatus(function(response) {
        if ("authResponse" in response && response.authResponse != null) {
          console.log(response);
          const param = {
            method: "POST",
            headers: {
              "Content-Type": "application/json; charset=utf-8"
            },
            body: JSON.stringify(response)
          };

          fetch("./verify.php", param)
            .then((res) => {
              return (res.json());
            })
            .then((json) => {
              console.log(json)
              if (!json.result) {
                alert('Login failure');
                return;
              }
              // ログイン成功ならマイページへリダイレクト
              location.href = './myPage.php';
            });
        }
      });
    };
  </script>

  <!-- ログインボタンについてはこちらも参照→ https://developers.facebook.com/docs/facebook-login/web/login-button -->
  <div id="fb-root"></div>
  <script async defer crossorigin="anonymous" src="https://connect.facebook.net/ja_JP/sdk.js#xfbml=1&version=v11.0&appId=<?php echo $appId; ?>&autoLogAppEvents=1" nonce="ITEWcCSh">
  </script>
  <div class="fb-login-button" data-width="" data-size="large" data-button-type="continue_with" data-layout="default" data-auto-logout-link="false" data-use-continue-as="true" onlogin="onLogin();"></div>
</body>
</html>
```

* common.phpはこのデモアプリ全体で使用するパラメータの設定を定義しています。[facebook for Developers](https://developers.facebook.com/)で表示される「アプリID」、「app secret」、「APIバージョン」のパラメータを書いています。

```common.php
<?php
//ここにアプリID,アプリシークレット,APIバージョンを設定してください。
$appId = "XXXXXXXXXXXXXXXX";
$appSecret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
$apiVersion = "v11.0";
```

![ベーシック.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/3cc00bbe-e41d-8e5e-00d4-cef578f28800.jpeg)
![アプリのバージョン.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/8ebe4772-f854-06d5-468c-4ff46a7bb116.jpeg)



* verify.phpは受け取ったやIDトークン(ユーザ情報含む)をの正当性を検証しています。

```verify.php
<?php 
require_once __DIR__ . "/common.php";

function webSafeBase64encode($JsonObj){
  return rtrim(strtr(base64_encode($JsonObj), '+/', '-_'), '=');
}

function webSafeBase64decode($JsonObj){
  return base64_decode(strtr($JsonObj, '-_', '+/') . str_repeat('=', 3 - (3 + strlen($JsonObj)) % 4));
}

function returnError($msg){
    echo '{"result":false, "message":"' . $msg . '"}';
    exit;
}

session_start();

//JSONデータを受け取る
$request_body = file_get_contents('php://input');
if ($request_body === null || $request_body === ""){
    // ログイン画面へ遷移する。
    header("Location: ./index.php", true, 307);
    return;
};

$JsonObj = (array)json_decode($request_body);

if (!array_key_exists("status", $JsonObj) || $JsonObj["status"] !== "connected"){
    returnError("Not connected");
}

if (!array_key_exists("authResponse", $JsonObj)){
    returnError("No authResponse");
}
$authResponse = (array)$JsonObj["authResponse"];

//facebookから発行されていることの確認
if ($authResponse["graphDomain"] !== "facebook"){
    returnError("Invalid Issure");
}

/*
//今回は試用しないパラメータ
$authResponse["accessToken"];
$authResponse["data_access_expiration_time"];
$authResponse["expiresIn"];
$authResponse["userID"];
*/

//id token(JWT [ヘッダー.ペイロード])をピリオドで分離
$jwt = explode('.', $authResponse["signedRequest"]);
if (count($jwt) !== 2){
    //ヘッダーとペイロードの2つを含んでいる場合のみ対応する。
    returnError("Unsupported format");
}; 

//ペイロード部分をデコード( https://developers.facebook.com/docs/reference/login/signed-request/ )
$payload = (array)json_decode(webSafeBase64decode($jwt[1]));
$_SESSION["userId"] = $payload["user_id"];

//OAuthコード
$_SESSION["OAuthCode"] = $payload["code"];

$alg = "";
switch ($payload["algorithm"]) {
    case "HMAC-SHA256":
        $alg = "sha256";
        break;
    case "HMAC-SHA384":
        $alg = "sha384";
        break;
    case "HMAC-SHA512":
        $alg = "sha512";
        break;
    default:
        returnError("Unsupported hash algorithm");
}

// 発行日時をチェックする。一応、5分以上の未来に発行されている場合は受け付けないことにした。
$now = time();
if ($now + 5 * 60 < (int) $payload["issued_at"]) {
    echo date('Y/m/d H:i:s', $payload["issued_at"]);
    returnError("Issued at the wrong time");
}

//署名を検証
if (webSafeBase64decode($jwt[0]) !== hash_hmac($alg ,$jwt[1] , $appSecret, $raw = true) ){
    returnError("Invalid signature");
}

//認証OK
echo '{"result":true}';
return;
```

* myPage.phpはこのデモアプリのログイン後を想定した画面です。非ログイン状態だと表示できません。画面上のボタンをクリックすると幾つかのAPIを実行できるように実装しました。 

```myPage.php
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>HTML Sample</title>
</head>
<body>
    <?php
    require_once __DIR__ . "/common.php";
    session_start();

    // ログイン後の画面を想定
    if (!array_key_exists("userId", $_SESSION)) {
        //未ログイン状態ならログイン画面へ遷移する。
        header("Location: ./index.php", true, 307);
        return;
    }
    echo "■UserId : " .  $_SESSION["userId"] . "<br />";
    echo "■OAuthCode : " .  $_SESSION["OAuthCode"] . "<br />";
    ?>

    <hr>
    <h2>My Page</h2>
    <!-- APIを実行するボタン -->
    <input type="button" value="Me" onclick="getMe();" /><br />
    <input type="button" value="Permissions" onclick="getPermissions();" /><br />
    <input type="button" value="Fields" onclick="getFields();" /><br />
    <input type="button" value="Logout" onClick="location.href='./logout.php'" /><br />

    <script>
        window.fbAsyncInit = function() {
            FB.init({
                appId: '<?php echo $appId; ?>',
                cookie: true,
                xfbml: true,
                version: '<?php echo $apiVersion; ?>'
            });
            FB.AppEvents.logPageView();
            FB.getLoginStatus();
        };

        (function(d, s, id) {
            var js, fjs = d.getElementsByTagName(s)[0];
            if (d.getElementById(id)) {
                return;
            }
            js = d.createElement(s);
            js.id = id;
            js.src = "https://connect.facebook.net/en_US/sdk.js";
            fjs.parentNode.insertBefore(js, fjs);
        }(document, 'script', 'facebook-jssdk'));


        function getMe() {
            FB.api('/me', function(response) {
                alert(JSON.stringify(response));
            });
        }

        function getPermissions() {
            FB.api('/me/permissions', function(response) {
                alert(JSON.stringify(response));
            });
        }

        function getFields() {
            FB.api('/me', {
                fields: 'id,first_name,last_name,middle_name,name,name_format,picture,short_name,email'
            }, function(response) {
                alert(JSON.stringify(response));
            });
        }
    </script>
</body>
</html>
```

* logout.phpはセッション情報を削除してログアウトする処理を実装したものです。

```logout.php
<?php
session_start();
session_destroy();
$_SESSION = array();

//ログイン画面へ遷移
header("Location: ./index.php", true, 307);
```

<b>以上でアプリ側の設定は終わりです。</b>

## 動かしてみる

1. XAMPPを起動する(既に起動している場合は不要)
1. [https://localhost/social-login/google/index.php](https://localhost/social-login/google/index.php)にウェブブラウザでアクセスする。
1. ログインボタンアイコンをクリックする。
![facebookでログイン.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/a1458f74-2cdb-148c-38dc-db29f54590bd.jpeg)

1. facebookの認証画面に遷移するので、認証する。(※facebookにログインしていない状態の場合)
![facebookの認証画面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/2ed69fbe-0c42-0e54-8076-67f28654d19d.jpeg)

1. 認証が成功するとmyPageが表示されます。[Me],[Permissions],[Fields]ボタンをクリックするとfacebookのAPIが実行され、ユーザ情報が取得できます。
![myPage.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/63f5793f-c6d3-b60a-a1c9-e73090a1333a.jpeg)


### 以上