Write a custom security token and handler in Windows Identity Foundation
In this article I will demonstrate how to write a token handler for a custom token in Windows Identity Foundation (WIF). The likely circumstances for requiring a new token type are:
- The token type is pre-existing and needs to be federated
- The new token type is an extension to a token type already supported by WIF
However, the purpose of this article is to demonstrate one of the extensibility points of WIF and so the reasons for creating a new token type are not so important.
Before continuing I should point out that I am not creating a new wire protocol for conveying tokens, but instead using an existing wire protocol (WS-Federation) to pass a new token type. WS-Federation is well suited to this because it is a means of conveying WS-Trust tokens using browser redirects. In turn, WS-Trust is essentially a container for …. well, any token type you like, and that is why we can create a new token type without risk of breaking any standards.
I am going to take a trial and error approach to this so that you can see some of the common pitfalls. I will simply fire a new token, wrapped within a WS-Trust 1.3 envelope and conveyed by the WS-Federation Passive Requestor Profile, as a sign-in response message to the Relying Party (RP) application. Then, I will fix the RP application by reacting to the errors that occur. Using this methodology you may get a fuller understanding of how to implement a new token handler and token.
First I need a Security Token Service (STS) to send a custom token to the RP. To do this I have captured a sign in response using Http Watch (a really good and easy to use HTTP sniffer), changed the token within the WS-Federation wresult parameter, and hardcoded it as the STS response; this was achieved using a ASP.NET website with a HttpHandler implementation.
The custom token to be consumed by the RP looks like the following:
<m:MyCustomToken
xmlns:m="urn:mycustomtoken"
m:Id="D416881A-130B-4AFF-8091-F412D7440E39"
m:Issuer="urn:mycustomtokenhandlersts"
m:Audience="https://mycustomtokenhandlerwebsite/"
m:ValidFrom="2011-01-01"
m:ValidTo="2099-12-31">
<m:Claim Name="GivenName" Namespace="urn:givenname">John</m:Claim>
<m:Claim Name="Surname" Namespace="urn:surname">Doe</m:Claim>
<m:Claim Name="Role" Namespace="urn:role">Manager</m:Claim>
<Signature xmlns="https://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="https://www.w3.org/2001/10/xml-exc-c14n#" />
<SignatureMethod Algorithm="https://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<Reference URI="">
<Transforms>
<Transform Algorithm="https://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<Transform Algorithm="https://www.w3.org/2001/10/xml-exc-c14n#" />
</Transforms>
<DigestMethod Algorithm="https://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>gK8K+94DbXsVHxE8X3ulh45WcEM=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>… removed for brevity …</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>… removed for brevity …</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
</m:MyCustomToken>
It contains many of the characteristics of common token types:
- Id – a unique identifier for the token (can be used to detect replay attacks)
- Namespace – the namespace for the token
- Issuer – the entity that issued the token to the RP
- ValidFrom/ValidTo – the validity period of the token
- Audience – the entity that the token is intended for
- Claims – a set of authoritative statements about the identity described by the token
- Digital signature – ensures that the message cannot be tampered with and also enforces the authoritative nature of the message
The <microsoft.identityModel> configuration section of the RP config file currently has no awareness of the new token type:
<microsoft.identityModel>
<service>
<audienceUris>
<add value="https://mycustomtokenhandlerwebsite/" />
</audienceUris>
<federatedAuthentication>
<wsFederation
passiveRedirectEnabled="true"
issuer="https://localhost:29460/MyCustomTokenHandlerSTS/STSHandler.ashx"
realm="https://mcthw"
requireHttps="false" />
<cookieHandler requireSsl="false" />
</federatedAuthentication>
<issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry">
<trustedIssuers />
</issuerNameRegistry>
</service>
</microsoft.identityModel>
OK, by browsing to the RP home page the browser is automatically redirected to the STS and the custom token is returned to the RP … Error!
“ID4014: A SecurityTokenHandler is not registered to read security token ('MyCustomToken', 'urn:mycustomtoken')”
This seems reasonable as I have not yet made any effort to recognise the token. I therefore need the bare bones of a custom SecurityTokenHandler implementation:
using System;
using Microsoft.IdentityModel.Tokens;
/// <summary>
/// Summary description for MyCustomTokenHandler
/// </summary>
public class MyCustomTokenHandler : SecurityTokenHandler
{
public MyCustomTokenHandler()
{
}
public override string[] GetTokenTypeIdentifiers()
{
throw new NotImplementedException();
}
public override Type TokenType
{
get { throw new NotImplementedException(); }
}
}
I also need a custom System.IdentityModel.Tokens.SecurityToken implementation representing the token type to be handled:
using System;
using System.IdentityModel.Tokens;
/// <summary>
/// Summary description for MyCustomToken
/// </summary>
public class MyCustomToken : SecurityToken
{
public MyCustomToken()
{
}
public override string Id
{
get { throw new NotImplementedException(); }
}
public override ReadOnlyCollection<SecurityKey> SecurityKeys
{
get { throw new NotImplementedException(); }
}
public override DateTime ValidFrom
{
get { throw new NotImplementedException(); }
}
public override DateTime ValidTo
{
get { throw new NotImplementedException(); }
}
}
Finally, I need to reference the handler in the RP config file by adding in a <securityTokenHandlers> element:
<securityTokenHandlers>
<add type="MyCustomTokenHandler" />
</securityTokenHandlers>
Browse to the RP again … Error!
“The method or operation is not implemented”
This occurs because I need to implement the handlers GetTokenTypeIdentifiers method to return the namespace of the new token, and also the TokenType property to return the tokens type:
public override string[] GetTokenTypeIdentifiers()
{
return new string[] { "urn:mycustomtoken" };
}
public override Type TokenType
{
get { return typeof(MyCustomToken); }
}
Browse to the RP again … Error!
“ID4014: A SecurityTokenHandler is not registered to read security token ('MyCustomToken', 'urn:mycustomtoken')”
This is a bit strange as I have implemented all of the methods mandated by the base class. After a quick peek into the WIF source I found the following:
public virtual bool CanReadToken(XmlReader reader)
{
return false;
}
By default all security token handler implementations are excluded as possible candidates for parsing the token! I therefore need to override this method and perform some checks on the incoming token to make sure that my code genuinely can read the token:
public override bool CanReadToken(XmlReader reader)
{
if (reader.LocalName.Equals("MyCustomToken") &&
reader.NamespaceURI.Equals("urn:mycustomtoken"))
{
return true;
}
return false;
}
Browse to the RP again … Error!
“ID4008: 'SecurityTokenHandler' does not provide an implementation for 'ReadToken'”
This seems self explanatory as I have indicated that I can read the token but have provided no code to do so. I need a SecurityToken implementation but the SecurityToken base class only offers a few read-only properties. It therefore seems that I must do most of the work myself. To achieve this I have created a internal representation of the token:
using System;
using System.Collections.Generic;
using Microsoft.IdentityModel.Claims;
/// <summary>
/// Summary description for MyCustomTokenInternal
/// </summary>
public class MyCustomTokenInternal
{
public string Id { get; set; }
public DateTime ValidFrom { get; set; }
public DateTime ValidTo { get; set; }
public string Audience { get; set; }
public string Issuer { get; set; }
public IEnumerable<Claim> Claims { get; set; }
}
I have then used the internal token type to populate an instance of MyCustomToken in the ReadToken implementation of MyCustomTokenHandler:
public override SecurityToken ReadToken(XmlReader reader)
{
// Check token signature using EnvelopedSignatureReader (more performant but more complex to use)
// or SignedXml (easier to use but less performant)
MyCustomToken token = new MyCustomToken(
new MyCustomTokenInternal()
{
Id = reader.GetAttribute("Id", TokenNamespace),
ValidFrom = XmlConvert.ToDateTime(reader.GetAttribute("ValidFrom", TokenNamespace)),
ValidTo = XmlConvert.ToDateTime(reader.GetAttribute("ValidTo", TokenNamespace)),
Audience = reader.GetAttribute("Audience", TokenNamespace),
Issuer = reader.GetAttribute("Issuer", TokenNamespace),
Claims = from el in XElement.Load(reader).Elements(XName.Get("Claim", TokenNamespace)) select new Claim(el.Attribute("Namespace").Value, el.Value)
});
return token;
}
Browse to the RP again … Error!
“ID4011: A SecurityTokenHandler is not registered to validate token type 'MyCustomToken'”
Ok … as well as being able to read the token, the token handler also needs to be able to validate it. I therefore need to override the ValidateToken method and I am also going to pre-empt the possibility that I need to provide a ValidateToken implementation (as for CanReadToken/ReadToken):
public override bool CanValidateToken
{
get
{
return true;
}
}
public override ClaimsIdentityCollection ValidateToken(SecurityToken token)
{
ClaimsIdentityCollection idColl = new ClaimsIdentityCollection();
IClaimsIdentity id = new ClaimsIdentity((token as MyCustomToken).Claims);
idColl.Add(id);
return idColl;
}
Browse to the RP again … Success!
Finally, to prove that the token conditions are being honoured by WIF I have changed the ValidTo date to a time in the past. When I now attempt to browse to the RP I get the following error:
“Specified argument was out of the range of valid values. Parameter name: validFrom”
Which is slightly odd as it is the ValidTo date that is incorrect, but at least an error occurs.
In conclusion, I have shown how it is possible to consume a custom token type in WIF using the SecurityToken and SecurityTokenHandler classes.
Finally I have included the complete classes below for reference.
Have fun!
Written by Bradley Cotier
MyCustomTokenHandler:
using System;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.IdentityModel.Claims;
using Microsoft.IdentityModel.Tokens;
/// <summary>
/// Summary description for MyCustomTokenHandler
/// </summary>
public class MyCustomTokenHandler : SecurityTokenHandler
{
private const string TokenNamespace = "urn:mycustomtoken";
public MyCustomTokenHandler()
{
}
public override string[] GetTokenTypeIdentifiers()
{
return new string[] { TokenNamespace };
}
public override Type TokenType
{
get { return typeof(MyCustomToken); }
}
public override bool CanReadKeyIdentifierClause(XmlReader reader)
{
if (reader.LocalName.Equals("X509Data"))
{
return true;
}
return false;
}
public override bool CanReadToken(XmlReader reader)
{
if (reader.LocalName.Equals("MyCustomToken") &&
reader.NamespaceURI.Equals("urn:mycustomtoken"))
{
return true;
}
return false;
}
public override SecurityToken ReadToken(XmlReader reader)
{
// Check token signature using EnvelopedSignatureReader (more performant but more complex to use)
// or SignedXml (easier to use but less performant)
MyCustomToken token = new MyCustomToken(
new MyCustomTokenInternal()
{
Id = reader.GetAttribute("Id", TokenNamespace),
ValidFrom = XmlConvert.ToDateTime(reader.GetAttribute("ValidFrom", TokenNamespace)),
ValidTo = XmlConvert.ToDateTime(reader.GetAttribute("ValidTo", TokenNamespace)),
Audience = reader.GetAttribute("Audience", TokenNamespace),
Issuer = reader.GetAttribute("Issuer", TokenNamespace),
Claims = from el in XElement.Load(reader).Elements(XName.Get("Claim", TokenNamespace)) select new Claim(el.Attribute("Namespace").Value, el.Value)
});
return token;
}
public override bool CanValidateToken
{
get
{
return true;
}
}
public override ClaimsIdentityCollection ValidateToken(SecurityToken token)
{
ClaimsIdentityCollection idColl = new ClaimsIdentityCollection();
IClaimsIdentity id = new ClaimsIdentity((token as MyCustomToken).Claims);
idColl.Add(id);
return idColl;
}
}
MyCustomToken:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IdentityModel.Tokens;
using Microsoft.IdentityModel.Claims;
/// <summary>
/// Summary description for MyCustomToken
/// </summary>
public class MyCustomToken : SecurityToken
{
private MyCustomTokenInternal tokenInt;
public MyCustomToken(MyCustomTokenInternal tokenInt)
{
this.tokenInt = tokenInt;
}
public override string Id
{
get { return this.tokenInt.Id; }
}
public override ReadOnlyCollection<SecurityKey> SecurityKeys
{
get { return null; }
}
public override DateTime ValidFrom
{
get { return this.tokenInt.ValidFrom; }
}
public override DateTime ValidTo
{
get { return this.tokenInt.ValidTo; }
}
public IEnumerable<Claim> Claims
{
get { return this.tokenInt.Claims; }
}
}
MyCustomTokenInternal:
using System;
using System.Collections.Generic;
using Microsoft.IdentityModel.Claims;
/// <summary>
/// Summary description for MyCustomTokenInternal
/// </summary>
public class MyCustomTokenInternal
{
public string Id { get; set; }
public DateTime ValidFrom { get; set; }
public DateTime ValidTo { get; set; }
public string Audience { get; set; }
public string Issuer { get; set; }
public IEnumerable<Claim> Claims { get; set; }
}
RP web.config <microsoft.identityModel> config section:
<microsoft.identityModel>
<service>
<audienceUris>
<add value="https://mycustomtokenhandlerwebsite/" />
</audienceUris>
<federatedAuthentication>
<wsFederation
passiveRedirectEnabled="true"
issuer="https://localhost:29460/MyCustomTokenHandlerSTS/STSHandler.ashx"
realm="https://localhost:58724/MyCustomTokenHandlerWebsite/"
requireHttps="false" />
<cookieHandler requireSsl="false" />
</federatedAuthentication>
<issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry">
<trustedIssuers />
</issuerNameRegistry>
<securityTokenHandlers>
<add type="MyCustomTokenHandler" />
</securityTokenHandlers>
</service>
</microsoft.identityModel>