Click here to Skip to main content
15,867,835 members
Articles / Web Development / HTML
Tip/Trick

MVC and Identity Framework 2.0 (Part 5)

Rate me:
Please Sign up or sign in to vote.
4.70/5 (14 votes)
9 Mar 2015CPOL11 min read 28.7K   256   27   13
In the past articles, we could see how a website with Identity Framework 2 is set up and customized to our own needs. We could also implement Facebook Login, Google Auth and Twitter Login. But in the real world, we need much more than that. In the current article, we will learn how to capture data.

Introduction

After setting up our project to work with IF2.0 and enable Facebook and Google authentication (yes, we also enabled Twitter but I don't really use it too much and I disabled it in most of my projects), we need to move forward and be able to get information from those services. There are a lot of new things we can do acquiring data from them, but we will just go through how it works and we will learn to capture data and save it using a standard procedure already implemented on IF 2.0. Of course, you can extend it as much as you need for your project but I believe this article is a good kick-off.

If you missed any of the previous articles and you need more help before starting with IF 2.0, find below the list of articles of this series:

Understanding Scopes

The default project created after installing IF 2.0 has a very useful link on the home page to start playing a bit with getting additional data.

Get more information from Social Providers

There's no introduction and you will see a code sample that works and it's easy to implement. We will see now a brief introduction about scopes.

Scopes

A scope is a subset of data you want to get from a Social Provider. Most of the major social platforms nowadays handle the same concept. The basic scope is called "Public Profile" and it's not necessary to request it. If we do nothing on the project to get more scopes, we will get a default scope with public data. Depending on the Social Provider, you will see they have different recommendations, Facebook for example indicates that even when it's not necessary to request public access, it's better to do it. In this case, you should add "public_profile" to the list of requested scopes, in this case it's redundant but it's a recommendation. We won't include it anyway, feel free to do it yourself.

The public scope changes depending on the provider, for example, Facebook public profile gives you the following information:

  • id
  • name
  • first_name
  • last_name
  • link
  • gender
  • locale
  • timezone
  • updated_time
  • verified

It's very important to understand what a public profile means, it's the set of data that's public, accessible for every application installed and it doesn't require extra revisions from the provider. This is very important, when you request more permissions Facebook, Google and any other provider can review your app to understand why you need this information. Some of them set off alarms instantly, like Age. There're many reasons to review your application if you require users's age, the first reason is age is sensitive data. If you need that information, you can do it yourself in your own website and Facebook, Google and so forth are not responsible for that. But there's something interesting too, if you require more and more information, you could use the provider to gather and analyze population with commercial or other intentions. You would be competing with them or even using them with non-clear intentions. So they're going to deny nearly anything that requires too much information. Keep it in mind at the moment of designing your connectors to social providers.

The scope is directly related to the provider and you need to review the documentation of Facebook, Google, Twitter, LinkedIn in order to understand what kind of information they can provide you. They also indicate nearly always if your application will go through a review if you request some data.

Providers Documentation

Note: Be careful about Twitter, they're going through a major change at the time of writing this article switching to Fabric.io, a totally new platform and it could change drastically most of the services. I guess they're not going to cancel everything so fast but we never know.

Claiming for More Information

I've already mentioned we don't need to do anything if we need public profile data. Just review the facebook public profile and you will see we don't have the email in the list and it's something we use a lot. To receive the email, we need to "claim" for "email scope". This is a simple case because the scope is the same name of the claim, but just check this claim "friends_about_me" that let you browse friends information. The scope is a set of data you can browse later, not only some specific data.

C#
var facebookOptions = new FacebookAuthenticationOptions();
facebookOptions.AppId = ExternalLoginConfig.FacebookAppId;
facebookOptions.AppSecret = ExternalLoginConfig.FacebookAppSecret;
facebookOptions.Scope.Add("email");

I've rearranged a bit the code of the previous article, because it's better to visualize the new features we're adding to the project. I create an object containing the options now, set app and secret id (they're now injected through a configuration in my case so I can handle it easily) and request the email scope.

