How to access Facebook private information by using ASP.NET Identity (OWIN)?

34,344

Solution 1

To retrieve additional information from facebook you can specify scopes you would like to include when you configure the facebook authentication options. Getting the additional information that's retrieved can be achieved by implementing the provider's OnAuthenticated method like this:

var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()
{
    Provider = new FacebookAuthenticationProvider()
    {
        OnAuthenticated = (context) =>
            {
                // All data from facebook in this object. 
                var rawUserObjectFromFacebookAsJson = context.User;

                // Only some of the basic details from facebook 
                // like id, username, email etc are added as claims.
                // But you can retrieve any other details from this
                // raw Json object from facebook and add it as claims here.
                // Subsequently adding a claim here will also send this claim
                // as part of the cookie set on the browser so you can retrieve
                // on every successive request. 
                context.Identity.AddClaim(...);

                return Task.FromResult(0);
            }
    }
};

//Way to specify additional scopes
facebookOptions.Scope.Add("...");

app.UseFacebookAuthentication(facebookOptions);

Per the code here i see the email is already retrieved and added as a claim here if facebook has sent. Are you not able to see it?

Solution 2

Create a new Microsoft.Owin.Security.Facebook.AuthenticationOptions object in Startup.ConfigureAuth (StartupAuth.cs), passing it the FacebookAppId, FacebookAppSecret, and a new AuthenticationProvider. You will use a lambda expression to pass the OnAuthenticated method some code to add Claims to the identity which contain the values you extract from context.Identity. This will include access_token by default. You must add email to the Scope. Other user properties are available from context.User (see link at bottom for example).

StartUp.Auth.cs

// Facebook : Create New App
// https://dev.twitter.com/apps
if (ConfigurationManager.AppSettings.Get("FacebookAppId").Length > 0)
{
    var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()
    {
        AppId = ConfigurationManager.AppSettings.Get("FacebookAppId"),
        AppSecret = ConfigurationManager.AppSettings.Get("FacebookAppSecret"),
        Provider = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationProvider()
        {
            OnAuthenticated = (context) =>
                {
                    context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, XmlSchemaString, "Facebook"));
                    context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:email", context.Email, XmlSchemaString, "Facebook"));
                    return Task.FromResult(0);
                }
        }

    };
    facebookOptions.Scope.Add("email");
    app.UseFacebookAuthentication(facebookOptions);
}

In AccountController, I extract the ClaimsIdentity from the AuthenticationManager using the external cookie. I then add it to the identity created using the application cookie. I ignored any claims that starts with "...schemas.xmlsoap.org/ws/2005/05/identity/claims" since it seemed to break the login.

AccountController.cs

private async Task SignInAsync(CustomUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);

// Extracted the part that has been changed in SignInAsync for clarity.
    await SetExternalProperties(identity);

    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

private async Task SetExternalProperties(ClaimsIdentity identity)
{
    // get external claims captured in Startup.ConfigureAuth
    ClaimsIdentity ext = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);

    if (ext != null)
    {
        var ignoreClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims";
        // add external claims to identity
        foreach (var c in ext.Claims)
        {
            if (!c.Type.StartsWith(ignoreClaim))
                if (!identity.HasClaim(c.Type, c.Value))
                    identity.AddClaim(c);
        } 
    }
}

And finally, I want to display whatever values are not from the LOCAL AUTHORITY. I created a partial view _ExternalUserPropertiesListPartial that appears on the /Account/Manage page. I get the claims I previously stored from AuthenticationManager.User.Claims and then pass it to the view.

AccountController.cs

[ChildActionOnly]
public ActionResult ExternalUserPropertiesList()
{
    var extList = GetExternalProperties();
    return (ActionResult)PartialView("_ExternalUserPropertiesListPartial", extList);
}

private List<ExtPropertyViewModel> GetExternalProperties()
{
    var claimlist = from claims in AuthenticationManager.User.Claims
                    where claims.Issuer != "LOCAL AUTHORITY"
                    select new ExtPropertyViewModel
                    {
                        Issuer = claims.Issuer,
                        Type = claims.Type,
                        Value = claims.Value
                    };

    return claimlist.ToList<ExtPropertyViewModel>();
}

And just to be thorough, the view:

_ExternalUserPropertiesListPartial.cshtml

@model IEnumerable<MySample.Models.ExtPropertyViewModel>

@if (Model != null)
{
    <legend>External User Properties</legend>
    <table class="table">
        <tbody>
            @foreach (var claim in Model)
            {
                <tr>
                    <td>@claim.Issuer</td>
                    <td>@claim.Type</td>
                    <td>@claim.Value</td>
                </tr>
            }
        </tbody>
    </table>
}

The working example and complete code is on GitHub: https://github.com/johndpalm/IdentityUserPropertiesSample

And any feedback, corrections, or improvements would be appreciated.

Solution 3

It worked for me with just this in Startup.Auth :

var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()        {
    AppId = "*",
    AppSecret = "**"
};
facebookOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookOptions);

And then in the method ExternalLoginCallback or ExternalLoginConfirmation you get the email with :

