When using the Twitter API, you can use your own (registered account) access_token, access_token_secret ↓ to get public user information and tweets. 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.
Ruby 2.6.6 Rails 5.2.4.3 Sorcery 0.15.0
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')
.
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
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.
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 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