【Rails/Devise】ログアウト実装でハマるRouting ErrorとHTTPメソッド設定の落とし穴
はじめに
以前、Railsでアプリ開発をしている際に「ログアウトをしようとするとRouting Error
が発生する」という事象に遭遇しました。
無事に解決できたのですが、私の通っているプログラミングスクールでも同じ事象に困っている方が多くいらっしゃいました。
皆さんの状況を聞くと、多くの場合は私と同じ原因でエラーが発生していました。
一方、Web検索でよくヒットする情報は、本来の解決策ではないように感じます。
そこで、本記事ではDeviseでログアウトが実装できない場合の対処法と、HTTPメソッドの不適切な設定による問題点について解説します。
対象読者
- Ruby on Rails初学者の方
- Deviseを使用している方
筆者環境
以下の環境にて、動作させています。
- Docker環境
- Windows11
- Rails 7.2.2.1
- ruby 3.3.6
- Devise 4.9.4
前提知識
Deviseとは
Deviseは、Ruby on Rails向けの認証ライブラリ(Gem)です。
ユーザー登録・ログイン・ログアウトなどの機能を簡単に実装できます。
Deviseでのログアウト実装時によくあるエラー
Deviseでのログアウトを実装するために、viewファイルに以下のようなコードを書きます。
<%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
そして、ログアウトを行おうとすると、次のようなエラーが発生してしまいます。
Routing Error
No route matches [GET] "/users/sign_out"
本エラーの対策として、Web検索でヒットする対応は、
- ログアウトに使うHTTPメソッドの設定を
DELETE
→GET
に変更する - HTTPメソッド
GET
で"/users/sign_out"にリクエストが飛んできた場合に、ログアウトを実行するルーティング設定を行う
というものです。
簡単に言うと、どちらもHTTPメソッドGET
でもログアウトできるようにする設定を行っています。
しかし、この方法にはセキュリティ上の問題点があります。
GETでログアウトをすることの問題点
HTTPメソッドでよく使用されるものはGET
,POST
,PUT
,DELETE
かと思います。
それぞれ以下のように利用されます。
メソッド | 利用目的 |
---|---|
GET | リソースの取得(状態変更しない) |
POST | リソースの作成 |
PUT | リソースの更新 |
DELETE | リソースの削除 |
以上を踏まえたうえで、ログアウト操作は本来どのようにするべきでしょうか?
ログアウトは、ログインセッションを破棄する操作になります。
そのため、ログアウト操作はHTTPメソッドDELETE
で本来実装されます。
では、HTTPメソッドGET
でログアウト操作ができる設定を行うと、どのようなことが起こるでしょうか?
以下のような状況を考えてみましょう。
AAAというWebアプリでHTTPメソッドGET
でログアウトできる設定を行ったとします。
悪意ある第三者が、BBBというサイトでhttps://AAA/users/sign_out
へのリンクを配置します。
するとどうでしょう、AAAのWebアプリのユーザーはBBBのリンクを踏むと、意図せずログアウトしてしまうのです。
これをCSRF(クロスサイトリクエストフォージェリ)攻撃といいます。
このような危険性があるため、HTTPメソッドのGET
メソッドでのログアウトは推奨されません。
本エラーの対応方法
では、今回のエラーへの対処はどのようにしたら良いでしょうか?
そのヒントはエラーメッセージに隠れています。
再度エラーメッセージを確認してみます。
エラーメッセージ
Routing Error
No route matches [GET] "/users/sign_out"
このメッセージを見ると「/users/sign_out
というURLに対して GET
リクエスト を受け取ったけれど、そのようなパスを処理するルート(ルーティング設定)が見つからない」と言っています。
ルーティング設定を確認すると、/users/sign_out
にDELETE
リクエストを受け取った場合は、devise/sessions
のdestroy
アクションを実行するルーティング設定になっていることが分かります。
では、/users/sign_out
へのリンクを生成しているコードを見てみましょう。
<%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
こちらのコードでは、method: :delete
でdestroy_user_session_path
へのリンク、つまりは DELETE
リクエスト で/users/sign_out
へアクセスするリンクを生成 しようとしています。
一方、今回のエラーから分かる通り、実際には GET
リクエスト で/users/sign_out
へアクセスするリンクを生成 してしまっています。
Rails 6以前とRails 7以降のリンク生成方法の違い
実は、Rails6系までは、link_to
ヘルパーのmethod: :delete
でDELETE
リクエストのリンクを生成することができました。
これはrails-ujsという仕組みを利用する記述方法でした。
Railsガイド > Rails で JavaScript を使用する (v6.1)
一方、Rails7系以降では、rails-ujsが廃止され、HotwireのTurboが標準搭載されました。このため、method: :delete
では動作せず、Turboの記法であるdata: { turbo_method: "delete" }
を使う必要があります。
Railsガイド > Rails で JavaScript を使用する (v7.2)
実際にコードを以下のように修正すると、正しくログアウト処理が行うことが出来ました。
<%= link_to "ログアウト", destroy_user_session_path, data: { turbo_method: :delete } %>
その他
生成AIに本エラーの対処法を聞くとrails-ujsを有効化するというものを推奨されました。
Rails 7で標準のTurboを使いたい場合は、rails-ujsの有効化は推奨されません。
最後に
今回は、Deviseのログアウト機能で発生するRouting Errorについて、
- GETでログアウトさせる方法の危険性
- Rails 6とRails 7での挙動の違い
- Turboでの正しいリンクの書き方
を解説しました。
Rails 7以降では、method: :delete
の代わりにdata: { turbo_method: :delete }
を使うことを覚えておきましょう。
Discussion