Thursday, October 1, 2009

Calling Oracle Service Bus from MSFT WCF Client Using an STS

I hope that this is the first of two posts. In the second post, I want to able to describe how to do this use case with out an STS. As people know from this blog, I think that an STS has a time and a place. When I first did this integration, there was a real reason for having the STS. We were implementing what was essentially the MSFT claims based authorization model. The STS was calling out to an entitlements system than needed to be invoked using native .net authentication. The alternative was to have OSB generate a Kerberos Ticket for a user that it didn't have the password, and call the entitlements service. Let's just say many people consider this against security best practices. Now that I'm faced with doing this again for another customer, I eager to figure out how to do this without the STS. That aside, here's the approach.

Also, I couldn't have done this without Symon Chang, Anand Kothari, Wil Hopkins - very very smart engineers.

Overview


WCF, by default uses windows authentication. Windows authentication is based on Kerberos, so from the WCF perspective, the most logical way of propagating identity would be to use WS-Security Kerberos Token Profile. This is the standard way of conveying a Kerberos Ticket in a SOAP Message. This is supported in WCF OOTB.


The problem is that OSB 10gR3 does not. OSB10gR3 has no support for Kerberos at the message level. OSB does have support for Kerberos as part of the transport level security provided by SPNEGO. As for message level, OSB 10gR3 has support for Usename/Password, X.509 Certificate, and SAML profiles for WS-Security 1.0. SAML provides the best fit for this use case since it allows for the identity in the windows environment to remain native, only relying on SAML when calling services on the OSB.


WCF also supports WS-Security SAML 1.1 Token Profile for WS-Security 1.0, so this seems like a good profile to use to meet the requirements, and therefore focus on. WCF requires a Security Token Service (STS) to generate the SAML Assertion. Microsoft provides a sample, but the sample needs to be modified to generate a SAML Assertion that OSB understands. Also, WCF favors symmetric bindings for WS-Security. This is probably because WS-Security Kerberos Token profile uses the Kerberos Session Id as the key. OSB 10gR3 only supports an asymmetric binding - X.509 certificates are used to sign the message and bind the SAML assertion to it.


On the OSB side, a pipeline needs to be configured to handle the WS-Security policies. The inbound policy is WS-Security SAML 1.1 Token Profile for WS-Security 1.0 and the outbound policy is that the message is signed by the service. This is because MSFT expects that an endpoint that is protected using WS-Security will secure the response as well. To support this OSB configuration, WLS Security realm needs to be configured to consume and validate SAML Assertions as well as configure Public/Private Key pairs and corresponding trust stores for the message signature operations dictated by the WS-Security policies.


The flow is that a WCF client calls the STS. The STS generates a SAML Assertion signed by the STS that contains the name of the user as the Subject. The SAML Assertion uses the sender-vouches confirmation method. The SAML Assertion is added to the WS-Security Header, and the message is signed by invoking service. The message is sent to OSB where the SAML Assertion is verified along with the message signature. Once the message is processed, the return message is signed by the OSB identity. The signature is validated by the WCF client to ensure that the message has not been tampered and was sent by the OSB.

Customizing the STS Sample to Work with OSB


The sample STS provided by Microsoft needs to be modified to work with OSB in this scenario. The sample STS has the following issues:



  • Sample STS needs to be modified to use X509RawCertificate format. OSB does not support SHA1Thumbprint
  • Sample STS needs to be modified to use Sender-Vouches confirmation method instead of Holder of Key.
  • Sample STS needs to be modified to use sign the assertion with the private key of the issuer, not the encrypted key. OSB does not the use of symmetric encrypted keys, only un-encrypted asymmetric keys.
  • Sample STS needs to be modified to include an AuthenticationStatement in the SAML Assertion. This is where OSB looks for the user's identity.
  • Sample STS needs to be modified to add a wsu:Id to the saml:Assertion, otherwise WCF cannot use it as an IssuedToken with an asymmetric binding


