Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Omniauth-Facebook: How to get Long-Lived Access Tokens?

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?

Let's get Personal.

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."

like image 710
AnthonyGalli.com Avatar asked Dec 19 '22 01:12

AnthonyGalli.com


2 Answers

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

Additional info:

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.

UPDATE:

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).

like image 142
Timur Mamedov Avatar answered Dec 27 '22 03:12

Timur Mamedov


You can use the exchange_access_token_info method from your instance of Koala::Facebook::OAuth

like image 20
Benoit Marilleau Avatar answered Dec 27 '22 05:12

Benoit Marilleau