大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【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の再暗号化を防ぐことができました!