今月の半ば、2023年9月13日に Rails 7.1 Beta1 がリリースされました。
また、9月27日にはrc1もリリースされており、Rails 7.1の正式リリースも着々と近づいています。
公式ブログの記事では、次の新機能等が紹介されています。
- アプリケーション作成時にDockerfileを生成
- 独自の認証機能構築の改善
- Active Recordへのクエリをより非同期に
- Trilogy MySQLアダプタのサポート
- Active Record: 複合主キーサポート
- Active Job: 大量のジョブキューイングを効率化するメソッド
- Autoloadingの拡張
- JavaScriptランタイム Bun のサポート
皆さんは気になる話題はありますでしょうか。
今回はアプリケーション構築に直接影響がありそうなところ、「独自の認証機能構築の改善」を見ていきたいと思います。
※このコラムのプログラム等は以下の環境で動作確認しています
(2023/09/30 最終確認)
ruby: ruby 3.2.2 [arm64-darwin22]
rails: rails (7.1.0.rc1)
独自の認証機能構築: has_secure_password
元々Railsには、ActiveModelのメソッドとしてhas_secure_password
が用意されていました。
こちらを利用すると、安全な認証機能を簡便に構築することができます。
ここで使い方をおさらいして、新しい機能を見ていきましょう。
Gemfileでbcrypt
を設定
has_secure_password
を利用するには、bcrypt
Gemが必要です。rails new
した際に作成されるGemfileではコメントアウトされていますので、有効化してbundle install
します。
Gemfile
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
gem "bcrypt", "~> 3.1.7"
認証用のモデルにカラムを設定
パスワードはbcryptによって適切にダイジェスト化され保存されます。
保存先のカラムとして、認証用に使うモデルにpassword_digest
(string)カラムを用意します。
今回は認証に使うモデルとしてUserというモデルを作成しました。
db/migration/create_users.rb
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :email, null: false
t.string :password_digest, null: false
t.timestamps
t.index :email, unique: true
end
end
end
モデルにメソッドを設定
認証に使うモデルにhas_secure_password
を追記します。
app/models/user.rb
class User < ApplicationRecord
has_secure_password
end
動作確認用のユーザー作成
今回はdb/seeds.rb
を利用して確認用のユーザーを作成します。
Rails consoleで実行しても良いでしょう。
db/seeds.rb
User.create!(email: "alice@example.jp", password: "passw0rd!")
コマンド
bin/rails db:seed
認証機能として利用する(従来の方法)
Rails consoleを使って認証機能を確認してみます。
Userを検索した後、`authenticate`メソッドを利用してパスワードを検証します。
bin/rails c
# ユーザーを検索
irb> user = User.find_by(email: 'alice@example.jp')
# パスワードを確認
## 正しいときは、userインスタンスがそのまま返る
irb> user&.authenticate('passw0rd!')
=> #<User:... id: 1, email: "alice@example.jp", password_digest: "[FILTERED]", ...>
## 正しくないときは、falseが返る
irb> user&.authenticate('invalid')
=> false
実はこの方法には「タイミング攻撃」に対する脆弱性があります。
対象のユーザーが存在するかどうかで処理時間に有意な差が出てしまうことから、たとえ画面上にはそのようなメッセージを表示しないとしても、攻撃者は ユーザーが存在することを知ることができてしまいます。
新機能: タイミング攻撃を緩和するauthenticate_by
そこで今回新しく追加されたクラスメソッドが、authenticate_by
です。
bin/rails c
# ユーザーを検索と同時にパスワードを検証
## 正しいときは、userインスタンスが返る
irb> user = User.authenticate_by(email: 'alice@example.jp', password: 'passw0rd!')
=> #<User:... id: 1, email: "alice@example.jp", password_digest: "[FILTERED]", ...>
## 正しくないときは、nilが返る
irb> user = User.authenticate_by(email: 'alice@example.jp', password: 'invalid')
=> nil
## ユーザーが存在しないときも、nilが返る
irb> user = User.authenticate_by(email: 'no-exist@example.jp', password: 'passw0rd!')
=> nil
このメソッドは、レコードの有無に関わらずパスワードダイジェストの計算をおこなうことで、タイミング攻撃に対する脆弱性を緩和する、とされています。find_by
のような検索を自分で書く必要もなくなるため、コードの見通しも少し良くなりそうです。
新機能: 自動的に正規化する normalizes
その他の追加メソッドも見ていきましょう。
指定したアトリビュートを自動的に正規化できる、normalizes
メソッドが追加されました。
例えば「emailを扱う際は小文字に統一する」、といったことを標準機能として記述することができます。
app/models/user.rb
class User < ApplicationRecord
has_secure_password
normalizes :email, with: -> email { email.strip.downcase }
end
bin/rails c
irb> User.new(email: 'BOB@EXAMPLE.JP')
=> #<User:... id: nil, email: "bob@example.jp", ...>
# 認証で検索する際も自動的に正規化
irb> user = User.authenticate_by(email: 'ALICE@EXAMPLE.JP', password: 'passw0rd!')
=> #<User:... id: 1, email: "alice@example.jp", password_digest: "[FILTERED]", ...>
新機能: パスワード変更時に現在のパスワードを検証する
パスワードを更新する際に現在のパスワードを検証するようになりました。
bin/rails c
# 現在のパスワードが誤っていると更新できない
irb> user.update(password: 'new_passw0rd!', password_challenge: 'invalid')
=> false
# 現在のパスワードが正しいと更新できる
irb> user.update(password: 'new_passw0rd!', password_challenge: 'passw0rd!')
=> true
新機能: 目的に応じたトークンを生成する generate_token_for
目的に応じたトークンを生成して、トークンによってレコードを検索できるgenerate_token_for
メソッドが追加されました。
有効期間や、トークンの有効性を検証するための処理を指定することもできます。
まとめ
Rails7.1で追加される予定の新機能のうち、「独自の認証機能構築の改善」を見てみました。
「Railsにおまかせ」でよりセキュアな認証機能を構築できるようになったことは、Railsコミュニティがきちんとセキュリティ問題に向き合っている証左と言えるでしょう。
認証機能はGemにお任せしてしまうことも多いと思いますが、Railsアプリケーション開発者としては、是非標準機能の進歩も抑えておきたいところです。
お知らせ
当委員会ではRails試験を全国300か所で一年中実施をしています。興味がある方は以下をご覧の上、是非受験ください。https://railsce.com/exam