Skip to content

Instantly share code, notes, and snippets.

@diligasi
Last active April 17, 2019 04:01
Show Gist options
  • Save diligasi/5135905764fe8e326a8c0d290631c92e to your computer and use it in GitHub Desktop.
Save diligasi/5135905764fe8e326a8c0d290631c92e to your computer and use it in GitHub Desktop.
Here you have a really brief HOWTO for the needed setup to authenticate through Facebook on your Rails API using devise_token_auth gem.

Implementation steps

Step #01

First, add the devise token auth gem to your Gemfile and run bundle to install it

Gemfile
gem 'devise_token_auth'

Step #02

Create a route to receive your Facebook authentication request

config/routes.rb
namespace :api, path: '/', defaults: { format: 'json' } do
  scope module: 'v1' do
    mount_devise_token_auth_for 'Customer', at: 'auth'
    post '/auth/facebook', to: 'customers/third_party_auth#create'
  end
end

Step #03

With the route in place, add a controller to handle it's requests, the main idea is to get the received token and asks Facebook if it is valid or not (if it is a Facebook token and if the user has granted your app Facebook access), whe these validations are all true, Facebook will return some informations that could be used to create and save a new user on your system.

app/controllers/api/v1/customers/third_party_auth.rb
module Api::V1::Customers
  class ThirdPartyAuthController < ::Api::V1::ApiController
    skip_before_action :authenticate_customer!
    
    def create
      response = verify_fb_access_token(params[:token])
      case response.code
      when '200'
        handle_login(response.body)
      when '400'
        raise FacebookLoginError.new(response.body['error']['message'])
      end
    rescue MissingAddressError => e
      # If you are not debugging, don't send your backtrace to the
      # front end app, that may expose your system architecture
      render status: 422, json: { status: 422, error: e.message, backtrace: e.backtrace }
    end
    
    private
    
    # Get the received token, validates it on Facebook and returns it's response.
    # The values assigned to the query attribute fields are the ones Facebook will
    # return from oit's user
    def verify_fb_access_token(token)
      require 'net/http'
      source = "https://graph.facebook.com/v2.11/me?access_token=#{token}"
      source += '&fields=id,name,email,picture.type(large)'
      resp = Net::HTTP.get_response(URI.parse(source))
      FacebookResponse.new resp.code, JSON.parse(resp.body)
    end
    
    # Create a new user or signs in an existent one according to Facebook's response
    def handle_login(response)
      @resource = Customer.first_or_create_facebook_user(response, addresses_params)
      create_client_id_and_token

      # Sign in the user using Devise's sign_in method
      sign_in(:user, @resource, store: false, bypass: false)
    end
    
    # Create the necessary tokens for user's authentication
    def create_client_id_and_token
      @client_id = SecureRandom.urlsafe_base64(nil, false)
      @token     = SecureRandom.urlsafe_base64(nil, false)
      @resource.tokens[@client_id] = {
        token: BCrypt::Password.create(@token),
        expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i
      }
      @resource.save
    end
    
    # An small class that organizes Facebook response
    class FacebookResponse
      attr_accessor :code, :body

      def initialize(code, body)
        @code = code
        @body = body
      end
    end
    
    # An optional class used to handle Facebook token auth requests
    class FacebookLoginError < Error; end
  end
end

Step #04

Add first_or_create_facebook_user method to your model Customer

app/models/customer.rb
class Customer < ApplicationRecord
  devise :database_authenticatable, :registerable, :recoverable,
         :rememberable, :trackable, :validatable # , :confirmable
  include DeviseTokenAuth::Concerns::User
  
  def self.first_or_create_facebook_user(user, addresses_attributes = nil)
    # Normalize Facebook email as it's an optional paramenter and may not be sent
    fb_email = user['email'] || "#{user['name'].parameterize}@facebook.com"

    # Find Customer by facebok_id or email
    customer = find_by(provider_id: user['id'])
    customer ||= find_by(email: fb_email)

    if customer.present?
      # If the user already has an account, we update him
      # to be a Facebook user and keep going with the request.
      if customer.provider_id.blank?
        customer.provider = 'facebook'
        customer.provider_id = user['id']
        customer.save
      end
    else
      user_data = {
        name: user['name'],
        email: fb_email,
        provider: 'facebook',
        provider_id: user['id'],
        password: SecureRandom.urlsafe_base64(nil, false),
        image: user['picture']['data']['url']
      }
      user_data[:uid] = user_data[:email]
      user_data[:password_confirmation] = user_data[:password]

      customer = create(user_data)
    end

    customer
  end
end

Step #05

Remember that you will need to create a Facebook application to your app inside Facebook's developers console. Your front end app still needs to make some configurations that may varies according to the language and/or plataform used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment