【Laravel】Laravel6系と7系以上でcookieが共有できない

## 背景
　Laravel6系のアプリと7系のアプリでsessionを共有するためにcookieを共有する設定を行ったのだが、どうしてもcookieが共有できなかった。
そこで調査を重ね、解決に至ったので他の方にも参考になればいいなと思い、記事を作成させていただこうと思いました。

## 前提
問題の本質と外れるので特に興味ない方は飛ばしちゃってください
### サービスの構成
全てのアクセスをnginx-proxyで受けてドメインごとに各サービスコンテナに振り分ける感じでコンテナを構成しました
参考：[jwilder/nginx-proxyで{ホスト名}/{パス}でコンテナにルーティングする方法](https://qiita.com/Ayumu_Usu/items/0891f3f733e355ed1265)）
![スクリーンショット 2022-09-23 14.19.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2859039/387b6ff1-2f0f-ce5c-f2d9-fcc47b051e09.png)

### sessionを共有するためのLaravelの設定
sessionの設定に必要なことは以下の二つです
* session情報を共通の場所に保管する
* cookieを共通にする

自分は下記のように設定しました
* session情報を共通の場所に保管する
    * redisコンテナを用意して、共通の保管場所とする
* cookieを共通にする
    * cookieのドメインを親ドメインにすることで、各ドメインでcookieを共有させることができる(同一ブラウザのみ)
    * 設定したcookieのドメイン：`.qiita.com`

参考：[[Laravel]異なるサイト間でCookieとSessionを共有してログイン状態を保持する](https://qiita.com/mgmgmogumi/items/a6a39aad036b395702c5)


### 処理の流れ
1. aaa.qiita.comにアクセス
1. nginx-proxyでaaa.qiita.comコンテナにフォワード
1. session、cookieの生成
    1. この時、cookieに`.qiita.com`ドメイン間で使うものと設定される
    1. sessionの保存名はcookieの名前を使ったものが生成される
1. redisに保存
1. cookieをブラウザに返却
1. bbb.qiita.comにアクセス
    1. `.qiita.com`ドメインなので、aaa.qiita.comで生成されたcookieが送信される
1. nginx-proxyでbbb.qiita.comコンテナにフォワード
1. cookieを読み込み
    1. sessionのkeyが作れる
1. redisでsessionを検索
    1. keyがあるので、aaa.qiita.comで生成されたsessionが得られる
1. コンテナにsession情報を返却
![スクリーンショット 2022-09-27 9.36.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2859039/39032ff5-f7b1-fbee-abbb-752ea6fec3a6.png)

## 本題
### なぜcookieが共有できなかったのか
#### Laravelにおけるcookie共有の流れ
Laravelはリクエスト(HTTP)が来ると下記流れでsessionを取得する（結構ざっくり）
1. `\App\Http\Middleware\EncryptCookies`
    1. cookieのデコード
1. `\Illuminate\Session\Middleware\StartSession`
    1. cookieからsessionIDを取得
    1. sessionを取得
1. ...その他の処理...
1.  `\Illuminate\Session\Middleware\StartSession`
    1. session情報格納
    1. cookieにsessionIDを格納
1. `\App\Http\Middleware\EncryptCookies`
    1. cookieをエンコード

#### 原因
上記処理の、`EncryptCookies`の処理が**Laravel6系と7系で変わっていた**ため、
6系でエンコードしたcookieが7系でデコードできず、
7系でエンコードされたcookieが6系でデコードできなかったため、cookieが共有されていなかった。

各バージョンのcookieのエンコードについて具体的には以下のように処理される（デコードに関してはこの逆）
* 6系
    1. cookieの値をencryptメソッドでエンコード
       ```php:\App\Http\Middleware\EncryptCookies
       /**
       * Encrypt the cookies on an outgoing response.
       *
       * @param  \Symfony\Component\HttpFoundation\Response  $response
       * @return \Symfony\Component\HttpFoundation\Response
       */
       protected function encrypt(Response $response)
       {
          foreach ($response->headers->getCookies() as $cookie) {
              if ($this->isDisabled($cookie->getName())) {
                  continue;
              }

              $response->headers->setCookie($this->duplicate(
                  $cookie, $this->encrypter->encrypt($cookie->getValue(), static::serialized($cookie->getName()))
              ));
          }

          return $response;
       }
       ```
* 7系
    1. cookieの値をcookie名、APP_KEYを使用してHash化
    1. Hash化したcookieの値をさらにエンコード
        ```php:\App\Http\Middleware\EncryptCookies
         /**
          * Encrypt the cookies on an outgoing response.
               *
          * @param  \Symfony\Component\HttpFoundation\Response  $response
          * @return \Symfony\Component\HttpFoundation\Response
          */
         protected function encrypt(Response $response)
         {
             foreach ($response->headers->getCookies() as $cookie) {
                 if ($this->isDisabled($cookie->getName())) {
                     continue;
                 }

                 $response->headers->setCookie($this->duplicate(
                     $cookie,
                     $this->encrypter->encrypt(
                         CookieValuePrefix::create(
                             $cookie->getName(), $this->encrypter->getKey()).$cookie->getValue(),
                             static::serialized($cookie->getName()
                         )
                     )
                 ));
             }

             return $response;
         }
        ```
### 解決方法
これの解決方法としては、3パターン考えられます
1. cookieの暗号化の方法を6系のものに合わせる
1. cookieの暗号化の方法を７系のものに合わせる
1. 独自のcookie暗号化クラスを作成し、Middlewareに登録する

今回は、「セキュリティ面」「将来Laravelをアップデートすること」を考え、2の「cookieの暗号化の方法を７系のものに合わせる」で対応することにしました

#### 1. オーバーライドしたクラスを格納するためのディレクトリを作成
`EncryptCookies`クラス以外にもcsrfのトークンも暗号化の処理が変わっていたので、そこもオーバーライドしました
```
$ tree app/Http/Middleware/OverrideEncryption 
app/Http/Middleware/OverrideEncryption
├── CookieValuePrefix.php
├── MyEncryptCookies.php
└── MyVerifyCsrfToken.php

```
#### 2. Laravel7系にある暗号化の際に使うクラスをLaravel6の中で実装
```php:App\Http\Middleware\OverrideEncryption\CookieValuePrefix.php
<?php

namespace App\Http\Middleware\OverrideEncryption;

class CookieValuePrefix
{
    /**
     * Create a new cookie value prefix for the given cookie name.
     *
     * @param string $cookieName
     * @param string $key
     * @return string
     */
    public static function create(string $cookieName, string $key): string
    {
        return hash_hmac('sha1', $cookieName . 'v2', $key) . '|';
    }

    /**
     * Remove the cookie value prefix.
     *
     * @param string $cookieValue
     * @return string
     */
    public static function remove(string $cookieValue): string
    {
        return substr($cookieValue, 41);
    }
}

```

#### 3. Laravel6系の実装でエンコード、デコードしているメソッドをオーバーライドする
```php:php:\App\Http\Middleware\OverrideEncryption\MyEncryptCookies.php
<?php

namespace App\Http\Middleware\OverrideEncryption;

use App\Http\Middleware\EncryptCookies;
use Illuminate\Contracts\Encryption\DecryptException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * override EncryptCookies.php to share cookie
 * cookie value changes hash code before encode, decode
 * FIXME Please delete this class if you update to Laravel7 or higher
 */
class MyEncryptCookies extends EncryptCookies
{
    /**
     * Decrypt the cookies on the request.
     *
     * @param Request $request
     * @return Request
     */
    protected function decrypt(Request $request): Request
    {
        foreach ($request->cookies as $key => $cookie) {
            if ($this->isDisabled($key) || is_array($cookie)) {
                continue;
            }

            try {
                $value = $this->decryptCookie($key, $cookie);

                $hasValidPrefix = strpos($value, CookieValuePrefix::create($key, $this->encrypter->getKey())) === 0;

                $request->cookies->set(
                    $key,
                    $hasValidPrefix ? CookieValuePrefix::remove($value) : null
                );
            } catch (DecryptException $e) {
                $request->cookies->set($key, null);
            }
        }

        return $request;
    }

    /**
     * Encrypt the cookies on an outgoing response.
     *
     * @param Response $response
     * @return Response
     */
    protected function encrypt(Response $response): Response
    {
        foreach ($response->headers->getCookies() as $cookie) {
            if ($this->isDisabled($cookie->getName())) {
                continue;
            }

            $response->headers->setCookie($this->duplicate(
                $cookie,
                $this->encrypter->encrypt(
                    CookieValuePrefix::create($cookie->getName(), $this->encrypter->getKey()) . $cookie->getValue(),
                    static::serialized($cookie->getName())
                )
            ));
        }

        return $response;
    }
}

```
もう一つ
```php:App\Http\Middleware\OverrideEncryption\MyVerifyCsrfToken.php
<?php

namespace App\Http\Middleware\OverrideEncryption;

use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Http\Request;

/**
 * override VerifyCsrfToken.php to share cookie
 * csrf token changes hash code before encode, decode
 * FIXME Please delete this class if you update to Laravel7 or higher
 */
class MyVerifyCsrfToken extends VerifyCsrfToken
{
    /**
     * Get the CSRF token from the request.
     *
     * @param Request $request
     * @return string
     */
    protected function getTokenFromRequest($request): string
    {
        $token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');

        if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
            try {
                $token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
            } catch (DecryptException $e) {
                $token = '';
            }
        }

        return $token;
    }
}

```
#### 4. Middlewareにオーバーライドしたクラスを登録する
```php:App\Http\Kernel.php
~~

/**
 * The application's route middleware groups.
 *
 * @var array
 */
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\OverrideEncryption\MyEncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\OverrideEncryption\MyVerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \App\Http\Middleware\Localization::class,
    ],

~~
```
## 最後に
今回この問題を解決するのにかなり時間がかかりました。
sessionが共有されていないというところから、cookieが問題、cookieの暗号化の部分が問題、とかなり遡って調査する必要があったからです。
そして、これに関する記事も自分が探した中では見つからなかったため、こうして記事を書かせていただきました。
少しでも皆さんのお役に立てれば幸いです。
## 参考サイト
* [jwilder/nginx-proxyで{ホスト名}/{パス}でコンテナにルーティングする方法](https://qiita.com/Ayumu_Usu/items/0891f3f733e355ed1265)）
* [[Laravel]異なるサイト間でCookieとSessionを共有してログイン状態を保持する](https://qiita.com/mgmgmogumi/items/a6a39aad036b395702c5)