前回の記事の続きです。
この記事のゴール
前回は公式の説明に加えてログインページとダッシュボードを実装しました。
ところが、このままだと問題が発生することがあります。
今回はこの問題の原因究明と対策を進めていきます。
発生する問題
アクセスの仕方によっては、コールバックページで
「Laravel\Socialite\Two\InvalidStateException」
が発生します。
![](https://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite2_01.png)
発生箇所はSocialiteのユーザー情報取得処理です。
![](https://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite2_02.png)
コールバックURLに付加されたパラメータは
「code」と「state」です。
![](https://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite2_03.png)
何が問題なのか
Socialite::driver()->user()
のコードを確認してみます。
「vendor/laravel/socialite/src/Two/AbstractProvider.php」
の236行目のif文の条件「$this->hasInvalidState()」がtrueのときに「InvalidStateException」がはスローされます。
![](https://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite2_04.png)
同じファイルの「function hasInvalidState()」を確認してみます。
288行目でsession()->pull()メソッドによって、セッションから「state」の値を取得と同時にセッションから削除し、取得した値を$stateに格納しています。
290行目で、
$stateがempty()
かまたは、
$stateがURLパラメータの’state’と異なる値
のときにtrueが返るようになっています。
![](https://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite2_05.png)
telescopeで確認してみます。
まずはGitHubへのリダイレクトURL。
![](https://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite2_06.png)
セッションに保存されている値は、
"state":
"B8fbQE1lWgf8PtnvizcS2IFkM6rLr3YWB7lRbgDP"
となっており、ブラウザへのレスポンスは、
Redirected to https://github.com/login/oauth/authorize?client_id=af57a7e6e5075154c33d&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Foauth%2Fgithub%2Fcallback&scope=user%3Aemail&response_type=code&state=B8fbQE1lWgf8PtnvizcS2IFkM6rLr3YWB7lRbgDP
となっています。
次に、コールバックページの確認をしてみます。
![](http://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite2_07.png)
URLパラメータは
code=e92a46c5a3291e5df442&state=B8fbQE1lWgf8PtnvizcS2IFkM6rLr3YWB7lRbgDP
となっています。ここの「state」の値はリダイレクト時のものと一致しています。
ところが、セッションの中に「state」がありません。
どのタイミングでキャプチャした情報かはわかりませんが、
取得と同時にセッションから「state」の値を削除しているので、これだけでは判別できません。
ということで、
「vendor/laravel/socialite/src/Two/AbstractProvider.php」
の「hasInvalidState()」メソッドの箇所で値をvar_dump()してdie()するようにいじってみました。
![](https://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite2_08.png)
![](https://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite2_09.png)
session()->pull()メソッドで取得した筈の$stateの値がnullになっています。
これが「InvalidStateException」の原因のようです。
真の原因はこれ
では、なぜ削除する前のセッション中の「state」が無くなっているのでしょうか?
その原因はアクセスの仕方にありました。
![](http://macocci7.net/blog/wp-content/uploads/2024/02/laravel_breeze_socialite_08.png)
php artisan serve
実行直後に表示されたURL
http://127.0.0.1:8000
でアクセスし、ログインページ
http://127.0.0.1:8000/login
からGitHubへのリダイレクトリンクを辿ったわけです。
http://127.0.0.1:8000/oauth/github/redirect
その後、GitHubのOAuth認証
https://github.com/login/oauth/authorize?(中略)&state=B8fbQE1lWgf8PtnvizcS2IFkM6rLr3YWB7lRbgDP
へリダイレクトされます。
GitHubでOAuth認証後、localhostのコールバック
http://localhost:8000/oauth/github/callback?(中略)&state=B8fbQE1lWgf8PtnvizcS2IFkM6rLr3YWB7lRbgDP
へ戻ってきて、ここで「InvalidStateException」が発生します。
気付いたでしょうか?
最初は「127.0.0.1:8000」でアクセスし、
コールバックが「localhost:8000」になっています。
セッションの扱い上、「異なるドメイン」となるので、
「127.0.0.1:8000」のセッションに保存されている「state」
が「localhost:8000」のセッションに保存されているはずがありません。
そりゃぁ、「empty($state) === true」になるわけですよ。
このアクセスの仕方では、何度やっても「InvalidStateException」がスローされます。
対策はよく考えよう
結論から言ってしまうと、今回のケースでは、
http://localhost:8000/
でアクセスしなさい。
で終わりです。
それだけだとつまらないので補足します。
「Laravel\Socialite\Two\InvalidStateException」
でネット検索すると、色々と対策案が出てきますが、
中でも多いのが、
Socialite::driver('github')->stateless()->user()
で解決。
と結論付けてしまうものです。
ちょっと待ってください!「stateless」って安直すぎませんか?
何のためにデフォルトで「ステートフル」になっているのでしょうか?
それは、CSRF対策の為です。
「stateless」にするということは、「CSRF対策をしない」ということですよ。
「Stack Overflow」などの質問サイトで見かける安直過ぎる危険な対応としては、
コールバック時のURLパラメータで返ってきた「state」の値でセッション情報を上書きしてドヤ顔しているものまであり、筆者から見るとドン引きレベルのセキュリティホールを対策として施してしまう素人回答まで見受けられます。
対策はよく考えて施しましょう。
結論
今回のケースでは、「stateless()」は施しません。
Socialite::driver('github')->user()
であるべきです。
OAuth認証へのリダイレクトとコールバックは同じドメインにしましょう。
というのが結論です。
今回はここまでです。お疲れさまでした。
続きはこちらです。
コメント