http://www.developer.com/net/net/article.php/2192901/Build-Secure-Web-Services-With-SOAP-Headers-and-Extensions.htm
Jeff Prosise is a co-founder of Wintellect and author of Programming Microsoft .NET. Legions of Web developers have built Web services using the Microsoft .NET Framework and have learned how easy it is to get a basic Web service up and running. Just create an ASMX file, add a class, decorate its methods with the [WebMethod] attribute, and presto! Instant Web service. The Framework does the hard part, mapping Web methods to class methods, leaving you to concentrate on the business logic that makes your Web service unique. Building Web services with the .NET Framework is easy—easy, that is, unless the Web services are secure. There is no standard, agreed-upon method for exposing Web services over the Internet in such a way that only authorized users can call them. WS-Security will one day change that, but for now, you're on your own. One Microsoft sample published last year demonstrates how to secure Web services by passing user IDs in method calls. While functional, that approach is less than optimal because it mixes real data with authentication data and, to first order, requires each and every Web method to perform an authorization check before rendering a service. A better solution is one that passes authentication information out-of-band (that is, outside the Web methods' parameter lists) and that "front-ends" each method call with an authentication module that operates independently of the Web methods themselves. SOAP headers are the perfect vehicle for passing authentication data out-of-band. SOAP extensions are equally ideal for examining SOAP headers and rejecting calls that lack the required authentication data. Combine the two and you can write secure Web services that cleanly separate business logic from security logic. In this column, I'll present one technique for building secure Web services using SOAP headers and SOAP extensions. Until WS-Security gains the support of the .NET Framework, it's one way to build Web services whose security infrastructure is both centralized and protocol-independent. Let's start with the Web service in Figure 1. Called "Quote Service," it publishes a single Web method named GetQuote that takes a stock symbol (for example, "MSFT") as input and returns the current stock price. Figure 1: Quotes1.asmx You can test Quotes1.asmx with the console application in Figure 2. First, use the .NET Framework SDK's Wsdl.exe utility to generate a Web service proxy. If Quotes1.asmx is stored in the local machine's wwwroot directory, the command generates a source code file named Quote Service.cs containing a proxy class named QuoteService. Next, compile Client1.cs and Quote Service.cs: Finally, test the resulting executable by issuing the following command: The application should respond by reporting a current stock price of 55. Figure 2: Client1.cs Quotes1.asmx's GetQuote method is simple enough that anyone can call it. And that's just the problem: Anyone can call it. Assuming you'd want to charge for such a service, you wouldn't want for it to be available to everyone. Rather, you'd want to identify the caller on each call to GetQuote and throw a SOAPException if the caller is not authorized. Figure 3 lists a revised version of Quote Service that uses a custom SOAP header to transmit user names and passwords. A SOAP header is a vehicle for passing additional information in a SOAP message. The general format of a SOAP message is: SOAP headers are optional. Here's a SOAP message that lacks a header: And here's the same message with a header containing a user name and password: The .NET Framework lets you define custom SOAP headers by deriving from SoapHeader, which belongs to the System.Web.Services.Protocols namespace. The following statements in Quotes2.asmx define a custom header named AuthHeader: The statement declares an instance of AuthHeader named Credentials in QuoteService, and the statement makes AuthHeader a required header for calls to GetQuote and transparently maps user names and passwords found in AuthHeaders to the corresponding fields in Credentials. Calls lacking AuthHeaders won't even reach GetQuote. Calls that do reach it can be authenticated by reading Credentials' UserName and Password fields. GetQuote checks the user name and password and fails the call if either is invalid: In the real world, of course, you'd check the caller's credentials against a database rather than hard-code a user name and password. I took the easy way out here to keep the code as simple and understandable as possible. Figure 2: Quotes2.asmx To call the Quotes2.asmx version of GetQuote, a client must include a SOAP header containing the user name "Jeff" and the password "imbatman." Figure 4 demonstrates how. Because AuthHeader is defined in Quotes2.asmx and is therefore described in the Web service's WSDL contract as a complex type, a proxy file generated by Wsdl.exe contains an AuthHeader definition, too. In Client2.cs, the statements create an AuthHeader and associate it with the proxy named qs. The .NET Framework turns AuthHeaders into SOAP headers each time the proxy's GetQuote method is called. Figure 4: Client2.cs Quotes2.asmx is a step in the right direction because it transmits authentication data in every request. Using SOAP headers for transport means the information is passed out-of-band and that it isn't tied to any particular protocol, such as HTTP. But Quotes2.asmx still leaves room for improvement. Currently, every Web method has to check user names and passwords. The next logical step is to get the authorization code out of the Web methods and into a module that examines incoming requests as they arrive and rejects those lacking valid credentials. A SOAP extension is the perfect tool for the job. Physically, a SOAP extension is a class derived from System.Web.Services.Protocols.SoapExtension. Logically, it's an object that sits in the HTTP pipeline and enjoys access to the SOAP messages entering and leaving. Quotes3.asmx (Figure 5) demonstrates how to use a SOAP extension to extract user names and passwords from SOAP headers. Observe that GetQuote no longer performs any checking of its own. Instead, it's attributed with [AuthExtension], which associates it with the SOAP extension class named AuthExtension. [AuthExtension] is an instance of AuthExtensionAttribute, which is a custom attribute class derived from the Framework's SoapExtensionAttribute class. In AuthExtensionAttribute, the following override ensures that AuthExtension will be called for any method attributed with [AuthExtension]: The SOAP extension class AuthExtension derives from SoapExtension. Its heart is its ProcessMessage method, which is called four times every time GetQuotes is invoked: once before the message is deserialized by the Framework, once after it's deserialized but before QuoteService.GetQuote is called, again after QuoteService.GetQuote has executed but before the response is serialized into a SOAP message, and a final time after the response is serialized. AuthExtension checks the message after it is deserialized by the .NET Framework. If the message contains no AuthHeader or if it contains an AuthHeader with invalid credentials, AuthExtension rejects it by throwing a SoapException. Significantly, GetQuote no longer has to do any authentication itself, because it never sees a message that hasn't been authenticated already. Figure 5: Quotes3.asmx For an added touch, you could move AuthHeader, AuthExtensionAttribute, and AuthExtension to a separate source code file, compile them into a DLL, and drop the DLL in the Web service's bin directory. Then, the ASMX file would reduce to this: If you add more methods to the Web service, simply decorate them with [AuthExtension] and [SoapHeader] attributes and they, too, will be authenticated by the SOAP extension. Quotes3.asmx demonstrates how to combine SOAP headers and SOAP extensions to authenticate callers to Web services and to do so without explicitly authenticating in the Web methods themselves. Do be aware that authenticating callers by encoding plaintext user names and passwords in SOAP headers is not secure unless you encrypt the message traffic. One way to encrypt SOAP messages is to write a SOAP extension that unencrypts requests and encrypts responses. Another way to do it is to use SSL—that is, to publish the Web service at an HTTPS address. How you do it isn't important; what's important is to avoid passing unencrypted credentials over unsecured connections. Jeff Prosise is a cofounder of Wintellect, a consulting, debugging, and education firm that specializes in .NET. His latest book, Programming Microsoft .NET, was published last year by Microsoft Press. Jeff is also a columnist for MSDN Magazine and asp.netPRO Magazine.
Build Secure Web Services With SOAP Headers and Extensions
April 17, 2003
The Quotes1 Web Service
<%@ WebService Language="C#" Class="QuoteService" %>
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService (
Name="Quote Service",
Description="Provides instant stock quotes to registered
users"
)]
public class QuoteService
{
[WebMethod (Description="Returns the current stock price")]
public decimal GetQuote (string symbol)
{
if (symbol.ToLower () == "msft")
return 55.0m;
else if (symbol.ToLower () == "intc")
return 32.0m;
else
throw new SoapException ("Unrecognized symbol",
SoapException.ClientFaultCode);
}
}
wsdl http://localhost/quotes1.asmx
csc client1.cs "quote service.cs"
client1 msft
using System;
class Client
{
static void Main (string[] args)
{
if (args.Length == 0) {
Console.WriteLine ("Please supply a stock symbol");
return;
}
QuoteService qs = new QuoteService ();
decimal price = qs.GetQuote (args[0]);
Console.WriteLine ("The current price of " +
args[0] + " is " + price);
}
}
The Quotes2 Web Service
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/
envelope/">
<soap:Header>
...
</soap:Header>
<soap:Body>
...
</soap:Body>
</soap:Envelope>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/
envelope/">
<soap:Body>
<GetQuote xmlns="http://tempuri.org/">
<symbol>msft</symbol>
</GetQuote>
</soap:Body>
</soap:Envelope>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/
envelope/">
<soap:Header>
<AuthHeader xmlns="http://tempuri.org/">
<UserName>jeff</UserName>
<Password>imbatman</Password>
</AuthHeader>
</soap:Header>
<soap:Body>
<GetQuote xmlns="http://tempuri.org/">
<symbol>msft</symbol>
</GetQuote>
</soap:Body>
</soap:Envelope>
public class AuthHeader : SoapHeader
{
public string UserName;
public string Password;
}
public AuthHeader Credentials;
[SoapHeader ("Credentials", Required=true)]
if (Credentials.UserName.ToLower () != "jeff" ||
Credentials.Password.ToLower () != "imbatman")
throw new SoapException ("Unauthorized",
SoapException.ClientFaultCode);
<%@ WebService Language="C#" Class="QuoteService" %>
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService (
Name="Quote Service",
Description="Provides instant stock quotes to registered
users"
)]
public class QuoteService
{
public AuthHeader Credentials;
[SoapHeader ("Credentials", Required=true)]
[WebMethod (Description="Returns the current stock price")]
public decimal GetQuote (string symbol)
{
// Fail the call if the caller is not authorized
if (Credentials.UserName.ToLower () != "jeff" ||
Credentials.Password.ToLower () != "imbatman")
throw new SoapException ("Unauthorized",
SoapException.ClientFaultCode);
// Process the request
if (symbol.ToLower () == "msft")
return 55.0m;
else if (symbol.ToLower () == "intc")
return 32.0m;
else
throw new SoapException ("Unrecognized symbol",
SoapException.ClientFaultCode);
}
}
public class AuthHeader : SoapHeader
{
public string UserName;
public string Password;
}
AuthHeader Credentials = new AuthHeader ();
Credentials.UserName = "jeff";
Credentials.Password = "imbatman";
qs.AuthHeaderValue = Credentials;
using System;
class Client
{
static void Main (string[] args)
{
if (args.Length == 0) {
Console.WriteLine ("Please supply a stock symbol");
return;
}
QuoteService qs = new QuoteService ();
AuthHeader Credentials = new AuthHeader ();
Credentials.UserName = "jeff";
Credentials.Password = "imbatman";
qs.AuthHeaderValue = Credentials;
decimal price = qs.GetQuote (args[0]);
Console.WriteLine ("The current price of " +
args[0] + " is " + price);
}
}
The Quotes3 Web Service
public override Type ExtensionType
{
get { return typeof (AuthExtension); }
}
<%@ WebService Language="C#" Class="QuoteService" %>
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService (
Name="Quote Service",
Description="Provides instant stock quotes to registered
users"
)]
public class QuoteService
{
public AuthHeader Credentials;
[AuthExtension]
[SoapHeader ("Credentials", Required=true)]
[WebMethod (Description="Returns the current stock price")]
public decimal GetQuote (string symbol)
{
if (symbol.ToLower () == "msft")
return 55.0m;
else if (symbol.ToLower () == "intc")
return 32.0m;
else
throw new SoapException ("Unrecognized symbol",
SoapException.ClientFaultCode);
}
}
public class AuthHeader : SoapHeader
{
public string UserName;
public string Password;
}
[AttributeUsage (AttributeTargets.Method)]
public class AuthExtensionAttribute : SoapExtensionAttribute
{
int _priority = 1;
public override int Priority
{
get { return _priority; }
set { _priority = value; }
}
public override Type ExtensionType
{
get { return typeof (AuthExtension); }
}
}
public class AuthExtension : SoapExtension
{
public override void ProcessMessage (SoapMessage message)
{
if (message.Stage == SoapMessageStage.AfterDeserialize) {
//Check for an AuthHeader containing valid
//credentials
foreach (SoapHeader header in message.Headers) {
if (header is AuthHeader) {
AuthHeader credentials = (AuthHeader) header;
if (credentials.UserName.ToLower () ==
"jeff" &&
credentials.Password.ToLower () ==
"imbatman")
return; // Allow call to execute
break;
}
}
// Fail the call if we get to here. Either the header
// isn't there or it contains invalid credentials.
throw new SoapException ("Unauthorized",
SoapException.ClientFaultCode);
}
}
public override Object GetInitializer (Type type)
{
return GetType ();
}
public override Object GetInitializer (LogicalMethodInfo info,
SoapExtensionAttribute attribute)
{
return null;
}
public override void Initialize (Object initializer)
{
}
}
<%@ WebService Language="C#" Class="QuoteService" %>
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService (...)]
public class QuoteService
{
public AuthHeader Credentials;
[AuthExtension]
[SoapHeader ("Credentials", Required=true)]
[WebMethod (Description="Returns the current stock price")]
public decimal GetQuote (string symbol)
{
...
}
}
Summary
About the Author...