【Laravel + Breeze + Socialite】GitHubソーシャルログインを実装(2)

GitHub

前回の記事の続きです。

この記事のゴール

前回は公式の説明に加えてログインページとダッシュボードを実装しました。

ところが、このままだと問題が発生することがあります。

今回はこの問題の原因究明と対策を進めていきます。

発生する問題

アクセスの仕方によっては、コールバックページで

「Laravel\Socialite\Two\InvalidStateException」

が発生します。

発生箇所はSocialiteのユーザー情報取得処理です。

コールバックURLに付加されたパラメータは

「code」と「state」です。

何が問題なのか

Socialite::driver()->user()

のコードを確認してみます。

「vendor/laravel/socialite/src/Two/AbstractProvider.php」

の236行目のif文の条件「$this->hasInvalidState()」がtrueのときに「InvalidStateException」がはスローされます。

同じファイルの「function hasInvalidState()」を確認してみます。

288行目でsession()->pull()メソッドによって、セッションから「state」の値を取得と同時にセッションから削除し、取得した値を$stateに格納しています。

290行目で、

$stateがempty()

かまたは、

$stateがURLパラメータの’state’と異なる値

のときにtrueが返るようになっています。

telescopeで確認してみます。

まずはGitHubへのリダイレクトURL。

セッションに保存されている値は、


"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

となっています。

次に、コールバックページの確認をしてみます。

URLパラメータは

code=e92a46c5a3291e5df442&state=B8fbQE1lWgf8PtnvizcS2IFkM6rLr3YWB7lRbgDP

となっています。ここの「state」の値はリダイレクト時のものと一致しています。

ところが、セッションの中に「state」がありません。

どのタイミングでキャプチャした情報かはわかりませんが、

取得と同時にセッションから「state」の値を削除しているので、これだけでは判別できません。

ということで、

「vendor/laravel/socialite/src/Two/AbstractProvider.php」

の「hasInvalidState()」メソッドの箇所で値をvar_dump()してdie()するようにいじってみました。

session()->pull()メソッドで取得した筈の$stateの値がnullになっています。

これが「InvalidStateException」の原因のようです。

真の原因はこれ

では、なぜ削除する前のセッション中の「state」が無くなっているのでしょうか?

その原因はアクセスの仕方にありました。

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対策の為です。

https://github.com/auth0/docs/blob/master/articles/protocols/oauth2/oauth-state.md

「stateless」にするということは、「CSRF対策をしない」ということですよ。

「Stack Overflow」などの質問サイトで見かける安直過ぎる危険な対応としては、

コールバック時のURLパラメータで返ってきた「state」の値でセッション情報を上書きしてドヤ顔しているものまであり、筆者から見るとドン引きレベルのセキュリティホールを対策として施してしまう素人回答まで見受けられます。

対策はよく考えて施しましょう。

結論

今回のケースでは、「stateless()」は施しません。

Socialite::driver('github')->user()

であるべきです。

OAuth認証へのリダイレクトとコールバックは同じドメインにしましょう。

というのが結論です。

今回はここまでです。お疲れさまでした。

続きはこちらです。

コメント

タイトルとURLをコピーしました