58

I'm using the omniauth gem with rails and it works great with loging in users, but everytime it takes you to the fb login page then redirects you back. I was wondering if there is a way to do what most pages do and show the fb login in a popup and then reload the parent div once that is complete. Any ideas?

Thanks!

Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
Danny
  • 4,724
  • 6
  • 42
  • 55
  • 9
    The confusion stems from misunderstanding the purpose of the `:display => "popup"` option for omniauth - this option is specifying to Facebook that we want the "display style" of the page to be sized/formatted for a popup window. The option does not mean "pop this up as a browser window" - the popping-up is to be performed by you, like how Chris Heald has shown. Try using Chris' answer and view the login page with and without the `:display => "popup"` option, restarting the server between each change of the option, and you will notice the difference. – Zabba Feb 10 '12 at 05:28

4 Answers4

125

Sure, you can easily.

In your view:

=link_to "Log in with Facebook", omniauth_authorize_path(:user, :facebook), :class => "popup", :"data-width" => 600, :"data-height" => 400

In your application.js:

function popupCenter(url, width, height, name) {
  var left = (screen.width/2)-(width/2);
  var top = (screen.height/2)-(height/2);
  return window.open(url, name, "menubar=no,toolbar=no,status=no,width="+width+",height="+height+",toolbar=no,left="+left+",top="+top);
}

$("a.popup").click(function(e) {
  popupCenter($(this).attr("href"), $(this).attr("data-width"), $(this).attr("data-height"), "authPopup");
  e.stopPropagation(); return false;
});

And then in your callback view:

:javascript
  if(window.opener) {
    window.opener.location.reload(true);
    window.close()
  }

This'll pop up your Facebook auth in a centered 600x400 popup, then when the user returns from authentication, the view will close the popup and refresh the parent page. It degrades gracefully if a user ctrl-clicks the link, or doesn't have Javascript enabled, too.

