I've had a lot of issues trying to get the client-side login to work, so I'm going to take the liberty of referencing a ton of other questions here... none have resulted in an answer that has worked for me.
CONTEXT
- Server-side login works fine
- Client-side login using JS SDK works fine on Safari* (have not tested in Firefox or IE or mobile non-Chrome), but not in Chrome, which is what this question is about (and majority of my users use Chrome so it's super important)
- Gem versions:
- ruby (2.1.2)
- rails (4.1.1)
- oauth (0.4.7)
- oauth2 (1.0.0)
- omniauth (1.2.2)
- omniauth-facebook (2.0.0)
- omniauth-oauth2 (1.2.0)
- This is for an app in development mode, where as the developer, I am definitely authorized to log in
*By works fine, I mean if you just copied the code from Ryan Bates' RailsCast (http://railscasts.com/episodes/360-facebook-authentication?view=asciicast) it works without any additional anything
TL:DR; I'm passing the following URL which should work...
`http://localhost:3000/auth/facebook/callback?signed_request=ASDF.JOUYOUY`
// Obviously signed_request is a much longer string
...but am still getting the error OmniAuth::Strategies::Facebook::NoAuthorizationCodeError (must pass either a
codeparameter or a signed request (via
signed_requestparameter or a
fbsr_XXXcookie)):
TROUBLESHOOTING TO DATE + OVERVIEW OF MANY OTHER ERRORS/ POTENTIAL SOLUTIONS
The code solutions below build on the code from Ryan Bates' RailsCast:
$('#sign_in').click (e) ->
e.preventDefault()
FB.login (response) ->
window.location = '/auth/facebook/callback' if response.authResponse
Hurdle #1: Are 3rd party cookies blocked?
Doesn't lead to an error necessarily, just makes it so that you can't connect the app to Facebook.
Connecting your app to Facebook first requires you to be logged in with Facebook because the code FB.login (response) -> window.location = "/auth/facebook/callback" if response.authResponse
depends on the validity of authResponse
and its contained signedRequest
parameter.
If cookies are blocked (i.e., Chrome Settings > Advanced Settings > Privacy > Content Settings > Block Third Party Cookies is CHECKED), you will never get a authResponse
object back. This is the question/answer combination here: FB.getLoginStatus always returns status='unknown'. In other words, if you do a FB.getLoginStatus
, regardless of how many times you click a Login button, the status will always return back as unknown
per the docs: https://developers.facebook.com/docs/reference/javascript/FB.getLoginStatus.
Code Solution so far...: If FB.login
returns a response of unknown
divert back to server-side login.
$('#sign_in').click (e) ->
e.preventDefault()
FB.login (response) ->
if response.status is "unknown"
window.location = "/auth/facebook"
else
window.location = "/auth/facebook/callback"
Hurdle #2: Are parameters passed appropriately?
If you have third party cookies unblocked, you might then encounter an error:
OmniAuth::Strategies::Facebook::NoAuthorizationCodeError (must pass either a `code` parameter or a signed request (via `signed_request` parameter or a `fbsr_XXX` cookie)):
Despite setting cookie: true
in the FB.init
, sometimes params are still not passed. As stated in:
- This SO answer (OmniAuth Strategies Facebook NoAuthorizationCodeError (must pass either a `code` parameter or a signed request (via `signed_request` parameter):
- Omniauth-facebook author in the final comment of this Github issue (https://github.com/mkdynamic/omniauth-facebook/issues/110)
...you might need to manually pass in the signed_request
parameter. Quick watch-out. Per Facebook docs (https://developers.facebook.com/docs/facebook-login/using-login-with-games), the signedRequest
needs to have two components separated by a .
. Not sure how, but I have gotten one without the .
before. The separation is important, because it's the latter half that is a base64url
coded JSON object containing the code
information.
Code Solution so far...: Manually append signed_request
parameter to callback URL
$('#sign_in').click (e) ->
e.preventDefault()
FB.login (response) ->
if response.status is "unknown"
window.location = "/auth/facebook"
else
window.location = ("/auth/facebook/callback?signed_request=" + response.authResponse.signedRequest)
Hurdle #3: Still nothing... still same error as #2
Maybe just me at this point?
Code Solution so far...: Go super pedantic, parse out the code
from the signedRequest
and append to callback URL
$('#sign_in').click (e) ->
e.preventDefault()
FB.login (response) ->
if response.status is "unknown"
window.location = "/auth/facebook"
else
parsed_signed_Request = response.authResponse.signedRequest.split(".")
key_info = base64_decode(parsed_signed_Request[1])
// decoded via http://www.simplycalc.com/base64-source.php
key_info_object = jQuery.parseJSON( key_info )
window.location = ("/auth/facebook/callback?code=" + key_info_object["code"])
Hurdle #4: CSRF detected, but not for why you'd think
Now I've moved right into a brick wall. When I run the code as above, I get an error CSRF detected. There are a non-related reason one might get this error:
- You can get this error if your Facebook app is in Dev mode and you are trying to login users live. In this case, FB doesn't allow any non-listed developers to log in. See first answer to this question: Rails + omniauth + facebook - csrf detected
But the problem in my case wasn't the above, it was that the code
parameter was presented without a state
parameter. Now, there have been answers saying that you can fix this by setting provider_ignores_state: true
in the omniauth.rb
config file, see second answer to question referenced above, BUT this is not a fix for me. Why? Because 1) I don't like turn things specifically designed to counter CSRF off and 2) it appears the config kicks in only for server-side log-in. Adding it simply didn't do anything for me.
Which means the bigger issue of the #3 solution was that it was trying to combine server-side login approach (takes code
and state
params) with client-side login approach (takes signed_request
param).
Which means I'm back to the original question... how to pass the signed_request
so that the client-side login works?
Since I've rambled on this much already, let me point out another error I've seen. This one has answers related to Facebook errors (Dealing with Oauth 2.0-facebook gem error 100: This authorization code has been used), but beyond that, I found something else that could trigger it.
As suggested in this tutorial (https://coderwall.com/p/bsfitw), you match
the callback route via both get
and post
. But when I do this, my logger
shows two requests to Facebook, the second obviously being blocked and triggering the error. (Also means that the first request did go through and the user is already authorized/ data is saved, whatever). My solution was to set route as so:
match 'auth/:provider/callback', to: 'signups#create_facebook', via: [:get]
For those who still have this problem. Try not to use "localhost:3000" or whatever. Change /etc/hosts to a new url like
Change facebook app setting at developers.facebook.com to point to the new url (instead of using localhost:3000, call it abc.com:3000)
and try the new login with js facebook login again. Should work fine!