67
votes

Update 2017!

The issue I had when I posted the original question has got nothing to do with the recent changes Facebook made when they forced everyone to version 2.3 of their API. For a solution to that specific problem, see sammy34's answer below. Version 2.3 of the /oauth/access_token endpoint now returns JSON instead of form-encoded values

For historical reasons, here's my original question/issue:

I've got an MVC5 Web application which is using the built-in support for authentication via Facebook and Google. When we built this app a few months ago, we followed this tutorial: http://www.asp.net/mvc/tutorials/mvc-5/create-an-aspnet-mvc-5-app-with-facebook-and-google-oauth2-and-openid-sign-on and everything worked great.

Now, all of a sudden, the Facebook authentication has just stopped working alltogether. The Google authentication still works great.

Description of the problem: We click the link to connect using Facebook, we are redirected to Facebook where we are prompted if we wan't to allow our Facebook app access to our profile. When we click "OK" we are redirected back to our site, but instead of being logged in we simply end up at the login screen.

I've gone through this process in debug mode and I've got this ActionResult in my account controller as per the tutorial mentioned above:

// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (loginInfo == null)
    {
        return RedirectToAction("Login");
    }
    ............

When stepping through the code and upon returning from Facebook, the loginInfo object is always NULL, which causes the user to be redirected back to the login.

In order to understand what is actually happening behind the scenes, I installed Fiddler and monitored the HTTP traffic. What I disovered is that upon clicking "OK" at the Facebook permission dialog, Facebook redirects back to our application with this URL:

https://localhost/signin-facebook?code=<access-token>

This URL is not an actual file and probably handled by some controller/handler built into this OWIN framework I'm guessing. Most likely, it is connecting back to Facebook using the given code to query information about the user which is trying to login. Now, the problem is that instead of doing that, we are redirected to:

/Account/ExternalLoginCallback?error=access_denied

Which I'm sure is something Facebook is doing, that is, instead of giving us the user data, it's redirecting us back with this error message.

This causes the AuthenticationManager.GetExternalLoginInfoAsync(); to fail and always return NULL.

I'm completely out of ideas. As far as we know, we did not change anything on our end.

I've tried creating a new Facebook app, I've tried following the tutorial again but I always have the same problem.

Any ideas welcome!

Update!

OK, this is driving me insane! I've now manually gone through the steps required to perform the authentication and everything works great when I do that. Why on earth is this not working when using the MVC5 Owin stuff?

This is what I did:

    // Step 1 - Pasted this into a browser, this returns a code
    https://www.facebook.com/dialog/oauth?response_type=code&client_id=619359858118523&redirect_uri=https%3A%2F%2Flocalhost%2Fsignin-facebook&scope=&state=u9R1m4iRI6Td4yACEgO99ETQw9NAos06bZWilJxJrXRn1rh4KEQhfuEVAq52UPnUif-lEHgayyWrsrdlW6t3ghLD8iFGX5S2iUBHotyTqCCQ9lx2Nl091pHPIw1N0JV23sc4wYfOs2YU5smyw9MGhcEuinvTAEql2QhBowR62FfU6PY4lA6m8pD3odI5MwBYOMor3eMLu2qnpEk0GekbtTVWgQnKnH6t1UcC6KcNXYY

I was redirected back to localhost (which I had shut down at this point to avoid being redirected immediately away).  The URL I was redirected to is this:

https://localhost/signin-facebook?code=<code-received-removed-for-obvious-reasons>

Now, I grabbed the code I got and used it in the URL below:

// Step 2 - opened this URL in a browser, and successfully retrieved an access token
https://graph.facebook.com/oauth/access_token?client_id=619359858118523&redirect_uri=https://localhost/signin-facebook&client_secret=<client-secret>&code=<code-from-step-1>

// Step 3 - Now I'm able to query the facebook graph using the access token from step 2!

https://graph.facebook.com/me?access_token=<access-token-from-step-2>

No errors, everything works great! Then why the hell is this not working when using the MVC5 Owin stuff? There's obviously something wrong with the OWin implementation.

11
I'm working the same issue - started happening yesterday. I have a MVC5 app using the process described here: stackoverflow.com/questions/18942196/… and here: stackoverflow.com/questions/20928939/…. Works fine on my local dev environment, on azure platform am getting alternating errors of access_denied and FB error "Given URL is not allowed by the App configuration.: One or more of the given URLs is not allowed by the App's settings..."GojiraDeMonstah
Good I'm not the only one :-) I'll be sure to let you know if I find a solution to this problem!HaukurHaf
I had the same problem, putting example.com/signin-facebook as redirect url on facebook solved the issueMajid
Following your URL testing approach I noticed that my app secret had changed! Cheers.Tim Tyler

11 Answers

112
votes

Update 22nd April 2017: Version 3.1.0 of the Microsoft.Owin.* packages are now available. If you're having problems after Facebook's API changes from the 27th March 2017, try the updated NuGet packages first. In my case they solved the problem (working fine on our production systems).

