CORS Issue with Rails 4, omniauth-facebook, rack-cors, Heroku

2.1k views Asked by At

Summary:

I have a CORS problem. I'm trying to authenticate with Facebook from a React app with a Rails API.

My app runs on localhost:8080 and my api runs on Heroku.

I'm able to login to facebook and create a session on callback, but the cookie set by facebook isn't picked up by omniauth. I use omniauth-facebook client-side authentication.

I think that the problem is somewhere in my configuration of rack-cors. It works when I run my api on localhost:3000.

Earlier, I received the error csrf-detected, but this was resolved by adding provider_ignores_state: true to omniauth configuration. Later, I found that xhrFields: { withCredentials: true } was required for cors calls to pass cookies.

Finale solution: At the end, I resolve this issue by deploying my app to AWS 53 with domain example.com and create CNAMES 'api.example.com` to my api on Heroku.

It takes a day to get CNAMES resolved. You can test your setup in terminal with commands host example.com and host api.example.com.

Alternative solution is to use nginx as proxy.

Versions:

Rails 4.2.2
Omniauth-facebook 3.0.0
Omniauth-oauth2 1.4.0
Rack-cors 0.4.0

Issue:

Getting error: no_authorization_code.

Routes:

  namespace :api do
    match "/auth/:provider/callback", to: "sessions#create", via: [:get, :post]
    get "/auth/failure", to: "sessions#failure"
    get "/logout", to: "sessions#destroy"

Application.rb:

config.middleware.insert_before 0, "Rack::Cors" do
  allow do
    origins "localhost:8000", "example.com"
    resource "*",
      :headers => :any,
      :methods => [:get, :post, :delete, :put, :options],
      :max_age => 1728000
  end
end

Omniauth.rb:

OmniAuth.config.path_prefix = "/api/auth"

OmniAuth.config.logger = Rails.logger

OmniAuth.config.on_failure = Proc.new { |env|
  OmniAuth::FailureEndpoint.new(env).redirect_to_failure
}

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook,
    Rails.application.secrets.facebook_key,
    Rails.application.secrets.facebook_secret, {
      provider_ignores_state: true,
      client_options: {
        :ssl => { :ca_file => "/usr/lib/ssl/certs/ca-certificates.crt" }
      }
    }
  provider :identity,
    fields: [:email],
    on_failed_registration: lambda { |env|
      Api::IdentitiesController.action(:new).call(env)
    }
end

Secret.yml:

development:
facebook_key: <%= ENV["FACEBOOK_KEY"] %>
facebook_secret: <%= ENV["FACEBOOK_SECRET"] %>

Client:

$.ajax({
  url: http://localhost:3000/api/auth/facebook,
  dataType: "json",
  type: "GET",
  xhrFields: { withCredentials: true },
  success: function(data) {
  }.bind(this),
  error: function(xhr, status, err) {
  }.bind(this)
})

SessionsController:

module Api
  class SessionsController < ApplicationController
    skip_before_action :verify_authenticity_token
    skip_before_action :restrict_access
    skip_after_action :verify_authorized

def create
  user = User.from_omniauth(auth_params)
  session[:user_id] = user.id
  render json: user, status: :ok
end

def destroy
  session[:user_id] = nil
  render json: {}, status: :ok
end

def failure
  render json: { errors: params[:message] }, status: :unauthorized
end

private

def auth_params
  request.env.fetch("omniauth.auth")
end end end

ApplicationsController:

class ApplicationController < ActionController::Base
  include Pundit
  before_action :restrict_access
  after_action :verify_authorized, :except => :index
  after_action :verify_policy_scoped, :only => :index

    respond_to :json

    protected

    attr_reader :current_user

    # Allows access to current_user in serializators.
    serialization_scope :current_user

    def restrict_access
      authenticate_or_request_with_http_token do |token, options|
      @current_user = User.find_by(token: token)
    end
  end
end
3

There are 3 answers

0
Dan On

I found out that my Ajax call need to include xhrFields: { withCredentials: true } in order to pass cookie to omniauth.

This is because that it is a cross-domain call.

See http://api.jquery.com/jquery.ajax/ for more details.

0
VestigoDan On

I was running into this same issue while using Devise to do omniauth in a rails app. Ultimately, the problem for me was that Facebook updated their API at the beginning of July, and it now requires you to specifically ask for fields in the info_fields param.

I would try changing your omniauth.rb to include:

provider :facebook,
 Rails.application.secrets.facebook_key,
 Rails.application.secrets.facebook_secret, {
   :scope => "email, user_birthday",
   :info_fields => "email, user_birthday", #<== this made the difference for me
   provider_ignores_state: true
 }

Here's a link from the devise wiki that mentions the issue. I know you're not using devise but it may be helpful. https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview

1
Tyler Gannon On

Maybe the CORS middleware isn't working for you. I don't have experience with it, but maybe just add a before action in your application controller, and do the following... just to make sure you know the appropriate header is there.

response.headers["Access-Control-Allow-Origin"] = "http://localhost:8080"