ClaimsIdentity ext = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
var email = ext.Claims.First(x => x.Type.Contains("emailaddress")).Value;

Solution 4

You need to create an instance of FacebookAuthenticationOptions and configurethe Provider. The Provider contains an event called OnAuthenticated that is triggered when you login.

var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions
{
    Provider = new FacebookAuthenticationProvider()
    {
        OnAuthenticated = (context) =>
        {
            context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, ClaimValueTypes.String, "Facebook"));

            return Task.FromResult(0);
        }
    },

    // You can store these on AppSettings
    AppId = ConfigurationManager.AppSettings["facebook:AppId"],
    AppSecret = ConfigurationManager.AppSettings["facebook:AppSecret"]
};

app.UseFacebookAuthentication(facebookOptions);

In the above code I am accessing the access_token by context.AccessToken and add it to Claims of the current loggedin user.

To access this value later you need to do this:

var owinContext = HttpContext.GetOwinContext();
var authentication = owinContext.Authentication;
var user = autentication.User;
var claim = (user.Identity as ClaimsIdentity).FindFirst("urn:facebook:access_token");

string accessToken;
if (claim != null)
    accessToken = claim.Value;

To simplify all of this, you can create a BaseController and make all your Controllers inherit from it.

The BaseController code would be:

public class BaseController : Controller
{
    public IOwinContext CurrentOwinContext
    {
        get
        {
            return HttpContext.GetOwinContext();
        }
    }

    public IAuthenticationManager Authentication
    {
        get
        {
            return CurrentOwinContext.Authentication;
        }
    }

    public new ClaimsPrincipal User
    {
        get
        {
            return Authentication.User;
        }
    }

    public ClaimsIdentity Identity
    {
        get
        {
            return Authentication.User.Identity as ClaimsIdentity;
        }
    }

    public string FacebookAccessToken
    {
        get
        {
            var claim = Identity.FindFirst("urn:facebook:access_token");

            if (claim == null)
                return null;

            return claim.Value;
        }
    }
}

Then to get the access token on your code you just need to access the property FacebookAccessToken.

string accessToken = FacebookAccessToken;

It is possible to retrieve some other values as

context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:username",
    context.User.Value<string>("username"), ClaimValueTypes.String, "Facebook"));

context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:name",
    context.User.Value<string>("name"), ClaimValueTypes.String, "Facebook"));

Note that not all fields will be available, to get the email you need to require the Scope email.

facebookOptions.Scope.Add("email");

Then access on the OnAuthenticated event as

context.User.Value<string>("email");

Solution 5

Here are some steps which will help you. I am in the process of writing up a blog post but it will take a while... - Add Scopes in Fb provider and add the data returned from FB as claims

app.UseFacebookAuthentication(new FacebookAuthenticationOptions()
        {
            AppId = "",
            AppSecret = "",
            //Scope = "email,user_about_me,user_hometown,friends_about_me,friends_photos",
            Provider = new FacebookAuthenticationProvider()
            {
                OnAuthenticated = async context =>
                {
                    foreach (var x in context.User)
                    {
                        context.Identity.AddClaim(new System.Security.Claims.Claim(x.Key, x.Value.ToString()));
                    }
                    //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));
                }
            },
            SignInAsAuthenticationType = "External",
        });         
  • Use the Access Token and call Facebook C# SDK to get the list of friends for the user

        var claimsIdentity = HttpContext.User.Identity as ClaimsIdentity;
        var access_token = claimsIdentity.FindAll("FacebookAccessToken").First().Value;
        var fb = new FacebookClient(access_token);
        dynamic myInfo = fb.Get("/me/friends");
        var friendsList = new List<FacebookViewModel>();
        foreach (dynamic friend in myInfo.data)
        {
            friendsList.Add(new FacebookViewModel() { Name = friend.name, ImageURL = @"https://graph.facebook.com/" + friend.id + "/picture?type=large" });
            //Response.Write("Name: " + friend.name + "<br/>Facebook id: " + friend.id + "<br/><br/>");
        }
    
Share:
34,344
Zelleriation
Author by

Zelleriation

First the serious things: I love MSX computers and the old classic Renault Twingo (the 1993-2007 series only; the newer models are as boring as all other cars). And now the boring stuff. I am a telecommunication engineer working as a software developer, mainly in C#. I have done a lot of Windows Forms development in the past, then WPF and Silverlight, but currently I am more in the web arena (mostly backend at first, now I'm in the frontend world too). When I have free time I like When I had free time I liked to program my MSX Turbo-R in Z80 assembler. In fact I have even developed a full TCP/IP stack for MSX (the source code is available, feel free to take a look!), which I presented as my master thesis in the university, with quite good results. See my software projects here: http://www.konamiman.com http://github.com/konamiman And feel free to contact me at [email protected]

Updated on June 08, 2020

