Rails 2.5のcredentialsを使うとDeviseからエラーが発生する問題と応急処置

Rails 2.5のcredentialsを使うとDeviseからエラーが発生する問題と応急処置

※この問題はDevise 4.4.3以下で起きます。次期バージョンである4.4.4(執筆時点では未リリース)では修正される見込みです。

発生した問題

とあるプロジェクトをRails 2.5で作っているのですが、Deviseを導入していくつかの設定を行った所、次のようなエラーが出てくるようになりました:

$ rails c
/usr/local/bundle/gems/devise-4.4.3/lib/devise/rails/routes.rb:500:in `raise_no_secret_key': Devise.secret_key was not set. Please add the following to your Devise initializer:

  config.secret_key = '1e2903bbf3c5f9b45b398332c421ab572c5e272253100d2603ad038b33fb3b1203e049c4b088a6b19434fe6018a37235f7bf528c453ba957cdb62a4b7374e912'

Please ensure you restarted your application after installing Devise or setting the key.
 (RuntimeError)
    from /usr/local/bundle/gems/devise-4.4.3/lib/devise/rails/routes.rb:228:in `devise_for'
    from /app/config/routes.rb:4:in `block in <main>'

Deviseではパスワードリセット等で使用するランダムなトークンの生成に、config/initialzers/devise.rbで設定した config.secret_key の値を使用します。
デフォルトではこの設定は未指定の場合、Railsのsecret_key_baseが使われる為コメントアウトされています。任意の値を設定したい時にコメントアウトして使う物なので、通常は設定する必要がありません。
では、何故このようなエラーが発生したのでしょうか。

Rails 2.5で新しく導入されたcredentials

Railsでは2.5から秘匿情報の扱いが変わりました。具体的には config/secrets.yml を廃止し、代わりに config/credentials.yml.enc が追加されました。secret_key_baseもこのファイルに保存します。(credentials.yml.encについてはこちらのQiitaが詳しいです)

さて、このcredentials.yml.encですが、中身はファイル名の通り暗号化されており、同じく新規に追加された config/master.key あるいは ENV["RAILS_MASTER_KEY"] (以下、複合キー) によって実行時に複合されます。
また、当然、credentials.yml.encには秘匿情報が格納されている為、master.keyはGitによるバージョン管理はされません。

そうなると、他の開発メンバーが加わった際は複合キーを共有する必要があります。しかし、開発メンバーのロールによってはこれらを共有する事ができないケースも考えられます。
では、複合キーがない状態でRailsを走らせるとどうなるのでしょうか?実際に試してみると、普通に動かす事ができます。
credentialsはsecretsとは違い、環境ごとに値を分ける事はできません。そもそも、秘匿情報の管理自体が本番環境でのみ必要となるので、環境ごとに分ける必要が無いと言うスタンスのようです。
この為、開発環境では(複合キーがない場合は)単に Rails::Application.credentials の中身が空になるだけで、エラーにはなりません。

さて、本番でしか使われない事がわかったcredentialsですが、それではsecret_key_baseはどうなってしまうのでしょうか?
答えは単純で、開発環境ではsecret_key_baseはプロジェクト毎に固定の値が使われるようになっています。
新しく追加された Rails::Application.secret_key_base によって取得が可能で、Applicationクラスのクラス名のmd5値が使われます。Applicationクラス名が “MyBlog::Application” なら、 fed42568a7c9da7d8f43ec1cd7e60b57 と言った具合です。
確かに、これであれば複合の必要自体がなくなります。また、開発環境のsecret_key_baseなんて漏れてもダメージはないのでこれで問題ありませんね。

Deviseでのsecret_key_baseの取扱

話を冒頭に戻します。Deviseはデフォルトではsecret_key_baseの値を参照しますが、実装はどのようになっているのでしょうか?Devise(
執筆時点で最新の4.4.3の)のソースを見ると、 Devise::SecretKeyFinder がその役を担っており、実装は次のようになっています:

# https://github.com/plataformatec/devise/blob/v4.4.3/lib/devise/secret_key_finder.rb#L10-L15
 
if @application.respond_to?(:credentials) && key_exists?(@application.credentials)
  @application.credentials.secret_key_base
elsif @application.respond_to?(:secrets) && key_exists?(@application.secrets)
  @application.secrets.secret_key_base
elsif @application.config.respond_to?(:secret_key_base) && key_exists?(@application.config)
  @application.config.secret_key_base
end

@application.credentials から @application.secrets 、@application.config と順番にsecret_key_baseのキーを検証し、最初に見つかったものを使うようになっている様です。しかし、先述したとおり、credentialsは開発環境では使いませんし、secretsも廃止済みなのでいずれも値は何もセットされません。
また、configには普通は secret_key_base は格納しないと思うので、ここにも値は存在しません。

本来であれば、全環境でsecret_key_baseを参照する場合、先述した Rails::Application.secret_key_base を使うべきなのですが、ご覧の通り実装のミスがあるようです。(このSecretKeyFinder自体4.4.3で追加された物であり、2.5で十分なテストをしていなかったのかもしれませんね)

ただこの点については、既にPRがあり、マージもされています。PRのコメントによれば、雰囲気的には次のバージョン4.4.4で解消されそうです。

4.4.4が出るまでの応急処置

最初に書いた様にDeviseのsecret_keyには任意の値をセットできるので、config/initialzers/devise.rbで Rails.application.secret_key_base を明示的にセットしてあげれば良さそうです。

Devise.setup do |config|
  # The secret key used by Devise. Devise uses this key to generate
  # random tokens. Changing this key will render invalid all existing
  # confirmation, reset password and unlock tokens in the database.
  # Devise will use the `secret_key_base` as its `secret_key`
  # by default. You can change it below and use your own secret key.
  config.secret_key = Rails.application.secret_key_base # 4.4.4が出るまでの応急処置
  
  # ...
end

最後に、Rails consoleできちんと設定されているか確認します:

$ rails c
Running via Spring preloader in process 1167
Loading development environment (Rails 5.2.0)
irb(main):001:0> Devise.secret_key
=> "fed42568a7c9da7d8f43ec1cd7e60b57"