Original answer:

In my case, I woke up on the 28th March 2017 to discover that our app's Facebook authentication had suddenly stopped working. We hadn't changed anything in the app code.

It turns out that Facebook did a "force upgrade" of their graph API from version 2.2 to 2.3 on 27th March 2017. One of the differences in these versions of the API seems to be that the Facebook endpoint /oauth/access_token responds no longer with a form-encoded content body, but with JSON instead.

Now, in the Owin middleware, we find the method protected override FacebookAuthenticationHandler.AuthenticateCoreAsync(), which parses the body of the response as a form and subsequently uses the access_token from the parsed form. Needless to say, the parsed form is empty, so the access_token is also empty, causing an access_denied error further down the chain.

To fix this quickly, we created a wrapper class for the Facebook Oauth response

public class FacebookOauthResponse
{
    public string access_token { get; set; }
    public string token_type { get; set; }
    public int expires_in { get; set; }
}

Then, in OwinStart, we added a custom back-channel handler...

        app.UseFacebookAuthentication(new FacebookAuthenticationOptions
        {
            AppId = "hidden",
            AppSecret = "hidden",
            BackchannelHttpHandler = new FacebookBackChannelHandler()
        });

...where the handler is defined as:

public class FacebookBackChannelHandler : HttpClientHandler
{
    protected override async System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        var result = await base.SendAsync(request, cancellationToken);
        if (!request.RequestUri.AbsolutePath.Contains("access_token"))
            return result;

        // For the access token we need to now deal with the fact that the response is now in JSON format, not form values. Owin looks for form values.
        var content = await result.Content.ReadAsStringAsync();
        var facebookOauthResponse = JsonConvert.DeserializeObject<FacebookOauthResponse>(content);

        var outgoingQueryString = HttpUtility.ParseQueryString(string.Empty);
        outgoingQueryString.Add(nameof(facebookOauthResponse.access_token), facebookOauthResponse.access_token);
        outgoingQueryString.Add(nameof(facebookOauthResponse.expires_in), facebookOauthResponse.expires_in + string.Empty);
        outgoingQueryString.Add(nameof(facebookOauthResponse.token_type), facebookOauthResponse.token_type);
        var postdata = outgoingQueryString.ToString();

        var modifiedResult = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(postdata)
        };

        return modifiedResult;
    }
}

Basically, the handler simply creates a new HttpResponseMessage containing the equivalent form-encoded information from the Facebook JSON response. Note that this code uses the popular Json.Net package.

