Last active
September 14, 2016 06:34
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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