14

I'm trying to retrieve user properties that are returned as the OnAuthenticated context and added as a claims following this example: How to access Facebook private information by using ASP.NET Identity (OWIN)?

I can see that data I am expecting is being returned at login and is being added as a Claim within Starup.Auth.cs. But, when I am within the Account Controller, the only claims that appears within the UserManager or UserStore is issued by LOCAL AUTHORITY. No claims can be found for Facebook (or other external providers). Where do the claims added to context end up? (I'm using VS2013 RTM.)

Full source and live site on Azure linked here: https://github.com/johndpalm/IdentityUserPropertiesSample/tree/VS2013rtm

Here is what I have in Startup.Auth.cs:

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) =>
            {
                const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string";
                foreach (var x in context.User)
                {
                    var claimType = string.Format("urn:facebook:{0}", x.Key);
                    string claimValue = x.Value.ToString();
                    if (!context.Identity.HasClaim(claimType, claimValue))
                        context.Identity.AddClaim(new System.Security.Claims.Claim(claimType, claimValue, XmlSchemaString, "Facebook"));

                }
                context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, XmlSchemaString, "Facebook"));
                return Task.FromResult(0);
            }
    }

};

facebookOptions.Scope.Add("email");

app.UseFacebookAuthentication(facebookOptions);

An alternative way to capture the external login properties would be to add a single claim for the access token and populate it with properties:

const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string";
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) =>
        {
            var claim = new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, XmlSchemaString, "Facebook");
            foreach (var x in context.User)
            {
                string key = string.Format("urn:facebook:{0}", x.Key);
                string value = x.Value.ToString();
                claim.Properties.Add(key, value);
            }

            context.Identity.AddClaim(claim);

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

NOTE - This sample does not work: Though it would be nice to pass a single claim with properties. The external cookie seems to note honor the claims properties. The properties are empty when retrieving them later from the identity.

Community
  • 1
  • 1
John Palmer
  • 1,032
  • 1
  • 9
  • 23

3 Answers3

16

I was able to create a working example, using MVC 5 RTM templates, OWIN, and ASP.NET Identity bits. You can find the complete source and a link to a live working example here: https://github.com/johndpalm/IdentityUserPropertiesSample

Here's what worked for me:

Create a new (insert provider name here) AuthenticationOptions object in Startup.ConfigureAuth (StartupAuth.cs), passing it the client id, client secret, 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.

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"));
                    foreach (var x in context.User)
                    {
                        var claimType = string.Format("urn:facebook:{0}", x.Key);
                        string claimValue = x.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);
}

NOTE: The Facebook auth provider works with the code used here. If you use this same code with the Microsoft Account provider (or Foursquare provider I created using the MS account code as a model), it fails to login. If you select just the access_token parameter, it works fine. Seems like some parameters break the login process. (An issue has been opened on katanaproject.codeplex.com if progress on this is of interest to you.) I'll update if I find the cause. I didn't do much with Twitter or Google beyond verifying that I could get the access_token.

var msaccountOptions = new Microsoft.Owin.Security.MicrosoftAccount.MicrosoftAccountAuthenticationOptions() 
{
    ClientId = ConfigurationManager.AppSettings.Get("MicrosoftClientId"),
    ClientSecret = ConfigurationManager.AppSettings.Get("MicrosoftClientSecret"),
    Provider = new Microsoft.Owin.Security.MicrosoftAccount.MicrosoftAccountAuthenticationProvider()
    {
        OnAuthenticated = (context) =>
            {
                context.Identity.AddClaim(new System.Security.Claims.Claim("urn:microsoftaccount:access_token", context.AccessToken, XmlSchemaString, "Microsoft"));
                return Task.FromResult(0);
            }
    }                   
};

app.UseMicrosoftAccountAuthentication(msaccountOptions);

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>
}

Again, 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
  • I'm getting a Google 404 error code when using your code. From what I can tell Google doesn't support this anymore, oauth2. – Tom McDonald Aug 21 '16 at 15:04
1

