46

Everything used to work perfect until fb upgraded it's api to 2.4 (I had 2.3 in my previous project).

Today when I add a new application on fb developers I get it with api 2.4.

The problem: Now I get null email from fb (loginInfo.email = null).

Of course I checked that the user email is in public status on fb profile,

and I went over the loginInfo object but didn't find any other email address.

and I google that but didn't find any answer.

please any help.. I 'm kind of lost..

Thanks,

My original code (which worked on 2.3 api):

In the AccountController.cs:

//
// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (loginInfo == null)
    {
        return RedirectToAction("Login");
    }
    //A way to get fb details about the log-in user: 
    //var firstNameClaim = loginInfo.ExternalIdentity.Claims.First(c => c.Type == "urn:facebook:first_name");  <--worked only on 2.3
    //var firstNameClaim = loginInfo.ExternalIdentity.Claims.First(c => c.Type == "urn:facebook:name"); <--works on 2.4 api

    // Sign in the user with this external login provider if the user already has a login
    var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
        case SignInStatus.Failure:
        default:
            // If the user does not have an account, then prompt the user to create an account
            ViewBag.ReturnUrl = returnUrl;
            ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
            return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });  //<---DOESN'T WORK. loginInfo.Email IS NULL
    }
}

In the Startup.Auth.cs:

    Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions fbOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()
    {
        AppId = System.Configuration.ConfigurationManager.AppSettings.Get("FacebookAppId"),
        AppSecret = System.Configuration.ConfigurationManager.AppSettings.Get("FacebookAppSecret"),
    };
    fbOptions.Scope.Add("email");
    fbOptions.Provider = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationProvider()
    {
        OnAuthenticated = (context) =>
        {
            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);
                string claimValue = claim.Value.ToString();
                if (!context.Identity.HasClaim(claimType, claimValue))
                    context.Identity.AddClaim(new System.Security.Claims.Claim(claimType, claimValue, "XmlSchemaString", "Facebook"));

            }
            return System.Threading.Tasks.Task.FromResult(0);
        }
    };
    fbOptions.SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie;
    app.UseFacebookAuthentication(fbOptions);
Dudi
  • 3,069
  • 1
  • 27
  • 23

6 Answers6

81

Taken from a Katana Thread I devised the following:

Change the FacebookAuthenticationOptions to include BackchannelHttpHandler and UserInformationEndpoint as seen below. Make sure to include the names of the fields you want and need for your implementation.

var facebookOptions = new FacebookAuthenticationOptions()
{
    AppId = "*",
    AppSecret = "*",
    BackchannelHttpHandler = new FacebookBackChannelHandler(),
    UserInformationEndpoint = "https://graph.facebook.com/v2.4/me?fields=id,name,email,first_name,last_name"
}

Then create a custom FacebookBackChannelHandler that will intercept the requests to Facebook and fix the malformed url as needed.

UPDATE: The FacebookBackChannelHandler is updated based on a 27 Mar 2017 update to the FB api.

public class FacebookBackChannelHandler : HttpClientHandler
{
    protected override async System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        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;
        }

        var content = await result.Content.ReadAsStringAsync();
        var facebookOauthResponse = JsonConvert.DeserializeObject<FacebookOauthResponse>(content);

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

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

        return modifiedResult;
    }
}

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

One useful addition would be to check for the version 3.0.1 of the library and throw an exception if and when it changes. That way you'll know if someone upgrades or updates the NuGet package after a fix for this problem has been released.

