Eric's Blog

Day to day experience in .NET
Welcome to Blogs @ IRM Sign in | Join | Help
 Search

Disclaimer

The content of this site is my own personal opinion and does not in any way represent my employer, it's subsideries or affiliates. These postings are provided "AS IS" with no warranties, and confer no rights.

This Blog

Manually Issue a SAML token

In my requirements for the STS implementation I wanted to be able to use the SAML token in the client and I also wanted to log in the user against the STS when the user logs in through the GUI and not when the first call to a service is made. To be able to fulfill this I need to manually issue a SAML token and set the token to my custom principal object covered in previous posts (here and here).

Following the ABC (Address, Behavior and Contract) of WCF for issuing the SAML token, I will start with the contract, since behaviors and addresses are configured based on the contract. I already have a IWSTrustFeb2005SecurityTokenService interface, but to make things a little bit easier, I have choosen to derive a new contract from it, which also derives from IClientChannel.

interface IWSTrustFeb2005SecurityTokenServiceChannel : 
IWSTrustFeb2005SecurityTokenService, IClientChannel { }

With the contract in place I can now set up a ChannelFactory, which will also read the configuration file for the Address and Behavior, based on the contract. Username and password is parameters to my method.

ChannelFactory<IWSTrustFeb2005SecurityTokenServiceChannel> cf = 
new ChannelFactory<IWSTrustFeb2005SecurityTokenServiceChannel>(endpointConfigurationName); WSHttpBinding b = cf.Endpoint.Binding as WSHttpBinding; if (b != null && b.Security.Message.ClientCredentialType == MessageCredentialType.UserName) { cf.Credentials.UserName.UserName = userName; cf.Credentials.UserName.Password = password; }

With the ChannelFactory in place so that it is possible to create Channel according to the interface, it is now time to create the request message.

RequestSecurityToken rst = new RequestSecurityToken();
rst.RequestType = Constants.Trust.RequestTypes.Issue;
rst.TokenType = Constants.Saml.TokenTypes.Saml11; //TODO: Configureable?
rst.AppliesTo = new EndpointAddress(appliesTo);

Message request = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, Constants.Trust.Actions.Issue, rst);

// Send message
IWSTrustFeb2005SecurityTokenServiceChannel c = cf.CreateChannel();
Message response = c.Issue(request);

RequestSecurityToken is the class representing the request message and the code is really straight forward. When the response message arrives, I need to parse it, by traversing a XmlReader. I got the reader by calling GetReaderAtBodyContents og the response message.

while (reader.Read())
{
    if (reader.LocalName == Constants.Trust.Elements.TokenType)
    {
        reader.Read();
        tokenType = reader.ReadContentAsString();
        reader.ReadEndElement();
    }
    
    if (reader.LocalName == Constants.Trust.Elements.RequestedSecurityToken)
    {
        reader.Read();
        token = new XmlDocument();
        token.Load(reader);
        reader.ReadEndElement();
    }
    if (reader.LocalName == Constants.Trust.Elements.RequestedAttachedReference)
    {
        reader.Read();
        requestAttacedReference = serializer.ReadKeyIdentifierClause(reader);
        reader.ReadEndElement();
    }
    if (reader.LocalName == Constants.Trust.Elements.RequestedUnattachedReference)
    {
        reader.Read();
        requestUnattacedReference = serializer.ReadKeyIdentifierClause(reader);
        reader.ReadEndElement();
    }
    if (reader.LocalName == Constants.Trust.Elements.RequestedProofToken)
    {
        reader.Read();
        proofToken = (BinarySecretSecurityToken)serializer.ReadToken(reader, null);
        reader.ReadEndElement();
    }
}

I need to load the requested security token into the XmlDocument, because since I cannot fully decrypt the security token, I need to create a GenericXmlSecurityToken from it, and that class requires a XmlElement. Note that all of the saml token is not encrypted, but rather a key for the relying party (business service). This is good because the GenericXmlSecurityToken also needs two dates to specifiy the range for which the token is valid. This dates are available in the saml conditions element.

XmlElement node = token.DocumentElement[Constants.Saml.Elements.Conditions, SamlConstants.Namespace];
XmlAttribute notBeforeAttribute = node.Attributes[Constants.Saml.Attributes.NotBefore];
XmlAttribute notOnOrAfterAttribute = node.Attributes[Constants.Saml.Attributes.NotOnOrAfter];

