Search code examples
c#asp.net-mvcfacebook-oauth

How do I get Facebook/Google/Twitter details during OAuth in MVC?


I realise this question has been asked and answered a lot in the last 10 years, but I need a more modern answer than most I could find.

I have an ASP.Net MVC project (C#), and have followed the MS guide to enabling Facebook and Google+ authentication. This was super simple, register the apps in the providers and uncomment three lines each in startup.auth.cs - super, super simple. It also "just works", bango I have new users created in the database that can login via these providers. Wunderbar.

Like so many applications though I need more than authentication, I also need a small amount of identity - firstname, surname, email, birthday, profile image where available.

Using a combination of Scope and Fields in the FacebookAuthenticationOptions I have managed to get the Facebook authorization screen to warn the user that the app wants their birthday, but try as I might I can't find anywhere that this information is returned in the OAuth process or any claims that might represent the half a dozen other fields I've asked for.

Many "solutions" online talked about overriding the "OnAuthenticated" delegate of the FacebookAuthenticationOptions.Provider (which is a FacebookAuthenticationProvider). I've pasted in some stolen code but it never hits the breakpoint in there.

After I get this working with Facebook, I had hoped to repeat with Google+, Twitter, and Microsoft account providers and I'm hoping it's a standardised approach where I can tell each provider what fields I want in their format, then get them all out using a standard getter somewhere. As I understand it that is actually the whole underlying point of OAuth - no?

So in my startup.auth.cs (fake Id and secret of course):

app.UseFacebookAuthentication(new FacebookAuthenticationOptions
      {
        AppId = "xxxxxxxxxxx",
        AppSecret = "xxxxxxxxxxxxxxxxxxxxx",
        Scope = { "user_birthday", "public_profile", "email" },
        Fields = { "user_birthday", "picture", "name", "email", "gender", "first_name", "last_name" },
        Provider = new FacebookAuthenticationProvider
        { 
          OnAuthenticated = async ctx =>
          {
            Console.WriteLine("auth"); //this breakpoint never hits
            ctx.Identity.AddClaim(new Claim("FacebookAccessToken", ctx.AccessToken));
            foreach (var claim in ctx.User)
            {
              var claimType = string.Format("urn:facebook:{0}", claim.Key);
              string claimValue = claim.Value.ToString();

              if (!ctx.Identity.HasClaim(claimType, claimValue))
              {
                ctx.Identity.AddClaim(new Claim(claimType, claimValue, "XmlSchemaString", "Facebook"));
              }
            }
          }

        }
      });

Solution

  • Thanks to @Jeremy Thompson in the comments that pointed me to what I originally thought was just another copy of the same thing I stole in the first place, but actually triggered me to solve this myself in a way.

    The solution Jeremy linked to "worked", it looked much like my original code except the OnAuthenticated method was firing. Unfortunately it didn't actually have the birthday in it, so I added it, and then it stopped working. Aha...

    So glazing over the how I got there with this, the problem seems to be that a silent exception occurs which prevents this OnAuthenticated from firing which in my case was because I had a field name incorrect. Here is the working code which returns my name, email, birthday, current town, and a profile picture (the essentials you need to create a profile in your own system!)

    In the OWIN startup class of course:

    app.UseFacebookAuthentication(new FacebookAuthenticationOptions
          {
            AppId = "...", //Your App Id of course
            AppSecret = "...", //Your App Secret of course
            Scope = { "user_birthday", "public_profile", "email", "user_gender", "user_location" },
            Fields = { "birthday", "picture", "name", "email", "gender", "first_name", "last_name", "location" }, //notice the fields are not prefixed with "user_". This was 
            Provider = new FacebookAuthenticationProvider
            {
              OnAuthenticated = async ctx =>
              {
                ctx.Identity.AddClaim(new Claim("FacebookAccessToken", ctx.AccessToken));
                foreach (var claim in ctx.User)
                {
                  var claimType = string.Format("urn:facebook:{0}", claim.Key);
                  string claimValue = claim.Value.ToString();
    
                  if (!ctx.Identity.HasClaim(claimType, claimValue))
                  {
                    ctx.Identity.AddClaim(new Claim(claimType, claimValue, "XmlSchemaString", "Facebook"));
                  }
                }
              }
    
            }
          });
    

    And now in my AccountController's ExternalLoginCallback function I can access a heap of properties by looking at the loginInfo.Claims property (loginInfo as below).

          var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();