Comments

  • Zelleriation
    Zelleriation almost 4 years

    I am developing a web site in ASP.NET MVC 5 (using RC1 version currently). The site will use Facebook for user authentication and for retrieving initial profile data.

    For the authentication system I am using the new OWIN based ASP.NET Identity engine (http://blogs.msdn.com/b/webdev/archive/2013/07/03/understanding-owin-forms-authentication-in-mvc-5.aspx), since it greatly simplifies the process of authenticating with external providers.

    The problem is that once a user first logs in, I want to get its email address from the Facebook profile, but this data is not included in the generated claims. So I have thought on these alternatives to get the address:

    1. Instruct the ASP.NET Identity engine to include the email address in the set of data that is retrieved from Facebook and then converted to claims. I don't know if this is possible at all.

    2. Use the Facebook graph API (https://developers.facebook.com/docs/getting-started/graphapi) to retrieve the email address by using the Facebook user id (which is included in the claims data). But this will not work if the user has set his email address as private.

    3. Use the Facebook graph API, but specifying "me" instead of the Facebook user id (https://developers.facebook.com/docs/reference/api/user). But an access token is required, and I don't know how to (or if it's possible at all) retrieve the access token that ASP.NET uses to obtain the user data.

    So the question is:

    1. How can I instruct the ASP.NET Identity engine to retrieve additional information from Facebook and include it in the claims data?

    2. Or alternatively, how can I retrieve the generated access token so that I can ask Facebook myself?

    Thank you!

    Note: for the authentication system my application uses code based on the sample project linked in this SO answer: https://stackoverflow.com/a/18423474/4574

  • Zelleriation
    Zelleriation over 10 years
    Thank you! The trick is to invoke facebookOptions.Scope.Add("email"), then as you say the email data is automatically added as a claim without having to parse the json data. However I have now a new problem: when using your code, invoking AuthenticationManager.GetExternalIdentity in the callback will return null instead of an instance of ClaimsIdentity. Do you have an idea of what can be happening? (and yes, I am adding the proper app id and secret to the facebookOptions object)
  • Zelleriation
    Zelleriation over 10 years
    Ok, found the solution investigating from here: forums.asp.net/p/1927914/…. The following must added to the facebookOptions object initialization: SignInAsAuthenticationType = "External". I will accept your answer but maybe you want to edit it so that it includes these tricks. :-)
  • Santosh
    Santosh over 10 years
    Can you please share sample code to show how you retrieved additional information (say BirthDate) inside callback actionresult task?
  • Praburaj
    Praburaj over 10 years
    Try adding the scope 'user_birthday' in the options.Scope.Add("..") call and check if you get the information. Profile scope information can be looked here: developers.facebook.com/docs/reference/login/…. Facebook high level scope information here: developers.facebook.com/docs/reference/login
  • Santosh
    Santosh over 10 years
    No, I did add it to the scope but I don't know how to retrieve it inside the callback so that it can be passed to ExternalLoginConfirmation view. I need that sample.
  • Praburaj
    Praburaj over 10 years
    The context.User object in the above sample code is a JObject returned from facebook (contains all the information returned by facebook). You will have to retrieve the extra information like birth date from this JObject yourself. Consult JObject documentation on how to retrieve specific fields from the JObject.
  • Piotr Stulinski
    Piotr Stulinski over 10 years
    i followed your sample, but claimsIdentity = HttpContext.User.Identity as ClaimsIdentity only contains the LOCAL claim and not the facebook ones. :~/
  • Piotr Stulinski
    Piotr Stulinski over 10 years
    When i add the object they are successfully added to the claims... but when i am redirected back to my registration page. The Claims are no longer there, only the local claim is there. How do we access the claims later on?
  • Praburaj
    Praburaj over 10 years
    Can you try HttpContext.Current.GetOwinContext().Authentication.User?
  • John Palmer
    John Palmer over 10 years
    I've posted a detailed walkthrough of how to access the external claims data after sign-in here: stackoverflow.com/questions/19456008/…
  • subsci
    subsci over 10 years
    Great so how do you get user email from MS Live external login with scopes, "wl.basic" and "wl.emails"?
  • PussInBoots
    PussInBoots over 10 years
    What does (context) => means on this line: OnAuthenticated = (context) => Is it shorhand syntax for something else?
  • creatiive
    creatiive over 9 years
    When I try this and add the 'accesstoken' claim, the subsequent call to User.Identity.GetUserId() method returns null. Just adding 1 claim seems to break the User.Identity object...
  • Furynation
    Furynation over 9 years
    @PeterStulinski I'm a little late here, but I was facing the same issue. The problem in this example is the SignInAuthenticationType = "External" line. "External" is not a valid string for external auth providers. Change it to "ExternalCookie" (or the typed DefaultAuthenticationTypes.ExternalCookie item) and you should start seeing the claims populated correctly.
  • Rony Tesler
    Rony Tesler over 9 years
    you can use this for the second code snippet: var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(); string email = loginInfo.Email;
  • Andrey
    Andrey over 8 years
    I added a Scope "email" but the context.User doesn't have email (it only contains user and id), and context.Email is empty as well. Any ideas why?
  • Giox
    Giox about 8 years
    The above solution doesn't work anymore. Only Name and Id are retrieved
  • Mist
    Mist about 6 years
    i am working with mvc5 and i want to get user name after fb auth. would you please give me some direction. thanks