This page looks best with JavaScript enabled
⚠️

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

 ·   ·  ☕ 6 分で読めます
✏️

この記事ですること

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認証でTwitter認証の実装ができていることを前提とします。

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

環境

Ruby 2.6.6
Rails 5.2.4.3
Sorcery 0.15.0

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

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

1
2
3
4
5
6
7
8
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は外してください。

1
2
3
4
5
6
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メソッドの内部です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 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_from_provider = 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にしてください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[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が保存されたときに一緒に保存するようになっている。

(追記)authentication登録済みのユーザーもログイン時にaccess_tokenを取得し直すようにする

例えば、ユーザーはTwitterの「設定→セキュリティとアカウントアクセス→アプリとセッション→連携しているアプリ」のページから、アプリとの連携を解除することができる。

image

この場合、Twitter側でアクセストークンが無効化されるため、アプリで保存しているアクセストークンを使って認証ができなくなる。
再度ログインをしても、ログイン時には「userと紐づくauthenticationがあるかどうか」を確認するだけなので、Authentication#access_tokenは無効なままだ。

これを解決するために、ログイン時(アプリ側でアクセストークンを取得する手段はこのときしかない)には、ユーザーと紐づくauthenticationが既にあろうと最新のアクセストークンを保存するようにする。

1
2
    # 送られてきた認証情報でログインできなかったとき(該当するユーザーがいない場合)、新規ユーザーを作成する
    create_user_from(provider) unless login_from(provider)

の部分を以下のように書き換えます。

1
2
3
4
5
6
7
8
9
    # 送られてきた認証情報でログインできなかったとき(該当するユーザーがいない場合)、新規ユーザーを作成する
    if user =  login_from(provider)
      user.authentication.update!(
        access_token: access_token.token,
        access_token_secret: access_token.secret
      )
    else
      create_user_from(provider)
    end

モデルの修正

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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をmoduleに切り出す

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
require 'twitter'

module TwitterAPIClient
  def client(user = nil)
    @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

使用するクラスでinclude TwitterAPIClientでモジュールをincludeすれば、client(user)Twitter::REST::Clientのインスタンスを呼び出すことができる。
コードとして書くときは、client(user).update("I'm tweeting with @gem!")のように使う。

Share on

END
END
@aiandrox

 
目次