[Dev Tip] 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="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
            <Reference URI="">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                </Transforms>
                <DigestMethod Algorithm="http://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="http://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!
image_2_2F5D7C2C[1]
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="http://localhost:29460/MyCustomTokenHandlerSTS/STSHandler.ashx" 
                realm="http://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>

REF: 
http://blogs.msdn.com/b/mcsuksoldev/archive/2011/03/25/write-a-custom-security-token-and-handler-in-windows-identity-foundation.aspx
https://github.com/IdentityServer/Thinktecture.IdentityServer3
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s