Dec 5, 2010

A custom STS in .NET, Part 4: Responding with practical claims data

Previously we set up a simple almost-out-of-the-box passive STS web application. Right now it should respond to passive requests and everything should be fine, except that the claims data it responds with probably isn't very useful. The default code simply replies with a hard-coded name and role (probably “Manager”). Why don't we improve that a bit?

In amongst all of the default plumbing is the GetOutputClaimsIdentity method of the CustomSecurityTokenService class. This is where the magic happens and we actually fill the response token with claims data (for both active and passive requests, actually). Right now there isn't much to see. The method needs to return something that implements the IClaimsIdentity interface. This is an extension of the .NET IIdentity interface used by generic identity providers. Right now it is probably creating a basic ClaimsIdentity object and filling it's Claims collection directly. Since we need a bit more intelligent behavior, you have a choice to make. Depending on the complexity of your requirements, you can either attempt to write a bunch of conditional loading logic to build the claims here or, as I did, create a custom class to do it.

I've opted for the latter and called it, uncreatively, CustomClaimsIdentity (we're going for quick-and-dirty here, remember?) and created a new .cs file for it. However you end up doing things, you'll eventually have to tackle one big problem. You can probably figure out how to get the right values based on the person (since you are provided an IClaimsPrincipal object to snoop around in), but how do you know what claims to provide in the first place? Each application probably wants something different. If they all want the same info, then I guess your course is clear, but I wasn’t so lucky.

In the passive case you won't have any indication directly what the application wants or, more importantly, expects. Each application will define claim types it expects in their respective web.config files, but this won't be transmitted to your STS in the passive case. In fact, you won't have much to go by other than the user's principal and the appliesTo URI.

Given that, I decided for a bit more work but that wouldn't be too hard to implement and would give the best flexibility for the time being. In the STS's config file I've defined a custom section as follows:

<sectionGroup name="claimsSettings">
<section name="FabrikamAirlinesWebSite" type="System.Configuration.NameValueSectionHandler" />
</sectionGroup>


The NameValueSectionHandler will function almost identically to the way the standard AppSettings does but I didn't want to fill up the AppSettings with this stuff and make parsing it difficult. This also makes checking for the existence of a supported website quick and easy, as we'll soon see. The name is going to be based on the appliesTo URI, and as you can see I've defined a section for the Fabrikam Airlines website I mentioned in the last post. You'll add a section to this claimsSettings section group for each passive URI you want to support. The actual settings look like this:



<claimsSettings>
<FabrikamAirlinesWebSite>
<add key="http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" value="" />
<add key="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="" />
</FabrikamAirlinesWebSite>
</claimsSettings>


I've defined for Fabrikam Airlines two claims: Windows Logon Name and Role. Not very exciting but it'll do for testing and let us see if things really are working. Put simply, we name the section after the web site URI and then define keys for each claim type it expects. By doing it in the STS's config file, we can easily modify it after deployment to add support for new sites and claim types without rebuilding the whole thing.



System.Collections.Specialized.NameValueCollection claimsSettings = (System.Configuration.ConfigurationManager.GetSection("claimsSettings/" + request.AppliesTo.Uri.AbsolutePath.Replace("/", "").Replace(" ", "_")) as System.Collections.Specialized.NameValueCollection);

if (claimsSettings != null)
{
foreach (string claimType in claimsSettings)
{
try
{
// fill claim here
}
catch (Exception)
{
// fail this ClaimType and continue
}
}
}


Apologies for the messy first line there — feel free to split it up into more bite-size lines if you like. We use the ConfigurationManager class to load in the appropriate section based on the URI of the AppliesTo property. Since it is a simple name-value collection we can foreach loop on it and process each claim type. From there you can easily set up a bunch of methods to handle each type of claim you expect to have to fulfill. Some will be easy (like Windows Logon Name) and others will require writing DB calls or Active Directory calls (via DirectorySearcher). I found that throwing together some LINQ to SQL classes was the easiest and quickest way to get the DB calls implemented, as Entity felt like too much for such simple needs. But really if you’re putting together an STS (even a simple one like this) I’m sure you can figure out your own way of doing DB calls.



If the site isn't supported (because no section has been defined), the collection will come back null. Be careful, because if you do not ever add anything to the Claims collection of the IClaimsIdentity you are returning you'll get a really confusing SAML Assertion error when the STS is called (see “A common mistake” section here). The trick is to always return something in the Claims collection even if all other paths failed. I like to return their Windows Logon Name since that's one of the few things you're pretty much guaranteed to know and, hey, it's something, and it avoids the nasty error. This trick (and the error) applies even when you move to something more substantial, like ADFS2.

No comments:

Post a Comment