Share via


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.