Skip to content

Instantly share code, notes, and snippets.

@fadhlirahim
Last active September 14, 2016 06:34
Show Gist options
  • Select an option

  • Save fadhlirahim/221ad986f856dd60efd42e2bc27d7d82 to your computer and use it in GitHub Desktop.

Select an option

Save fadhlirahim/221ad986f856dd60efd42e2bc27d7d82 to your computer and use it in GitHub Desktop.
A generic solution for facebook graph user integration with rails with the assumption that client already secured a Facebook Access Token. Not a login implementation
require 'date'
require 'net/http'
require 'active_support/concern'
# Module for verifying/creating users from authenticated facebook_token
#
# This will make a call to FB api and fetch the details of your user and store in your users database table.
#
# Ideal in use cases where you're clients are logged in/signed up using Facebook and you want to store user details and let your
# backend make the server-to-server call
#
# Assumptions:
#
# You already have a facebook app with permissions scoped to email & basic_profile
#
# Assumption for your users table:
#
# create_table "users", force: :cascade do |t|
# t.string "username"
# t.string "email", null: false
# t.string "first_name"
# t.string "last_name"
# t.string "encrypted_password", default: "", null: false
# t.jsonb "identity", default: {}, null: false
# end
#
# Example usage :
#
# class User < ActiveRecord::Base
# include Facebook
#
# def authenticate_with_fb(facebook_token)
# return if facebook_token.blank?
# User.find_by_facebook(facebook_token)
# end
# end
#
module Facebook
extend ActiveSupport::Concern
FB_DOMAIN = "graph.facebook.com".freeze
FB_ME_URL = "/me?fields=email,first_name,last_name,picture.type(large)&access_token=".freeze
class FbError < RuntimeError
end
module ClassMethods
def find_by_facebook_token(facebook_token)
return nil if facebook_token.blank?
http = Net::HTTP.new(FB_DOMAIN, 443)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # this is not safe, change it to peer verified to ensure secured
res = http.get("#{FB_ME_URL}#{facebook_token}")
return nil if res.code != "200"
data = JSON.parse(res.body)
user = self.find_by_facebook_id(data['id'])
return user if user.present?
user = self.link_to_facebook(data['id'], data['email'])
return user if user.present?
self.create_from_facebook(data)
end
def find_by_facebook_id(facebook_id)
User.where('identity @> ?', { facebook: facebook_id}.to_json).first
end
def link_to_facebook(facebook_id, email)
u = User.where('email = ?', email).first
return if u.nil?
user_fb_id = u.identity["facebook"]
raise Facebook::FbError if !user_fb_id.nil? && (user_fb_id != facebook_id)
u.identity["facebook"] = facebook_id
u.save
u
end
def create_from_facebook(facebook_data)
User.create!(extract_facebook_data(facebook_data))
end
private
def extract_facebook_data(facebook_data)
profile_pic = facebook_data.fetch('picture', {}).fetch('data', {}).fetch('url', nil)
{
first_name: facebook_data['first_name'],
last_name: facebook_data['last_name'],
email: facebook_data['email'],
identity: { "facebook" => facebook_data['id'], "profile_pic" => profile_pic},
password: Devise.friendly_token[0,20] # assumption is using devise gem, so this needs to be filled in
}
end
end
end
require 'rails_helper'
require 'webmock/rspec'
describe Facebook do
let(:test_klass) { Class.new{include Facebook} }
describe "find_by_facebook_token" do
let (:valid_facebook_response) { {:body => {id: 1, email: 'arya@stark.gov', first_name: "Arya",
last_name: "Stark", picture: { data: { url: "/path/to/photo.png"}}}.to_json} }
it "should work when there is a user with the facebook id" do
stub_request(:get, "https://#{Facebook::FB_DOMAIN}#{Facebook::FB_ME_URL}311").to_return(valid_facebook_response)
expect(test_klass).to receive(:find_by_facebook_id).with(1) { 'user' }
expect(test_klass.find_by_facebook_token(311)).to eq 'user'
end
it "doesn't return a user if facebook token is invalid" do
stub_request(:get, "https://#{Facebook::FB_DOMAIN}#{Facebook::FB_ME_URL}311").to_return(:status => "400")
expect(test_klass.find_by_facebook_token(311)).to be_nil
end
it "should link user if emails match but it has no facebook_id" do
user = create(:user)
stub_request(:get, "https://#{Facebook::FB_DOMAIN}#{Facebook::FB_ME_URL}311").to_return(valid_facebook_response)
expect(test_klass).to receive(:find_by_facebook_id).with(1) { nil }
expect(test_klass).to receive(:link_to_facebook).with(1, 'arya@stark.gov') { 'user' }
expect(test_klass.find_by_facebook_token(311)).to eq 'user'
end
it "should create a new user if neither email or facebook_id are found" do
stub_request(:get, "https://#{Facebook::FB_DOMAIN}#{Facebook::FB_ME_URL}311").to_return(valid_facebook_response)
expect(test_klass).to receive(:find_by_facebook_id) { nil }
expect(test_klass).to receive(:link_to_facebook) { nil }
expect(test_klass).to receive(:create_from_facebook).with({"id" => 1, "email" => 'arya@stark.gov',
'first_name' => 'Arya', 'last_name' => 'Stark',
'picture' => { 'data' => { 'url' => "/path/to/photo.png"}}}) { 'user' }
expect(test_klass.find_by_facebook_token(311)).to eq 'user'
end
end
describe "find_by_facebook_id" do
it "should return the user when found" do
expect(User).to receive(:where).with("identity @> ?", {facebook: 'rand1235'}.to_json) { ["user"] }
expect(test_klass.find_by_facebook_id('rand1235')).to eq "user"
end
it "should return nil when not found" do
expect(User).to receive(:where).with("identity @> ?", {facebook: 'rand1235'}.to_json) { [] }
expect(test_klass.find_by_facebook_id("rand1235")).to be_nil
end
end
describe "link_to_facebook" do
it "should link the user to facebook when emails coincide" do
user = create(:user, email: 'ned@stark.gov')
expect(User).to receive(:where).with('email = ?', 'ned@stark.gov') { [user] }
expect(user.identity["facebook"]).to be_nil
expect(test_klass.link_to_facebook('rand1235', 'ned@stark.gov')).to eq user
expect(user.identity["facebook"]).to eq 'rand1235'
end
it "should raise exception if the email is found but facebook_id is different" do
user = create(:user, email: 'ned@stark.gov', identity: { "facebook" => "456fb" })
# User.should_receive(:where) { [user] }
expect(User).to receive(:where) { [user]}
expect { test_klass.link_to_facebook('rand1235', 'ned@stark.gov') }.to raise_error(test_klass::FbError)
expect(user.identity["facebook"]).to eq "456fb"
end
it "should return nil if the email is not found" do
expect(User).to receive(:where).with("email = ?", "an_email") { [] }
expect(test_klass.link_to_facebook('rand1235', "an_email")).to be_nil
end
end
describe "create_from_facebook" do
it "should create a new user with the facebook information" do
facebook_data = {'id' => '1234567890',
'name' => 'John Snow', 'first_name' => 'John', 'last_name' => 'Snow',
'username' => 'leto', 'email' => 'johnsnow@stark.gov',
'timezone' => 8, 'locale' => "en_US", "picture" => { "data" => { "url" => "/path/to/photo.png"}}}
test_klass.create_from_facebook(facebook_data)
u = User.last
expect(u.first_name).to eq 'John'
expect(u.last_name).to eq 'Snow'
expect(u.email).to eq 'johnsnow@stark.gov'
expect(u.identity["facebook"]).to eq '1234567890'
expect(u.identity["profile_pic"]).to eq '/path/to/photo.png'
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment