Freigeben über


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 an 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

and 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 preempt 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

 

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!

 

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>

written by [bradleycotier]

Comments

  • Anonymous
    January 15, 2013
           <trustedIssuers>         <add thumbprint="B30E0E2FA1A04D27FD07286A6EA24BA4E3DA7264" name="appbyavi.accesscontrol.windows.net/" />       </trustedIssuers>     </issuerNameRegistry>     <securityTokenHandlers>       <add type="MyCustomTokenHandler" />     </securityTokenHandlers>     <certificateValidation certificateValidationMode="None" />   </service> </microsoft.identityModel></configuration>i have added all the classes and seciritytokenhandler(in webconfig) to my website and these are at app_code.when i am running the application its throwing exception  :A SecurityTokenHandler is not registered to read security token ('BinarySecurityToken', 'docs.oasis-open.org/.../oasis-200401-wss-wssecurity-secext-1.0.xsd&)......
  • Anonymous
    January 17, 2013
    Hi AvinashAs discussed offline, a couple of things might be going on here. Either your security token handler is not registered or the CanReadToken implementation is not returning true. Can you debug the CanReadToken method to confirm if this is the case?Brad