🙆

「Cookie-based authentication with Rails」を読んだ

2024/10/17に公開

Cookie-based authentication with Rails」を読んだので、コメントを残す。

Rails8でauthentication generatorが導入されたので、Devise gem以外を導入する契機になるかと思い読み進めました。

この記事では下記の機能が実装されています。

  • サインイン
  • サインアップ・サインアウト
  • パスワードリセット
  • パスワード変更

Userモデル

認証機能を実装するにあたり下記の違いを理解することが重要。

  • Hashing
  • Encrypting
  • Singing

Hashing

ハッシュ化は与えられた文字列を固定サイズの意味不明な文字列に変換するプロセス。
特徴としてはdeterministic(同じ入力をhash関数に与えると同じ出力が与えられる)、one-way(一方向の操作であり、出力から元の出力を逆にして取得することは不可能)の2点。

ただし、ハッシュアルゴリズムすべてがパスワードの保存に最適ではない。SHA256はパスワードの保存には適していない。

  1. SHA256のハッシュ化が早いことを利用して総当たり攻撃を実施。可能なパスワードをすべて試す。これはブルートフォース攻撃と呼ばれる
  2. データベースにアクセスできることを前提(例えば、Evilな開発者)。データベースからハッシュ化後のパスワードを参照し、ハッシュ化後のパスワードと前のパスワードの組み合わせて持っていくことでパスワードを特定する。全パターン網羅する巨大なDBを持つことで前の文字列に戻さなくても攻撃可能。
    ただし、ソルトを利用することでレインボーテーブルが各ソルト値毎で必要になってくるため計算不可能へ持ち込むことが可能。

そのため、bcryptを利用したhashアルゴリズムを利用する
ソルトとデータを組み合わせることで、同じデータでも異なるハッシュ結果となる。

hash_with_salt("secret123") = "somesalt123:xyz123abc789"
hash_with_salt("secret123") = "somesalt456:qldjv6qo4idd"
hash_with_salt("secret123") = "somesalt789:mvnq23qvmqp0"

今回のセッションでは、サインアップ時のパスワード保存、サインイン時のユーザー検証で利用している。

Encrypting

暗号化は、与えられた文字列を意味不明な文字列に変換するプロセス。
ハッシュ化との違いの特徴は、reversible: 可逆で暗号化キーがあれば元に戻すことが可能。

今回のセッションでは実装に利用されていない。

Singing

署名は、特定の文字列に署名を追加して、その文字列が改ざんされていないことを確認するプロセス。
署名は秘密鍵を使用して生成され、同じ鍵を使用して検証可能。

特徴は、下記の2つ。

  • public: 署名された文字列自体(payload)はpublic。Singingされたデータから復号可能。
  • unique to payload: 署名自体はpayloadに対してユニーク

署名された文字列自体は公開(Public)されているため復号可能だが、改ざんは困難。

今回のsessionではcookieに保存するデータを署名化することで利用している。
cookieを改ざんして特定のユーザーへのなりすまし(cookie上のuser_idを1から2に変更する)を防ぐために、署名してユーザーのなりすましを困難にしている。

    cookies.signed.permanent[:user_id] = user.id

また、パスワードリセット時のパスワードURLの発行でも署名化が利用されている。

SecureRandomを使ってパスワードリセットURLを作ることも可能だが、データベースにハッシュ化された値を保存し検証する手間があるため、generates_token_forが利用されている。

今回のセッションだと下記のpassword_resetURLが発行される。

signed_token = "eyJfcmFpbHMiOnsiZGF0YSI6WzIsIjV3b25WN1JXWGUiXSwiZXhwIjoiMjAyNC0xMC0xN1QwMzoxNjo0NC4xNTFaIiwicHVyIjoiVXNlclxucGFzc3dvcmRfcmVzZXRcbjM2MDAifX0%3D--0d9dca2948de028164741aaee48049cf739a5f21"
payload, signature = signed_token.split("--")
decoed_payload = Base64.decode64(payload)
=> "{\"_rails\":{\"data\":[2,\"5wonV7RWXe\"],\"exp\":\"2024-10-17T03:16:44.151Z\",\"pur\":\"User\\npassword_reset\\n3600\"}}7"

ユーザー個別のパスワードリセットトークンではないため、ユーザー識別情報としてIDが署名情報に含まれている。DB保存ではない方式で簡易化されている一部抜け道はあるようなイメージ。

Sessionモデル

署名されたcookie情報にuser_idが含まれているため、システムからのログアウトが困難。

    cookies.signed.permanent[:user_id] = user.id

ユーザーが複数のセッション情報を持ち、session_idをCookieに保存するように変更する。

    cookies.signed.permanent[:session_id] = { value: session.id, http_only: true }

参考として、githubでも同様のsession管理を実施していて、次のページで確認できる。
https://github.com/settings/sessions

クロージング

railsを使った認証機能の実装できるセッションとなっている。
また、ほかのhotrailsでturboを学習できるためこちらもオススメ。

実際にrails8で実装される認証を確認したい場合は下記で生成されるコマンドを確認可能。

gem exec rails new authentication --master --devcontainer
cd authentication
bin/rails generate authentication

Discussion