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.
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:
facebookOptions.Provider = new FacebookAuthenticationProvider()
{
OnAuthenticated = async facebookContext =>
{
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.
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:
var externalUserIdentity = HttpContext.GetOwinContext().Authentication.GetExternalIdentityAsync
(DefaultAuthenticationTypes.ExternalCookie);
All the claims are contained within the external identity.
var claims = externalUserIdentity.Result.Claims;
Now we just need to read every claim, the code is easy to understand:
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:
if (facebookFirstNameClaim != null)
{
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:
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:
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:
public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model,
string returnUrl) { ...
The user saving code in this method looks like that when it's created:
...
if (ModelState.IsValid)
{
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:
...
if (ModelState.IsValid)
{
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);
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:
private async Task StoreClaims(ApplicationUser user)
{
ClaimsIdentity claimsIdentity = await
AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
if (claimsIdentity != null)
{
var currentClaims = await UserManager.GetClaimsAsync(user.Id);
var tokenClaims = claimsIdentity.Claims
.Where(c => c.Type.StartsWith("urn:twitter:"));
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.