DateTime effectiveTime = DateTime.Parse(notBeforeAttribute.Value, CultureInfo.InvariantCulture);
DateTime expirationTime = DateTime.Parse(notOnOrAfterAttribute.Value, CultureInfo.InvariantCulture);
GenericXmlSecurityToken t = 
new GenericXmlSecurityToken(token.DocumentElement, proofToken, effectiveTime, expirationTime,
requestAttacedReference, requestUnattacedReference, null);

Finally I need to extract the claims from the attribute statement in the SAML token.

private static IList<Claim> ExtractClaimsFromSamlToken(XmlDocument token)
{
    Guard.ArgumentNotNull(token, "token");

    //Get the saml:AttributeStatement node
    XmlNamespaceManager namespaceManager = new XmlNamespaceManager(token.NameTable);
    namespaceManager.AddNamespace("saml", SamlConstants.Namespace);
    XmlNode node = token.DocumentElement.SelectSingleNode("saml:AttributeStatement", namespaceManager);
    node = node.CloneNode(true); //get a copy to be able to modify it without affecting original

    //Remove all nodes, except for saml:Attribute
    foreach (XmlNode childNode in node.ChildNodes)
    {
        if (childNode.LocalName != Constants.Saml.Elements.Attribute)
            node.RemoveChild(childNode);
    }

    using (System.IO.StringReader stringReader = new System.IO.StringReader(node.OuterXml))
    using (XmlReader reader = XmlReader.Create(stringReader))
    {
        reader.Read(); //saml:AttributeStatement
        return GetClaims(XmlDictionaryReader.CreateDictionaryReader(reader));
    }
}

private static IList<Claim> GetClaims(XmlDictionaryReader reader)
{
    Guard.ArgumentNotNull(reader, "reader");

    List<Claim> claims = new List<Claim>();
    SamlSerializer ser = new SamlSerializer();

    reader.Read(); //Get to first saml:Attribute

    while (!reader.EOF && reader.LocalName == Constants.Saml.Elements.Attribute)
    {
        SamlAttribute attr = ser.LoadAttribute(reader, null, null);
        claims.AddRange(attr.ExtractClaims());
    }

    return claims;
}

The claims can then be used to set up a principal object for the client, that can provide the same functionallity for the client, even though it is not possible to deserializethe security token.

Note that I have removed exception handling and some other details, just make this post a little bit less code heavy than it already is.

Published den 30 januari 2008 14:58 by ericqu

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

Erics Blog said:

By using the SAML token in the client, it is possible to set up the principal and identity objects also

januari 30, 2008 15:07
 

Erics Blog said:

By using the SAML token in the client, it is possible to set up the principal and identity objects also

februari 3, 2008 17:32
 

Arvind said:

Can you please post the code ...Thanks in Advance....
augusti 12, 2008 23:44
 

ericqu said:

@Arvind: Unfortunately, I can't post the complete code because it is baked in with a lot of other code that I can't make public.

I belive that all code that is needed to manually issue a security token is in the post. If anything is missing, please let me know and I will update the post.

augusti 13, 2008 09:57
 

Scott said:

Eric, I've been working on a similar implementation as you have here. I have it working to get the GenericXmlSecurityToken out of the response. However, when I attempt to reuse this token (ala DurableIssuedTokenProvider example in the wcf sdk) I get some identity validation errors. When I just cache the token obtained through the indirect calls to the STS through configuration, everything works fine, but just as you stated, I don't want to issue tokens in this manner. Did you come across anything similar?
februari 9, 2009 23:15
 

ericqu said:

Unfortunately it has been a long while since I did this and I can't remember all problems I ran into when doing it. When I did this I ran into a lot of problems, but slowly Microsoft is solving this problem (we shouldn't write this kind of infrastructure code) with Geneva (https://connect.microsoft.com/site/sitehome.aspx?SiteID=642). I would definately evalute if Geneva isn't a more appropiate way to go if I started something today.

februari 12, 2009 21:24
 

Scott said:

If I didn't actually have to have production code in 2 months, I would consider it. As of right now it isn't even usable on XP or 2003 server. Just not realistic requirements.
februari 24, 2009 22:37

Leave a Comment

(required) 
(optional)
(required) 
Submit
Powered by Community Server, by Telligent Systems