These issues can be addressed mainly by modifying the SamlTokenCreator.class

//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
using System;

using System.Collections.Generic;
using System.Collections.ObjectModel;

using System.IdentityModel.Tokens;

using System.ServiceModel;
using System.ServiceModel.Security;
using System.ServiceModel.Security.Tokens;
using System.Text;
using System.Xml;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.Configuration;
using System.Security.Principal;
using Common;


namespace Microsoft.ServiceModel.Samples.Federation
{
public sealed class SamlTokenCreator
{
#region CreateSamlToken()
/// <summary>
/// Creates a SAML Token with the input parameters
/// </summary>
/// <param name="stsName">Name of the STS issuing the SAML Token</param>
/// <param name="proofToken">Associated Proof Token</param>
/// <param name="issuerToken">Associated Issuer Token</param>
/// <param name="proofKeyEncryptionToken">Token to encrypt the proof key with</param>
/// <param name="samlConditions">The Saml Conditions to be used in the construction of the SAML Token</param>
/// <param name="samlAttributes">The Saml Attributes to be used in the construction of the SAML Token</param>
/// <returns>A SAML Token</returns>
public static SamlSecurityToken CreateSamlToken(string stsName,
BinarySecretSecurityToken proofToken,
SecurityToken issuerToken,
SecurityToken proofKeyEncryptionToken,
SamlConditions samlConditions,
IEnumerable<SamlAttribute> samlAttributes)
{





// Create a security token reference to the issuer certificate
SecurityKeyIdentifierClause skic = issuerToken.CreateKeyIdentifierClause<X509RawDataKeyIdentifierClause>();
SecurityKeyIdentifier issuerKeyIdentifier = new SecurityKeyIdentifier(skic);

//Get the user
WindowsIdentity wi = ServiceSecurityContext.Current.WindowsIdentity;

// Create a SamlSubject
SamlSubject samlSubject = new SamlSubject(SamlConstants.UserNameNamespace,
SamlConstants.UserName,
wi.Name);
//Set the Confirmation method to Sender-Vouches
samlSubject.ConfirmationMethods.Add(SamlConstants.SenderVouches);

//Create the Authentication Statement
SamlAuthenticationStatement samlAuthStatement = new SamlAuthenticationStatement();
samlAuthStatement.SamlSubject = samlSubject;

// Put the SamlAttributeStatement into a list of SamlStatements
List<SamlStatement> samlSubjectStatements = new List<SamlStatement>();
samlSubjectStatements.Add(samlAuthStatement);

// Create a SigningCredentials instance from the key associated with the issuerToken.
SigningCredentials signingCredentials = new SigningCredentials(issuerToken.SecurityKeys[0],
SecurityAlgorithms.RsaSha1Signature,
SecurityAlgorithms.Sha1Digest,
issuerKeyIdentifier);


// Create the SamlAssertion
String assertionId = "_"+Guid.NewGuid().ToString();

SamlAssertion samlAssertion = new SamlAssertion(assertionId,
"uri:"+stsName.Replace(' ','_'),
DateTime.UtcNow,
samlConditions,
new SamlAdvice(),
samlSubjectStatements
);

//Wrap the SamlAssertion so that the wsu:Id can be added
CustomSamlAssertion customAssertion = new CustomSamlAssertion(samlAssertion);

// Set the SigningCredentials for the SamlAssertion
customAssertion.SigningCredentials = signingCredentials;


// Create a SamlSecurityToken from the SamlAssertion and return it
SamlSecurityToken st = new SamlSecurityToken(customAssertion);

return st;
}

#endregion

private SamlTokenCreator() { }

static X509Certificate2 LookupCertificate(StoreName storeName, StoreLocation storeLocation, string thumbprint)
{
X509Store store = null;
try
{
store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certs = store.Certificates.Find(X509FindType.FindByThumbprint,
thumbprint, false);
if (certs.Count != 1)
{
throw new Exception(String.Format("FedUtil: Certificate {0} not found or more than one certificate found", thumbprint));
}
return (X509Certificate2)certs[0];
}
finally
{
if (store != null) store.Close();
}
}

}
}


