【Rails】アクセストークンは別テーブルで管理
はじめに
こんにちは!大ちゃんの駆け出し技術ブログです。
以前Slack API を使用する上でアクセストークンを管理する記事を出しました。
【Rails】Slack認証時のアクセストークンの保存 - 大ちゃんの駆け出し技術ブログ
この記事の中で暗号化をするために以下のメソッドをbefore_save
で使用しました。
before_save :encrypt_access_token def encrypt_access_token key_len = ActiveSupport::MessageEncryptor.key_len secret = Rails.application.key_generator.generate_key('salt', key_len) crypt = ActiveSupport::MessageEncryptor.new(secret) self.access_token = crypt.encrypt_and_sign(access_token) end
しかしながら、今回別の機能を実装したことで上記の実装ではできないことに気づきました。その経緯を備忘録として記録します。
ユーザの更新機能
今回追加する機能としては、ユーザごとにアプリからSlackへの投稿機能を1日に一回にするというものです。そのため、enumで投稿を既に行ったかどうかを示すカラムを定義しました。
新規カラムをusersテーブルに追加
t.integer :share_right, null: false, default: 0
user.rb
でカラムをenumとして設定
enum share_right: { not_shared_yet: 0, already_shared: 1 }
あとはユーザが投稿機能を使用したらUserに対してalready_shared!
メソッドを使用することでユーザの更新を行います。
def update_share_right if @user.not_shared_yet? # おそらく必要はないが念のため @user.already_shared! render json: @user, serializer: UserSerializer end end
そして定期実行で全てのユーザのステータスを元のnot_shared_yet
に戻します。
namespace :users do desc "全てのユーザのslack共有の権利を更新" task update_share_right: :environment do User.find_each do |user| user.not_shared_yet! if user.already_shared? end end end
ところがこのユーザの更新が問題でした。
before_saveの理解不足
しばらく気づかなかったのですが、どうもユーザが一度投稿してenumのメソッドを実行した後にAPIメソッドが使用できなくなっていました。理由はbefore_save
にありました
実はbefore_save
は作成時だけでなく、更新時にも実行されます。
バリデーションに成功し、実際にオブジェクトが保存される直前で実行されます。INSERT される場合も、UPDATE される場合も呼び出されます。INSERT もしくは UPDATE の場合だけ実行したい処理があるときは、後述する before_create / before_update を使用します。
Railsのコールバックまとめ | TECHSCORE BLOG
つまり、ユーザの更新を行ってしまうと暗号化されたアクセストークンが再度暗号化されてしまうのです。
更新前の暗号化済みトークン
RHrIxenifGqbqi4vvi7ybE79cg/AhbQDMiLh
暗号化済みトークンを再度暗号化(値が変わっている!!!)
kXUHH9fVb7ZhrqASKKi+dK2c65YHWKlhw6np
よって一度の復号ではなく二度復号処理を行わないと、復号された値を取り出すことができなくなりました。
def set_access_token encrypted_access_token = current_user.access_token key_len = ActiveSupport::MessageEncryptor.key_len secret = Rails.application.key_generator.generate_key('salt', key_len) crypt = ActiveSupport::MessageEncryptor.new(secret) decrypted_access_token = crypt.decrypt_and_verify(encrypted_access_token) decrypted_access_token = crypt.decrypt_and_verify(decrypted_access_token) # 再復号処理 access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], decrypted_access_token) return access_token end
しかし、定期実行でもユーザーの更新を行うため、その時にも復暗号化済みのトークンが再度暗号化されてしまいます。よって、ユーザごとにアクセストークンが暗号化されている回数が違います。
考えられる解決策
解決策について考えたところ、最初はユーザカラムの状態を変えるのが面倒なので、復号化された値が取り出せるまで複合処理を繰り返し行う実装を考えました。
def set_access_token encrypted_access_token = current_user.authentication.access_token key_len = ActiveSupport::MessageEncryptor.key_len secret = Rails.application.key_generator.generate_key('salt', key_len) crypt = ActiveSupport::MessageEncryptor.new(secret) while encrypted_access_token.kind_of?(String) encrypted_access_token = crypt.decrypt_and_verify(decrypted_access_token) end decrypted_access_token = encrypted_access_token access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], decrypted_access_token) return access_token end
暗号化されている値はStringクラスなのでencrypted_access_token.kind_of?(String)
でaccess_tokenがStringクラスの間復号処理を行うというものです。正直、これによって暗号化された回数に関わらず全てのアクセストークンが復号できるのでこれでもいいかと思いました。
しかし、少し適当すぎるかなと思い別の解決策を考えたところ、before_create
を使用することを考えました。
before_save の後に実行されます。オブジェクトが登録されるとき (new_record? が true のとき) は before_create が実行されます。
Railsのコールバックまとめ | TECHSCORE BLOG
before_create
であれば更新時には実行されることはないので安心だと思いました。
before_create :encrypt_access_token def encrypt_access_token key_len = ActiveSupport::MessageEncryptor.key_len secret = Rails.application.key_generator.generate_key('salt', key_len) crypt = ActiveSupport::MessageEncryptor.new(secret) self.access_token = crypt.encrypt_and_sign(access_token) end
しかし、userカラムにaccess_tokenを残しておくこと自体が少し危険だと思い始めました。今回のように後々ユーザのカラムに今回のように更新したい値が出てきた時に、毎度before_create
を使用して退避しなければなりません。加えて、誤って気づかないうちにユーザカラムを更新する処理を行っていたら、いつのまにかバグになってしまいそうです。
そのため、少々面倒ですがaccess_tokenを別で管理するテーブルをhas_oneの関係で用意することにしました。
class User < ApplicationRecord has_one :authentication, dependent: :destroy def check_authentication_existence(hash_token) if self.authentication.present? self.authentication.update!(access_token: hash_token) else @authentication = self.build_authentication(access_token: hash_token) @authentication.save! end end end
class Authentication < ApplicationRecord # before before_save :encrypt_access_token belongs_to :user def encrypt_access_token key_len = ActiveSupport::MessageEncryptor.key_len secret = Rails.application.key_generator.generate_key('salt', key_len) crypt = ActiveSupport::MessageEncryptor.new(secret) self.access_token = crypt.encrypt_and_sign(access_token) end end
別テーブルで管理することでユーザの更新を行っても、access_tokenが更新されることはありません。加えて、check_authentication_existence
メソッドをslack認証時に使用することで、アクセストークンの更新も行えるようにしておきます。
これでなんとかaccess_tokenの再暗号化を防ぐことができました!