(Updated to build, work in C# 5 without new nameof feature)

Chris Moschini
  • 36,764
  • 19
  • 160
  • 190
Mike Trionfo
  • 1,127
  • 7
  • 11
  • tnx, sound interesting. I will check it on my the next project. – Dudi Sep 18 '15 at 19:41
  • Very good! Worked like a charm without need to call extra APIs to get the email. – Douglas Gandini Sep 24 '15 at 15:57
  • Oh man you saved my life! Can't thank you enough! Been banging my head trying to solve this for a few days thus far! – Hajjat Nov 06 '15 at 06:10
  • Love you man..I am hitting my head for the whole day for this.Thanks for this awesome solution. – Vipul Kumar Dec 25 '15 at 16:18
  • 1
    You are welcome. I also spent an entire day trying to fix this problem. It felt immoral to not post the solution once I discovered it. – Mike Trionfo Dec 29 '15 at 21:42
  • Can you please explain above method in details. if possible give me demo project or its link. – Sachin Mar 11 '16 at 10:21
  • 1
    still not getting email. – Bimal Das Jul 04 '16 at 07:44
  • The need for this is due to a breaking change made by Facebook in v2.4: "To try to improve performance on mobile networks, Nodes and Edges in v2.4 requires that you explicitly request the field(s) you need for your GET requests. For example, GET /v2.4/me/feed no longer includes likes and comments by default, but GET /v2.4/me/feed?fields=comments,likes will return the data." – Chris Moschini Aug 18 '16 at 22:24
  • 1
    Just want to add that this solution is still needed in Facebook API 2.7 and Microsoft.Owin.Security.Facebook 3.0.1 – roberth Sep 13 '16 at 19:08
  • I used {UserInformationEndpoint = "https://graph.facebook.com/me?fields=id,email"} so that the url would not age out in a couple years. – Jeff Mar 14 '17 at 17:41
  • 1
    After the update 3.1.0-rc email is always empty, This code fixed it for me. Thank you! – Elger Mensonides Mar 28 '17 at 14:46
  • Based on the help from this thread http://stackoverflow.com/questions/22364442/asp-net-mvc5-owin-facebook-authentication-suddenly-not-working I updated the Facebook back channel handler. – Mike Trionfo Mar 28 '17 at 16:00
  • thank you for updating this answer. this answer fixed this issue for me 9 months ago and again today after fb updated their API's on 27th March :) – Anna Mar 30 '17 at 10:24
  • @MikeTrionfo where is the FacebookOauthResponse class definition? – Sebastián Rojas Mar 31 '17 at 19:19
  • Added FacebookOauthResponse from other answer, workaround for C# 5 (only C# 6 has nameof()) – Chris Moschini Apr 08 '17 at 22:40
  • Oh, dear Lord. Thank you. – optim1st Apr 10 '17 at 19:29
  • Using v3.1.0 (release date 10 April) didn't work with this code, however from https://github.com/aspnet/Security/issues/435 with the main change being 'options.BackchannelHttpHandler = new HttpClientHandler();' works fine – Phil Kermeen Apr 12 '17 at 08:09
23

For me this Issue was solved by upgrading to Microsoft.Owin.Security.Facebook 3.1.0 and adding 'email' to the Fields collection:

var options = new FacebookAuthenticationOptions
{
    AppId = "-------",
    AppSecret = "------",    
};
options.Scope.Add("public_profile");
options.Scope.Add("email");

//add this for facebook to actually return the email and name
options.Fields.Add("email");
options.Fields.Add("name");

app.UseFacebookAuthentication(options);
Issac
  • 943
  • 1
  • 6
  • 13
  • 1
    Simpler than some of the other answers and works as discribed. I was suprised that name which was normally included has to be specifically called out. – JSWilson Jul 23 '17 at 23:23
  • 2
    This does seem like the simplest and best solution. Be sure to add the scope AND the name field. Also I was getting the name field returned until I added the email field. Make sure to add it as well if you need both. – DrewF Aug 23 '17 at 07:01
  • How do I get the field values after they login? – David Nov 08 '17 at 19:35
  • Options.Fields... returns an error "FacebookAuthenticationOptions" does not contain a definition for Fields... – niico Nov 12 '17 at 16:09
  • this worked for me until now. Today I found it is broken and email claim is null again. what's going on? – Paolo Sanchi Nov 13 '17 at 10:21
  • It sounds very bad to have to ask for it twice. It sounds even worse to have to make subsequent calls as i read in other solutions. In my opinion, this answer is by far the simplest. – Giannis Paraskevopoulos Jan 22 '18 at 14:44
  • 1
    @niico to get the definition for Fields to show up, update your Microsoft.Owin.Security.Facebook reference to version 3.1.0 (you're probably on 3.0.1) – TomEberhard Feb 04 '18 at 08:59
20

To resolve this you need to install Facebook SDK from NuGet packages.

In StartUp File

 app.UseFacebookAuthentication(new FacebookAuthenticationOptions
            {
                AppId = "XXXXXXXXXX",
                AppSecret = "XXXXXXXXXX",
                Scope = { "email" },
                Provider = new FacebookAuthenticationProvider
                {
                    OnAuthenticated = context =>
                    {
                        context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
                        return Task.FromResult(true);
                    }
                }
            });

In Controller or Helper

var identity = AuthenticationManager.GetExternalIdentity(DefaultAuthenticationTypes.ExternalCookie);
var accessToken = identity.FindFirstValue("FacebookAccessToken");
var fb = new FacebookClient(accessToken);
dynamic myInfo = fb.Get("/me?fields=email,first_name,last_name,gender"); // specify the email field

With this you can get the EmailId,First-last Name, Gender.

You can also add your additional required properties in that query string.

Hope this will help someone.

Palak Patel
  • 235
  • 2
  • 6
  • 2
    Thanks! This solved my issue too. If you want also to get the values from dynamic myInfo then there is easy way to do this: myInfo.email (instead of email it can be gender, first_name, etc.) – Münich Feb 04 '16 at 20:10
13

Just want to add on Mike's answer that this line

facebookOptions.Scope.Add("email");

still needs to be added after

var facebookOptions = new FacebookAuthenticationOptions()
{
    AppId = "*",
    AppSecret = "*",
    BackchannelHttpHandler = new FacebookBackChannelHandler(),
    UserInformationEndpoint = "https://graph.facebook.com/v2.4/me?fields=id,name,email,first_name,last_name,location"
}

And if you already registered your facebook account to your dev website with no "email permission". After changing the code and trying again, you will still not get the email because the email permission isn't granted to your dev website. The way I do is go to https://www.facebook.com/settings?tab=applications, remove my facebook app, and redo the process again.

paibamboo
  • 2,804
  • 17
  • 16
3
1

Read the changelog, this is by design. You need to explicitly request the fields and edges you want retuned in the response:

Declarative Fields To try to improve performance on mobile networks, Nodes and Edges in v2.4 requires that you explicitly request the field(s) you need for your GET requests. For example, GET /v2.4/me/feed no longer includes likes and comments by default, but GET /v2.4/me/feed?fields=comments,likes will return the data. For more details see the docs on how to request specific fields.

ifaour
  • 38,035
  • 12
  • 72
  • 79
  • 1
    Yes, I knew it, but don't know how to modify the GET prefix url in the external facebook oauth-2 on the C# code. – Dudi Aug 21 '15 at 19:45
  • The only way which worked for me was to install the "Facebook SDK for .NET" by following those posts: [first post](http://stackoverflow.com/questions/31909920/facebook-v2-4-with-oauth2-stopped-working-and-only-getting-name-and-id) , [second post](http://stackoverflow.com/questions/31714500/access-email-address-in-the-oauth-externallogincallback-from-facebook-v2-4-api-i/31715332#31715332). However, I wish I could find a solution without installing any additional packages. – Dudi Aug 21 '15 at 19:47
  • @Dudi, Mike Trionfo answer works very well. I just made an adaptation on it to don't fix a specific API version (UserInformationEndpoint = "https: //graph.facebook.com/me?fields=id,name,email"). – Douglas Gandini Sep 24 '15 at 16:00