Programming with Web Services Enhancements 1.0 for Microsoft .NET
Tim Ewald
Microsoft Corporation
December 2002
Applies to:
Microsoft® ASP.NET Web service APIs
Microsoft Web Services Enhancements 1.0 for Microsoft .NET
Web Services Specifications (WS-Security, WS-Routing, DIME, WS-Attachments)
Microsoft .NET Framework
Summary: Tim Ewald examines the architecture of WSE and explains how to use it to build a simple, WSE-enabled ASP.NET Web service and client. (17 printed pages)
Download Web Services Enhancements 1.0 for Microsoft .NET.
Contents
Introduction
How WSE Works
Integration with ASP.NET Web Service Proxies
Integration with ASP.NET Web Services
Diagnostics
Summary
Introduction
Web Services Enhancements 1.0 for Microsoft .NET, or WSE, is a new .NET class library for building Web services using the latest Web services protocols, including WS-Security, WS-Routing, DIME, and WS-Attachments. WSE integrates with ASP.NET Web services, offering a simple way to extend their functionality. This paper explores the architecture of WSE and explains how to use it to build a simple Web service. But before I get into the details, let me provide a little background and make some observations about Web Services Enhancements in general.
The standards at the heart of basic Web services—XML, SOAP, XSD, WSDL, and UDDI—provide enough functionality to build simple distributed systems. There are lots of toolkits that support these protocols, mapping them to a range of programming models, from raw XML messaging to method invocations against objects. While the basic Web service standards are necessary, they are not sufficient for building complex applications. For instance, there is no standard approach to building secure Web services. If you want to build a secure Web service, you have to roll your own solution. Most people rely on the security infrastructure of their service's underlying transport protocol, typically HTTP.
Security is just one of the architectural problems that the basic Web service stack does not address. There is no standard approach to routing messages, ensuring reliable delivery, coordinating distributed work using compensating transactions, or a number of other common problems. Given that these problems are not application specific, it makes sense to develop common solutions as part of the next generation Web service infrastructure. This is the goal of a collection of Web service architecture protocols written by Microsoft, IBM, and others describing ways to implement these features. WSE is one implementation of (a subset of) these protocols. The first version of the kit focuses on the basic message-level protocols: WS-Security, WS-Routing (and WS-Referral), and DIME and WS-Attachments.
The design of WSE reflects the principles of the protocols themselves:
- Decentralization and federation
- Modularity
- XML-based data model
- Transport neutrality
- Application domain neutrality
WSE provides a very "close to the metal" programming model focused on directly manipulating the protocol headers included in SOAP messages. The advantage of this approach is that you can use Web Services Enhancements to build a wide range of applications and infrastructure. The disadvantage is that you have to do much of the work required to integrate the protocols with your application yourself. Consider security, for instance. The WSE implementation of WS-Security gives you direct control over when and how a message is authenticated, but you have to write some code to map usernames to passwords or interpret the meaning of particular digital certificates.
From my perspective, the flexibility of WSE is a huge asset. It provides plumbing to handle the grungy details of building messages with advanced protocol headers without forcing you to adopt a particular programming model. That means you can use WSE to solve a very wide range of problems without having to stay inside a particular set of architectural boundaries.
However, it is important to note that, today at least, you do have to stay inside a particular set of interoperability boundaries. Unlike the basic Web service protocols, which are widely implemented across many platforms, there are very few toolkits that implement any of the more advanced Web service protocols. If you use WSE to build a Web service, you have to use the WSE or a compatible toolkit to build the client. In the short term, that essentially restricts the use of WSE to integrating applications within an enterprise (EAI) or, more interestingly, across core organizational boundaries, i.e., with particular business-to-business partners. Since having support for message-based security that is not tied to particular HTTP connections and the ability to route messages are both extremely useful in these scenarios, having to adopt a toolkit that understands these protocols is a small price to pay to enable them.
Beyond the limited number of tools that understand the latest protocols, there is another reason WSE is best used in situations where you and the person implementing the code at the other end of the pipe can collaborate on the design of your application. Specifically, WSE does not generate WSDL definitions that reflect how particular protocols are being used. The reason for this is simple: there are not yet standard ways to convey the service requirements of either a client or a service. How do you specify that a service wants request messages authenticated with a username and a hashed password, while a client wants response messages to be encrypted using the public key of from its digital certificate? (While you could write WSDL extensions that convey a server's requirements, you can't do the same thing for a client, which isn't described in WSDL.) In the absence of a generally accepted solution to this problem, WSE leaves it up to you. In short, Web Services Enhancements give you both a lot of power and a lot of responsibility.
Now, let's get to the details!
How WSE works
At its heart, WSE is an engine for applying advanced Web service protocols to SOAP messages. This entails writing headers to outbound SOAP messages and reading headers from inbound SOAP messages. It may also require transforming the SOAP message body—for instance, encrypting an outbound message's body and decrypting an inbound message's body, as defined by the WS-Security specification. This functionality is encapsulated by two sets of filters, one for outbound messages and one for inbound messages. All messages leaving a process—request messages from a client or response messages from server—are processed using the outbound message filters. All messages arriving in a process—request messages to a server or response messages to a client—are processed using the inbound message filters. Figure 1 shows this simple architecture.
Figure 1. How input and output filters are applied to messages
The WSE filter chains are integrated with the ASP.NET Web services infrastructure, which makes them quite simple to work with, as you’ll see from the examples below. The filter chains can also be accessed directly through the Pipeline class. See Inside the Web Services Enhancements Pipeline for more information about this low-level API.
Integration with ASP.NET Web Service Proxies
WSE input and output filters are exposed to ASP.NET Web services clients through a new proxy base class called Microsoft.Web.Services.WebServicesClientProtocol. WebServicesClientProtocol extends the default base class for Web service proxies, System.Web.Services.SoapHttpClientProtocol. The new proxy base class ensures that WSE filters have a chance to process the SOAP messages that are exchanged whenever a client invokes one of a proxy's methods. In order to use WSE, you need to change the base class of each of your proxies to WebServicesClientProtocol. This is true whether you generate your proxy code using the wsdl.exe
command line tool or Visual Studio .NET Add Web Reference or Update Web Reference commands.
The WebServicesClientProtocol proxy base class is implemented using two new communication classes called Microsoft.Web.Services.SoapWebRequest and Microsoft.Web.Services.SoapWebResponse. These classes derive from the standard System.Net communication classes, i.e., WebRequest and WebResponse, respectively.
The behavior of the SoapWebRequest is very simple. It parses a request stream containing a SOAP message into an instance of the SoapEnvelope class, an extension of System.Xml.XmlDocument, the standard .NET DOM API. Then it passes the request through the chain of output filters. Each filter has the chance to modify the request data any way it likes. Often, a filter simply adds protocol headers, but in some cases they modify the body of a message too (for example, encryption).
The behavior of the filters is controlled through the SoapContext class. Each SoapContext object records particular protocol options—the presence of a username token or digital certificate, creation and expiration timestamps, routing paths, and so on—using a simple object model. The SoapWebRequest class has a SoapContext property that is an instance of the SoapContext class. When a SoapWebRequest processes a request stream, it is the data in the SoapContext object that tells the output filters what to do.
The diagram below illustrates the behavior of SoapWebRequest and the role of SoapContext.
Figure 2. How SoapWebRequest processes requests
The behavior of the SoapWebResponse is also very simple—it just does the opposite of SoapWebRequest. It parses a response stream containing a SOAP message into an instance of the SoapEnvelope class and passes it through the chain of input filters. Each filter has the chance to examine and modify the response data any way it likes. Input filters check the validity of protocol headers and modify the contents of the message's body as needed to undo the work of an output filter (decryption, for example). Like SoapWebRequest, SoapWebResponse exposes a SoapContext property. However, where the SoapWebRequest class uses its SoapContext for input, SoapWebResponse uses it for output. The SoapContext object attached to a SoapWebResponse is updated to reflect the protocol headers present in the response message when the response stream is read.
Figure 3 illustrates the behavior of SoapWebResponse and the role of SoapContext.
Figure 3. How SoapWebResponse processes responses
The WebServicesClientProtocol encapsulates the use of the SoapWebRequest and SoapWebResponse classes so that you don't have to deal with them directly. However, you still need a way to manipulate the protocol properties for both outbound and inbound messages. To that end, the WebServicesClientProtocol class exposes two properties, RequestSoapContext and ResponseSoapContext, both of type SoapContext. These objects reflect the protocol properties of the next message to be sent and the last message that was received, respectively. Figure 4 shows the complete client-side architecture.
Figure 4. How the WebServicesClientProtocol class integrates with ASP.NET Web service client proxies
A sample client
The code below shows an ASP.NET Web service client that uses a WebServicesClientProtocol-derived proxy to send a SOAP message. The example is shown in both C# and Visual Basic .NET.
// C#
static void Main()
{
// create WebServiceClientProtocol-derived proxy class
UsernameReflector proxy = new UsernameReflector();
// use proxy’s RequestSoapContext property to set
// protocol properties for request message
SoapContext reqCtx = proxy.RequestSoapContext;
UsernameToken tok = new UsernameToken(Environment.UserName,
Environment.UserName.ToUpper(),
PasswordOption.SendHashed));
reqCtx.Security.Tokens.Add(tok);
reqCtx.Security.Elements.Add(new Signature(tok));
// send message and process result
Console.WriteLine("Username is: " + proxy.GetUsername());
}
' Visual Basic .NET
Sub Main()
' create WebServicesClientProtocol-derived proxy class
Dim proxy As New UsernameReflector()
' use proxy’s RequestSoapContext property to set
' protocol properties for request message
Dim reqCtx As SoapContext = proxy.RequestSoapContext
Dim tok As New UsernameToken(Environment.UserName,
Environment.UserName.ToUpper(),
PasswordOption.SendNone)
reqCtx.Security.Tokens.Add(tok)
reqCtx.Security.Elements.Add(new Signature(tok))
' send message and process result
Console.WriteLine(proxy.GetUsername())
End Sub
The request message includes a username token and a hashed password, as defined by WS-Security. It assumes that the user's password is simply their username in uppercase. That's fine for a simple example about how WSE works, but clearly you would never put anything like this into a production system. For more detailed information on applying the WSE implementation of WS-Security, see WS-Security Authentication and Digital Signing with Web Services Enhancements.
The client sets these options using the proxy's RequestSoapContext property, which exposes the SoapContext for the new message to be sent. Then the client calls GetUsername and the server responds simply by echoing the username back to the client. If the client wanted to read the protocol properties of the response message, it would access the proxy's ResponseSoapContext property after the call to GetUsername completed.
In this example, the initial SOAP message generated by the standard Web service proxy marshaling plumbing looks like this:
<!-- sample request SOAP message generated by client -->
<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<q:UsernameRequest xmlns:q="https://msdn.microsoft.com/examples"/>
</soap:Body>
</soap:Envelope>
The SoapWebRequest class passes the message through the WSE output filters in order to produce the message that will actually be sent to the service. The contents of the modified message are shown below. The output filters add the entire SOAP Header block and all its contents (highlighted in italics).
<!-- sample request message after WSE processing -->
<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Header>
<wsrp:path soap:actor="https://schemas.xmlsoap.org/soap/actor/next"
soap:mustUnderstand="1"
xmlns:wsrp="https://schemas.xmlsoap.org/rp">
<wsrp:action>https://msdn.microsoft.com/examples/GetUsername<
/wsrp:action>
<wsrp:to>https://localhost/WSEBasic/service1.asmx</wsrp:to>
<wsrp:id>uuid:716c9d3b-8971-4293-8734-08192863eb4d</wsrp:id>
</wsrp:path>
<wsu:Timestamp
xmlns:wsu="https://schemas.xmlsoap.org/ws/2002/07/utility">
<wsu:Created>2002-10-31T19:25:33Z</wsu:Created>
<wsu:Expires>2002-10-31T19:30:33Z</wsu:Expires>
</wsu:Timestamp>
<wsse:Security soap:mustUnderstand="1"
xmlns:wsse="https://schemas.xmlsoap.org/ws/2002/07/secext">
<wsse:UsernameToken
xmlns:wsu="https://schemas.xmlsoap.org/ws/2002/07/utility"
wsu:Id="SecurityToken-8fbfc982-3d9e-4646-bf83-f19d38c2911b">
<wsse:Username>tewald</wsse:Username>
<wsse:Nonce>nKysjryCSsnpmeapQNc4Fw==</wsse:Nonce>
<wsu:Created>2002-10-31T19:25:33Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
<soap:Body>
<GetUsername xmlns="https://msdn.microsoft.com/examples" />
</soap:Body>
</soap:Envelope>
Integration with ASP.NET Web Services
WSE input and output filters are exposed to ASP.NET Web services through a new server-side SOAP extension, Microsoft.Web.Services.WebServicesExtension. The goal of the new extension is to ensure that WSE filters have a chance to process the SOAP messages that are exchanged whenever a service's methods are invoked.
The WebServicesExtension class copies inbound messages into memory streams. It processes them using WSE input filters before the message is deserialized into input parameters for the target method. The extension also sets up a memory stream for the target method to serialize its output parameters into. After that serialization step occurs, the output message is passed through WSE output filter and then sent on its way.
The WebServicesExtension class has to expose the protocol properties for input and output messages to the server code handling a request. To that end, it adds references to two SoapContext objects—one containing information about the request message and the other about the response message—to the current System.Web.HttpContext Items collection, that is, HttpContext.Current.Items, using the keys "RequestSoapContext" and "ResponseSoapContext", respectively. For convenience, these objects are exposed directly as the static read-only properties of the WSE Microsoft.Web.Services.HttpSoapContext class, shown below in both C# and Visual Basic .NET:
// C#
public sealed class HttpSoapContext
{
public static SoapContext RequestContext { get; }
public static SoapContext ResponseContext { get; }
}
' Visual Basic .NET
Public NotInheritable Class HttpSoapContext
Public Shared ReadOnly Property RequestContext As SoapContext
Public Shared ReadOnly Property ResponseContext As SoapContext
End Class
These static property accessors simply perform lookups against the current HttpContext Items collection.
Figure 5 illustrates the entire architecture, showing how WSE integrates with the ASP.NET Web services infrastructure.
Figure 5. How WSE integrates with ASP.NET Web services
A Sample Service
Here is the source code for the UsernameReflector
service used by the client I presented earlier. The example is shown in both Visual Basic .NET and C#.
// C#
public class UsernameReflector
{
[WebMethod]
public string GetUsername()
{
// use request message’s SoapContext object,
// provided via WebServicesExtension, to look
// for username token
string Username = string.Empty;
SoapContext reqCtx = HttpSoapContext.RequestContext;
foreach(SecurityToken tok in reqCtx.Security.Tokens)
{
if (tok is UsernameToken)
{
Username = ((UsernameToken)tok).Username;
break;
}
}
if (Username == string.Empty)
throw new Exception("No username");
// return username from method
return Username;
}
}
' Visual Basic .NET
Public Class UsernameReflector
<WebMethod()> _
Public Function GetUsername() As String
' use request message’s SoapContext object,
' provided via WebServicesExtension, to look
' for username token
Dim username As String
Dim reqCtx As SoapContext = HttpSoapContext.RequestContext
Dim tok As SecurityToken
For Each tok In reqCtx.Security.Tokens
If TypeOf tok Is UsernameToken Then
username = CType(tok, UsernameToken).Username
Exit For
End If
Next
If username = String.Empty Then
Throw New Exception("No username")
' return username from method
Return username
End Function
End Class
The server uses the HttpSoapContext.RequestContext object to extract information about the protocol headers in the SOAP request message. Specifically, it looks for a UsernameToken from which it can extract a name to send back to the client. If it finds one, it builds a SOAP response message that looks like this (assuming the client sent my username):
<!-- sample response message generated by service -->
<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetUsernameResponse xmlns="https://msdn.microsoft.com/examples">
<GetUsernameResult>tewald</GetUsernameResult>
</GetUsernameResponse>
</soap:Body>
</soap:Envelope>
The WebServicesExtension class passes the message through the WSE output filters in order to produce the message that will actually be returned to the client. The contents of the modified message are shown below. The output filters add the entire SOAP Header block and all its contents (highlighted in italics).
<!-- sample response message after WSE processing -->
<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Header>
<wsu:Timestamp xmlns:wsu=
"https://schemas.xmlsoap.org/ws/2002/07/utility">
<wsu:Created>2002-11-01T19:30:27Z</wsu:Created>
<wsu:Expires>2002-11-01T19:35:27Z</wsu:Expires>
</wsu:Timestamp>
</soap:Header>
<soap:Body>
<GetUsernameResponse xmlns="https://msdn.microsoft.com/examples">
<GetUsernameResult>tewald</GetUsernameResult>
</GetUsernameResponse>
</soap:Body>
</soap:Envelope>
Note that the headers in the service's response message are much simpler than the headers generated for the client's request message. This simply reflects the fact that the there are a lot more protocol requirements for the request message, which needs to include a username token and a hash. As I mentioned in the introduction, WSE provides a very "close to the metal" way to deal with advanced protocols. You can see from this example that how WSE modifies a basic SOAP message depends almost entirely on how much you ask it to do.
Remember that while the ASP.NET Web service shown above expects request messages to contain a valid username and hashed password, this fact is NOT reflected in the WSDL the service generates on demand. Any client that wants to consume the service would simply have to know what protocol headers are required, gaining that information through some out-of-band technique, i.e., documentation. As I noted at the beginning of this paper, this is simply a reflection of the fact that there is not yet a standard way to convey this sort of information for both servers and clients.
Configuration
For the code above to execute correctly, the Web service must be configured to use the WebServicesExtension when it processes messages. The default way to configure the extension is to add an entry to the web.config
file for the virtual directory where the Web service is deployed. Specifically, you need to add a /configuration/system.web/webServices/soapExtensionTypes/add element with a type attribute that references the WebServicesExtension type using its 5-part strong name. Here is an example:
<configuration>
<system.web>
<webServices>
<soapExtensionTypes>
<!-- add reference to WebServicesExtension,
note that type name is wrapped for readability,
real type name should not contain newlines -->
<add type="Microsoft.Web.Services.WebServicesExtension,
Microsoft.Web.Services,Version=1.0.0.0,
Culture=neutral,PublicKeyToken=31bf3856ad364e35"
priority="1" group="0"/>
</soapExtensionTypes>
</webServices>
</system.web>
... <!-- other elements omitted for simplicity -->
</configuration>
Beyond the basic WebServicesExtension configuration, a Web service often needs to configure specific aspects of WSE behavior. In this case, the service needs to initialize the input filter for WS-Security with a password provider. A password provider is a class that implements the Microsoft.Web.Services.Security.IPasswordProvider interface and maps Microsoft.Web.Services.Security.UsernameToken objects to passwords. When a message containing a username and a hashed password arrives, the security input filter calls the configured password provider, passing in the username and getting back a password. It uses the password to repeat the hashing operation performed on the client (that is, it performs a one-way hash of the password, the unique number included in the message, and the message's creation time) and compares the result to the hash in the message. If the hashes match, the client has the right password. Here is the code for the server's password provider, in both C# and Visual Basic .NET. It is overly simplistic, but gets the point across. In a real system, the password provider would map to a table in a database or some other backend store.
// C#
public class PasswordProvider : IPasswordProvider
{
public string GetPassword(UsernameToken usernameToken)
{
// THIS IS A VERY SIMPLISTIC ALGORITHM THAT YOU WOULD
// NEVER USE IN PRODUCTION – IT IS SHOWN HERE FOR
// EDUCATIONAL PURPOSES ONLY
return usernameToken.Username.ToUpper();
}
}
' Visual Basic .NET
Public Class PasswordProvider
Implements IPasswordProvider
Public Function GetPassword( _
ByVal usernameToken As UsernameToken) As String _
Implements IPasswordProvider.GetPassword
' THIS IS A VERY SIMPLISTIC ALGORITHM THAT YOU WOULD
' NEVER USE IN PRODUCTION – IT IS SHOWN HERE FOR
' EDUCATIONAL PURPOSES ONLY
Return usernameToken.Username.ToUpper()
End Function
End Class
WSE can be configured to use this password provider simply by further modifying the server's web.config
file, as shown below.
<configuration>
<configSections>
<!-- add reference to config section, note that
type name is wrapped for readability, real
type name should not contain newlines -->
<section name="microsoft.web.services"
type="Microsoft.Web.Services.Configuration.
WebServicesConfiguration,Microsoft.Web.Services,
Version=1.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35"
/>
</configSections>
<!-- WSE config section -->
<microsoft.web.services>
<security>
<passwordProvider type="WSEIntro.PasswordProvider, WSEIntro" />
</security>
</microsoft.web.services>
... <!-- other elements omitted for simplicity -->
</configuration>
The /configuration/microsoft.web.services element controls the configuration of all WSE input and output filters. The /configuration/configSections/section element tells the .NET plumbing which class to use to parse the WSE-specific configuration data. Any process that uses WSE can include these elements in its application .config
file. This is very important because WSE features are not tied specifically to either clients or servers. WSE focuses on messages moving into and out of processes. For instance, the service code in this example could mirror the behavior of the client and add a username and hashed password to its response message. In that case, the client would have to have a .config
file that identified a password provider the input filter chain could use when it received a message back from the service. (For more information on configuration, see Configuration File Schema and Common Configuration Issues in the WSE documentation.)
Diagnostics
Debugging Web services and clients that use advanced protocols can be challenging. In order to help, WSE provides support for tracing messages and for controlling the level of detail in error messages. You enable tracing with the /configuration/microsoft.web.services/diagnostics/trace element of your .config
file, as shown below.
<configuration>
<microsoft.web.services>
<diagnostics>
<trace enabled="true" />
</diagnostics>
</microsoft.web.services>
</configuration>
By default, the tracing filters write input and output messages to inputTrace.webinfo
and outputTrace.webinfo
, respectively. The format is a simple XML document that lists the messages verbatim inside a wrapper element. You can override the file names by adding input and output attributes to the trace element in your .config
file. (See the WSE documentation for more information.)
When one of the WSE filters encounters a problem, it generates a fault. You can control how specific the fault description is using ASP.NET's custom error mechanism.
Summary
Web Services Enhancements for Microsoft .NET provides support for advanced Web service protocols. They are implemented as a set of filters that integrate with ASP.NET Web services and clients and augment the SOAP messages they generate. In short, WSE radically extends support for Web service development using .NET by starting to make the latest Web service protocols concrete.