Introduction
In this article you can find an example of the code, demo and explanation of how to authenticate users through social networks (Facebook, Google and Microsoft). How to extract an additional information about a user such as avatars, emails and full names.
You can view the demo and download the latest version of the code on my website: SupperSlonic.com
Basic concepts
Owin Middleware
The Owin Middleware modules are responsible for handling the authentication with external authentication providers (such as Facebook, Google e.t.c.) and establishing an application session through a cookie. On all subsequent calls the application cookie middleware extracts the contents of the incoming application cookie and sets the claims identity of the current context.
What is a token?
A token is a string value that represents an encrypted list of System.Security.Claims
. You can use any claims you like. My application has its own defined list of claims used across the project. Let's call them Application Claims.
Application Claims is a well-defined list of claims encrypted by OWIN into the issued token. Each time a service receives a token, it tries to decrypt it with its IIS machine key back into the list of claims and populate a User.Identity object.
To determine either it is an External Bearer token or a local one it checks the Issuer field of the claims. For a local one it must be always ClaimsIdentity.DefaultIssuer
.
Note, that for the encryption OWIN uses the IIS machine key, that's why you must apply some custom solution for using the same token across several WEB-services.
Here is how I build my Application Claims list:
private static ClaimsIdentity CreateIdentity(ClaimsMapper claimsMapper, string authenticationType)
{
IList<claim> claims = new List<claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, claimsMapper.Id, null, claimsMapper.Issuer, claimsMapper.OriginalIssuer));
claims.Add(new Claim(ClaimTypes.Email, claimsMapper.Email, null, claimsMapper.Issuer, claimsMapper.OriginalIssuer));
claims.Add(new Claim(ClaimTypes.GivenName, claimsMapper.FullName, null, claimsMapper.Issuer, claimsMapper.OriginalIssuer));
claims.Add(new Claim(ClaimTypes.Sid, claimsMapper.Sid, null, claimsMapper.Issuer, claimsMapper.OriginalIssuer));
claims.Add(new Claim(ClaimTypes.Version, claimsMapper.Version, null, claimsMapper.Issuer, claimsMapper.OriginalIssuer));
claims.Add(new Claim(ClaimTypeIsVerified, claimsMapper.IsVerified, null, claimsMapper.Issuer, claimsMapper.OriginalIssuer));
claims.Add(new Claim(ClaimTypeAvatarUrl, claimsMapper.AvatarUrl, null, claimsMapper.Issuer, claimsMapper.OriginalIssuer));
return new ClaimsIdentity(claims, authenticationType);
}
</claim></claim>
So, I can always get all this information from any token issued by my WEB-service. ClaimsMapper
is an abstract strategy that knows how to map different data models to my claims list.
Authentication flows
OAuth2 authentication
How does an application get a user information from Facebook, Google e.t.c.?
All the information about a user received from an external provider (Facebook, Google e.t.c.) is encrypted in an external cookie.
Here is the description of the communication flow between an application and an external provider:
[AllowAnonymous]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("externalLogin", Name = "externalLogin")]
public async Task<ihttpactionresult> GetExternalLogin(string provider, string error = null)
{
if (error != null)
{
return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
}
ExternalLoginProvider loginProvider;
if (!Enum.TryParse<externalloginprovider>(provider, ignoreCase: true, result: out loginProvider) ||
loginProvider == ExternalLoginProvider.None)
{
return InternalServerError();
}
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(loginProvider, this);
}
ExternalLoginModel externalLogin = ExternalLoginModel.FromIdentity(User.Identity as ClaimsIdentity);
if (externalLogin == null)
{
return InternalServerError();
}
if (externalLogin.Provider != loginProvider)
{
Request.GetOwinContext().Authentication.SignOut(
DefaultAuthenticationTypes.ExternalCookie,
OAuthDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType);
return new ChallengeResult(loginProvider, this);
}
User user = await this.UserProvider.FindAsync(externalLogin.Provider, externalLogin.ProviderKey);
if (user != null)
{
OwinHelper.SingIn(Request.GetOwinContext(), user, externalLogin);
}
else
{
OwinHelper.SingIn(Request.GetOwinContext(), externalLogin);
}
return Ok();
}
</externalloginprovider></ihttpactionresult>
Facebook user's data extraction:
public class FacebookOAuthProvider : FacebookAuthenticationProvider
{
private const string ApiBaseUrl = "https://graph.facebook.com";
public override Task Authenticated(FacebookAuthenticatedContext context)
{
string avatarUrl = GetAvatarUrl(context.User.GetValue("id").ToString(), 240);
context.Identity.AddClaim(
new Claim(OwinHelper.ClaimTypeAvatarUrl, avatarUrl));
return base.Authenticated(context);
}
public static string GetAvatarUrl(string facebookUserId, int size)
{
return string.Format("{0}/{1}/picture?width={2}&height={2}",
ApiBaseUrl,
facebookUserId,
size);
}
}
Google user's data extraction:
public class GoogleOAuthProvider : GoogleOAuth2AuthenticationProvider
{
public override Task Authenticated(GoogleOAuth2AuthenticatedContext context)
{
string avatarUrl = context.User
.SelectToken("image.url")
.ToString()
.Replace("sz=50", "sz=240");
context.Identity.AddClaim(
new Claim(OwinHelper.ClaimTypeAvatarUrl, avatarUrl));
return base.Authenticated(context);
}
}
Microsoft user's data extraction:
public class MicrosoftOAuthProvider : MicrosoftAccountAuthenticationProvider
{
public override void ApplyRedirect(MicrosoftAccountApplyRedirectContext context)
{
context = new MicrosoftAccountApplyRedirectContext(
context.OwinContext,
context.Options,
context.Properties,
context.RedirectUri + "&display=touch");
base.ApplyRedirect(context);
}
public override Task Authenticated(MicrosoftAccountAuthenticatedContext context)
{
string avatarUrl = string.Format("https://apis.live.net/v5.0/{0}/picture",
context.User.GetValue("id").ToString());
context.Identity.AddClaim(
new Claim(OwinHelper.ClaimTypeAvatarUrl, avatarUrl));
return base.Authenticated(context);
}
}
Token issuance
Once a user is authenticated an application has three different flows for issuing tokens (detailed view on step 10):
- A new user is authorized through an external provider: After an External Bearer token is issued, the user can register in the application using this token and then re-authorize again, to get a new token (Local Bearer).
public class NotRegisteredExternal : ClaimsMapper
{
public NotRegisteredExternal(ExternalLoginModel extLogin)
{
this.Id = string.Empty;
this.Email = extLogin.Email ?? string.Empty;
this.FullName = extLogin.FullName ?? string.Empty;
this.AvatarUrl = extLogin.AvatarUrl ?? string.Empty;
this.Sid = extLogin.ProviderKey;
this.Version = string.Empty;
this.IsVerified = false.ToString();
this.Issuer = extLogin.Provider.ToString();
this.OriginalIssuer = this.Issuer;
}
}
- An existing user is authorized through an external provider:
public class RegisteredExternal : ClaimsMapper
{
public RegisteredExternal(User user, ExternalLoginModel extLogin)
{
this.Id = user.Id.ToString();
this.Email = user.Email;
this.FullName = user.FullName ?? string.Empty;
this.AvatarUrl = UserProvider.GetAvatarUrl(user);
this.Sid = extLogin.ProviderKey;
this.Version = this.GetVersion(user.TimeStamp);
this.IsVerified = user.IsVerified.ToString();
this.Issuer = ClaimsIdentity.DefaultIssuer;
this.OriginalIssuer = extLogin.Provider.ToString();
}
}
- An existing user is authorized through a login/password:
public class RegisteredLocal : ClaimsMapper
{
public RegisteredLocal(User user)
{
this.Id = user.Id.ToString();
this.Email = user.Email;
this.FullName = user.FullName ?? string.Empty;
this.AvatarUrl = UserProvider.GetAvatarUrl(user);
this.Sid = string.Empty;
this.Version = this.GetVersion(user.TimeStamp);
this.IsVerified = user.IsVerified.ToString();
this.Issuer = ClaimsIdentity.DefaultIssuer;
this.OriginalIssuer = ClaimsIdentity.DefaultIssuer;
}
}
Registering your application with external providers
Facebook Configuration
- Navigate to the Facebook Developers Page and log in by entering your Facebook credentials;
- If you aren’t already registered as a Facebook developer, click Register as a Developer and follow the directions to register;
- Under the My Apps tab, click + Add a New App button:
- Select a Website as an app platform:
- Enter an App Name and Category, then click Create App.
This must be unique across Facebook. The App Namespace is the part of the URL that your App will use to access the Facebook application for authentication (for example, https://apps.facebook.com/{App Namespace}). If you don't specify an App Namespace, the App ID will be used for the URL. The App ID is a long system-generated number that you will see in the next step. - On the Basic settings section of the page:
- Enter Contact Email;
- Enter Site URL that will send requests to Facebook.
Note that only you will be able to authenticate using the email alias you have registered. Other users and test accounts will not be able to register.
You can grant test users access to the application under the Roles menu.
For all other Facebook accounts your application must be approved by Facebook. For futher instructions please view Status & Review menu. - To disable sandbox mode for you app go to the Status & Review menu on the left and select Yes:
Google Configuration
- Navigate to the Google Developers Console;
- Click the Create Project button and enter a project name and ID (you can use the default values). In a few seconds the new project will be created and your browser will display the new projects page;
- In the left tab, click APIs & Auth, and then click Consent screen:
- Enter email address;
- Enter product name:
- In the left tab, click APIs & Auth, and then click APIs:
- Enable Google+ API to support user’s avatar access:
- In the left tab, click APIs & Auth and then click Credentials.
- Click the Create New Client ID under OAuth:
- In the Create Client ID dialog, keep the default Web application for the application type;
- Set the Authorized JavaScript origins to the SSL URL of the service, for example: https://supperslonic.com/;
- Set the Authorized redirect URI to: https://supperslonic.com/signin-google.
- Copy and paste the AppId and App Secret into the Credentials.resx file for Google.
Microsoft Configuration
- Navigate to the Microsoft Developer Account;
- Press Create application reference;
- In Basic information enter valid Application name service URLs:
- In API Settings select that it is a mobile application and enter a valid redirect URL:
- Note to add the signin-microsoft to your Redirect URLs.
- In App Settings copy and paste the AppId and App Secret into the Credentials.resx file for Microsoft.