So this article explains how this all works pretty well: Decoupling owin external auth

But the short answer is, when you get authenticated from facebook, that is giving you an external identity. You then need to take that external identity and 'sign in' a local app identity, its in that stepthat you need to add any claims you want from the external identity to the ClaimsIdentity that becomes User.Identity.

Edit: To clarify further, you could do it inside of ExternalLoginCallback:

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

        // Sign in this external identity if its already linked
        var user = await UserManager.FindAsync(loginInfo.Login);
        if (user != null) {
            await SignInAsync(user, isPersistent: false);
            return RedirectToLocal(returnUrl);
        }

    private async Task SignInAsync(ApplicationUser user, bool isPersistent) {
        AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
        var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
        AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
    }

So you will need to pass in extra data to the SignIn, which will look something like this:

   ClaimsIdentity id = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);

This ClaimsIdentity will have your added claim, and you will need to add that claim to the identity created in the SignInAsync method for it to show up.

Hao Kung
  • 28,040
  • 6
  • 84
  • 93
  • Hao, that is my understanding... When I debug in ExternalLoginCallback I don't see any claims but "LOCAL AUTHORITY" in AuthenticationManager (or UserManager). Where should I see the claims I added in the code above? – John Palmer Oct 19 '13 at 20:26
  • I have exactly the same problem. I am adding the claims to and i can see them being added, but when i try read them later on from the Context all i see is a single LOCAL AUTHORITY claim :/ – Piotr Stulinski Oct 20 '13 at 21:40
  • The only way i have managed to get any bit of info from Startup.Auth class is via a cookie which is just horrible and would need to be encrypted. Session is not available and the AddClaim method is empty. Adding an Encrypted cookie every time just seems to wrong. – Piotr Stulinski Oct 21 '13 at 10:07
  • @peter-stulinski, it is already using a cookie for storing external auth information. You should see some thing like this in Startup.Auth.cs: app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); – John Palmer Oct 21 '13 at 17:50
  • @hao, your "to clarify" is just a copy of those methods in the default Account Controller methods. What does that clarify? – John Palmer Oct 22 '13 at 03:45
  • @John - no what i mean is that the only means that i have of getting data from external provider (email/address or even the token) back to my "registration" page to use is to store it in additional cookies. Storing it in AddClaim (does nothing and is removed), Session is not available in OnAuthenticated, in fact putting it almost anywhere in a response get its eliminated. Im fascinated that it works in AddClaim for some people and not for others. – Piotr Stulinski Oct 22 '13 at 08:26
  • The point I was trying to make is that, the Claimsidentity you are adding to in OnAuthenticated is the Claimsidentity that comes out of a call to GetExternalIdentity, and you need to add any custom claims you want to the Claimsidentity you are creating in the SignIn method. – Hao Kung Oct 22 '13 at 16:45
  • @hao, thanks for your input. It helped shed some light on what's going on and led me to the answer posted above. – John Palmer Oct 23 '13 at 16:33
  • @Peter, check out my answer above. It uses both an external cookie (pre-login) and an application cookie (post-login) but the cookie mechanics are built into ASP.NET Identity. I'm also working on extending my GitHub example to persist some of the claims data in the AspNetUserClaims table rather than the application cookie. The short answer is that UserManager uses the EF DB and ApplicationManager uses the app cookie. – John Palmer Oct 23 '13 at 16:39
  • @John - thanks downloaded your sample already and played around with it (thanks for find the solution) Dunno how you found that, this framework is so great, pity the docs are :(((. – Piotr Stulinski Oct 26 '13 at 19:08
  • When I add the claim to the identity used for signing in, whenever I next view User.Identitty (on the next API call) the Identity object is mostly empty and it breaks. If I don't add a claim, it works normally. How do I add the claim..? – creatiive Nov 12 '14 at 17:08
0

In short the line that is required once AddClaim is used is as follows:

Taken from johns answer above.

ClaimsIdentity ext = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
Piotr Stulinski
  • 9,241
  • 8
  • 31
  • 46