[RUBY] [Rails] Get access_token at the time of Twitter authentication with Sorcery and save it in DB

What is this article

When using the Twitter API, you can use your own (registered account) access_token, access_token_secret ↓ to get public user information and tweets. スクリーンショット 2020-10-05 8.23.35.png However, you must use a unique access_token and access_token_secret for each user in order to get information about private users and for users to tweet through the app. In this article, I will write an implementation that stores and uses the access_token and access_token_secret of individual users in the database.

It is assumed that [Rails] Twitter authentication with Sorcery -Qiita has implemented Twitter authentication.

【Caution】 This article assumes that there are no users at the time of implementation. If you already have a user (if you can't reset the User data), that user's access_token will be null, so you'll need to add a bit of annoying conditional branching.

environment

Ruby 2.6.6 Rails 5.2.4.3 Sorcery 0.15.0

Modifying model associations

In my case, external authentication was supposed to be Twitter only, so I changed the association to has_one as follows.

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

If you are working with other services and user has_many: authentications, replace the description such as ʻuser.authentication.hoge with ʻuser.authentications.find_by! (Provider:'twitter').

Creating a migration file

Add the access_token and access_token_secret columns to the authentications table. If you are using a link other than Twitter, remove the optional 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

Add controller

What has changed from the previous article is the inside of the create_user_from method.

app/controllers/api/v1/oauths_controller.rb


class OauthsController < ApplicationController
  skip_before_action :require_login # applications_before with controller_action :require_If login is set

  def oauth
    login_at(auth_params[:provider])
  end

  def callback
    provider = auth_params[:provider]
    if auth_params[:denied].present?
      redirect_to root_path, notice: "I canceled my login"
      return
    end
    #Create a new user if you cannot log in with the credentials sent (if there is no such user)
    create_user_from(provider) unless (@user = login_from(provider))
    redirect_to root_path, notice: "#{provider.titleize}I logged in with"
  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 is a method of Sorcery. Put the data passed from provider (: twitter) to Sorcery as attributes of User instance (@user).

② Create an authentication instance. Since @user_hash, ʻaccess_tokencontains the data received from Twitter, use it. By the way,build_authentication is a method of has_one, so in the case of has_many, please set it to ʻuser.authentications.build.

[11] pry(#<OauthsController>)> @user_hash
=> {:token=>"111111111111111111111",
 :user_info=>
  {"id"=>1048451188209770497,
   "id_str"=>"1048451188209770497",
   "name"=>"END",
   "screen_name"=>"aiandrox",
   "location"=>"Okayama all the time → Yamanashi a little → Tokyo Imakoko",
   "description"=>"A person who recently became an engineer, working as an elementary school teacher or a Nakai. Enjoy solving the mystery.#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,
    ...

③ Save each associated authentication. Authentication instances created by @ user.build_authentication, @ user.authentications.build, etc. are saved together with the User when it is saved.

Model modification

It is dangerous to save access_token and access_token_secret as they are in the database, so save them encrypted.

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

Cut out Twitter :: REST :: Client to service class

Cut out the logic related to the Twitter client to the service class. If user is included in the argument, use user's access_token etc., and if user is not passed, use default access_token etc.

app/services/twitter_api_client.rb


require 'twitter'

class ApiClient
  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     = 'premium'
      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

You can call an instance of Client with TwitterApiClient.call (user). When writing as code, use something like TwitterApiClient.call (user) .update ("I'm tweeting with @gem! ").

Recommended Posts

[Rails] Get access_token at the time of Twitter authentication with Sorcery and save it in DB
Get the value of enum saved in DB by Rails with attribute_before_type_cast
Get YouTube video information with Retrofit and keep it in the Android app.
Environment construction method and troubleshooter at the time of joint development (rails, docker and github)
[JavaScript] I can't get the response body at the time of error with axios (ajax)
Build a bulletin board API with authentication authorization in Rails # 12 Association of user and post
Graph the sensor information of Raspberry Pi in Java and check it with a web browser
Generate a serial number with Hibernate (JPA) TableGenerator and store it in the Id of String.
[Rails] How to get the URL of the transition source and redirect
Check the actual date and time at parse with Java's SimpleDateFormat
[Java] Get the dates of the past Monday and Sunday in order
Get the current date and time by specifying the time zone in Thymeleaf
[Rails] About Uglifier :: Error: Unexpected token: at the time of deployment
[Rails] Save start time and end time
Which class should I use to get the date and time in my Rails app (Time, DateTime, TimeWithZone)
[Java] Output the result of ffprobe -show_streams in JSON and map it to an object with Jackson
[Rails] Difference in behavior between delegate and has_many-through in the case of one-to-one-to-many
What you are doing in the confirmation at the time of gem update
Draw a bar graph and a line graph at the same time with MPAndroidChart
Email sending function with Action Mailer at the time of new registration
Specify the favorite IP of the host network with docker-compose and start it
[Rails 6] Register and log in with Devise + SNS authentication (multiple links allowed)
[Rails] How to get the user information currently logged in with devise
How to implement the email authentication function at the time of user registration
[Rails] How to solve the time lag of created_at after save method
Change the save destination of the image to S3 in the Rails app. Part 2
Get the result of POST in Java
The identity of params [: id] in rails
[Rails 6] Change redirect destination at the time of new registration / login by devise
I received the data of the journey (diary application) in Java and visualized it # 001
Java: Download the file and save it in the location selected in the dialog [Use HttpClient]
How to get the ID of a user authenticated with Firebase in Swift
[Rails] Get the path name of the URL before the transition and change the link destination
[Rails] How to get rid of flash messages in a certain amount of time