With this custom handler, the problems seem to be resolved (although we're yet to deploy to prod :)).

Hope that saves somebody else waking up today with similar problems!

Also, if anybody has a cleaner solution to this, I'd love to know!

27
votes

Noticed this problem yesterday. Facebook does not support Microsoft.Owin.Security.Facebook version 3.0.1 anymore. For me it worked to install version 3.1.0. To update to 3.1.0, run the command Install-Package Microsoft.Owin.Security.Facebook in Package Manager Console: https://www.nuget.org/packages/Microsoft.Owin.Security.Facebook

9
votes

Ok I've got a solution to the problem.

This is the code I had previously in my Startup.Auth.cs file:

var x = new FacebookAuthenticationOptions();
            //x.Scope.Add("email");
            x.AppId = "1442725269277224";
            x.AppSecret = "<secret>";
            x.Provider = new FacebookAuthenticationProvider()
            {
                OnAuthenticated = async context =>
                {
                        //Get the access token from FB and store it in the database and
                    //use FacebookC# SDK to get more information about the user
                    context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken",context.AccessToken));
                    context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:name", context.Name));
                    context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:email", context.Email));
                }
            };
            x.SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie;
            app.UseFacebookAuthentication(x);

Notice how the

x.Scope.Add("email")

line has been commented out, but still I'm query-ing for the e-mail later in the OnAuthenticated handler? Yup, that's right. For some reason this worked flawlessly for a few weeks.

My solution was to simply uncomment the x.Scope.Add("email"); line to make sure that the scope=email variable was present in the initial request to Facebook.

Now everything works like it did!

I cannot understand why this worked before like it was. The only explanation I can come up with is that Facebook changed something on their end.

4
votes

I had this same issue with the Google Authentication. The following worked for me: Changes to Google OAuth 2.0 and updates in Google middleware for 3.0.0 RC release

3
votes

The last Facebook upgrade was on 2015-02-09 (https://www.nuget.org/packages/Microsoft.AspNet.WebPages.OAuth/)

The latest version of the API at that point was version 2.2. Version 2.2 expired on the 25th of March 2017, which is coincidentally when the problem started. (https://developers.facebook.com/docs/apps/changelog)

I'm guessing Facebook probably automatically upgraded the API and now the MS OAUTH library is unable to parse the new response.

tldr: The Microsoft WebPages OAuth library is outdated (for FB at least) and you'll probably have to find another solution

2
votes

The above solutions didn't work for me. In the end, it seemed to be related to the Session. By "waking up" the session in the previous call, it would no longer return null from the GetExternalLoginInfoAsync()

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public ActionResult ExternalLogin(string provider, string returnUrl)
    {
        Session["WAKEUP"] = "NOW!";
        // Request a redirect to the external login provider
        return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
    }

Like the OP, I had the 3rd party auth working fine for a long time then suddenly it stopped. I beleive it was due to the changes made in my code when I set up the Session to use Redis Cache on Azure.

2
votes

I had this problem as well, but it wasn't caused by the scope setting. Took me a long time to figure that out, but what finally clued me in was by setting a custom logger by setting the following in OwinStartup.Configuration(IAppBuilder app).

app.SetLoggerFactory(new LoggerFactory()); 
// Note: LoggerFactory is my own custom ILoggerFactory

This outputted the following:

2014-05-31 21:14:48,508 [8] ERROR
Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware
[(null)] - 0x00000000 - Authentication failed
System.Net.Http.HttpRequestException: An error occurred while sending the request. ---> System.Net.WebException: The remote name could not
be resolved: 'graph.facebook.com' at
System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
at System.Net.Http.HttpClientHandler.GetResponseCallback(IAsyncResult ar) --- End of inner exception stack trace --- at
System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task
task) at
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() at
Microsoft.Owin.Security.Facebook.FacebookAuthenticationHandler.d__0.MoveNext()

Based on the above call stack I found that my Azure VM was unable to resolve graph.facebook.com. All I had to do to fix that was to run "ipconfig /registerdns" and I was all fixed...

2
votes

I have been working on solution for three days. And I've just found it on github(https://github.com/aspnet/AspNetKatana/issues/38#issuecomment-290400987)

var facebookOptions = new FacebookAuthenticationOptions()
{
    AppId = "xxxxx",
    AppSecret = "xxxxx",
};

// Set requested scope
facebookOptions.Scope.Add("email");
facebookOptions.Scope.Add("public_profile");

// Set requested fields
facebookOptions.Fields.Add("email");
facebookOptions.Fields.Add("first_name");
facebookOptions.Fields.Add("last_name");

facebookOptions.Provider = new FacebookAuthenticationProvider()
{
    OnAuthenticated = (context) =>
        {
            // Attach the access token if you need it later on for calls on behalf of the user
            context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));

            foreach (var claim in context.User)
            {
                //var claimType = string.Format("urn:facebook:{0}", claim.Key);
                var claimType = string.Format("{0}", claim.Key);
                string claimValue = claim.Value.ToString();

                    if (!context.Identity.HasClaim(claimType, claimValue))
                        context.Identity.AddClaim(new System.Security.Claims.Claim(claimType, claimValue, "XmlSchemaString", "Facebook"));
            }

            return Task.FromResult(0);
       }
};

app.UseFacebookAuthentication(facebookOptions);

And to get values

var info = await AuthenticationManager.GetExternalLoginInfoAsync();

if (info != null)
{
    var firstName = info.ExternalIdentity.Claims.First(c => c.Type == "first_name").Value;
    var lastName = info.ExternalIdentity.Claims.First(c => c.Type == "last_name").Value;
}
0
votes

Check you get an outside internet connection from your application. If not, fix your outside internet connection. My problem was I was using an EC2 AWS instance that suddenly stopped connecting to the internet. It took me a while to realize that was the problem.

0
votes

This drove me insane. All was working until I deployed to my staging environment. I was using Microsoft.Owin.Security.Facebook version 3.0.1 from Nuget. Updated it to the prelease version 3.1.0 from Nuget and I no longer got the access denied error...

0
votes

Even though i did everything what sammy34 said, it did not work for me. I was at the same point with HaukurHaf: When i make apirequest manually on browser it works perfect, but if i use my mvc app, GetExternalLoginInfoAsync() always returns null.

So i changed some rows on sammy34's codes like on this comment: https://stackoverflow.com/a/43148543/7776015

Replaced:

if (!request.RequestUri.AbsolutePath.Contains("/oauth"))
{
request.RequestUri = new Uri(request.RequestUri.AbsoluteUri.Replace("?access_token", "&access_token"));
}
var result = await base.SendAsync(request, cancellationToken);
if (!request.RequestUri.AbsolutePath.Contains("/oauth"))
{
return result;
}

Instead of:

var result = await base.SendAsync(request, cancellationToken);
if (!request.RequestUri.AbsolutePath.Contains("access_token"))
return result;

And added this row into my FacebookAuthenticationOptions:

UserInformationEndpoint = "https://graph.facebook.com/v2.8/me?fields=id,name,email,first_name,last_name,picture"

and now it works.(fields and that parameters optional)

Note: I did not update Microsoft.Owin.Security.Facebook