This code above references another class - CustomSAMLAssertion.class. This class fixes the issue of the SAMLAssertion not having a wsu:Id

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Text;
using System.Xml;
using System.IO;

namespace Common
{
class CustomSamlAssertion: SamlAssertion
{


public CustomSamlAssertion(SamlAssertion theAssertion):
base(
theAssertion.AssertionId,
theAssertion.Issuer,theAssertion.IssueInstant,
theAssertion.Conditions,
theAssertion.Advice,
theAssertion.Statements)
{


}

public override void WriteXml(System.Xml.XmlDictionaryWriter writer, SamlSerializer samlSerializer, System.IdentityModel.Selectors.SecurityTokenSerializer keyInfoSerializer)
{
StringBuilder myBuilder = new StringBuilder();
XmlDictionaryWriter myWriter = XmlDictionaryWriter.CreateDictionaryWriter(XmlDictionaryWriter.Create(myBuilder));


base.WriteXml(myWriter, samlSerializer, keyInfoSerializer);

myWriter.Close();

String contents = myBuilder.ToString();

//contents = contents + "";


XmlDictionaryReader reader =
XmlDictionaryReader.CreateDictionaryReader(XmlDictionaryReader.Create(new StringReader(contents)));


StringBuilder myBuilder2 = new StringBuilder();
XmlDictionaryWriter myWriter2 = XmlDictionaryWriter.CreateDictionaryWriter(XmlDictionaryWriter.Create(myBuilder2));

try
{
while (reader.Read())
{

WriteShallowNode(reader, writer);


}
}
catch (Exception e)
{
Console.Out.WriteLine(e);
//writer.Flush();
//String contents2 = myBuilder2.ToString();

//throw e;

return;


}

//writer.Flush();

}

void WriteShallowNode(XmlReader reader, XmlWriter writer)
{

if (reader == null)
{

throw new ArgumentNullException("reader");

}

if (writer == null)
{

throw new ArgumentNullException("writer");

}



switch (reader.NodeType)
{

case XmlNodeType.Element:

//writer.WriteStartElement(reader.LocalName);

writer.WriteStartElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);

writer.WriteAttributes(reader, true);

if (reader.LocalName.Equals("Assertion")) {


writer.WriteAttributeString(
"wsu",
"Id",
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
this.AssertionId);
}



if (reader.IsEmptyElement)
{

writer.WriteEndElement();

}

break;

case XmlNodeType.Text:

writer.WriteString(reader.Value);

break;

case XmlNodeType.Whitespace:

case XmlNodeType.SignificantWhitespace:

writer.WriteWhitespace(reader.Value);

break;

case XmlNodeType.CDATA:

writer.WriteCData(reader.Value);

break;

case XmlNodeType.EntityReference:

writer.WriteEntityRef(reader.Name);

break;

case XmlNodeType.XmlDeclaration:

break;


case XmlNodeType.ProcessingInstruction:

writer.WriteProcessingInstruction(reader.Name, reader.Value);

break;

case XmlNodeType.DocumentType:

writer.WriteDocType(reader.Name, reader.GetAttribute("PUBLIC"), reader.GetAttribute("SYSTEM"), reader.Value);

break;

case XmlNodeType.Comment:

writer.WriteComment(reader.Value);

break;

case XmlNodeType.EndElement:

writer.WriteEndElement();

break;

}

}


}
}


Configuring the WCF Client


WCF supports a large number of authentication methods and profile bindings simply and easily. This is typically done by modifying the configuration file through the WCF Service Configuration Editor. Unfortunately, there is no way through configuration to set-up the client. This needs to be done programmatically. The WS-Policy that OSB uses is essentially as follows:

