🧀

Ruby on Railsを通して認証を少し学んだ備忘録

に公開

はじめに

最近 Ruby on Rails の必要にせまられて、練習がてら小さいアプリケーションを開発していたのですが、認証機能でつまづいたり追加で調べたりするうちに「わたし、認証について、あんまり知らないで使っているな……」みたいなことに気づきました。日常でチーム開発してて認証さわることほぼないので、こういうのは個人でちょこちょこ触っている特権ですよね。

ということで、Ruby on Rails の認証について紹介しつつ、学んだことや感想をさらっとまとめてみます。

リクエストを許可する

そもそもフロントエンドのブラウザから API リクエストを受け付ける際には、CORS 対策が必要になります。
Ruby on Rails でいうと、config/initializers/cors.rb に以下のようにリクエスト元をホワイトリスト方式で指定します。

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "URL"

    resource "*",
      headers: :any,
      methods: [ :get, :post, :put, :patch, :delete, :options, :head ]
  end
end

ただ認証機能を入れるとなると、フロントエンドからのリクエストには認証情報なるものが Cookie を通してや Authorization Header などを通して送られてきますよね。その場合は別途、この認証情報を受け付ける設定を入れる必要があります。

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "URL"

    resource "*",
      headers: :any,
+     methods: [ :get, :post, :put, :patch, :delete, :options, :head ],
+     credentials: true
  end
end

この設定によってAccess-Control-Allow-Credentialsを有効にすることができます。
フロントエンドも認証資格情報を送ることができるようになりました。

const menus = await (
    await fetch(
        "Fetch URL",
        {
        method: "GET",
        // 認証情報を含んで送信するフラグを忘れずに…
        credentials: "include",
        headers: {
            "Content-Type": "application/json",
        },
        }
    )
).json();

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials

このcredentials: true設定が漏れている場合、バックエンドへリクエストは到達しているはずなのに CORS エラーが出るという不可解な状態になります。その際には以下のエラーが出ますので、同じエラーにもし引っかかった方がいらしたら助けになればと思います。

Access to fetch at 'fetchするURL' from origin 'リクエスト元オリジン' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.

Ruby on Rails のデフォルトの認証の流れとその特徴

Ruby on Rails はさまざまな認証手段を提供してくれていますが、今回は以下 2 つについて方式とその違い、特徴を並べてみました。

  • 認証情報を直接 Cookie に保存する方法
  • 認証情報を サーバーのデータストアに保存する方法

2 つの手法は流れで比較するととてもわかりやすいので、まずは最初の認証情報を直接 Cookie に保存する方法を以下にまとめました。

まずは 1 つ目の方法です。

ログイン時の流れ

ログイン以降の流れ

設定は config/application.rb に以下を追記するだけで利用できるようになります。

    config.session_store :cookie_store, key: "_app_session"
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use config.session_store, config.session_options

session_storeの詳しいオプションについては、Cookie そのものへの理解が必要であり、こちらについて説明すると記事がかなり長くなるため、別記事にまとめています。

https://zenn.dev/moepyxxx/articles/c80d0be15356cd

Ruby on Rails の設定については以下が参考になります。

https://api.rubyonrails.org/v5.2.1/classes/ActionDispatch/Session/CookieStore.html

なお、詳しい Model や Controller については以下を参考に実装しました。このあたりはコードベースで見ていただければわかると思いますので、紹介に留めます。

https://medium.com/@zachlandis91/authentication-and-authorization-in-ruby-on-rails-c44c1ccea94d

この手法の認証方式は以下のリポジトリで実際に実装してみました。この記事は概念的な解説にとどめているので、記事を読んでいただく中で Ruby on Rails の具体実装が気になった場合はこちらも参考になれば幸いです!

https://github.com/moepyxxx/teinei_ni_tabeyou

このパターンの特徴は以下の通りです。

  • リクエストごとに Store に問い合わせのアクセスをしないため、パフォーマンス効果が期待できる
  • フロントエンドに渡す Cookie にユーザーの情報を入れてしまっているため、セッションが盗まれた時にユーザー情報が漏れるリスクがある
    • このデメリットを Ruby on Rails はサーバーの秘密鍵を元に暗号化することにより、万が一盗まれても解読できないようにする機構を備えている
  • フロントエンドは Cookie に値があれば送ってきてしまうので、Expired していた場合には明示的にセッションをリフレッシュするなり削除するなりしないと、ゾンビ化する可能性がある(後述する属性情報でうまく Expired を指定するべき?)
  • 延命(リクエストがあった際にはアクティブなユーザーとしてセッションの期限を伸ばす)がなんか楽そう ※

※楽というより……デフォルトで Ruby on Rails がこの処理を入れてくれています。そのため、ログイン後リクエストのたびに Set-Cookie の値が差し代わっています。

認証情報を サーバーのデータストアに保存する方法

続いて 2 つ目の方法です。

ログイン時の流れ

ログイン以降の流れ

このパターンの場合、フロントエンドに渡す Cookie の中に直接ユーザーの情報を入れることをしません。フロントエンドには、ストアのキーとなるようなトークンを発行しその情報を渡しておき、ID などユーザーを特定する直接の情報はストアのキーに対応するバリューに入れておきます。

このパターンの特徴は以下の通りです。

  • フロントエンドに渡す Cookie にユーザーの情報を入れていないため、セッションが盗まれた時にユーザー情報が漏れるリスクがある。また、漏れたと分かった場合は一度 Store をリフレッシュすれば悪用がされない状態になる
  • トークンをキーにして認証情報を問い合わせるだけのため、Redis のように Expired した際には自動で消えるような機構があれば判定の手間がない。1 つ目よりもゾンビ可するリスクが少ない
  • Expired を変える時は該当のキーバリューを一旦削除して再度登録する必要があるため、延命(リクエストがあった際にはアクティブなユーザーとしてセッションの期限を伸ばす)がなんか大変そう ※

※延命することがない限りは、ログイン後リクエストのたびに Set-Cookie が差し代わることはなく、明示的にログアウトするか期限が切れるまで同じ Cookie の値を使うことになります。

どちらがより強固なセキュリティレベルを備えているかについては、諸説ありそうという感じです。以下のような記事のように、クライアントサイドに管理を丸投げするのは、なかなか厳しいのでは?というご意見もあり、とっても参考になりました。

https://qiita.com/okazu_dm/items/08c5cbeed7df005e9154

おわりに

Ruby on Rails の認証スタイルの基本についてここまで調べてみました。どちらも Ruby on Rails の認証の管理方法として簡単に選択し分けることができるようになっているため(便利なんですね〜)、どちらがより良いかをプロジェクトで判断いただいて使うのが良いのかなと思いました。

今回は Ruby on Rails をベースに調べたことをまとめてみましたが、基本的に Cookie を利用した認証方式は概ねどんな言語やフレームワークを利用してもこのような形がベースになるのかなと思いました。色々と調べてみてよかったです。

ここまで認証について、そうなんだふーん、となったところで、今回はこの認証のために利用している Cookie という技術が気になってきました。そこでさらに Cookie について掘り下げてみました。以下で Cookie についてのお話をしており、認証方式と合わせるともしかしたら理解が深まるかもしれません。気になったら、ぜひ読んでみてください!

https://zenn.dev/moepyxxx/articles/c80d0be15356cd

ここまでお読みいただきありがとうございました。

Discussion