514

Facebook callback has started appending #_=_ hash underscore to the Return URL

Does anyone know why? What is the solution?

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
zing ming
  • 5,141
  • 3
  • 15
  • 4

22 Answers22

245

via Facebook's Platform Updates:

Change in Session Redirect Behavior

This week, we started adding a fragment #____=____ to the redirect_uri when this field is left blank. Please ensure that your app can handle this behavior.

To prevent this, set the redirect_uri in your login url request like so: (using Facebook php-sdk)

$facebook->getLoginUrl(array('redirect_uri' => $_SERVER['SCRIPT_URI'],'scope' => 'user_about_me'));

UPDATE

The above is exactly as the documentation says to fix this. However, Facebook's documented solution does not work. Please consider leaving a comment on the Facebook Platform Updates blog post and follow this bug to get a better answer. Until then, add the following to your head tag to resolve this issue:

<script type="text/javascript">
    if (window.location.hash && window.location.hash == '#_=_') {
        window.location.hash = '';
    }
</script>

Or a more detailed alternative (thanks niftylettuce):

<script type="text/javascript">
    if (window.location.hash && window.location.hash == '#_=_') {
        if (window.history && history.pushState) {
            window.history.pushState("", document.title, window.location.pathname);
        } else {
            // Prevent scrolling by storing the page's current scroll offset
            var scroll = {
                top: document.body.scrollTop,
                left: document.body.scrollLeft
            };
            window.location.hash = '';
            // Restore the scroll offset, should be flicker free
            document.body.scrollTop = scroll.top;
            document.body.scrollLeft = scroll.left;
        }
    }