<?xml version="1.0"?>
<wsp:Policy
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702"
>
<sp:AsymmetricBinding>
<wsp:Policy>
<sp:InitiatorToken>
<wsp:Policy>
<sp:X509Token
sp:IncludeToken="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702/IncludeToken/AlwaysToRecipient">
<wsp:Policy>
<sp:WssX509V3Token10/>
</wsp:Policy>
</sp:X509Token>
</wsp:Policy>
</sp:InitiatorToken>
<sp:RecipientToken>
<wsp:Policy>
<sp:X509Token
sp:IncludeToken="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702/IncludeToken/Never">
<wsp:Policy>
<sp:WssX509V3Token10/>
</wsp:Policy>
</sp:X509Token>
</wsp:Policy>
</sp:RecipientToken>
<sp:AlgorithmSuite>
<wsp:Policy>
<sp:Basic256/>
</wsp:Policy>
</sp:AlgorithmSuite>
<sp:Layout>
<wsp:Policy>
<sp:Lax/>
</wsp:Policy>
</sp:Layout>
<sp:IncludeTimestamp/>
<sp:ProtectTokens/>
<sp:OnlySignEntireHeadersAndBody/>
</wsp:Policy>
</sp:AsymmetricBinding>
<sp:SignedSupportingTokens>
<wsp:Policy>
<sp:SamlToken
sp:IncludeToken="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702/IncludeToken/AlwaysToRecipient">
<wsp:Policy>
<sp:WssSamlV11Token10/>
</wsp:Policy>
</sp:SamlToken>
</wsp:Policy>
</sp:SignedSupportingTokens>
<sp:Wss10>
<wsp:Policy>
<sp:MustSupportRefKeyIdentifier/>
<sp:MustSupportRefIssuerSerial/>
</wsp:Policy>
</sp:Wss10>
</wsp:Policy>


The translation between this policy and the WCF APIs is pretty straight forward with one exception - the SAML Token itself. In WCF, the SAML Token is retrieved from the STS, so the WCF client needs to be configured to communicate to it. In WCF, authentication from a token retrieved from an STS is called IssuedToken. All of this can be done programmatically through the WCF APIs. For simplicity sake, the creation of the custom AsymmetricSecurity binding can be encapsulated as a WCF Binding Element Extension. This allows for the inclusion of custom binding elements () inside of a custombinding.


<extensions>
<bindingElementExtensions>
<add name="osbsecurity" type="OSBWCFExtensions.OSBSecurityElement, OSBWCFExtensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=63fc46aa660659ca" />
</bindingElementExtensions>
</extensions>

<bindings>
<customBinding>
<binding name="HelloWorldServiceServiceSoapBinding">
<textMessageEncoding maxReadPoolSize="64" maxWritePoolSize="16"
messageVersion="Soap12" writeEncoding="utf-8">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
</textMessageEncoding>
<osbsecurity STSAddress="http://fedtest/FederationSample/HomeRealmSTS/STS.svc"/>
<httpsTransport manualAddressing="false" maxBufferPoolSize="524288"
maxReceivedMessageSize="165536" allowCookies="false" authenticationScheme="Anonymous"
bypassProxyOnLocal="false" hostNameComparisonMode="WeakWildcard"
keepAliveEnabled="true" maxBufferSize="165536" proxyAuthenticationScheme="Anonymous"
realm="" transferMode="Buffered" unsafeConnectionNtlmAuthentication="false"
useDefaultWebProxy="true" requireClientCertificate="true"/>



</binding>
</customBinding>

Inside of the OSBSecurityElement, the WCF API calls are made that create the proper binding for sending a SAML Assertion to OSB.