Chris Heald
  • 61,439
  • 10
  • 123
  • 137
  • 1
    I'm using the Railscast Omniauth method, and the callback uses 4 different redirects potentially based on various logic conditions. So I put your callback javascript in the application.js and it works assuming I won't be using anymore popups. The only thing I don't like about this approach is that after you sign in by typing your FB password, the next time you go to login, the popup still comes up briefly. I'd like to be able to only have the popup display when it require user input. Is there any way to do this? – Dex Feb 08 '11 at 21:38
  • Unfortunately, I don't know any way around the brief-popup issue. The way you'd normally do that is to do your auth cycle in the main page, so it just looks like a page reload to the user, but with the popup solution, you obviously can't do that. Regarding auth failures, I just have a special popup template that I use to render the error message for those in, so it still looks nice. – Chris Heald Feb 08 '11 at 22:45
  • I just made /auth/failure point to a separate controller action that just renders the callback javascript to close the window. – Dex Feb 08 '11 at 23:40
  • 3
    Another gotcha is that `window.opener.location.whatever` doesn't work in IE7,8. You need to put that code in the parent window and call it as a function like `window.opener.my_goto_function(myurl)` – Dex Jun 04 '11 at 03:35
  • 4
    It should also display as a popup, instead of a page. To do this, follow the instructions in http://stackoverflow.com/questions/4483598/omniauth-display-facebook-connect-as-popup – e3matheus Jun 29 '11 at 21:05
  • to also display as popup instead of page use: /auth/facebook?display=popup .Or in other words omniauth_authorize_path(:user, :facebook,:display=>"popup") – montrealmike Dec 12 '11 at 01:37
  • I was using this solution... Until I noticed that this way, the page is somehow loaded twice! So I don't get the correct flash message like "You successfully logged in using provider" but the message "You are already signed in.". Has anybody found a better way to do this? Or am I doing something wrong? I didn't understood what Chris Heald meant when he said to put that code in my callback view (where is that?) so I just put it in my application.js with the rest of his code... – Ashitaka Feb 10 '12 at 23:26
  • 2
    I agree with Ashitaka. I am losing the flash message because the popup window is getting redirected to a page that displays the flash message (so briefly the user doesn't really see it), and then it closes the popup and reloads the main page. The flash message is gone, so the user doesn't know things went well. How can we close the popup and reload the page, instead of hitting the necessary redirects? – Kevin Elliott Feb 29 '12 at 23:05
  • I finally solved this problem! It may not be the best solution, but it worked for me. See my Answer below. It's an answer that's not really specific to the OP's question but maybe it will help people that are using Devise and Omniauth and end up on this page! – Ashitaka Mar 06 '12 at 21:29
  • You missed `authorize_params: { display: 'popup' }` in `devise.rb`. – elado Feb 19 '13 at 09:27
  • This works great. But I wonder if there is a way that `popup` comes only when I do not log in Facebook already? i.e if FB is already authenticated and logged in, it should redirect to sign_in path (without popup open and close) – Venkat Ch Oct 11 '16 at 02:33
29

Ok, so there's a problem with Chris Heald's solution if you are using OmniAuth in conjunction with Devise. The problem is that when you reload the window (that is on the login page), Devise will take you to the root_path, completely ignoring the url you were trying to access and will yield the error message "You are already signed in". This makes sense because Devise protects a logged in user from accessing the login page by redirecting to the homepage. By reloading the login page immediately after logging in, you'll end up with this problem.

So my solution for someone using Devise is as follows:

# add this wherever needed in your_authentications_or_callbacks_controller.rb
sign_in user
@after_sign_in_url = after_sign_in_path_for(user)
render 'callback', :layout => false

So normally, after finding or creating a user using the hash returned by a certain provider (Facebook, Twitter, etc..), we'd call the Devise function sign_in_and_redirect. But we can't redirect just yet (remember, right now, the user is currently in a popup window) so we simply sign_in the user.

Next up, we need to pass the url the user was trying to access to the view, and we can get that url using Devise's method after_sign_in_path_for.

Finally, we need to render the view. Since we'll only use the view to call some JavaScript, there is no need to render the layout, so we turn it off to not slow us down. Here's that view:

# views/your_authentications_or_callbacks/callback.html.erb
<script type="text/javascript">
  window.opener.location = '<%= @after_sign_in_url %>';
  window.close();
</script>

This way the user is redirected to the proper url after signing in and the correct flash message is displayed.

With JavaScript disabled

After some testing I realised that this solution wasn't allowing authentication without JavaScript so I'd like to make an addendum.

function popupCenter(linkUrl, width, height, name) {
    var separator = (linkUrl.indexOf('?') !== -1) ? '&' : '?',
        url = linkUrl + separator + 'popup=true',
        left = (screen.width - width) / 2,
        top = (screen.height - height) / 2,
        windowFeatures = 'menubar=no,toolbar=no,status=no,width=' + width +
            ',height=' + height + ',left=' + left + ',top=' + top;
    return window.open(url, name, windowFeatures);
}

The change here is adding a simple parameter called popup to the url using JavaScript. OmniAuth will be kind enough to store any query params added to the request url. So finally we check for that param in the controller. If it exists, it's because JavaScript is enabled:

if request.env['omniauth.params']['popup']
  render 'callback', :layout => false
else
  redirect_to @after_sign_in_url
end

Also, don't forget to do the same for your failure action, which is called when the user does not accept logging in.

I could not have done this without Chris Heald's solution, so.. Thank you so much!

Ashitaka
  • 19,028
  • 6
  • 54
  • 69
  • This is a great and simple way to handle popup login, but I wonder how you can in this case use an 'adapted facebook login form'. Because the login form will display in a really ugly way – Codii Jan 16 '13 at 10:46
  • What do you mean ugly way? It's a facebook popup. You can customize it a bit (add an image, change the heading) but that's it. You should not change it because otherwise people won't recognize the popup as belonging to Facebook. Like Chris Heald said in another comment here, "An integral part of delegated authentication is the trust conferred by the authenticating site not being a part of the authentication process." By changing the form, you'll undermine that trust. – Ashitaka Jan 16 '13 at 11:45
  • Yes I totally agree, but when I load the facebook signin page in the popup, it doesn't fit in it.. Maybe there are some arguments to send to have the adapted and responsive login form that would fit in my popup ? – Codii Jan 16 '13 at 14:41
  • Take a closer look at Chris Heald's answer. Look at his `link_to`. He is adding some data params to his link. If you use Rails' new hash syntax, you can make it look even better. Like this: `data: {width: '640', height: '330'}` – Ashitaka Jan 16 '13 at 16:37
  • Thanks for your answer, that was not the problem. I just had to add the option `:display => 'popup'` in my devise initializer. I'm not using `link_to` helper. – Codii Jan 16 '13 at 17:03
  • Do you really consider that it *should* work with js disabled nowadays? I know if it *can* why not, but if someone wants to implement a popup version, it is probably to apply more JS logic afterwards, isn't it? – Augustin Riedinger Jul 13 '15 at 15:22
  • Not necessarily. For example, in this case, I only use JS to open a popup with a different page inside. I could just as easily redirect to that page. However, I use JS because it makes the experience more seamless. But there are users with NoScript, users on slow connections whose JS has yet to load... So I believe progressive enhancement is still important. – Ashitaka Jul 31 '15 at 23:06
4

Posting in case it helps others. I was using Chris Heald's answer but ran into trouble with the final bit of javascript closing any new window links. For example, if I posted a link to my site onto Facebook, when users clicked the link the new window would automatically close in Chrome because the condition only checks for "if(window.opener)"

I ended up solving this with the use of a global variable (popupValue). There may be more elegant solutions but thought I'd share in case others hit the same issue:

function popupCenter(url, width, height, name) {
 var left = (screen.width/2)-(width/2);
 var top = (screen.height/2)-(height/2);
 popupValue = "on";
 return window.open(url, name, "menubar=no,toolbar=no,status=no,width="+width+",height="+height+",toolbar=no,left="+left+",top="+top     );
}

$(document).ready(function(){
$("a.popup").click(function(e) {
popupCenter($(this).attr("href"), $(this).attr("data-width"), $(this).attr("data-height"), "authPopup");
e.stopPropagation(); return false;
});


if(window.opener && window.opener.popupValue === 'on') {
 delete window.opener.popupValue;
 window.opener.location.reload(true);
 window.close()
}
});
pejmanjohn
  • 1,057
  • 3
  • 12
  • 26
  • delete window.opener.popupValue; line doesn't work in IE9 for some reason - I get "object doesn't support this action". Works fine in Chrome and FF. – Alexander Savin Oct 31 '12 at 09:16
3

I wound up using Facebook's JS SDK since it's easier.

# In facebook.js.coffee
jQuery ->
  $('body').prepend('<div id="fb-root"></div>')

  $.ajax
    url: "#{window.location.protocol}//connect.facebook.net/en_US/all.js"
    dataType: 'script'
    cache: true

window.fbAsyncInit = ->
  FB.init(appId: 'YOUR-APP-ID', cookie: true)

  $('#sign_in').click (e) ->
    e.preventDefault()
    FB.login (response) ->
      window.location = '/auth/facebook/callback' if response.authResponse

  $('#sign_out').click (e) ->
    FB.getLoginStatus (response) ->
      FB.logout() if response.authResponse
    true

Then in your views:

<%= link_to "Sign in with Facebook", "/auth/facebook", id: "sign_in" %>
<%= link_to "Sign out", signout_path, id: "sign_out" %>

This is directly from Sergio Gutierrez's tip here - https://coderwall.com/p/bsfitw

Han
  • 5,374
  • 5
  • 31
  • 31