3

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
Dan
  • 781
  • 1
  • 7
  • 16
  • You probably need `skip_before_action :verify_authenticity_token` in your API controller. – TuteC Jun 24 '15 at 18:17
  • Hi Tute, I added my SessionsController and ApplicationController to question above. I changed my ApplicationController as you recommended. However this didn't resolve the CSRF issue. – Dan Jun 24 '15 at 19:04
  • This may be helpful http://stackoverflow.com/questions/11597130/omniauth-facebook-keeps-reporting-invalid-credentials – Ravindra Jun 24 '15 at 20:29
  • @Ravindra, unfortunately not, I'm not able to login to facebook and google, but I'm getting this message: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. – Dan Jun 25 '15 at 20:18
  • Can you post the requests and response headers and body for both the Facebook login request and the resulting call to your API? Despite your configuration, the error seems to be complaining that the header is not there so you should ensure that it is. – Tyler Gannon Jun 26 '15 at 23:45

3 Answers3

2

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.

Dan
  • 781
  • 1
  • 7
  • 16
0

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"
Tyler Gannon
  • 872
  • 6
  • 19
  • thanks. I'll give it a try. In the meantime, I decided to implement some of the other features. – Dan Jul 10 '15 at 14:22
0

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