🚨

【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)です。
ユーザー登録・ログイン・ログアウトなどの機能を簡単に実装できます。
https://github.com/heartcombo/devise

Deviseでのログアウト実装時によくあるエラー

Deviseでのログアウトを実装するために、viewファイルに以下のようなコードを書きます。

<%= link_to "ログアウト", destroy_user_session_path, method: :delete %>

そして、ログアウトを行おうとすると、次のようなエラーが発生してしまいます。
Routing Error
No route matches [GET] "/users/sign_out"

本エラーの対策として、Web検索でヒットする対応は、

  • ログアウトに使うHTTPメソッドの設定をDELETEGETに変更する
  • 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_outDELETEリクエストを受け取った場合は、devise/sessionsdestroyアクションを実行するルーティング設定になっていることが分かります。
Image from Gyazo

では、/users/sign_outへのリンクを生成しているコードを見てみましょう。

<%= link_to "ログアウト", destroy_user_session_path, method: :delete %>

こちらのコードでは、method: :deletedestroy_user_session_pathへのリンク、つまりは DELETEリクエスト で/users/sign_outへアクセスするリンクを生成 しようとしています。
一方、今回のエラーから分かる通り、実際には GETリクエスト で/users/sign_outへアクセスするリンクを生成 してしまっています。

Rails 6以前とRails 7以降のリンク生成方法の違い

実は、Rails6系までは、link_toヘルパーのmethod: :deleteDELETEリクエストのリンクを生成することができました。
これは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