Now I will explain something else that's important. When we receive claims (public and customized ones), we get the whole name including the namespace. Sometimes it's easy to read that data, sometimes it's a bit messy, so I added some code to standardize names on every single Provider. It's also easy to identify them later, since the claims that are not connected to a login we could get all of them assorted and it's better to know the root of every claim so we know if they belong to a specific provider (easily of course).

To achieve this, I browse every child of User in the facebook context, check if the identity has this claim and add it using a common root (urn:facebook:{property})

Here's the code:

C#
facebookOptions.Provider = new FacebookAuthenticationProvider()
{
    OnAuthenticated = async facebookContext =>
    {
        // Save every additional claim we can find in the user
        foreach (JProperty property in facebookContext.User.Children())
        {
            var claimType = string.Format("urn:facebook:{0}", property.Name);
            string claimValue = (string)property.Value;
            if (!facebookContext.Identity.HasClaim(claimType, claimValue))
                facebookContext.Identity.AddClaim(new Claim(claimType, claimValue, 
                      "http://www.w3.org/2001/XMLSchema#string", "External"));
            }
        }
};

The facebook provider triggers OnAuthenticated method when any user is authenticated. In the context we receive the authenticated user, then we can browse every property, detect if it's a claim and set a common name with a base name. You don't need to do it, but check this code because it's at least interesting to see how the architecture is designed inside of the context.

This code will end up having the following list of claims received from Facebook:

  • urn:facebook:link
  • urn:facebook:id
  • urn:facebook:email
  • urn:facebook:first_name
  • urn:facebook:gender
  • urn:facebook:last_name
  • urn:facebook:locale
  • rn:facebook:timezone
  • urn:facebook:updated_time
  • urn:facebook:verified

Pretty, isn't it? :)

Reading Claims

We already added code to our project claiming for more information, but we have to do something with that. First of all, we need to read it of course.

Where to read it?

We will have this information available in the External Login callback method, this is the method called by the framework after a user is authenticated, accepted permissions, installed the application and so forth. Once everything is ready, the framework returns to our application providing everything about the user authenticated.

