Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OmniAuth with Locale #2813

Closed
kot-begemot opened this issue Dec 27, 2013 · 4 comments
Closed

OmniAuth with Locale #2813

kot-begemot opened this issue Dec 27, 2013 · 4 comments

Comments

@kot-begemot
Copy link

Hey. I am developing rails-localization gem. Everything went pretty well, until I tried to use Devise + OmniAuth inside localized scope.

tldr

The solution I found is to use devise_for :user two times inside routes.rb

MyApp::Application.routes.draw do
  devise_for :users, skip: [:session, :password, :registration, :confirmation], :controllers => {
    omniauth_callbacks: "users/omniauth_callbacks"
  }

  localized({...}) do
    devise_for :users, path: '', skip: [:omniauth_callbacks]
  end
end

But I am not sure that this is a good practice and I would like to hear a better option.

The problem

Point is, this scope yields content like that

scope("(:locale)", ...) { yield }

All other URL are happy with that, but not if you use omniauth with devise_for

localized({"en" => "English", "ru" => "Русский"}) do
  devise_for :users, path: '', :controllers => {
    omniauth_callbacks: "users/omniauth_callbacks"
  }
end

Then, Rails will fail on initialization with that error.
So far so good. I tried to implement it like that

localized({"en" => "English", "ru" => "Русский"}) do
  devise_for :users, path: '', skip: [:omniauth_callbacks]
end

match "/users/auth/:provider",
  :constraints => { :provider => /facebook|vkontakte/ },
  :to => "users/omniauth_callbacks#passthru",
  :as => :user_omniauth_authorize,
  :via => [:get, :post]

match "/users/auth/:action/callback",
  :constraints => { :action => /facebook|vkontakte/ },
  :to => "users/omniauth_callbacks",
  :as => :user_omniauth_callback,
  :via => [:get, :post]

It failed with that error.
Next attempt was that one

localized({"en" => "English", "ru" => "Русский"}) do
  devise_for :users, path: '', skip: [:omniauth_callbacks]
end

devise_scope :user do
  match "/users/auth/:provider",
    :constraints => { :provider => /facebook|vkontakte/ },
    :to => "users/omniauth_callbacks#passthru",
    :as => :user_omniauth_authorize,
    :via => [:get, :post]

  match "/users/auth/:action/callback",
    :constraints => { :action => /facebook|vkontakte/ },
    :to => "users/omniauth_callbacks",
    :as => :user_omniauth_callback,
    :via => [:get, :post]
end

And test failed =(. Here is its output

1) Authentication click login with facebook
   Failure/Error: page.should have_content('Successfully authenticated from Facebook account.')
     expected to find text "Successfully authenticated from Facebook account." in "Not found. Authentication passthru."
   # ./spec/features/registration_spec.rb:37:in `block (3 levels) in <top (required)>'

And here is test file content

# encoding: utf-8
require "spec_helper"

feature 'Authentication' do
  background do
    visit root_path
    OmniAuth.config.test_mode = true
    OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new(OAuthHelper.facebook)
  end

  scenario 'click login with facebook' do
    visit "/users/auth/facebook"

    page.should have_content('Successfully authenticated from Facebook account.')
  end
end

Just to mention, that this test IS PASSING if I am defining devise_for outside of localized scope

localized({...}) do; ....; end

devise_for :users, path: '', :controllers => {
  omniauth_callbacks: "users/omniauth_callbacks"
}

Some of my playing arounds

I thought that root of that Not found. Authentication passthru. problem is that Devise.mappings[:user] is missing those things

#<Devise::Mapping:0x000000072959d0 
  ...
  @controllers=
    {...
     :omniauth_callbacks=>"users/omniauth_callbacks"},
  ...
  @used_routes=[..., :omniauth_callback]>

And the black magic, somewhere inside Devise::OmniauthCallbacksController is checking for those things, so I tried to insert those things into Mapping instance manually

def append_omniauth_to_devise(scope)
  mapping = Devise.mappings[scope]
  mapping.controllers.merge!({omniauth_callbacks: "users/omniauth_callbacks"})
  mapping.used_routes << :omniauth_callback
end

