9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

株式会社クライドAdvent Calendar 2022

Day 1

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

Last updated at Posted at 2022-09-23

背景

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

前提

問題の本質と外れるので特に興味ない方は飛ばしちゃってください

サービスの構成

全てのアクセスをnginx-proxyで受けてドメインごとに各サービスコンテナに振り分ける感じでコンテナを構成しました
参考:jwilder/nginx-proxyで{ホスト名}/{パス}でコンテナにルーティングする方法
スクリーンショット 2022-09-23 14.19.05.png

sessionを共有するためのLaravelの設定

sessionの設定に必要なことは以下の二つです

  • session情報を共通の場所に保管する
  • cookieを共通にする

自分は下記のように設定しました

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

参考:[Laravel]異なるサイト間でCookieとSessionを共有してログイン状態を保持する

処理の流れ

  1. aaa.qiita.comにアクセス
  2. nginx-proxyでaaa.qiita.comコンテナにフォワード
  3. session、cookieの生成
    1. この時、cookieに.qiita.comドメイン間で使うものと設定される
    2. sessionの保存名はcookieの名前を使ったものが生成される
  4. redisに保存
  5. cookieをブラウザに返却
  6. bbb.qiita.comにアクセス
    1. .qiita.comドメインなので、aaa.qiita.comで生成されたcookieが送信される
  7. nginx-proxyでbbb.qiita.comコンテナにフォワード
  8. cookieを読み込み
    1. sessionのkeyが作れる
  9. redisでsessionを検索
    1. keyがあるので、aaa.qiita.comで生成されたsessionが得られる
  10. コンテナにsession情報を返却
    スクリーンショット 2022-09-27 9.36.23.png

本題

なぜcookieが共有できなかったのか

Laravelにおけるcookie共有の流れ

Laravelはリクエスト(HTTP)が来ると下記流れでsessionを取得する(結構ざっくり)

  1. \App\Http\Middleware\EncryptCookies
    1. cookieのデコード
  2. \Illuminate\Session\Middleware\StartSession
    1. cookieからsessionIDを取得
    2. sessionを取得
  3. ...その他の処理...
  4. \Illuminate\Session\Middleware\StartSession
    1. session情報格納
    2. cookieにsessionIDを格納
  5. \App\Http\Middleware\EncryptCookies
    1. cookieをエンコード

原因

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

各バージョンのcookieのエンコードについて具体的には以下のように処理される(デコードに関してはこの逆)

  • 6系
    1. cookieの値をencryptメソッドでエンコード
      \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化
    2. Hash化したcookieの値をさらにエンコード
      \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系のものに合わせる
  2. cookieの暗号化の方法を7系のものに合わせる
  3. 独自のcookie暗号化クラスを作成し、Middlewareに登録する

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

1. オーバーライドしたクラスを格納するためのディレクトリを作成

EncryptCookiesクラス以外にもcsrfのトークンも暗号化の処理が変わっていたので、そこもオーバーライドしました

$ tree app/Http/Middleware/OverrideEncryption 
app/Http/Middleware/OverrideEncryption
├── CookieValuePrefix.php
├── MyEncryptCookies.php
└── MyVerifyCsrfToken.php

2. Laravel7系にある暗号化の際に使うクラスをLaravel6の中で実装

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:\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;
    }
}

もう一つ

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にオーバーライドしたクラスを登録する

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の暗号化の部分が問題、とかなり遡って調査する必要があったからです。
そして、これに関する記事も自分が探した中では見つからなかったため、こうして記事を書かせていただきました。
少しでも皆さんのお役に立てれば幸いです。

参考サイト

9
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?