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

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

【Rails】Slack認証時のアクセストークンの保存

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今日はslack認証時に得られるアクセストークンをDBに保存する方法です。(立て続けにslackについての記事を書いておりますが、最近ようやく実装できたことなのでたくさんアウトプットの題材が残っているのでアウトプットしておきます。)アクセストークンを保存することで 、下記メソッドを使用することで認証時以外でもslack APIのメソッドを使用することができます。

access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], hash_token)

それでは記事内容を見ていきましょう。

JSON型で保存

外部認証時の返却値request.env['omniauth.strategy']からアクセストークンを生成するメソッドにつなげることができます。

bot_token = request.env['omniauth.strategy'].access_token
hash_token = bot_token.to_hash
access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], hash_token)

繰り返しになりますが、保存すべき値はhash_tokenの値になります。

このhash_tokenの値をDBに保存するのですがhash_tokenはJSONです。

hash_token
=> {"ok"=>true,
 "app_id"=>"xxxxxxxxxx",
 "authed_user"=>
  {"id"=>"xxxxxxxxxx",
   "scope"=>"identity.basic,identity.email,identity.avatar,identity.team",
   "access_token"=>"xoxp-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx",
   "token_type"=>"user"},
 "scope"=>"chat:write,groups:write,im:write,channels:manage,channels:read,groups:read",
 "token_type"=>"bot",
 "bot_user_id"=>"xxxxxxxxxx",
 "team"=>{"id"=>"xxxxxxxxxx", "name"=>"prof"},
 "enterprise"=>nil,
 "is_enterprise_install"=>false,
 :access_token=>"xoxb-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx",
 :refresh_token=>nil,
 :expires_at=>nil}

よってDBに保存する型はJSON型を使用します。(はじめてJSON型というものを使用しました、、、。)基本的にhashは全てJSON型の方が良さそうだと思いました。

ActiveModelのバリデータでJSON型のカラムを検証する

したがってUserカラムに追加するaccess_tokenはjson型で作成しましょう。

class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table(:users) do |t|
      ## User Info
            ・
            ・
            ・
      t.json    :access_token
    end
  end
end

あとは保存時にhash_tokenを引数として指定して、保存をしてあげるだけです。

@user = User.from_omniauth(request.env['omniauth.auth'], user_info, hash_token, channel)
# app/models/user.rb
class User < ApplicationRecord

  def self.from_omniauth(auth, user_info, hash_token, channel)
    user = find_or_initialize_by(provider: auth.provider, uid: auth.uid)
    user.password = Devise.friendly_token[0, 20] # ランダムなパスワードを作成
    user.name = user_info.dig('user', 'name')
    user.email = user_info.dig('user', 'email')
    user.access_token = hash_token
    user.remote_image_url = user_info.dig('user', 'image_192')
    user.check_team_existence(user_info.dig('team'), channel)
    user.save!
    user
  end
end

暗号化

slack APIについては今回生成したアクセストークンの値などが記されたhash_tokenを直接盗み見られてもそこまで問題ではありません。繰り返しアクセストークンを生成するメソッドを例にしますが、slackアプリのIDとなるSLACK_CLIENT_IDとSLACK_CLIENT_SECRETが知られない限り、アクセストークンを知られたとしても基本的に何もできません。

access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], hash_token)

SLACK_CLIENT_IDとSLACK_CLIENT_SECRETがあることでsecureな状態を保ってくれています。

しかし、アクセストークンを直接保存することはあまり良くないので、暗号化してから保存しましょう。

# app/models/user.rb
class User < ApplicationRecord

  # before
  before_save :encrypt_access_token, :if => Proc.new {|u|  u.access_token.present? }

  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

  def self.from_omniauth(auth, user_info, hash_token, channel)
    user = find_or_initialize_by(provider: auth.provider, uid: auth.uid)
    user.password = Devise.friendly_token[0, 20] # ランダムなパスワードを作成
    user.name = user_info.dig('user', 'name')
    user.email = user_info.dig('user', 'email')
    user.access_token = hash_token
    user.remote_image_url = user_info.dig('user', 'image_192')
    user.check_team_existence(user_info.dig('team'), channel)
    user.save!
    user
  end
end

before_save :encrypt_access_tokenで保存する前にencrypt_access_tokenを実行するようにします。そのUserがアクセストークンを持っていない場合は実行しないように:ifオプションをつけています。

  before_save :encrypt_access_token, :if => Proc.new {|u|  u.access_token.present? }

暗号化方法にはいくつかの方法がありますが、上述したようにアクセストークンを暗号化する重要性がそこまで高くはないため、少し簡易的な方法をとっています。

  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

これを復号するときは以下のようにするば復号できます。

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)
    access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], decrypted_access_token)
    return access_token
  end

Slack認証時に保存するべき値

以上までの説明でhash_tokenを保存することで再度slack用のアクセストークンを作成することができます。しかし、アプリによってはアクセストークン以外にも認証時にカラムに保存しておくべき値があります。例えば、DMをuserに送信するアプリの場合、ユーザのIDがわかっていないと送ることができません。

chat.postMessage Slack API Method

chat.postMessageメソッドはアクセストークンの値に加えて、channelのIDを必要とします。(DMを送る場合はuserのIDを必要とする)

https://i.gyazo.com/71e198f2409cfb142112c5206a70b412.png

よって認証時にuserのIDは控えておけば、のちのちDMを認証時以外で送る場合により簡単にDMを送ることができるでしょう。

アプリの用途によって必要なAPIメソッドが変わるのでそれに合わせて必要な値を認証時に保存しておきましょう。

今回は以上です。