開発環境のLaravelアプリケーションにphp-graph-sdkを使用してFacebookログイン機能を組み込む時に詰まったこと

## LaravelでFacebookログインボタンを設置して、AccessTokenを取得する処理を書きたい

とりあえず以下のライブラリを使うことに。

https://github.com/facebookarchive/php-graph-sdk


Dockerfileとdocker-compose.ymlを使用して開発環境を作成。`http://localhost:3000/facebook/login`でボタンを表示するようにしました。
以下にコードを記しますが、[Facebookのドキュメント](https://github.com/facebookarchive/php-graph-sdk/blob/master/docs/examples/facebook_login.md)のまんまです。

`Controllers/FacebookController.php`の一部

```php
//ログインボタンを表示する処理

public function login()
{
  $fb = new Facebook\Facebook([
    'app_id' => '{app-id}',
    'app_secret' => '{app-secret}',
    'default_graph_version' => 'v2.10',
  ]);

  $helper = $fb->getRedirectLoginHelper();

  $permissions = ['email']; // Optional permissions

  //callback先はhttp://localhost:3000/facebook/callbackに
  $loginUrl = $helper->getLoginUrl('http://localhost:3000/facebook/callback', $permissions);

  echo '<a href="' . $loginUrl . '">Log in with Facebook!</a>';
}
```

```php
//callback先でAccessTokenを表示する処理

public function callback()
{
  $fb = new Facebook\Facebook([
    'app_id' => '{app-id}',
    'app_secret' => '{app-secret}',
    'default_graph_version' => 'v2.10',
  ]);

  $helper = $fb->getRedirectLoginHelper();

  try {
    //後々ここでエラー発生!!!!!!!!!
    $accessToken = $helper->getAccessToken();
  } catch(Facebook\Exception\ResponseException $e) {
    echo 'Graph returned an error: ' . $e->getMessage();
    exit;
  } catch(Facebook\Exception\SDKException $e) {
    echo 'Facebook SDK returned an error: ' . $e->getMessage();
    exit;
  }

  $oAuth2Client = $fb->getOAuth2Client();
  $tokenMetadata = $oAuth2Client->debugToken($accessToken);
  $tokenMetadata->validateAppId($config['app_id']);
  $tokenMetadata->validateExpiration();

  if (! $accessToken->isLongLived()) {
    try {
      $accessToken = $oAuth2Client->getLongLivedAccessToken($accessToken);
    } catch (Facebook\Exception\SDKException $e) {
      echo "<p>Error getting long-lived access token: " . $e->getMessage() . "</p>\n\n";
      exit;
    }

    echo '<h3>AccessToken</h3>';
    var_dump($accessToken->getValue());
  }
}
```


## 意気揚々とログインを実行するとエラーが発生...
エラー内容はこちらです。

```text
Graph returned an error:
Please make sure your redirect_uri is identical to the one you used in the OAuth dialog request
```

Googleログインなどでもよくある、APIの管理画面でリダイレクトURLを設定忘れだと思い管理画面へアクセス。

ドキュメント読むとなんとFacebookログインのリダイレクト先はhttps必須とのこと..
> 2018年10月6日から、すべてのアプリでHTTPSの使用が必須になります。

しかし！！
管理画面を見てみると以下の記述がありました！
「http://localhost のリダイレクトは開発モードでのみ自動的に許可され、ここに追加する必要はありません」
![スクリーンショット 2021-03-03 18.54.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546062/341603f9-77d3-68c4-3bd4-2727fe6d9709.png)

### 開発モードの場合はhttp://localhost からもログインできる!
Facebookはリダイレクト先のhttps強制。
しかし、localhostは別とのこと。良かった!!

開発モードについては[こちら](https://developers.facebook.com/docs/development/build-and-test/app-modes#development-mode)を参照。

ざっくりまとめると以下のようなことが書かれている。
>アプリのモードは、誰がアプリを使用できるかを決定します。
開発モードのアプリは、アプリ上の役割を持つアプリユーザー、またはアプリを主張しているビジネスの役割を持つアプリユーザーのみが使用できます。

## ってことは自分のアプリ開発モードになっていない説..?

調べたところ..
自分のはビジネスアプリとして作成したてのアプリだったので確かに開発モードになっていました。
厳密に言うと、ビジネスアプリで作成したアプリには開発モードという概念は無いそうですが、一般に公開をしていない限り開発モードと同じらしい。

> 新しく作成されたアプリはすべて開発モードでスタートし、アプリの開発が完了するまでライブモードに切り替えないでください。
ビジネスアプリにはアプリモードがなく、同じ機能を提供するアクセスレベルに依存していることに注意してください。

## ということはなぜリダイレクトエラー出るの？?解決法

エラーが出ている箇所は上記のFacebookController.phpの`callback()`内の以下の一文でした。
`$accessToken = $helper->getAccessToken();`

**結論ですが、以下のようにcallback先のURLを明示的に渡してあげると解決しました。**

```php
$accessToken = $helper->getAccessToken(url(/facebook/callback)');
//または
$accessToken = $helper->getAccessToken(http://localhost:3000/facebook/callback);
```

## なぜこの方法で解決できた？？

ライブラリ内の`getAccessToken()`を覗いてみます。こちらの行に関数が書かれています。

https://github.com/facebookarchive/php-graph-sdk/blob/2d8250638b33d73e7a87add65f47fabf91f8ad9b/src/Facebook/Helpers/FacebookRedirectLoginHelper.php#L215

getAccessToken()の引数はnullを渡しても、ログインボタンに埋め込まれているURLからリダイレクトして、現在地のURLをよしなに取得してくれるはず。

```php
//ログインボタンの所はこう書きました
$loginUrl = $helper->getLoginUrl('http://localhost:3000/facebook/callback', $permissions);
echo '<a href="' . $loginUrl . '">Log in with Facebook!</a>';
```

```php
//ライブラリの中身抜粋 (現在地からURLを取得してくれる処理)
$redirectUrl = $redirectUrl ?: $this->urlDetectionHandler->getCurrentUrl();
$redirectUrl = FacebookUrlManipulator::removeParamsFromUrl($redirectUrl, ['code', 'enforce_https', 'state']);
echo $redirectUrl
```
この関数を手元で実行してみると....
以下が返ってきました。ポート番号の:3000が消えてる...これだとredirectUrlが確かに一致していません。
> http://localhost/facebook/callback

理想は以下のURLを返したい
> 理想：http://localhost:3000/facebook/callback

さらに探っていくと原因が分かりました。
ポート番号を取得する処理はこんな感じで書かれていました。

```php
$port = isset($_SERVER["HTTP_X_FORWARDED_PORT"]) ? $_SERVER["HTTP_X_FORWARDED_PORT"] : '';
```

これを実行すると、`''`が返ってきました。
`$_SERVER["HTTP_X_FORWARDED_PORT"]`に値が入っていないようです。

HTTP_X_FORWARDED_PORTがよく分かっていないですが、、調べてみると

https://www.php.net/manual/en/reserved.variables.server.php#93599

>If you are serving from behind a proxy server, you will almost certainly save time by looking at what these $_SERVER variables do on your machine behind the proxy.  

>日本語訳: プロキシサーバの後ろからサービスを提供している場合、これらの $_SERVER 変数がプロキシの後ろのマシンで何をしているかを見れば、ほぼ確実に時間を節約することができます。 


https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/x-forwarded-headers.html
> X-Forwarded-Port リクエストヘッダーは、ロードバランサーへの接続にクライアントが使用した送信先ポートを識別するために役立つそうです。

なるほど、今回はDockerを使っています。ポート番号3000をブラウザのURLの入力欄に入力すると、nginxの80番にポートフォワードするようにしています。プロキシサーバーは使用していないので、`$_SERVER["HTTP_X_FORWARDED_PORT"]`に値が入っていないのですね。おそらく。

`$port = isset($_SERVER["HTTP_X_FORWARDED_PORT"]) ? $_SERVER["HTTP_X_FORWARDED_PORT"] : $_SERVER["HTTP_HOST"];`

こう書くと"localhost:3000"を返してくれますが..
どうするのがベストなのだろう？？


ということでプロキシサーバーを使用せずに、このライブラリを使用したFacebookログイン機能で詰まるかもしれません。

そんな場合は以下のように書いてみてください！

```php
$accessToken = $helper->getAccessToken(url(/facebook/callback)');
//または
$accessToken = $helper->getAccessToken(http://localhost:3000/facebook/callback);
```

以上です。
コメントや指摘等お待ちしております。