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.