</script>
Ryan
  • 14,682
  • 32
  • 106
  • 179
  • I'm having issues with this too, using Java though, but the hash thing is in the way nonetheless. – josef.van.niekerk Sep 10 '11 at 13:03
  • 11
    what field is left blank? This is very cryptic – user210504 Sep 15 '11 at 05:53
  • Agreed. If you're having this problem, please comment on the Facebook Platform Update: https://developers.facebook.com/blog/post/552/ – Ryan Sep 15 '11 at 16:14
  • 12
    @Ryan Update almost works for me, I still get a hash (/#) on the end. Not happy with FB. – LenPopLilly Jun 03 '12 at 17:00
  • 2
    I still get the /# as well. anyone update here? to get the # removed – Loonb Sep 08 '12 at 22:03
  • 6
    This solution will erase the hash: Just be sure this to be the first tag in the head element. – Gorgi Rankovski Nov 12 '12 at 16:33
  • what does this refer to: "when this field is left blank" ... what 'field'? – dsdsdsdsd Jun 11 '13 at 15:16
  • typical facebook documentation. – Petrogad Oct 12 '13 at 16:11
  • This link is broken. http://developers.facebook.com/bugs/196125357123225 Does anyone have the updated bug link? I'd like to see the status of this issue in the facebook api – Spundun Apr 13 '14 at 18:42
  • 1
    I noticed your bug report here: https://developers.facebook.com/bugs/1424488987806270/ I also tried my hand at searching "fragment request_uri" with the same results. – Ryan Apr 14 '14 at 03:44
  • Hey, @niftylettuce here, can you please update your answer to this? https://github.com/jaredhanson/passport-facebook/issues/12#issuecomment-40427410 –  Oct 17 '14 at 05:10
  • Why is this being appended? The Facebook documentation doesn't say. – Justin Skiles Aug 29 '15 at 13:16
  • 1
    I think this solution DOES work. You just need to use the `https` protocol in your callback URL. – Michael Lynch Aug 15 '16 at 16:43
  • As @GorgiRankovski cited above, this works perfectly, just ensure it's the FIRST script that runs in the HEAD element. Same is true for PapaSierra's solution below. – jeremytripp Nov 22 '16 at 17:37
  • Only to complete this discussion: Facebook also explains, why they added this anchor - go to: [https://developers.facebook.com/bugs/318390728250352/](https://developers.facebook.com/bugs/318390728250352/) – olivmir Feb 23 '17 at 09:04
  • that will leave the # sign :( – Amir Hassan Azimi Aug 15 '17 at 00:39
  • @Ryan I tried this solution which seems to be point at right direction but the redirect does not work for me. It also redirect to /#_=_. What could I be missing? I am asking in terms of the original answer (not the update). Help would be appreciated. – TheTechGuy Aug 27 '17 at 09:46
  • @GorgiRankovski, your solution worked best as it removes the final hash. – Dom DaFonte Jan 14 '20 at 13:44
  • @Ryan if there's a search string in the initial url, it will be gone. I would just add append window.location.search here: `window.history.pushState("", document.title, window.location.pathname + window.location.search);` – Big_Boulard Jul 11 '23 at 08:12
123

TL;DR

if (window.location.hash === "#_=_"){
    history.replaceState 
        ? history.replaceState(null, null, window.location.href.split("#")[0])
        : window.location.hash = "";
}

Full version with step by step instructions

// Test for the ugliness.
if (window.location.hash === "#_=_"){

    // Check if the browser supports history.replaceState.
    if (history.replaceState) {

        // Keep the exact URL up to the hash.
        var cleanHref = window.location.href.split("#")[0];

        // Replace the URL in the address bar without messing with the back button.
        history.replaceState(null, null, cleanHref);

    } else {

        // Well, you're on an old browser, we can get rid of the _=_ but not the #.
        window.location.hash = "";

    }

}

Step by step:

  1. We'll only get into the code block if the fragment is #_=_.
  2. Check if the browser supports the HTML5 window.replaceState method.
    1. Clean the URL by splitting on # and taking only the first part.
    2. Tell history to replace the current page state with the clean URL. This modifies the current history entry instead of creating a new one. What this means is the back and forward buttons will work just the way you want. ;-)
  3. If the browser does not support the awesome HTML 5 history methods then just clean up the URL as best you can by setting the hash to empty string. This is a poor fallback because it still leaves a trailing hash (example.com/#) and also it adds a history entry, so the back button will take you back to #_-_.

Learn more about history.replaceState.

Learn more about window.location.

Paul Schwarz
  • 1,828
  • 1
  • 15
  • 24
  • 2
    Worked perfectly for me too. The other solution gets rid of any query parameters. – AdeelMufti Jan 10 '16 at 12:27
  • It does the same thing for google omniauth, so I get an error no route matches, it appends # (hashtag) after request uri https://.....herokua‌​pp.com/auth/google_oa‌​uth2/callback?state=1‌​9feaacfe23423jh5jhhGS‌​DFwb419049ebb18dabdf8‌​&code=4/glrY3-mSlTzwe‌​rwERTEG334eXcn3hOSxGu‌​c51BAlglPa4AU# – Shalafister's Aug 17 '16 at 09:29
  • Worked for me better than the solution of @Ryan, as it does not delete the query. – olivmir Feb 23 '17 at 09:43
  • This solution worked better than the Ryan's solution. I pass some parameters to the url after it goes thru facebook's authentication and Ryan's solution, for some reason just removes every parameter from the url. This solution works perfectly in my case. – BlueSun3k1 Oct 14 '17 at 22:40
66

This was implemented by Facebook by design for security reasons. Here's the explanation from Eric Osgood, a Facebook Team member:

This has been marked as 'by design' because it prevents a potential security vulnerability.

Some browsers will append the hash fragment from a URL to the end of a new URL to which they have been redirected (if that new URL does not itself have a hash fragment).

For example if example1.com returns a redirect to example2.com, then a browser going to example1.com#abc will go to example2.com#abc, and the hash fragment content from example1.com would be accessible to a script on example2.com.

Since it is possible to have one auth flow redirect to another, it would be possible to have sensitive auth data from one app accessible to another.

This is mitigated by appending a new hash fragment to the redirect URL to prevent this browser behavior.

If the aesthetics, or client-side behavior, of the resulting URL are of concern, it would be possible to use window.location.hash (or even a server-side redirect of your own) to remove the offending characters.

Source: https://developers.facebook.com/bugs/318390728250352/

Mark Murphy
  • 2,834
  • 1
  • 31
  • 29
  • 13
    This is the only answer that actually explains why this happens, thanks, I think I'll leave the offending chars in my urls now that I know they're not an issue. – stephenmurdoch Mar 08 '17 at 08:56
  • 1
    This is also implemented by Tumblr in their redirects. (as of mid-'19) Thanks for pointing to the FB explanation. Easily solved in a simplistic Passport app by merely pointing the successful redirect to "/#" instead of just "/" (which explains why I see more trailing octothorps on the web, I think...) – R.L. Brown Jul 10 '19 at 19:30
  • Well for people using vue router they're a big issue.. As it will crash because vue router is expecting a valid js selector which this isn't, they could have just set `#` with nothing else and the vulnerability would still be mitigated, but no they had to put some more weird stuff requiring dirty workarounds – Tofandel Sep 01 '21 at 22:47
  • This is so fat bs design it annoys me back and forth. If they want to use redirect_url parameter then allow us to pass query params for that purpose, otherwise the default behaviour will clear them – Bartłomiej Sobieszek Oct 30 '21 at 11:15
61

if you want to remove the remaining "#" from the url

$(window).on('load', function(e){
  if (window.location.hash == '#_=_') {
    window.location.hash = ''; // for older browsers, leaves a # behind
    history.pushState('', document.title, window.location.pathname); // nice and clean
    e.preventDefault(); // no page reload
  }
})
Community
  • 1
  • 1
likebeats
  • 876
  • 7
  • 18
13

Not sure why they're doing this but, you could get around this by reseting the hash at the top of your page:

if (window.location.hash == "#_=_")
  window.location.hash = "";
sakibmoon
  • 2,026
  • 3
  • 22
  • 32
mixmasteralan
  • 489
  • 4
  • 11
9

You can also specify your own hash on the redirect_uri parameter for the Facebook callback, which might be helpful in certain circumstances e.g. /api/account/callback#home. When you are redirected back, it'll at least be a hash that corresponds to a known route if you are using backbone.js or similar (not sure about jquery mobile).

pkiddie
  • 323
  • 1
  • 4
  • 13
8

Major annoying, especially for apps that parse the URI and not just read the $_GET... Here's the hack I threw together... Enjoy!

<html xmlns:fb='http://www.facebook.com/2008/fbml'>
<head>
        <script type="text/javascript">
        // Get rid of the Facebook residue hash in the URI
        // Must be done in JS cuz hash only exists client-side
        // IE and Chrome version of the hack
        if (String(window.location.hash).substring(0,1) == "#") {
                window.location.hash = "";
                window.location.href=window.location.href.slice(0, -1);
                }
        // Firefox version of the hack
        if (String(location.hash).substring(0,1) == "#") {
                location.hash = "";
                location.href=location.href.substring(0,location.href.length-3);
                }
        </script>
</head>
<body>
URI should be clean
</body>
</html>
sakibmoon
  • 2,026
  • 3
  • 22
  • 32
Jeremy Whitt
  • 261
  • 1
  • 4
  • 5
  • Be careful about making assumptions when parsing any data that you don't create. URI fragment identifiers were spec'd as early as RFC 1738 (in 1994) so if you use a correct URI parser, this should never be a problem. – AndrewF Oct 27 '13 at 22:28
6

This can become kind of a serious issue if you're using a JS framework with hashbang (/#!/) URLs, e.g. Angular. Indeed, Angular will consider URLs with a non-hashbang fragment as invalid and throw an error :

Error: Invalid url "http://example.com/#_=_", missing hash prefix "#!".

If you're in such a case (and redirecting to your domain root), instead of doing :

window.location.hash = ''; // goes to /#, which is no better

Simply do :

window.location.hash = '!'; // goes to /#!, which allows Angular to take care of the rest
neemzy
  • 1,899
  • 23
  • 25
5

I do not see how this problem is related to facebook AJAX. In fact the issue also occurs with JavaScript disabled and purely redirect based logins.

An example exchange with facebook:

1. GET <https://www.facebook.com/dialog/oauth?client_id=MY_APP_ID&scope=email&redirect_uri=MY_REDIRECT_URL> RESPONSE 302 Found Location: <https://www.facebook.com/connect/uiserver.php?[...]>  
2. GET <https://www.facebook.com/connect/uiserver.php?[...]> RESPONSE 302 Found MY_REDIRECT_URL?code=FB_CODE#_  
3. GET MY_REDIRECT_URL?code=FB_CODE#_  

Happens only with Firefox for me too.

sakibmoon
  • 2,026
  • 3
  • 22
  • 32
Sebastian Tusk
  • 331
  • 1
  • 9
4

Adding this to my redirect page fixed the problem for me ...

if (window.location.href.indexOf('#_=_') > 0) {
    window.location = window.location.href.replace(/#.*/, '');
}
sakibmoon
  • 2,026
  • 3
  • 22
  • 32
designosis
  • 5,182
  • 1
  • 38
  • 57
3

With angular and angular ui router, you can fix this

    app.config(function ($stateProvider, $urlRouterProvider, $locationProvider) {

      // Make a trailing slash optional for all routes
      // - Note: You'll need to specify all urls with a trailing slash if you use this method.
      $urlRouterProvider.rule(function ($injector, $location) {
        /***
        Angular misbehaves when the URL contains a "#_=_" hash.

        From Facebook:
          Change in Session Redirect Behavior
          This week, we started adding a fragment #_=_ to the redirect_uri when this field is left blank.
          Please ensure that your app can handle this behavior.

        Fix:
          http://stackoverflow.com/questions/7131909/facebook-callback-appends-to-return-url#answer-7297873
        ***/
        if ($location.hash() === '_=_'){
          $location.hash(null);
        }

        var path = $location.url();

        // check to see if the path already has a slash where it should be
        if (path[path.length - 1] === '/' || path.indexOf('/?') > -1) {
          return;
        }
        else if (path.indexOf('?') > -1) {
          $location.replace().path(path.replace('?', '/?'));
        }
        else {
          $location.replace().path(path + '/');
        }
      });

      // etc ...
    });
});
rebelliard
  • 9,592
  • 6
  • 47
  • 80
3

If you're using vue-router, you can append to the list of routes:

{
  path: '/_=_',
  redirect: '/', // <-- or other default route
},
Slawomir
  • 3,194
  • 1
  • 30
  • 36
2

For me, i make JavaScript redirection to another page to get rid of #_=_. The ideas below should work. :)

function redirect($url){
    echo "<script>window.location.href='{$url}?{$_SERVER["QUERY_STRING"]}'</script>";        
}
Eng Cy
  • 1,527
  • 11
  • 15
2

A change was introduced recently in how Facebook handles session redirects. See "Change in Session Redirect Behavior" in this week's Operation Developer Love blog post for the announcement.

Dhiren Patel
  • 645
  • 4
  • 6
1

A workaround that worked for me (using Backbone.js), was to add "#/" to the end of the redirect URL passed to Facebook. Facebook will keep the provided fragment, and not append its own "_=_".

Upon return, Backbone will remove the "#/" part. For AngularJS, appending "#!" to the return URL should work.

Note that the fragment identifier of the original URL is preserved on redirection (via HTTP status codes 300, 301, 302 and 303) by most browsers, unless the redirect URL also has a fragment identifier. This seems to be recommended behaviour.

If you use a handler script that redirects the user elsewhere, you can append "#" to the redirect URL here to replace the fragment identifier with an empty string.

Ivo Smits
  • 237
  • 1
  • 10
1

I know this reply is late, but if you are using passportjs, you might want to see this.

return (req, res, next) => {
    console.log(req.originalUrl);
    next();
};

I have written this middleware and applied it to express server instance, and the original URL I've got is without the "#_=_". Looks like it when we apply passporJS' instance as middleware to the server instance, it doesn't take those characters, but are only visible on the address bar of our browsers.

Krishna
  • 673
  • 3
  • 6
  • 21
  • 3
    "#_=_" it only available on the client. Review: https://en.wikipedia.org/wiki/Fragment_identifier – alditis Sep 28 '17 at 18:06
1

I use this one, to delete '#' symbol as well.

<script type="text/javascript">
    if (window.location.hash && window.location.hash == '#_=_') {
        window.location.href = window.location.href.split('#_=_')[0];
    }
</script>
Simon
  • 22,637
  • 36
  • 92
  • 121
1

For PHP SDK users

I fixed the problem simply by removing the extra part before forwarding.

 $loginURL = $helper->getLoginUrl($redirectURL, $fbPermissions);
 $loginURL = str_replace("#_=_", "", $loginURL);
 header("Location: " . $loginURL);
Nanoturka
  • 421
  • 4
  • 15
1

This would remove the appended characters to your url

<script type="text/javascript">
 var idx=window.location.toString().indexOf("#_=_"); 
   if (idx > 0) { 
     window.location = window.location.toString().substring(0, idx); 
   } 
</script>
Rotimi
  • 4,783
  • 4
  • 18
  • 27
0

Using Angular 2 (RC5) and hash-based routes, I do this:

const appRoutes: Routes = [
  ...
  {path: '_', redirectTo: '/facebookLoginSuccess'},
  ...
]

and

export const routing = RouterModule.forRoot(appRoutes, { useHash: true });

As far as I understand, the = character in the route is interpreted as part of optional route parameters definition (see https://angular.io/docs/ts/latest/guide/router.html#!#optional-route-parameters), so not involved in the route matching.

rcomblen
  • 4,579
  • 1
  • 27
  • 32
0

The easiest and clean solution to remove "#_=_" (PHP):

Instead of "header("Location: xxx.php");" to use "echo ("location.href = 'xxx.php';");"

user3806621
  • 278
  • 1
  • 6
0

For those who are looking for simple answer Just add this , it worked for me

if (window.location.hash === "#_=_"){
    history.replaceState 
        ? history.replaceState(null, null, window.location.href.split("#")[0])
        : window.location.hash = "";
}

Check the full answer by Paul Schwarz as well

https://stackoverflow.com/a/18305085/2694806

Akitha_MJ
  • 3,882
  • 25
  • 20
  • This is just a copy/paste of my answer https://stackoverflow.com/a/18305085/2694806 – Paul Schwarz Mar 05 '21 at 12:52
  • 1
    I just add this to reference my self and updated the answer with link to you full answer all credits goes to you @PaulSchwarz I am also referring to your answer and already up voted yours – Akitha_MJ Mar 10 '21 at 04:45