This is the second in a series of articles looking at using Windows Azure Active Directory Access Control (formally Windows Azure Access Control Service), to integrate the authentication services provided by social identity providers to authenticate users with a website. This walkthrough will continue on directly from Website Authentication with Social Identity Providers and ACS Part 1, and add integration with the new .NET Universal Profile Provider.
Integrating ACS and Social Identity Providers with the .NET Universal Profile Provider
The site currently allows users to authenticate with a number of social identity providers, but does not allow users to build up a site profile, or store any user specific information. In many cases, once a user is authenticated, there will be a requirement for the user to store information or configure a site profile. In this step the ASP.NET Universal Profile Provider will be used to store user profile information. The profile provider will need to be integrated with the claims provided by ACS so that the authenticated user can be matched to their saved profile.
The architecture of the proposed implementation is shown below.
.NET Universal Providers
The .NET Universal Providers provide integration with multiple data sources, including SQL Server, SQL Server Compact and, most importantly for Azure developers, Windows Azure SQL Database. Scott Hanselman has an excellent blog post Introducing System.Web.Providers - ASP.NET Universal Providers for Session, Membership, Roles and User Profile on SQL Compact and SQL Azure, which provides some background on the providers.
Installing the Microsoft .NET Universal Providers
The Microsoft ASP.NET Universal Providers is installed as a NuGet package, the searching for “universal” in the Manage NuGet Pachaged dialog box will find the package in the search results. Clicking the Install button will install the universal providers and their dependencies.
The installation of the NuGet package will also make changes to the Web.config file for the application to configure the providers.
<system.web> <authorization> <!--<deny users="?" />--> </authorization> <authentication mode="None" /> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5" requestValidationMode="4.5" /> <profiledefaultProvider="DefaultProfileProvider"> <providers> <add name="DefaultProfileProvider" type="System.Web.Providers.DefaultProfileProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" applicationName="/" /> </providers> <properties> <addname="IdentityProvider" /> <addname="Name" /> <addname="Email" /> <addname="Twitter" /> <addname="Location"/> </properties> </profile> <membershipdefaultProvider="DefaultMembershipProvider"> <providers> <addname="DefaultMembershipProvider"type="System.Web.Providers.DefaultMembershipProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" /> </providers> </membership> <roleManagerdefaultProvider="DefaultRoleProvider"> <providers> <addconnectionStringName="DefaultConnection"applicationName="/" name="DefaultRoleProvider"type="System.Web.Providers.DefaultRoleProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </providers> </roleManager> <!-- If you are deploying to a cloud environment that has multiple web server instances, you should change session state mode from "InProc" to "Custom". In addition, change the connection string named "DefaultConnection" to connect to an instance of SQL Server (including SQL Azure and SQL Compact) instead of to SQL Server Express. --> <!--<sessionState mode="InProc" customProvider="DefaultSessionProvider"> <providers> <add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" /> </providers> </sessionState>--> </system.web> |
A connection string for the providers has also been added at the end of the Web.config file.
<system.identityModel.services> <federationConfiguration> <cookieHandler requireSsl="false" /> <wsFederation passiveRedirectEnabled="true" issuer="https://acsscenario.accesscontrol.windows.net/v2/wsfederation" realm="http://win7base/RelyingPartyApp/" requireHttps="false" /> </federationConfiguration> </system.identityModel.services> <connectionStrings> <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=aspnet-RelyingPartyApp-20130315201440;Integrated Security=SSPI" /> </connectionStrings> </configuration> |
Instead of using a local database in SQL Server Express, a database named RelyingPartyApp will in Windows Azure SQL Database will be used. The configuration string is modified as follows with sensitive information replaced. The RelyingPartyApp database has been created as an empty database in Windows Azure SQL Database, but no tables for the profile information have been added yet.
<system.identityModel.services> <federationConfiguration> <cookieHandler requireSsl="false" /> <wsFederation passiveRedirectEnabled="true" issuer="https://acsscenario.accesscontrol.windows.net/v2/wsfederation" realm="http://win7base/RelyingPartyApp/" requireHttps="false" /> </federationConfiguration> </system.identityModel.services> <connectionStrings> <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Server=tcp:SERVERNAME.database.windows.net,1433;Database=RelyingPartyApp;User ID=USER@ SERVERNAME;Password=PASSWORD;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;" /> </connectionStrings> </configuration> |
Configuring the Universal Profile Provider
The universal profile provider will store profile information for the site members. The values that will be stored in the profile for each user are specified in the profiler configuration. The following configuration is added in the profile section, it specifies that IdentityProvider, Name, Email, Twitter and Location exist as fields for each users profile.
<profile defaultProvider="DefaultProfileProvider"> <providers> <add name="DefaultProfileProvider" type="System.Web.Providers.DefaultProfileProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" applicationName="/" /> </providers> <properties> <addname="IdentityProvider" /> <addname="Name" /> <addname="Email" /> <addname="Twitter" /> <addname="Location" /> </properties> </profile> |
Creating the Membership and Profile Tables
The next step is to create the storage tables for the site membership and profile information in the Windows Azure SQL Database. This can be performed using the ASP.NET Configuration tool in Vislau Studio.
With the RelyingPartyApp application project selected in the solution explorer, the ASP.NET Configuration option can be selected from the project window.
This will open the ASP.NET Web Site Administration Tool in the portal. When the welcome page is opened, the tool will use the database connection string specified in the configuration file and connect to the database. If the database does not contain the tables required to store the website membership and profile information, these will be created.
If the database connection string is configured correctly, the tables will be created in the Windows Azure SQL Database. The following screenshot shows the six empty tables created in the RelyingPartyApp database.
If the tables are not created, check the database connection string and firewall rules to ensure that the ASP.NET Web Site Administration Tool can connect successfully with the database.
Identifying a Unique User
One of the challenges of using social identity providers to authenticate users is identifying users based on the claims supplied by these providers. The claims provided by the three social identity providers are summarized in the table below.
Claim | Supplied by | Notes |
Name Identifier | Microsoft Account, Yahoo, Google | Guaranteed to be unique for user within identity provider. Guaranteed to be immutable. |
Name | Google, Yahoo | No guarantee of uniqueness or immutability. |
Google, Yahoo | Guaranteed to be unique for user within identity provider. Guarenteed to be immutable. | |
Inditity Provider | ACS | Uniquly identifies identity provider. |
The email address is not usable as it is not supplied by Microsoft Account. The name is not suitable as it is not supplied by Microsoft Account, and there is no guarantee of uniqueness, and can be changed by the account holder.
The only claim that is supplied by all three social identity providers is the name identifier claim. This is guaranteed to be unique within the identity provider, and also guaranteed to be immutable, meaning that once an account has been created, the name identifier value for that account will not change.
Name identifier, combined with the identity provider claim supplied by ACS is the best option, as this will ensure that the values are unique for each provider. Although the name identifier alone may seem suitable, it could be possible for a rouge identity provider to submit a name identifier claim that belongs to an account in another identity provider.
The Users table in the website membership and profile database specifies that the UserName for the user is datatype ncarchar (50). This is shown in the screenshot below.
In order to integrate with the universal profile provider database, the name that is used to uniquely identify the user must be 50 characters or less. The length of the name identifier claim supplied by the social identity providers is typically larger than 50 characters, and when combined with the identity provider as will, it will be even larger. One option to resolve this issue is to hash the combination of the identity provider and name identifier claims. This will also add an extra level of security, as the identity provider and name identifier values cannot theoretically be determined from the hash of their values. This will be done using a custom authentication manager in the website in a later section.
Modifying Claim Transformation Rules in ACS
As the user is going to be identified in the universal profile provider by the hashed values of the identity provider and name identifier claims, this value will be supplied to the application as the name claim, and used as the username for that identity. This means that the values for the name claim supplied by Google and Yahoo will need to be mapped in ACS and sent to the ASP.NET relying party application as another claim.
The claim transformation rules in ACS can be used to map the name claims supplied by Google and Yahoo to the given name claim. This claim can then be used by the ASP.NET relying party application to set the name used by the site visitor in the profile. In order to do this the two rules that pass the value of the name claim for Google and Yahoo will need to be modified to output the claim value as the given name claim. The following screenshot shows the modification name to the name claim for Yahoo.
With these changes made, the rule set can be saved. The same changes are made for the rule for Google, the modified rule group is shown below.
Combining the values of the identity provider and name identifier claims and hashing them is currently not possible using the rule sets in ACS. The next section will show how the name claim can be added to the incoming claims from ACS when the user is authenticated in the relying party application.
Creating a Custom Authentication Manager
Windows Identity Framework is an extensible framework that can be used in a number of levels of complexity. So far we have used the Identity and Access wizard, and modified the configuration file. In this section the core functionality of WIF will be extended to add a custom authentication manager to the ASP.NET relying party application, and then the configuration will be modified to make use of the custom authentication manager.
Claims Authentication Managers
Claims authentication managers are used in the Windows Identity Foundation authenticating pipeline and provide a place where the claims in the incoming security token can be processed before they reach the application code. Common tasks are claims validation, claims filtering, or adding claims to the security token. Developers can create their own custom claims authentication managers in order to process and manipulate claims in the Windows Identity Foundation authentication pipeline.
Creating a Custom Authentication Manager
Creating a custom authentication manager for the relying party application is fairly straightforward. A folder named Code as added to the web project, and then a class named CustomCam is created in that folder. A project reference to System.IdentityModel is then added, and the CustomCam class derived from System.Security.Claims.ClaimsAuthenticationManager, with the Authenticate and LoadCustomConfiguration overridden.
using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.Text;
namespace RelyingPartyApp.Code { publicclassCustomCam : ClaimsAuthenticationManager { publicoverrideClaimsPrincipal Authenticate (string resourceName, ClaimsPrincipal incomingPrincipal) { returnbase.Authenticate(resourceName, incomingPrincipal); }
publicoverridevoid LoadCustomConfiguration(System.Xml.XmlNodeList nodelist) { base.LoadCustomConfiguration(nodelist); } } } |
Modifying Claims in a Custom Authentication Manager
The Authenticate method will extract the values for the incoming identity provider and name identifier claims, combine them together, create an MD5 hash of the resulting string, and then add a new name claim with the hashed string set as the value. The implementation for this is shown below.
using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.Text;
namespace RelyingPartyApp.Code { publicclassCustomCam : ClaimsAuthenticationManager { publicoverrideClaimsPrincipal Authenticate (string resourceName, ClaimsPrincipal incomingPrincipal) { // Get the values of the IP and name identifyer caims. string identityProvider = ""; string nameIdentifier = ""; foreach (Claim claim in incomingPrincipal.Claims) { // IdentityProvider if (claim.Type.Equals ("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider")) { identityProvider = claim.Value; }
// NameIdentifier if (claim.Type.Equals(ClaimTypes.NameIdentifier)) { nameIdentifier = claim.Value; } }
// Create an MD5 hash of the value. string text = identityProvider + nameIdentifier; MD5 md5 = MD5.Create(); byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(text); byte[] hash = md5.ComputeHash(inputBytes); StringBuilder sb = newStringBuilder(); for (int i = 0; i < hash.Length; i++) { sb.Append(hash[i].ToString("X2")); } string hashedIdentifier = sb.ToString();
// Add a name claim with the hashed identifier. incomingPrincipal.Identities.First().AddClaim(newClaim(ClaimTypes.Name, hashedIdentifier));
returnbase.Authenticate(resourceName, incomingPrincipal); }
public override void LoadCustomConfiguration(System.Xml.XmlNodeList nodelist) { base.LoadCustomConfiguration(nodelist); } } } |
Configuring the Custom Claims Authentication Manager
With the custom claims authentication manager created, it must be defined in the configuration so that the appropriate methods will be executed when the security token is received by the relying party application. The configuration is made in the identityConfiguration section of the system.identityModel section.
<system.identityModel> <identityConfiguration> <claimsAuthenticationManagertype="RelyingPartyApp.Code.CustomCam, RelyingPartyApp" /> <certificateValidation certificateValidationMode="None" /> <audienceUris> <add value="http://win7base/RelyingPartyApp/" /> </audienceUris> <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <trustedIssuers> <add thumbprint="B52D78084A4DF22E0215FE82113370023F7FCAC4" name="https://acsscenario.accesscontrol.windows.net/" /> </trustedIssuers> </issuerNameRegistry> </identityConfiguration> </system.identityModel> |
Testing the Custom Claims Authentication Manager
With the custom claims authentication manager now created and configured in the relying party application, the functionality can be tested. Logging onto the application using Yahoo as an identity provider shows the following claims.
The custom claims authentication manager has added a new name claim with a value of the hash of the identity provider and the name identifier. This will provide a unique value that can be inserted in the appropriate tables in the membership and profile database.
Creating and Modifying Site Profiles
In this step a web page will be added to allow site members to edit and save their profile information. The profile information will include name, email, twitter id and location. A web form named EditProfile.aspx will be created in the Members folder; the front side code for the interface is shown below.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="EditProfile.aspx.cs" Inherits="RelyingPartyApp.Members.EditProfile" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="form1" runat="server"> <div> <article> <p>Identity Provider: <asp:LabelID="lblIdentityProvider"runat="server"/></p> <p></p>
<p>Name</p> <p><asp:TextBoxID="txtName"runat="server"/></p> <p></p>
<p>Email address</p> <p><asp:TextBoxID="txtEmail"runat="server"/></p> <p></p>
<p>Twitter ID</p> <p><asp:TextBoxID="txtTwitter"runat="server"/></p> <p></p>
<p>Location</p> <p><asp:TextBoxID="txtLocation"runat="server"/></p> <p></p>
<asp:ButtonID="btnSave"Text="Save"OnClick="btnSave_Click"runat="server"/> </article> </div> </form> </body> </html> |
When a new member visits the profile page, the name and email address claim values will be displayed in on the page. The user will have the option of modifying these values, and also adding twitter and location information, before the profile is saved.
The code behind that implements this is shown below.
using System; using System.Security.Claims; using System.Threading; using System.Web; using System.Web.Profile;
namespace RelyingPartyApp.Members { public partial class EditProfile : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { // Get the claim principal for the user. ClaimsPrincipal claimsPrincipal = Thread.CurrentPrincipal asClaimsPrincipal;
// Set the value for the identity provider. foreach (Claim claim in claimsPrincipal.Claims) { if (claim.Type.Equals ("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider")) { lblIdentityProvider.Text = claim.Value; } }
// Get the current user profile ProfileBase profile = HttpContext.Current.Profile;
if (!string.IsNullOrEmpty(profile["Name"].ToString())) { // If the profile exists, set the user interface details. txtName.Text = profile["Name"].ToString(); txtEmail.Text = profile["Email"].ToString(); txtTwitter.Text = profile["Twitter"].ToString(); txtLocation.Text = profile["Location"].ToString(); } else { // If there is no profile, set the details from the claims provided by ACS. foreach (Claim claim in claimsPrincipal.Claims) { // Set the given name. if (claim.Type.Equals(ClaimTypes.GivenName)) { txtName.Text = claim.Value; }
// Set the email address. if (claim.Type.Equals(ClaimTypes.Email)) { txtEmail.Text = claim.Value; } } } } }
protectedvoid btnSave_Click(object sender, EventArgs e) { // Update the profile with the values in the text boxes. ProfileBase profile = HttpContext.Current.Profile; profile["Name"] = txtName.Text; profile["Email"] = txtEmail.Text; profile["Twitter"] = txtTwitter.Text; profile["IdentityProvider"] = lblIdentityProvider.Text; profile["Location"] = txtLocation.Text; profile.Save(); }
} } |
Testing the Implementation
The implementation can now be tested to verify that the Universal Profile Provider integrates with the Access Control Service (ACS) and Windows Identity Foundation. In order to do this the members page is browsed to, causing ACS to display the identity provider selector page. As name and email will be needed to test the setting in the user interface, Yahoo or Google will need to be used. The application will still function with a Microsoft account, but the name and email claim values will not be available in the security token, and will not be set in the user interface.
In this case, Yahoo will be used as an identity provider.
When the user navigates to the EditProfile page, the claim values from the security token provided by Yahoo, and transformed by ACS are set in the user interface.
Remember that the name claim has been mapped to the given name claim, and its value is displaced in the name text box.
The user can then make modifications to the details, selecting a different name and email address, and also adding twitter and location information. In this example I changed the name form Alan Azure to Alan Smith, and added my twitter handle and location.
When the user saves the profile information, a new user and a new profile are created in the membership and profile database in Windows Azure SQL Database.
The following screenshot shows the new user.
The next screenshot shows the profile, which is linked to the user with the UserId value. The details entered in the profile are shown in the PropertyValueStrings column.
The next time the user authenticates using the same social identity, they will be able to access, modify and save their own profile information. The site will also be able to maintain profile information for users authenticating with social identity providers.