Exercise 1: Enabling claims based access for an ASP.NET Web Application by generating a local STSHandling access control for a Web application is mainly a matter of handling three tasks: - Authenticate the incoming user’s credentials
- Retrieve the attributes of the authenticated user and use them for granting or deny access
- Use the same attributes for customizing the user experience
In traditional development practice, you would implement those tasks by coding directly against the credentials that your application uses: that would require you to be skilled in security matters and would generate code that is difficult to maintain or re-host. Windows Identity Foundation changes the game. As a first step, we externalize authentication. Instead of authenticating the users from our application, we trust an external authority to do so: we call such an authority Security Token Service, or STS. We configure our application to accept security tokens from the STS, transmitted using standard protocols. Those tokens are the proof that the user successfully authenticated with the STS, which is all we need for considering the user authenticated with our application: hence we don’t need to worry about managing credentials anymore. For this reason, it is common in the security jargon to call an application that accepts tokens from an STS relying party (or RP). The security tokens can also contain information about users: we call those information claims. A claim can literally be anything: groups or roles the user belongs to, attributes like name or email, or permissions such as CanRead. The STS embeds in the token claims about the user at issuance time: the tokens are digitally signed, hence after issuance their values cannot be tampered with. If our application trusts the STS, it consider the claims in the tokens it produce as actually describing the user; as a result, our application no longer needs to look up user attributes for authorization or customization purposes. If this all sounds a bit confusing, don’t worry: it will all become clear as we walk through the lab. In this first exercise we will start from an existing ASP.NET application, which contains only some UI elements which need to be initialized with identity information about the current user. We will show how to secure the application and obtaining the identity info we need by taking advantage of a local STS. Since we don’t have a local STS available, we will create one: you’ll see how easy it is. .png)
Figure 8The redirects sequence and claims flow in the basic federation scenario implemented in exercise 1 Task 1 - Exploring the Initial Solution- Open Microsoft Visual Studio 2010 with administrator privileges. From Start | All Programs | Microsoft Visual Studio 2010, right-click on Microsoft Visual Studio 2010 and select Run as administrator.
- Open the ClaimsEnableWebSite.sln solution file located in the %YourInstallationFolder%\Labs\WebSitesAndIdentity\Source\Ex1-ClaimEnableASPNET\Begin folder.
- Press Ctrl+F5 to run the Web site.
.png)
The begin solution consist of a simple Default page with two labels; one for the user name and another to display the remaining days for the user birthday. A " Go to secret page" hyperlink navigates to a second page which is supposed to be shown only if the user is in Manager role. There is no authentication code or configuration in place. This is as simple as it gets: the purpose here is showing you how to use the object model of Windows Identity Foundation. You easily transport what you will learn in this lab to your own realistic scenarios. - Click on Go to secret page link.
.png)
In the following tasks you will modify the Web site to federate authentication and implement the remaining functionality to show the user name and days to birthday and authorizing the access to the secret page. - Close the browser.
Task 2 - Adding a Local STS to the SolutionOur website has no authentication code at all. As mentioned in the overview, we will outsource authentication to an STS we trust. In production we will have such an STS: but for now, there are none available. Fortunately, Windows Identity Foundation allows us to easily create local STSes that can be used for development purposes. - On the Solution Explorer, right-click the https://localhost/ClaimsEnableWebSiteEx01 project and select Add STS reference.
.png)
Figure 11Add STS reference menu option - When the Federation Utility window shows up perform the following task for each step in the wizard.
This wizard helps to establish a trust relationship between a relying party application (https://localhost/ClaimsEnableWebSiteEx01, in this case) and an STS. In this exercise you will use the wizard to generate a local STS that we will use for development purposes. - On the Welcome page click Next to continue using the pre-populated fields.
.png)
- On the STS options page select the second radio button "Create a new STS project in the current solution" option and click Next.
.png)
Figure 13Selecting a STS option - On the summary page review the changes that will be made and click Finish.
.png)
Once you will click Finish, a new project will be added to the solution. The name of this project is the same as the application name, with a trailing suffix " _STS" (example: https://localhost/ClaimsEnableWebSiteEx01_STS). This project is the local STS that Windows Identity Foundation generated for us, and that our application is now configured to use. By default the local STS uses Forms Authentication, and will issue two test claims with hard coded values. ATTENTION! The local STS comes with a username/password gathering page, but in fact it does not perform any credential check. Do not use it as is under any circumstances other than developing a relying party application. In the optional exercise 2, you will see how you can modify the local STS for actually checking credentials against a sample ASP.NET membership store. .png) - You will modify the certificate used by the STS and the Relying Party application to encrypt/decrypt the tokens. To do this, double-click the Web.config file inside the https://localhost/ClaimsEnableWebSiteEx01_STS project.
- Modify the value of the SigningCertificateName application setting to IdentityTKStsCert.
<configuration> ... <appSettings> <add key="IssuerName" value="PassiveSigninSTS"/> <add key="SigningCertificateName" value="CN=IdentityTKStsCert"/>
<add key="EncryptingCertificateName" value=""/> </appSettings> ... <configuration> - Now open the web.config file inside the https://localhost/ClaimsEnableWebSiteEx01 project by double-clicking it in the Solution Explorer.
- Remove the current entry inside the microsoft.identityModel/service/issuerNameRegistry/trustedIssuers section and add the following one.
(Code Snippet – Web Sites And Identity Lab - Ex01 New TrustedIssuer) <configuration> ... <microsoft.identityModel> <service> ... <issuerNameRegistry ...> <trustedIssuers> <add thumbprint="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" name="https://localhost/ClaimsEnableWebSiteEx01_STS/" /> <add thumbprint="40A1D2622BFBDAC80A38858AD8001E094547369B" name="CN=IdentityTKStsCert" />
</trustedIssuers> </issuerNameRegistry> </service> </microsoft.identityModel> ... <configuration> - On the Solution Explorer right-click on the original project (https://localhost/ClaimsEnableWebSiteEx01) and select Set as StartUp project.
- Run the Web site (Ctrl+F5). Notice you are redirected to the STS Web site and a login page comes up with a pre-populated user name “Adam Carter”.
.png)
this redirect happened because the Add STS Reference… wizard configured our application to expect tokens from our local STS. Hence from now on every unauthenticated user will be redirected to the STS pages, and only the ones that will successfully authenticate here will be able to access the application pages. Everything according to the plan. - Click Submit in the login page.
.png)
We took care of the authentication part, but now we have to think about how to acquire the claims we need for our application: Name, Birthdate and Role. Since we are using a local STS, we will have to modify the STS project in order to generate the appropriate claims. - Close the browser.
Task 3 - Adding Custom Claims- Open the web.config from the relying party (https://localhost/ClaimsEnableWebSiteEx01).
- Add a new claim required by the application. To do this, insert the following element (shown in bold) under microsoft.identityModel/service/applicationService/claimTypeRequired.
(Code Snippet – Web Sites And Identity Lab - Ex01 Custom Required Claim) <microsoft.identityModel> <service> ... <applicationService> <claimTypeRequired> <claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" optional="true" /> <claimType type="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" optional="true" /> <claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth" optional="true" />
</claimTypeRequired> </applicationService> ... </service> </microsoft.identityModel> You won’t modify FederationMetadata.xml for this lab; however, this is important for “advertising” the claims that your application needs. This would be useful if you would allow an external STS, such as an Active Directory Federation Services (ADFS) instance, to automatically configure itself for providing claims to your application. - Open the CustomSecurityTokenService.cs file located on the STS project (https://localhost/ClaimsEnableWebSiteEx01_STS) under the App_Code folder.
- Add the custom output claim to the STS. To do this, update the method GetOutputClaimsIdentity with the following code shown in bold.
(Code Snippet – Web Sites And Identity Lab - Ex01 Custom Output Claim) protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope ) { if ( null == principal ) { throw new ArgumentNullException( "principal" ); } ClaimsIdentity outputIdentity = new ClaimsIdentity(); // Issue custom claims. // TODO: Change the claims below to issue custom claims required by your application. // Update the application's configuration file too to reflect new claims requirement. outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name ) ); outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Manager" ) ); outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.DateOfBirth, "5/5/1955"));
return outputIdentity; } } GetOutputClaimsIdentity is the place in which a custom STS runs the logic that retrieves claims values and embeds them in the token that will be issued. By default, the STS template maps the user Name from the principal to a Name claim and, in addition, it adds a Role claim with the hardcoded Manager value. We need the birthdate claim as well, hence we add it following the commented sample lines provided by the template.
Task 4 - Using the Issued Claims- Open the code behind of the Default page from the relying party (https://localhost/ClaimsEnableWebSiteEx01).
- Add the following using statements.
(Code Snippet – Web Sites And Identity Lab - Ex01 Default Page Usings) using System.Linq;
using System.Threading;
using Microsoft.IdentityModel.Claims;
Here we will use the Windows Identity Foundation object model for retrieving claims values and driving the applications’ behavior. Some time we will not even notice we are using Windows Identity Foundation, since its object model is seamlessly integrated in the existing ASP.NET model. - Find the Page_Load event handler and type the following line of code to show the user name.
protected void Page_Load(object sender, EventArgs e) { this.txtName.Text = User.Identity.Name;
} This is exactly the code you write for accessing the user’s name in an ASP.NET application, regardless of the authentication type: Windows Identity Foundation is no exception. - Add the following code to the Page_Load event handler to get the birthday claim value and show the days to user's birthday.
(Code Snippet – Web Sites And Identity Lab - Ex01 Days To Birthday) protected void Page_Load(object sender, EventArgs e) { this.txtName.Text = User.Identity.Name; IClaimsIdentity claimsIdentity = ((IClaimsPrincipal)(Thread.CurrentPrincipal)).Identities[0];
DateTime birthDay = DateTime.Parse(
(from c in claimsIdentity.Claims
where c.ClaimType == System.IdentityModel.Claims.ClaimTypes.DateOfBirth
select c.Value).FirstOrDefault());
int days = (birthDay.DayOfYear - DateTime.Today.DayOfYear);
this.txtDaysToBday.Text =
((days < 0) ? days + 365 + (DateTime.IsLeapYear(DateTime.Today.Year) ? 1 : 0) : days).ToString();
} What you see here is the claim identity model in action. First, we obtain an IClaimsIdentity from the current thread: this is equivalent to obtaining an IIdentity in classic ASP.NET. Once we have that, we have a mean of inspecting the claims we received from the STS: the next line queries (via Linq) the incoming claims collection, searching for the birth date of the user. The rest of the code just calculates how many days are left in the year before the user’s birthday. Nothing of the code above gives away any detail about which authentication technology has been used: the Windows Identity Foundation took care of all the details for us, and we are free to concentrate on our application logic. - Open the web.config file of the relying party Web site (https://localhost/ClaimsEnableWebSiteEx01).
- Insert the following configuration under the configuration element section to authorize access to SecretPage.aspx page for Manager role only.
(Code Snippet – Web Sites And Identity Lab - Ex01 SecretPage Authorization Settings) <configuration> ... </configSections> <location path="SecretPage.aspx">
<system.web>
<authorization>
<allow roles="Manager"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
<location path="FederationMetadata"> ... </configuration> Again, this is exactly what you would do using ASP.NET roles. This means that if you already have code which uses those features you can move to a claims based model without having to rewrite anything.
Task 5 - Verifying the Role Based AccessIn order to verify current solution, proceed as follows: - Start the application by pressing Ctrl+F5. The relying party application (https://localhost/ClaimsEnableWebSiteEx01) will redirect to the STS to authenticate.
.png)
- Leave the default credentials and click on Submit.
.png)
The STS sent to our application the claims it was expecting and the code we added takes advantage of them. - Click on Go to secret page link.
.png)
Since the STS includes a hardcoded claim for the Manager role you can navigate to the SecretPage.aspx page which is limited to Manager users only. Let’s put this to test and see what happens if we change Adam’s role from Manager to Sales: this should make Adam unable to reach SecretPage.aspx. - Close the browser.
- Open the CustomSecurityTokenService.cs file located on the STS project (https://localhost/ClaimsEnableWebSiteEx01_STS) under the App_Code folder.
- Comment the "Manager" role claim out and add a "Sales" role claim in the GetOutputClaimsIdentity method to match the following snippet.
protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope ) { if ( null == principal ) { throw new ArgumentNullException( "principal" ); } ClaimsIdentity outputIdentity = new ClaimsIdentity(); // Issue custom claims. // TODO: Change the claims below to issue custom claims required by your application. // Update the application's configuration file too to reflect new claims requirement. outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name ) ); //outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Manager" ) );
outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Sales"));
outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.DateOfBirth, "5/5/1955")); return outputIdentity; } - Start the application by pressing Ctrl+F5 and authenticate using the pre-populated credentials.
- Click on Go to secret page link.
.png)
Figure 20Access denied for SecretPage.aspx You get an unauthorized error message since the STS did not issue a role claim with the Manager role.
Task 6 - Granting or Denying Access to the Website According to the User’s AgeIn this task we are going to delete the ASP.NET Role settings and add a claims authorization manager to change the permissions criteria. The fact that Windows Identity Foundation integrates well with existing authentication practices is a big advantage, since it allows you to protect existing skills and investments: however claims have much more expressive power than the classic roles and attributes, and can be used for achieving things that would have been impossible with traditional approaches. One simple example is using claims for expressing attributes that have non-string values, such as dates (date of birth, expiration date, etc), numbers (spending limit, weight, etc) or even structured data. Those data can be processed using criteria that are more sophisticated than the simple existence check (ie the user has claims A with value X, or he doesn’t): one example of such a criteria would be imposing that all the users of the website should be older than 21. The Windows Identity Foundation object model offers an extensibility point in the claims processing pipeline, which is specifically designed to allow you to inject your own claims authorization code. In order to do that, you are required to derive from the ClaimsAuthorizationManager class and implement your claim authorization logic in the method CheckAccess. It is common practice to express the authorization conditions outside of the code, for example in configuration files, and implement in CheckAccess generic evaluation logic so that the values & conditions can be changed at deployment time. This ensures that a change in the authorization conditions can be applied without recompiling anything. The conditions can be associated to the resources they refer to using some notation convention directly in the configuration files: however it is also possible to use explicit invocations from code, or decorating the resources themselves with attributes which will tie to authorization conditions and will fire the associated logic. In this task we are going to add a simple custom ClaimsAuthorizationManager whose only capability is making sure that only callers older than a certain threshold age are granted access to a given web resource. - Right-click on the App_Code folder from the https://localhost/ClaimsEnableWebSiteEx01 project and select Add New Item.
- Create a new Class and name it AgeThresholdClaimsAuthorizationManager.cs
- Replace the content of the new class with the following to check access based on the DateOfBirth claim from the issued token.
(Code Snippet – Web Sites And Identity Lab - Ex01 AgeThresholdClaimsAuthorizationManager) namespace ClaimsBasedAuthorization
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Configuration;
using System.Xml;
using Microsoft.IdentityModel.Claims;
using Microsoft.IdentityModel.Configuration;
public class AgeThresholdClaimsAuthorizationManager : ClaimsAuthorizationManager
{
private static Dictionary<string, int> _policies = new Dictionary<string, int>();
public AgeThresholdClaimsAuthorizationManager(object config)
{
XmlNodeList nodes = config as XmlNodeList;
foreach (XmlNode node in nodes)
{
XmlTextReader rdr = new XmlTextReader(new StringReader(node.OuterXml));
rdr.MoveToContent();
string resource = rdr.GetAttribute("resource");
rdr.Read();
string claimType = rdr.GetAttribute("claimType");
if (claimType.CompareTo(System.IdentityModel.Claims.ClaimTypes.DateOfBirth) != 0)
throw new NotSupportedException("Only birthdate claims are supported");
string minAge = rdr.GetAttribute("minAge");
_policies[resource] = int.Parse(minAge);
}
}
public override bool CheckAccess(AuthorizationContext pec)
{
Uri webPage = new Uri(pec.Resource.First().Value);
if (_policies.ContainsKey(webPage.PathAndQuery))
{
int minAge = _policies[webPage.PathAndQuery];
string userBirthdate = pec.Principal.Identities[0].Claims
.Where(c => c.ClaimType == System.IdentityModel.Claims.ClaimTypes.DateOfBirth)
.First().Value;
int userAge = DateTime.Now.Subtract(DateTime.Parse(userBirthdate)).Days / 365;
if (userAge < minAge)
{
return false;
}
}
return true;
}
}
}
The code above is very simple. The class constructor takes care of reading the authorization conditions from the web.config. The CheckAccess method retrieves the dateofbirth claim associated with the incoming user, and simply verifies that the corresponding age is above the threshold established by minAge. - Open the web.config file of the relying party Web site (https://localhost/ClaimsEnableWebSiteEx01).
- Delete the SecretPage authorization settings.
<configuration>
...
</configSections>
<location path="SecretPage.aspx">
<system.web>
<authorization>
<allow roles="Manager"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
<location path="FederationMetadata">
...
</configuration>
- Add the ClaimsAuthorizationModule in the httpModules configuration.
(Code Snippet – Web Sites And Identity Lab - Ex01 ClaimsAuthorizationModule httpModule) <system.web> ... <httpModules> ... <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules> ... </system.web> - Add the ClaimsAuthorizationModule in the system.webServer modules configuration.
(Code Snippet – Web Sites And Identity Lab - Ex01 ClaimsAuthorizationModule module) <system.webServer> ... <modules> ... <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
</modules> ... </system.webServer> - Insert the ClaimsAuthorizationManager configuration to set the access policy for the SecretPage.
(Code Snippet – Web Sites And Identity Lab - Ex01 ClaimsAuthorizationManager configuration) <microsoft.identityModel> <service> ... <claimsAuthorizationManager type="ClaimsBasedAuthorization.AgeThresholdClaimsAuthorizationManager">
<policy resource="/ClaimsEnableWebSiteEx01/SecretPage.aspx" action="GET">
<claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth" minAge="21" />
</policy>
</claimsAuthorizationManager>
</service> </microsoft.identityModel> - Change the Secret Page message. To do this open the SecretPage page from the relying party (https://localhost/ClaimsEnableWebSiteEx01) and delete the sentence “This is a secret page; you can get here only if you are a manager”.
…
<form id="form1" runat="server">
<div>
<p>
This is a secret page; you can get here only if you are a manager.
</p>
</div>
</form>
…
- In the same page, add the following sentence shown in bold:
… <form id="form1" runat="server"> <div> <p> This is a secret page; you can get here only if you are a 21 years old or older.
</p> </div> </form> …
Exercise 1: VerificationIn order to verify that you have correctly performed all steps in exercise one, proceed as follows: - Start the application by pressing Ctrl+F5. The relying party application (https://localhost/ClaimsEnableWebSiteEx01) will redirect to the STS to authenticate.
.png)
- Leave the default credentials and click on Submit.
.png)
The STS sent to our application the claims it was expecting and the code we added takes advantage of them. - Click on Go to secret page link.
.png)
Since the STS includes a hardcoded claim for the Birthdate you can navigate to the SecretPage.aspx page which is limited to users with age greater or equal than 21 only. Let’s put this to test and see what happens if we change Adam’s Birthdate to 5/5/2009: this should make Adam unable to reach SecretPage.aspx. - Close the browser.
- Open the CustomSecurityTokenService.cs file located on the STS project (https://localhost/ClaimsEnableWebSiteEx01_STS) under the App_Code folder.
- Change the Birthdate claim to 5/5/2009 in the GetOutputClaimsIdentity method to match the following snippet.
protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope ) { if ( null == principal ) { throw new ArgumentNullException( "principal" ); } ClaimsIdentity outputIdentity = new ClaimsIdentity(); // Issue custom claims. // TODO: Change the claims below to issue custom claims required by your application. // Update the application's configuration file too to reflect new claims requirement. outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name ) ); //outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Manager" ) ); outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Sales")); outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.DateOfBirth, "5/5/2009"));
return outputIdentity; } - Start the application by pressing Ctrl+F5 and authenticate using the pre-populated credentials.
- Click on Go to secret page link.
.png)
Figure 24Access denied for SecretPage.aspx You get an unauthorized error message since the STS issued a claim with your Birthdate that the ClaimsAuthorizationManager rejected.
| |