protected override System.ServiceModel.Channels.BindingElement CreateBindingElement()
{

//Retrieve the STS Address from the config
ConfigurationProperty stsConfig = this.Properties["STSAddress"];


//Set-up the Asymmetric binding with the recipient and initiator's parameters
//Keys are identified by the issuersSerial as required by the policy
//OSB does not support derived keys, so they are disabled
X509SecurityTokenParameters initiatorParams = new X509SecurityTokenParameters(X509KeyIdentifierClauseType.IssuerSerial, SecurityTokenInclusionMode.AlwaysToRecipient);
initiatorParams.RequireDerivedKeys = false;


X509SecurityTokenParameters recipientParams = new X509SecurityTokenParameters(X509KeyIdentifierClauseType.IssuerSerial, SecurityTokenInclusionMode.Never);
recipientParams.RequireDerivedKeys = false;

AsymmetricSecurityBindingElement security = new AsymmetricSecurityBindingElement(recipientParams, initiatorParams);


security.SecurityHeaderLayout = SecurityHeaderLayout.Lax;
security.MessageSecurityVersion = MessageSecurityVersion.WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10;
security.SetKeyDerivation(false);

//Configure the STS and the resulting SAML Assertion as a signed supporting token
WSHttpBinding stsBinding = new WSHttpBinding();

//This credential type is how the caller identifies themself to the STS
stsBinding.Security.Message.ClientCredentialType = MessageCredentialType.Windows;

IssuedSecurityTokenParameters issuedTokenParamters =
new IssuedSecurityTokenParameters("", new EndpointAddress((String)base["STSAddress"]), stsBinding);

issuedTokenParamters.RequireDerivedKeys = false;
issuedTokenParamters.ReferenceStyle = SecurityTokenReferenceStyle.Internal;
issuedTokenParamters.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;
security.EndpointSupportingTokenParameters.Signed.Add(issuedTokenParamters);

//Set this to process the signature of the response
security.AllowSerializedSigningTokenOnReply = true;

return security;
}
}

By using the custom binding element extension, the client code remains unchanged:

HelloWorldClient client = new HelloWorldClient();
Console.Out.WriteLine(client.test1("WCF Client"));

Configuring OSB Domain's Security Domian


The inbound SAML processing requires the creation and configuration of a SAML Identity Asserter. For this scenario, the SAML V2 Identity Asserter should be used. It supports SAML 1.1 sender-vouches subject confirmation method. It needs to be configured with an asserting party that corresponds to the STS. Since the SAML Assertion is signed, OSB needs to be configured to trust the signer of the assertion. This can be done my adding the certificate authorities (CAs) that make up the STS's certificate chain to the list of trusted CAs. Which keystore to add them to depends of the trust mode that the OSB domain is running, but by default, these can be added to the cacerts keystore found in JRE_HOME/jre/lib/security.

In some scenarios, the identity being asserted by the SAML assertion can be trusted, and in others, the identity needs to be validated against some other authentication source - mainly Active Directory. OSB domain can be configured to support both. To trust the identity, a SAML Authentication Provider needs to be added to the realm. Make sure to configure it with an appropriate JAAS Control Flag. The simplest way to avoid any conflicts is to mark all of the authentication providers as OPTIONAL. Also, the asserting party configuration in the SAML Identity Asserter needs to be configured to Allow Virtual Users. Otherwise, the SAML Authentication Provider will not work. If "Allow Virtual Users" is not checked for the asserting party, then the security realm will try to validate the user against the authentication providers configured for the realm. The name that the STS above generates is of the form domain/username. In most cases, a custom username mapper will need to be written and configured on the SAML Identity Asserter to split off the domain portion of the name.

A PKI CredMapper needs to be configured so that OSB can generate digital signatures for outbound requests. The PKI CredMapper is configured to point to a Java Keystore. The identity of the OSB should be available in this keystore, and should be the same identity as the ServiceCert configured in the WCF client.

Configuring the OSB Pipeline


