31

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

Community
  • 1
  • 1
Konamiman
  • 49,681
  • 17
  • 108
  • 138

6 Answers6

29

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.

John Palmer
  • 1,032
  • 1
  • 9
  • 23
  • 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... – creatiive Nov 12 '14 at 15:39
24

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?

Martin Booth
  • 8,485
  • 31
  • 31
Praburaj
  • 11,417
  • 1
  • 23
  • 20
  • 4
    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) – Konamiman Sep 22 '13 at 21:09
  • 1
    Ok, found the solution investigating from here: http://forums.asp.net/p/1927914/5516103.aspx?p=True&t=635154678323993438. 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. :-) – Konamiman Sep 22 '13 at 21:33
  • 1
    Can you please share sample code to show how you retrieved additional information (say BirthDate) inside callback actionresult task? – Santosh Oct 04 '13 at 13:02
  • 2
    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: https://developers.facebook.com/docs/reference/login/extended-profile-properties/. Facebook high level scope information here: https://developers.facebook.com/docs/reference/login/ – Praburaj Oct 04 '13 at 14:41
  • 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. – Santosh Oct 05 '13 at 06:59
  • 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. – Praburaj Oct 05 '13 at 14:46
  • 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? – Piotr Stulinski Oct 21 '13 at 08:04
  • I've posted a detailed walkthrough of how to access the external claims data after sign-in here: http://stackoverflow.com/questions/19456008/how-do-i-access-microsoft-owin-security-xyz-onauthenticated-context-addclaims-va – John Palmer Oct 23 '13 at 18:14
  • What does (context) => means on this line: OnAuthenticated = (context) => Is it shorhand syntax for something else? – PussInBoots Dec 05 '13 at 13:10
  • 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? – Andrey Dec 21 '15 at 16:22
  • The above solution doesn't work anymore. Only Name and Id are retrieved – Giox Mar 09 '16 at 18:48
22

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;
Julien
  • 833
  • 8
  • 20
  • 3
    you can use this for the second code snippet: var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(); string email = loginInfo.Email; – Rony Tesler Nov 25 '14 at 14:57
  • i am working with mvc5 and i want to get user name after fb auth. would you please give me some direction. thanks – Mist Apr 19 '18 at 11:21
5

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");
BrunoLM
  • 97,872
  • 84
  • 296
  • 452
2

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/>");
        }
    
friism
  • 19,068
  • 5
  • 80
  • 116
pranav rastogi
  • 4,124
  • 23
  • 23
  • i followed your sample, but claimsIdentity = HttpContext.User.Identity as ClaimsIdentity only contains the LOCAL claim and not the facebook ones. :~/ – Piotr Stulinski Oct 20 '13 at 22:41
  • Can you try HttpContext.Current.GetOwinContext().Authentication.User? – Praburaj Oct 21 '13 at 14:45
  • Great so how do you get user email from MS Live external login with scopes, "wl.basic" and "wl.emails"? – subsci Nov 21 '13 at 07:56
  • @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. – Furynation Nov 19 '14 at 03:56
0

my couple cents to all answers... if you still want to ask Facebook yourself, it makes sense to take a look on already existing Facebook package. It provides some great functionality that already implemented, so you don't have to re-implement it yourself... some example how to use it inside ASP.NET MVC application you can find here.

Mr. Pumpkin
  • 6,212
  • 6
  • 44
  • 60