Introduction
Web services are still somewhat of a grey area for many developers. The pace at which this part of the industry has progressed has in some cases caused a hesitation in the adoption of web services technologies, and in other cases has led to very bad architectural decisions. Many developers are battling to keep up with the frequent changes in the WS-* specifications, which are addressing a number of issues of concern; including security & trust, reliability, addressing & routing, and policy (and numerous others). This article will be showing how to implement a basic policy in your web services in order to clearly define and enforce a full contract between the service and the client.
What is WS-Policy?
All web services have a minimum set of requirements that must be met in order to be consumed by a client. These are normally documented for the client developer, who takes it into consideration when building a consumer of the service. This documentation would typically include the following:
- Must the message be digitally signed?
- Must the message be encrypted?
- What kind of security token must be used (i.e. Kerberos, x509, UsernameToken, etc.)?
- What is the expiration of the message?
WS-Policy provides a means of documenting these minimum requirements, and having them automatically enforced and validated on your behalf, without having to affect too much code (if any). WS-Policy conforms to a set XML schema, and is very easy to implement. Once you have a few basic examples working, you’ll quickly see how easy it is to cater for policy assertions in your new and existing web services.
Making the Policy available
When web services originally came out, WSDL was the only standardised means of documenting service composition. Since then, the WS-* specifications have provided a number of extensions to provide additional functionality. Each of these specifications is completely separate and none are required for a web service to operate correctly. “Composability” is the new term describing the WS-*. It basically means that you can select whichever extensions you want, without having to implement them all.
I’m sure many of you that have created web services will know that there’s no place in WSDL that supports policy attachments as part of the standard. The recommended way of way of getting your policy recognised within WSDL is documented in the WS-PolicyAttachment specification here. I’m not going into this in detail, but I assume you can read up on it outside of this article. Unfortunately it complicates the WSDL file quite a bit, and the purpose of this article is more to demonstrate the power and simplicity of WS-Policy. For now, you’re best off publishing this to the same virtual directory as the web service itself; that’s assuming your hosting your web service in IIS. WSE 2.0 now supports hosts other than IIS for web services, but that’s not the topic of this article. If you’d like me to do an article on that, contact me and I’ll make it happen.
How does WS-Policy work?
Policies can be defined for three different message types: request, response, and fault. Quite often these have complementary features. For example, a policy may require that a request be digitally signed with an x509 certificate, which then allows the response to be sent back using the requester’s x509 certificate to encrypt the data.
The WSE does all the hard work of verifying compliance with the policies, and returns a SOAP fault to the caller if the message does not fully adhere to the requirements. This is all done at runtime and is completely transparent to the developer. No code needs to be written for this, as it is catered for by the WSE. The WSE filters operate on the message pipeline and do whatever work is necessary before any custom code is reached. If a request message is non-compliant, the operation (or web method) is not even called. A SOAP fault is immediately sent back to the client and no code within the web service is executed. The important factor to note here is that the SOAP fault can only be sent back to the client. It is very important that the service developer ensures compliance with the policy document, otherwise the client could receive a SOAP fault due to no fault of their own!
The Structure of a WS-Policy Assertion
The structure of a basic policy assertion is really very simple. These are the minimum required elements for a policy assertion file:
<policyDocument>
<mappings>
<endpoint uri="[endpointURI]">
<defaultOperation>
<request policy="[policyID]" />
<response policy="[policyID]" />
<fault policy="[policyID]" />
</defaultOperation>
</endpoint>
</mappings>
<policies>
<policy />
</policies>
</policyDocument>
A policy document is made up of two main sections, <mappings> and <policies>. The <mappings> section defines the endpoints to which the policy applies. This is where you would define the web service URI that the policy applies to, as well as the ID’s of the policies that must be applied for the request, response, and fault. The <policies> section contains any number of <policy> elements, each defining rules that must be complied with. This would typically contain rules that define the integrity, confidentiality, etc. of the message. The WSE 2.0 supports the following main elements in policy declarations:
- Integrity (digital signatures)
- Confidentiality (encryption)
- MessagePredicate (specifies message parts that SOAP messages must contain)
- MessageAge
- SecurityToken
Of course, this is not the complete list (see the WSE 2.0 documentation for a full reference of the WS-Policy schema). You can also build your own extensions if required.
In the next few steps, we’ll be building a basic web service and client application. These will initially contain no security code whatsoever. Once we have these working, we’ll modify them to use WS-Policy to enforce some very basic security requirements. The purpose of the “coding lab” is to show how much WS-Policy reduces the coding required by the developer, as well as the flexibility it offers. This lab assumes you know how to create a web service and understand the concepts surrounding web services in their most basic form.
Example: Building a Simple Web Service
So as not to complicate the example, I’ll be using the standard web service created by Visual Studio for this example. Please follow these steps to configure your web service:
1. Open Visual Studio .NET and create a new Web Service Project (the language is irrelevant, but my example is given in C#).
2. Uncomment the sample “HelloWorld” web method. It should look like this:
[WebMethod]
public string HelloWorld()
{
return "Hello World";
}
3. Save the project.
That’s all you need for this example! I’ve deliberately kept the web method as simple as possible to demonstrate the power of WS-Policy. Please note that at this stage, there are no WSE 2.0 features in the web service you just created.
Example: Building a Simple Client
Building a client application is almost as simple.
1. Add a new Windows Forms Project to the solution.
2. Add a button to the form, then double-click to button to create a Click event handler.
3. Add the following code as your event handler:
try
{
PolicyService.PolicyService serviceProxy = new TestClient.PolicyService.PolicyService();
MessageBox.Show(serviceProxy.HelloWorld());
}
catch (Exception ex)
{
StringBuilder sb = new StringBuilder();
if (ex is System.Web.Services.Protocols.SoapException)
{
System.Web.Services.Protocols.SoapException se = ex as System.Web.Services.Protocols.SoapException;
sb.Append("SOAP-Fault code: " + se.Code.ToString());
sb.Append("\n");
}
if (ex != null)
{
sb.Append(ex.ToString());
}
MessageBox.Show(sb.ToString());
}
4. Make sure to replace the PolicyService class name with whatever you decided to name your proxy class.
At this stage, we have a fully functional web service and client application. This example doesn’t do anything useful, but once we’ve configured a policy you’ll understand the power we’re trying to demonstrate.
Example: Extending the Web Service with WS-Policy
Now we’re going to move on to the good stuff, and enable some new features that form part of the WSE 2.0!
1. Right-click the Web Service Project and select the “WSE Settings 2.0…” option. This is a new option only available once you’ve installed WSE 2.0. You’ll need to add the following “using” directives to your ASMX code as follows:
using Microsoft.Web.Services2;
using Microsoft.Web.Services2.Security;
using Microsoft.Web.Services2.Security.Tokens;
2. You’ll see two checkboxes on the first tab: “Enable this project for Web Services Enhancements” and “Enable Microsoft Web Service Enhancements Soap Extensions”. Select them both. These will automatically add a reference to the Microsoft.Web.Services2.dll assembly, which contains the full set of classes that enable WSE 2.0 functionality. They’ll also create some new entries in the web.config file.
3. Right-click the project and select “Add New Item…” and choose XML File. Name the file “policyCache.config” and click Open.
4. Type the following XML snippet into your policyCache file. You’ll probably notice that Microsoft has provided a “Policy” tab on the WSE Settings page. I’ve tried using this and found it quite difficult and inflexible. I could just be doing it wrong, so by all means give it a try. I’d very highly recommend typing it all manually until you’re comfortable with how it all hangs together.
<?xml version="1.0" encoding="utf-8"?>
<policyDocument xmlns="http://schemas.microsoft.com/wse/2003/06/Policy">
<mappings>
<endpoint uri="http://localhost/policytestservice/policyservice.asmx">
<defaultOperation>
<request policy="#SignedX509" />
<response policy="#SignedUsername" />
<fault policy="" />
</defaultOperation>
</endpoint>
</mappings>
<policies xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
xmlns:wssp="http://schemas.xmlsoap.org/ws/2002/12/secext"
xmlns:wsp="http://schemas.xmlsoap.org/ws/2002/12/policy">
<!-- This policy ensures that the message is signed with a UsernameToken -->
<wsp:Policy wsu:Id="SignedUsername">
<wssp:Integrity wsp:Usage="wsp:Required">
<wssp:TokenInfo>
<wssp:SecurityToken>
<wssp:TokenType>http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken</wssp:TokenType>
</wssp:SecurityToken>
</wssp:TokenInfo>
<wssp:MessageParts Dialect="http://schemas.xmlsoap.org/2002/12/wsse#part">wsp:Body()</wssp:MessageParts>
</wssp:Integrity>
</wsp:Policy>
<!-- This policy ensures that the message is signed with a X509 certificate -->
<wsp:Policy wsu:Id="SignedX509">
<wssp:Integrity wsp:Usage="wsp:Required">
<wssp:TokenInfo>
<wssp:SecurityToken>
<wssp:TokenType>http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3</wssp:TokenType>
</wssp:SecurityToken>
</wssp:TokenInfo>
<wssp:MessageParts Dialect="http://schemas.xmlsoap.org/2002/12/wsse#part">wsp:Body()</wssp:MessageParts>
</wssp:Integrity>
</wsp:Policy>
</policies>
</policyDocument>
You should recognise some familiar element names that were covered earlier in the article. Take some time to read through this policy document and make sense of what it’s trying to achieve. This is a lot to take in, so don’t worry if you don’t understand a lot of it. Once you see the demo working, you’ll be able to change things here and there to see how it all hangs together.
We’ve created two separate policies in this document, both related to security. The first policy, SignedUsername, requires that a message be signed with a UsernameToken; and the second policy, SignedX509, requires that a message be signed with an X509SecurityToken. Please note that the <policies> do not, and cannot, define the specific message to which they are applied. They are completely generic and document the policy conditions only. It is the <mappings> that associate a <policy> with an <endpoint>. If you look at the policyCache.config file, you’ll notice that we’ve enforced all request messages to be signed with an X509SecurityToken, and all response messages to be signed with a UsernameToken. We’ve left the policy empty for SOAP faults, as we don’t need this.
You’ve now created a policy file! At the moment there’s nothing linking this policy assertion to the web service itself, so we’ll create that link now. Although the <mappings> section maps endpoints to policies, the web service must have an entry in the web.config to know about the policy file at all. Without this, it will not process the policy assertions.
5. Open the web.config file and find the <microsoft.web.services2> element.
6. Add the following new sub-elements to this configuration section as follows:
<policy>
<cache name="policyCache.config" />
</policy>
<security>
<x509 allowTestRoot="true" allowRevocationUrlRetrieval="false" verifyTrust="false" storeLocation="LocalMachine"/>
</security>
Note that the <policy> element links the policy file to the web service. The <security> element is necessary because we’re enforcing a digital signature with an X509SecurityToken. The WSE pipeline needs to know where to look to validate the signature, which is why we’ve defined the additional <x509> element in the web.config. Removing this element will cause a SOAP fault.
You’ll notice that if you try to run your client application now, it’ll return a SOAP Fault. As described earlier in the article, the WSE filters recognised that the request did not conform to the requirements laid out in the policy attached to the web service and rejected it with a SOAP Fault.
There’s one more thing that we haven’t yet done to our service. If you recall from the policy document, we required that the response message be signed with a UsernameToken. So change your web method to look like this:
[WebMethod]
public string HelloWorld()
{
UsernameToken token = new UsernameToken(@"DOMAIN\Username", "Password", PasswordOption.SendPlainText);
ResponseSoapContext.Current.Security.Tokens.Add(token);
ResponseSoapContext.Current.Security.Elements.Add(new MessageSignature(token));
return "Hello World";
}
Change the “DOMAIN\Username” and “Password” of the UsernameToken to be credentials of a valid user on your local computer or domain. The PasswordOption must be SendPlainText in order for this example to work. The security itself is not the focus of this article, so I’m not going into detail about it. If you want more information on this (or another article on WS-Security and/or WS-Secure Conversation) then please contact me.
Now we’ll update the client application to conform to the requirements of the policy.
Example: Updating the Client to use the Policy
Look back at the policy document we just created, and you find that all request messages require digital signing with an X509SecurityToken. So that’s all we need to implement to allow our client to communicate successfully with our service. Please note that this is not an explanation on X509 certificates, so I’m not going to labour on about how they work. I assume that you either already understand this or will research it later.
1. First things first… right-click the Windows Forms Project and enable it for WSE 2.0. Notice that only the top checkbox is available for selection. This will add the necessary references for using the WSE. You’ll also need to add some “using” directives to your Form as follows:
using Microsoft.Web.Services2.Security;
using Microsoft.Web.Services2.Security.Tokens;
using Microsoft.Web.Services2.Security.X509;
2. Open the code for the Form you created earlier, and add the following method:
private X509SecurityToken GetX509Token()
{
// This keyID shouldn’t need to change, provided you’re using the X509 certificates that come with WSE 2.0
string keyId = "gBfo0147lM6cKnTbbMSuMVvmFY4=";
X509SecurityToken token = null;
X509CertificateStore store = X509CertificateStore.CurrentUserStore(X509CertificateStore.MyStore);
if (!store.OpenRead())
return null;
X509CertificateCollection certificates = store.FindCertificateByKeyIdentifier(Convert.FromBase64String(keyId));
if (certificates.Count > 0)
token = new X509SecurityToken(certificates[0]);
return token;
}
This method will retrieve the X509 certificate from the certificate store and return it to the caller as an X509SecurityToken.
3. Now go back to the button click event handler. The code in the try-catch block is going to change to allow signing of the request message with the X509SecurityToken we just retrieved. Replace the code in the try {…} block with the following:
PolicyService.PolicyService serviceProxy = new TestClient.PolicyService.PolicyService();
X509SecurityToken token = GetX509Token();
if (token == null)
throw new Exception("Couldn’t find X509 Certificate.");
serviceProxy.RequestSoapContext.Security.Tokens.Add(token);
serviceProxy.RequestSoapContext.Security.Elements.Add(new MessageSignature(token));
MessageBox.Show(serviceProxy.HelloWorld());
The process now includes additional steps to retrieve the security token and use it to sign the message. Signing the message is a very simple two-step process. Firstly, you must add the SecurityToken to the RequestSoapContext.Security.Tokens collection. Secondly, you need to attach a MessageSignature (based on the SecurityToken) to the RequestSoapContext.Security.Elements collection. You’ve now digitally signed the request message.
We’ve done all the necessary changes to both the client and the service, so go ahead and run the example to see it in action!
That’s all
This is a fairly simple example of how you can use WS-Policy to enforce usage requirements for your web services. I’m sure many of you can already see places where you’d like to be using it. The effective use of policies can cut down huge amounts of code that the developer would ordinarily need to write, and save a lot of testing time too! WS-Policy is only one of a number of relatively new web services specifications that are set to transform the way web services can be used. Hopefully this article has helped you understand the role that WS-Policy plays in gearing towards a service-oriented architecture.
References
If you’re interested in reading more on the actual specifications, you can get the full list of WS-* Specifications on the Microsoft site here. There’s also an excellent book on the WSE 2.0 with a small intro to Indigo, called Expert Service-Oriented Architecture in C#. If you develop web services, then this book is an absolute MUST!