MyApp::Application.routes.draw do
  localized({...}) do
    devise_for :users, path: '', skip: [:omniauth_callbacks]
  end

  append_omniauth_to_devise(:user)
  devise_scope :user do
    match "/users/auth/:provider",
      :constraints => { :provider => /facebook|vkontakte/ },
      :to => "users/omniauth_callbacks#passthru",
      :as => :user_omniauth_authorize,
      :via => [:get, :post]

    match "/users/auth/:action/callback",
      :constraints => { :action => /facebook|vkontakte/ },
      :to => "users/omniauth_callbacks",
      :as => :user_omniauth_callback,
      :via => [:get, :post]
  end
end

And the did not worked out =(

@josevalim
Copy link
Contributor

The solution you found is correct. The thing is that omniauth does not support dynamic paths, so you can't have locale in it. Calling Devise twice or defining the omniauth routes directly is the way to go. :)

@gurix
Copy link

gurix commented Aug 21, 2014

@kot-begemot It's a while ago you came up with this problem now I was facing too. I think I found an elegant solution for this.

First we need a controller inside the locale scope that sets us the current locale in the session.
routes.rb:

Rails.application.routes.draw do
  # We need to define devise_for just omniauth_callbacks:uth_callbacks otherwise it does not work with scoped locales
  # see https://github.com/plataformatec/devise/issues/2813
  devise_for :users, skip: [:session, :password, :registration, :confirmation], controllers: { omniauth_callbacks: 'omniauth_callbacks' }

  scope '(:locale)' do
    # We define here a route inside the locale thats just saves the current locale in the session
    get 'omniauth/:provider' => 'omniauth#localized', as: :localized_omniauth

    devise_for :users, skip: :omniauth_callbacks, controllers: { passwords: 'passwords', registrations: 'registrations' }
  end
end

omniauth_controller.rb:

class OmniauthController < ApplicationController
  def localized
    # Just save the current locale in the session and redirect to the unscoped path as before
    session[:omniauth_login_locale] = I18n.locale
    redirect_to user_omniauth_authorize_path(params[:provider])
  end
end

Now we can force the locle inside the omniauth_callbacks_controller.rb:

class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def twitter
    handle_redirect('devise.twitter_uid', 'Twitter')
  end

  def facebook
    handle_redirect('devise.facebook_data', 'Facebook')
  end

  private

  def handle_redirect(_session_variable, kind)
    # here we force the locale to the session locale so it siwtches to the correct locale
    I18n.locale = session[:omniauth_login_locale] || I18n.default_locale
    sign_in_and_redirect user, event: :authentication
    set_flash_message(:notice, :success, kind: kind) if is_navigational_format?
  end

  def user
    User.find_for_oauth(env['omniauth.auth'], current_user)
  end
end

That's all, everytime you login via link_to t('.sign_up_with_twitter'), localized_omniauth_path(:twitter), or whatever, it forces the intended locale.

Also as gist https://gist.github.com/gurix/4ed589b5551661c1536a

@maxcal
Copy link

maxcal commented Sep 5, 2014

@jefvlamings
Copy link

jefvlamings commented Oct 25, 2017

@gurix

I used your solution for a while, but seem to bump into one issue lately.
When authentication via OmniAuth fails, the wrong controller is chosen.
This only occurs if you have your devise routes namespaced and localized.

Apparently you cannot call devise_for twice in the routes file, without side effects.
Only the routes of the last devise_for statement are added to the Devise mapping.
When Devise (in lib/devise/omniauth.rb) tries to match the failure redirect with the right controller, the namespaced controller is not picked up.

A solution to this problem is to redeclare the omniauth controller in the second devise_for statement.

If you want to namespace all controllers under /user, you should declare the routes like this:

Rails.application.routes.draw do
  devise_for :users, skip: [:sessions, :registrations, :passwords, :unlocks, :confirmations], controllers:{
      omniauth_callbacks: 'users/omniauth_callbacks'
  }
  localized do
    devise_for :users, skip: :omniauth_callbacks, controllers: {
        sessions: 'users/sessions',
        registrations: 'users/registrations',
        passwords: 'users/passwords',
        unlocks: 'users/unlocks',
        confirmations: 'users/confirmations',
        omniauth_callbacks: 'users/omniauth_callbacks'
    }
  end 
end

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

No branches or pull requests

5 participants