LoginSignup
4
4

More than 3 years have passed since last update.

【Rails】SorceryでTwitter認証時にaccess_tokenを取得してDBに保存する

Last updated at Posted at 2020-10-04

この記事ですること

TwitterAPIを使うときには、開発者アカウントのaccess_token, access_token_secret↓を使用することで、公開されているユーザー情報やツイートの取得などを行うことができます。
スクリーンショット 2020-10-05 8.23.35.png
しかし、非公開ユーザーやフォロワーの情報を取得したり、ユーザーがアプリを通してツイートをするためには、ユーザーごとに一意のaccess_tokenとaccess_token_secretを使用する必要があります。
この記事では、個別のユーザーのaccess_token, access_token_secretをデータベースに保存し、利用する実装を書きます。

【Rails】SorceryでTwitter認証 - QiitaでTwitter認証の実装ができていることを前提とします。

【注意】
この記事は、実装時点ではユーザーがいないことを前提としています。
既にユーザーがいる場合(Userデータをリセットできない場合)は、そのユーザーのaccess_tokenがnullということになるので、ちょっと面倒な条件分岐を追加する必要があります。

環境

Ruby 2.6.6
Rails 5.2.4.3
Sorcery 0.15.0

モデルのアソシエーションの修正

私の場合は、外部認証はTwitterのみを想定していたので、以下のようにアソシエーションをhas_oneに変更しました。

app/models/user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!
- has_many :authentications, dependent: :destroy
- accepts_nested_attributes_for :authentications
+ has_one :authentication, dependent: :destroy
+ accepts_nested_attributes_for :authentication
  ...
end

他のサービスとも連携していてuser has_many :authenticationsの場合はuser.authentication.hogeなどの記述をuser.authentications.find_by!(provider: 'twitter')のように置き換えてください。

マイグレーションファイルの作成

authenticationsテーブルにaccess_tokenとaccess_token_secretカラムを追加します。
Twitter以外の連携をしている場合は、オプションのnull: falseは外してください。

db/migrate/20200613022618_add_access_token_columns_to_authentications.rb
class AddAccessTokenColumnsToAuthentications < ActiveRecord::Migration[5.2]
  def change
    add_column :authentications, :access_token, :string, null: false
    add_column :authentications, :access_token_secret, :string, null: false
  end
end

コントローラを追記

前回の記事と変わったのはcreate_user_fromメソッドの内部です。

app/controllers/api/v1/oauths_controller.rb
class OauthsController < ApplicationController
  skip_before_action :require_login # applications_controllerでbefore_action :require_loginを設定している場合

  def oauth
    login_at(auth_params[:provider])
  end

  def callback
    provider = auth_params[:provider]
    if auth_params[:denied].present?
      redirect_to root_path, notice: "ログインをキャンセルしました"
      return
    end
    # 送られてきた認証情報でログインできなかったとき(該当するユーザーがいない場合)、新規ユーザーを作成する
    create_user_from(provider) unless (@user = login_from(provider))
    redirect_to root_path, notice: "#{provider.titleize}でログインしました"
  end

  private

  def auth_params
    params.permit(:code, :provider, :denied)
  end

  def create_user_from(provider)
    @user = build_from(provider) # ①
    @user.build_authentication(uid: @user_hash[:uid],
                               provider: provider,
                               access_token: access_token.token,
                               access_token_secret: access_token.secret) # ②
    @user.save! # ③
    reset_session
    auto_login(@user)
  end

build_fromはSorceryのメソッド。
provider:twitter)からSorceryに渡されたデータをUserインスタンス(@user)のattributesとして入れる。

②認証インスタンスを作成する。
@user_hash, access_tokenにはTwitterから受け取ったデータが入っているので、それを使う。
ちなみに、build_authenticationはhas_oneのメソッドなので、has_manyの場合はuser.authentications.buildにしてください。

[11] pry(#<OauthsController>)> @user_hash
=> {:token=>"111111111111111111111",
 :user_info=>
  {"id"=>1048451188209770497,
   "id_str"=>"1048451188209770497",
   "name"=>"END",
   "screen_name"=>"aiandrox",
   "location"=>"岡山ずっと→山梨ちょっと→東京イマココ",
   "description"=>"小学校の先生とか仲居とかやってて、最近エンジニアになった人。謎解きエンジョイ勢。 #RUNTEQ",
   "url"=>"https://t.co/zeP2KN6GMM",
    ...
  }
}

[12] pry(#<OauthsController>)> access_token
=> #<OAuth::AccessToken:0x00007f2fc41402d0
 @consumer=
  #<OAuth::Consumer:0x00007f2fc405c008
   @debug_output=nil,
   @http=#<Net::HTTP api.twitter.com:443 open=false>,
   @http_method=:post,
   @key="aaaaaaaaaaaaaaaaa",
   @options=
    {:signature_method=>"HMAC-SHA1",
     :request_token_path=>"/oauth/request_token",
     :authorize_path=>"/oauth/authenticate",
     :access_token_path=>"/oauth/access_token",
     :proxy=>nil,
     :scheme=>:header,
    ...

③関連付けられたauthenticationごと保存する。
@user.build_authentication@user.authentications.buildなどで生成されたAuthenticationインスタンスはUserが保存されたときに一緒に保存するようになっている。

モデルの修正

access_tokenやaccess_token_secretをそのままデータベースに保存すると危険なので、暗号化して保存する。

app/models/authentication.rb
class Authentication < ApplicationRecord
  before_save :encrypt_access_token
  belongs_to :user

  validates :uid, presence: true
  validates :provider, presence: true

  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)
    self.access_token_secret = crypt.encrypt_and_sign(access_token_secret)
  end
end

Twitter::REST::Clientをサービスクラスに切り出す

Twitterクライアントに関するロジックをサービスクラスに切り出す。
引数にuserがあるときはuserのaccess_tokenなどを使い、userが渡されないときはデフォルトのaccess_tokenなどを用いる。

(追記)多分本当はmoduleに切り出した方がいいです。検証していないのでコードはないですが……。

app/services/twitter_api_client.rb
require 'twitter'

class TwitterApiClient
  def self.call(user = nil)
    new.call(user)
  end

  def call(user)
    @user = user
    @client ||= begin
      Twitter::REST::Client.new do |config|
        config.consumer_key        = Rails.application.credentials.twitter[:key]
        config.consumer_secret     = Rails.application.credentials.twitter[:secret_key]
        config.access_token        = access_token
        config.access_token_secret = access_token_secret
        config.dev_environment     = 'hoge' # Sandboxを使用する場合は設定した名前を書く
      end
    end
  end

  private

  attr_reader :user

  def access_token
    @access_token ||= user ? crypt.decrypt_and_verify(user.authentication.access_token)
                           : Rails.application.credentials.twitter[:access_token]
  end

  def access_token_secret
    @access_token_secret ||= user ? crypt.decrypt_and_verify(user.authentication.access_token_secret)
                                  : Rails.application.credentials.twitter[:access_token_secret]
  end

  def crypt
    key_len = ActiveSupport::MessageEncryptor.key_len
    secret = Rails.application.key_generator.generate_key('salt', key_len)
    ActiveSupport::MessageEncryptor.new(secret)
  end
end

TwitterApiClient.call(user)でClientのインスタンスを呼び出すことができる。
コードとして書くときは、TwitterApiClient.call(user).update("I'm tweeting with @gem!")のように使う。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4