C#
public async Task<ActionResult> ExternalLoginCallback(string returnUrl) { ...

Part of the code is already set, but we need to add some extra code to get the claim information. The default project sets the user identity object:

C#
var externalUserIdentity = HttpContext.GetOwinContext().Authentication.GetExternalIdentityAsync
                           (DefaultAuthenticationTypes.ExternalCookie);

All the claims are contained within the external identity.

C#
var claims = externalUserIdentity.Result.Claims;

Now we just need to read every claim, the code is easy to understand:

C#
var facebookFirstNameClaim = claims.FirstOrDefault(c => c.Type.Equals("urn:facebook:first_name"));

This is the claim itself, the object, it could be null if there's not any claim with this name. It can happen since we handle all the claims for every provider in the same code, so imagine you logged in with Google, there won't be any claim belonging to facebook. There's not a different process per provider so we need to handle it here, checking there's not some public claim for facebook tell us this is not a facebook login.

That's why you always need to check for null objects:

C#
if (facebookFirstNameClaim != null)
{
    // It's a facebook account, read facebook data
    string firstName = facebookFirstNameClaim.Value;
    ...
}

Google Auth 2.0

Google works pretty much the same and it's not a big deal to modify the code to make it work. The difference is the public profile data. I've added two more scopes in my case:

C#
googleAuthenticationOptions.Scope.Add("https://www.googleapis.com/auth/plus.login");
googleAuthenticationOptions.Scope.Add("https://www.googleapis.com/auth/userinfo.email");

Find below the list of claims obtained:

  • urn:google:profile
  • urn:google:sub
  • urn:google:name
  • urn:google:given_name
  • urn:google:family_name
  • urn:google:picture
  • urn:google:email
  • urn:google:email_verified
  • urn:google:gender
  • urn:google:locale

Remember I rename every claim name for organization purposes, here's the code to do it:

C#
var googleAuthenticationOptions = new GoogleOAuth2AuthenticationOptions
{
    ClientId = ExternalLoginConfig.GoogleClientId,
    ClientSecret = ExternalLoginConfig.GoogleClientSecret,
    Provider = new GoogleOAuth2AuthenticationProvider()
    {
        OnAuthenticated = async googleContext =>
        {
            string profileClaimName = string.Format("urn:google:{0}" "profile");
            foreach (JProperty property in googleContext.User.Children())
            {
                var claimType = string.Format("urn:google:{0}", property.Name);
                string claimValue = (string)property.Value;
                if (!googleContext.Identity.HasClaim(claimType, claimValue))
                googleContext.Identity.AddClaim(new Claim(claimType, claimValue, 
                      "http://www.w3.org/2001/XMLSchema#string", "External"));
            }
        }
    }
};

Twitter

Twitter is the less useful provider, despite being able to login using the service, there's nothing else we can obtain from the platform. In fact, there's no user in the context, we don't have more information to pull.

You can retrieve only two useful values from the context, the access token and the access token secret value, in case you want to implement something else later consuming data from Twitter through the user. You will need that data for example to get the image (at the time being the great Twitter project for .NET is abandoned and there're more than one branch with different changes).

Using and Saving Data

There're two different ways of using claims data and they depend on the way you want to handle your own developments.

Once you capture data from the External Login callback, you can just use it in your own model. In my case, I extended the ExternalLoginConfirmationViewModel class and added more properties (FirstName, LastName, etc.). I capture data from the claim and set my value, period, it helps me to create a user later with more information from the external provider.

The second option is using the generic repository that IF 2.0 has built-in.

To persist this information using the framework, we don't need to add anything to the external login callback, unless we do something with that data (like showing the first name, gender or something like that). We can persist everything in the post method that confirms the external login:

C#
public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, 
   string returnUrl) { ...

The user saving code in this method looks like that when it's created:

C#
...

if (ModelState.IsValid)
{
    // Get the information about the user from the external login provider
    var info = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        return View("ExternalLoginFailure");
    }
    var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
    var result = await UserManager.CreateAsync(user);
    if (result.Succeeded)
    {
        result = await UserManager.AddLoginAsync(user.Id, info.Login);
        if (result.Succeeded)
        {
            await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
            return RedirectToLocal(returnUrl);
        }
}

...

To be able to save claims data without using our own tables just relying on the framework itself, we need to get data in this method and save it. Honestly, it doesn't make too much sense since this data is obviously part of our application but there're some cases where this is necessary. We won't discuss much more about it, I will show you how to do it and you will decide yourself what's the best scenario in case you need to use it.

First, look at the code, we don't have the external identity here, so we will need to get it before saving anything.

Second, we cannot save anything before the user is saved, so our code will be included after the AddLoginAsync has been confirmed. To make the code more clear, the claims saving part is included in a new method that receives the user as a parameter.

That idea was taken from the following article:

Unfortunately, that article is obsolete because the Twitter Framework for .NET doesn't work anymore. At least until some good samaritan starts maintaining the project again.

So our code will be just adding one more call to a method:

C#
...

if (ModelState.IsValid)
{
    // Get the information about the user from the external login provider
    var info = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        return View("ExternalLoginFailure");
    }
    var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
    var result = await UserManager.CreateAsync(user);
    if (result.Succeeded)
    {
        result = await UserManager.AddLoginAsync(user.Id, info.Login);
        if (result.Succeeded)
        {
            await StoreClaims(user); // Save claims
            await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
            return RedirectToLocal(returnUrl);
        }
}

...

We just need to identify what kind of claim we want to save. In the example, I choose every claim that's related to Twitter. Remember I've renamed all the claims to a very specific format "urn:{provider}:" so it's easy to identify them. You can save whatever you want:

C#
private async Task StoreClaims(ApplicationUser user)
{
    // Get the claims identity
    ClaimsIdentity claimsIdentity = await 
        AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);

    if (claimsIdentity != null)
    {
        // Retrieve the existing claims
        var currentClaims = await UserManager.GetClaimsAsync(user.Id);

        // Get the list of twitter claims
        var tokenClaims = claimsIdentity.Claims
            .Where(c => c.Type.StartsWith("urn:twitter:"));

        // Save claims
        foreach (var tokenClaim in tokenClaims)
        {
            if (!currentClaims.Contains(tokenClaim))
            {
                await UserManager.AddClaimAsync(user.Id, tokenClaim);
            }
        }
    }
}

The key is in the bolded code. We use the UserManager built-in to add a claim. The data goes to the UserClaim table used by the framework as a dictionary "ClaimType, ClaimValue". We don't need to set anything extra in our project and let the framework handle this data. But in my experience, it's not so useful, we cannot validate nearly anything in that dictionary, it's not type-safe and I found few cases that I could need to use the method.

But it worth learning!

Conclusion

I really enjoyed writing this series of articles. I'm still thinking what's the next topic I could write about Identity Framework 2.0 and MVC so if you have any suggestions, just let me know. I will update the articles with code soon when my daughter lets me do it!

We covered (I think) most of the main aspects of IF 2.0, it's a great framework and incredible useful. It's powerful and extensible, I have to thank the people that worked on that because it's a great job.

See you all soon!

Clean Project

I've added a clean project that works, replacing of course my services keys. My original code is not exactly like that, every claim and configuration is injected through a Factory pattern into the StartUp but I think you can refactor it as you feel more comfortable.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Argentina Argentina
Software developer, amateur writer, sports fan. I like more travelling than breathing Smile | :) I play any existing sport on earth but I love soccer (forever). I'm working on bigdata now and the topic and future of this is more than exciting.

