この記事ですること
TwitterAPIを使うときには、開発者アカウントのaccess_token, access_token_secret↓を使用することで、公開されているユーザー情報やツイートの取得などを行うことができます。
しかし、非公開ユーザーやフォロワーの情報を取得したり、ユーザーがアプリを通してツイートをするためには、ユーザーごとに一意の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
に変更しました。
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
は外してください。
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
メソッドの内部です。
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をそのままデータベースに保存すると危険なので、暗号化して保存する。
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に切り出した方がいいです。検証していないのでコードはないですが……。
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!")
のように使う。