The OSB service needs to be configured to process the WS-Security header sent by WCF. The inbound request message needs to be configured with the SAML Token Profile 1.0 - Sender Vouches policy.
<wsp:Policy
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
xmlns:wssp="http://www.bea.com/wls90/security/policy"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
xmlns:wls="http://www.bea.com/wls90/security/policy/wsee#part"
wsu:Id="samlPolicy-sender-vouches-simple">
<wssp:Identity>
<wssp:SupportedTokens>
<wssp:SecurityToken TokenType="http://docs.oasis-open.org/wss/2004/01/oasis-2004-01-saml-token-profile-1.0#SAMLAssertionID">
<wssp:Claims>
<wssp:ConfirmationMethod>sender-vouches</wssp:ConfirmationMethod>
</wssp:Claims>
</wssp:SecurityToken>
</wssp:SupportedTokens>
</wssp:Identity>
</wsp:Policy>

The OSB response needs be signed. This can be done by creating a Service Key Provider that points to the identity of the OSB, and then adding the predefined Sign.xml policy to the response operation.

References


The mapping of WCF configuration to WS-Policy and the security capabilities are nicely described

A description of why wsu:Id needs to be added to the SAML Assertion

A good discussion of a variation of this use case

The sample STS from Microsoft, which has extended to integrate with OSB

7 comments:

  1. Great post, any chance of sharing the sample?

    ReplyDelete
  2. The MSFT sample for the STS is here

    The rest of the code is available on the blog. Is there more detail that you need?

    JB

    ReplyDelete
  3. Great Post. Where you able to achieve this with an STS?

    ReplyDelete
  4. Hello Josh, this is a very interesting scenario.

    I’m trying to make it works for an OSB Proof of Concept, but as I’m not a WCF specialist, I’m a bit lost for the custom binding implementation.

    My understanding is we need to create a .Net assembly “OSBWCFExtensions” that expose a class “OSBSecurityElement” that extends the “BindingElementExtensionElement” class and override the method “CreateBindingElement()”, correct ?

    After in the bookstore client we need to define the “bindingElementExtensions” and create a “customBinding” that will be used instead of the WSFederationHttpBinding to configure the BuyBook endpoint…

    It is a bit confusing that you illustrate this part with this “HelloWorldServiceServiceSoapBinding” instead of presenting the modification of the BookStoreClient App.config file…

    Would it be possible to have more details for this WCF client configuration part?

    Actually today, when I try to add the bindingElementExtension and the custom binding as described in the blog I’ve got an error at client instantiation telling me that the “STSAddress” attribute is not recognized… Maybe I’m missing something obvious but if you can help on this part, it would save me a lot of time
    Best regards,

    ReplyDelete
  5. Josh, you wrote that "OSB does have support for Kerberos as part of the transport level security provided by SPNEGO.".

    Can you explain how to do kerberos auth for proxy service? This moment I have linux machine with correctly working kerberos auth for applications (for example WebLogic and OSB Consoles)...

    I tried several variants... for example:

    proxy service settings:
    - Authentication: Custom Authentication (See Advanced Settings)
    - Authentication Header: WWW-Authenticate or Authorization
    - Authentication Token Type: Authorization.Negotiate or WWW-Authenticate.Negotiate

    ReplyDelete
  6. Josh,
    I worked with you 18 months ago to get this going for a client in Perth, West Australia - to be fair you did all the work and I did the testing :)

    I'm at another client - trying to get this going again. So thanks for the great post!
    Todd

    ReplyDelete
  7. Adding the wsu:Id to the SAML Assertion was never a good idea, but now it's entirely unnecessary.

    With the hotfix described in support.microsoft.com articleId=974842, adding the wsu:Id to the SAML Assertion is no longer necessary. WCF now implicitly signs the Assertion based on the AssertionID, or, alternatively, by setting a new property, STR-Transform, you can get WCF to generate the STR-Transform .

    OSB accepts SAML Assertions signed directly against the AssertionId, even if it is vaguely non-compliant, so setting the STR-Transform is not strictly necessary.

    Also, STS encryption of the SAML Assertion is not necessary for sender-vouches scenarios, so it's not even necessary to add the wsu:Id to the element in those cases.

    Hope this information spares someone the headbanging it took me to figure this out.

    ReplyDelete

Note: Only a member of this blog may post a comment.