Comments and Discussions

 
GeneralMy vote of 4 Pin
Luis M. Teijón29-Oct-16 6:17
Luis M. Teijón29-Oct-16 6:17 
QuestionMuchas Gracias y Pregunta Pin
DavikSeg26-Oct-15 3:37
DavikSeg26-Oct-15 3:37 
AnswerRe: Muchas Gracias y Pregunta Pin
Maximiliano Rios29-Nov-15 5:06
Maximiliano Rios29-Nov-15 5:06 
QuestionThanks & a typical question Pin
devimurugan23-Mar-15 11:11
devimurugan23-Mar-15 11:11 
AnswerRe: Thanks & a typical question Pin
Maximiliano Rios24-Mar-15 4:41
Maximiliano Rios24-Mar-15 4:41 
QuestionloginInfo always returns null Pin
Member 114924462-Mar-15 8:47
Member 114924462-Mar-15 8:47 
AnswerRe: loginInfo always returns null Pin
Maximiliano Rios2-Mar-15 8:54
Maximiliano Rios2-Mar-15 8:54 
GeneralRe: loginInfo always returns null Pin
Member 114924462-Mar-15 11:18
Member 114924462-Mar-15 11:18 
GeneralRe: loginInfo always returns null Pin
Maximiliano Rios3-Mar-15 3:22
Maximiliano Rios3-Mar-15 3:22 
GeneralRe: loginInfo always returns null Pin
Member 114924463-Mar-15 3:53
Member 114924463-Mar-15 3:53 
GeneralMy vote of 5 Pin
Humayun Kabir Mamun11-Feb-15 19:27
Humayun Kabir Mamun11-Feb-15 19:27 
GeneralRe: My vote of 5 Pin
Maximiliano Rios11-Feb-15 23:46
Maximiliano Rios11-Feb-15 23:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.