I followed the great Ryan Bates to help me setup ominauth-facebook, but in his tutorial he only shows how to set it up with short-lived access tokens (hour or 2). How can we revise his tutorial to set it up with a long-lived access token (60 days)?
I've been reading over the Facebook docs about it and this older SO question.
Both links suggest adding some code like this (although the code varies slightly for each):
GET /oauth/access_token?
grant_type=fb_exchange_token&
client_id={app-id}&
client_secret={app-secret}&
fb_exchange_token={short-lived-token}
Where would one add this to?
From what I read it seems like they are making this awfully more complicated than it should be. Can someone put this in beginner friendly language?
My code varies slightly from Mr. Bates.
initializers/ominauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :facebook, "1540352575225959", "ee957abf5e851c98574cdfaebb1355f4", {:scope => 'user_about_me'}
end
routes.rb
get 'auth/:provider/callback', to: 'sessions#facebook'
sessions_controller
def facebook
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
user.rb
class User < ActiveRecord::Base
acts_as_tagger
acts_as_taggable
has_many :notifications
has_many :activities
has_many :liked_comments, through: :comment_likes, class_name: 'Comment', source: :liked_comment
has_many :valuation_likes
has_many :habit_likes
has_many :goal_likes
has_many :stat_likes
has_many :comment_likes
has_many :authentications
has_many :habits, dependent: :destroy
has_many :levels
has_many :valuations, dependent: :destroy
has_many :comments
has_many :goals, dependent: :destroy
has_many :stats, dependent: :destroy
has_many :results, through: :stats
has_many :notes
accepts_nested_attributes_for :habits, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :notes, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :stats, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :results, :reject_if => :all_blank, :allow_destroy => true
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :passive_relationships, class_name: "Relationship",
foreign_key: "followed_id",
dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
has_many :followers, through: :passive_relationships, source: :follower
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }, format: { with: /\A[a-z\sA-Z]+\z/,
message: "only allows letters" }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }, unless: -> { from_omniauth? }
has_secure_password
validates :password, length: { minimum: 6 }
def name
read_attribute(:name).try(:titleize)
end
def count_mastered
@res = habits.reduce(0) do |count, habit|
habit.current_level == 6 ? count + 1 : count
end
end
def count_challenged
@challenged_count = habits.count - @res
end
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_initialize.tap do |user|
user.provider = auth.provider
user.image = auth.info.image
user.uid = auth.uid
user.name = auth.info.name
user.oauth_token = auth.credentials.token
user.oauth_expires_at = Time.at(auth.credentials.expires_at)
user.password = (0...8).map { (65 + rand(26)).chr }.join
user.email = (0...8).map { (65 + rand(26)).chr }.join+"@mailinator.com"
user.save!
end
end
def self.koala(auth)
access_token = auth['token']
facebook = Koala::Facebook::API.new(access_token)
facebook.get_object("me?fields=name,picture")
end
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
# Returns a random token.
def User.new_token
SecureRandom.urlsafe_base64
end
# Remembers a user in the database for use in persistent sessions.
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
# Forgets a user. NOT SURE IF I REMOVE
def forget
update_attribute(:remember_digest, nil)
end
# Returns true if the given token matches the digest.
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
# Activates an account.
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
end
# Sends activation email.
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
# Sends password reset email.
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
# Returns true if a password reset has expired.
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
def good_results_count
results.good_count
end
# Follows a user.
def follow(other_user)
active_relationships.create(followed_id: other_user.id)
end
# Unfollows a user.
def unfollow(other_user)
active_relationships.find_by(followed_id: other_user.id).destroy
end
# Returns true if the current user is following the other user.
def following?(other_user)
following.include?(other_user)
end
private
def from_omniauth?
provider && uid
end
# Converts email to all lower-case.
def downcase_email
self.email = email.downcase unless from_omniauth?
end
# Creates and assigns the activation token and digest.
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
facebook.js.coffee.erb
jQuery ->
$('body').prepend('<div id="fb-root"></div>')
$.ajax
url: "#{window.location.protocol}//connect.facebook.net/en_US/all.js"
dataType: 'script'
cache: true
window.fbAsyncInit = ->
FB.init(appId: '<%= 1540372976229929 %>', cookie: true)
$('#sign_in').click (e) ->
e.preventDefault()
FB.login (response) ->
window.location = '/auth/facebook/callback' if response.authResponse
if $('#sign_out').length > 0
FB.getLoginStatus (response) ->
window.location = $('#sign_out').attr("href") if !response.authResponse
Source: omniauth-facebook.
"Being a good programmer is 3% talent and 97% not being distracted by the internet."
I had the same issue like you did before with the app that I am currently building. The solution I found suggests using Koala authentication to prolong the initial short-lived token immediately after its creation.
The adaptation of solution that worked well for me is as follows...
First, include gem 'koala', '2.0.0'
in your Gemfile. Also, visit Koala github site for more info.
Now let's apply the above to user.rb's from_omniauth
method...
def self.from_omniauth(auth)
# Sets 60 day auth token
oauth = Koala::Facebook::OAuth.new("1540352575225959", "ee957abf5e851c98574cdfaebb1355f4")
new_access_info = oauth.exchange_access_token_info auth.credentials.token
new_access_token = new_access_info["access_token"]
new_access_expires_at = DateTime.now + new_access_info["expires"].to_i.seconds
where(provider: auth.provider, uid: auth.uid).first_or_initialize.tap do |user|
user.provider = auth.provider
user.image = auth.info.image
user.uid = auth.uid
user.name = auth.info.name
user.oauth_token = new_access_token # auth.credentials.token <- your old token. Not needed anymore.
user.oauth_expires_at = Time.at(auth.credentials.expires_at)
user.password = (0...8).map { (65 + rand(26)).chr }.join
user.email = (0...8).map { (65 + rand(26)).chr }.join+"@mailinator.com"
user.save!
end
end
I have also noticed that you are generating some dummy values for User's email and password. May I suggest you rather use SecureRandom library for doing so. Whilst the possibility that your code is very unlikely to result in a collision of values, at least for the email I suggest changing (0...8).map { (65 + rand(26)).chr }.join+"@mailinator.com"
to a more stable SecureRandom.hex + "@mailinator.com"
. This is especially important if you are using emails as the way to log users in the application.
I hope this resolves your issue. All best, Tim.
I am glad that we managed to answer your question and solve all your other issues together, however here is an update in regards to refactoring of code I was talking about earlier...
Consider a situation, whereby a user, that logged in through Facebook, changes some data about themselves on your website. Let's say they change their name, email or an avatar. This will work fine until the same user tries to log back in again. What will happen is that from_omniauth method will get triggered again and override those changes. This is bad and the way to prevent this is by doing the following...
def self.from_omniauth(auth)
.
.
.
where(provider: auth.provider, uid: auth.uid).first_or_initialize.tap do |user|
user.provider = auth.provider
user.image = auth.info.image unless user.image != nil
user.uid = auth.uid
user.name = auth.info.name unless user.name != nil
user.oauth_token = new_access_token
user.oauth_expires_at = Time.at(auth.credentials.expires_at)
user.password = SecureRandom.urlsafe_base64 unless user.password != nil
user.email = SecureRandom.hex + "@mailinator.com" unless user.email != nil
user.activated = true
user.save!
end
The key is the use of unless user.image != nil
which ensures that your image is only going to be set if the initial value of :image
is nil
. If say the user changes the image, the value won't be nil
and from_omniauth won't change it. The same goes for name, email and password. Don't set it to anything else.
Also, note that I used SecureRandom.urlsafe_base64
instead of your (0...8).map { (65 + rand(26)).chr }.join
. This is because urlsafe_base64
generates a random URL-safe base64 string, like i0XQ-7gglIsHGV2_BNPrdQ==
which is perfect for a dummy password and looks elegant too.
On the other hand, the reason I used hex
for emails is because it creates a random hexadecimal string, like eb693ec8252cd630102fd0d0fb7c3485
, which will not upset your validations, if you happen to have some regex to validate emails (which ideally you should).
You can use the exchange_access_token_info
method from your instance of Koala::Facebook::OAuth
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With