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.
The Quotes Web Service
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 I
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 int GetQuote (string symbol)
{
if (symbol.ToLower () == "msft")
return 55;
else if (symbol.ToLower () == "intc")
return 32;
else
throw new SoapException ("Unrecognized symbol", SoapException.ClientFaultCode);
}
}
Now you can call this web service from your application inorder to test. I guess you know how to test a web service. I did a small test with the Console Application as below. The application should respond by reporting a current stock price of 55.
Figure II:
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 ();
int price = qs.GetQuote (args[0]);
Console.WriteLine ("The current price of " + args[0] + " is " + price);
}
}
Now the 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 III 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:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"]
[soap:Header]
...
[/soap:Header]
[soap:Body]
...
[/soap:Body]
[/soap:Envelope]
SOAP headers are optional. Here's a SOAP message that lacks a header:
[soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"]
[soap:Body]
[GetQuote xmlns="http://tempuri.org/"]
[symbol]msft[/symbol]
[/GetQuote]
[/soap:Body]
And here's the same message with a header containing a user name and password:
[soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"]
[soap:Header] 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.
The Quotes Web Service
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 I
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 int GetQuote (string symbol)
{
if (symbol.ToLower () == "msft")
return 55;
else if (symbol.ToLower () == "intc")
return 32;
else
throw new SoapException ("Unrecognized symbol", SoapException.ClientFaultCode);
}
}
Now you can call this web service from your application inorder to test. I guess you know how to test a web service. I did a small test with the Console Application as below. The application should respond by reporting a current stock price of 55.
Figure II:
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 ();
int price = qs.GetQuote (args[0]);
Console.WriteLine ("The current price of " + args[0] + " is " + price);
}
}
Now the 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 III 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:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"]
[soap:Header]
...
[/soap:Header]
[soap:Body]
...
[/soap:Body]
[/soap:Envelope]
SOAP headers are optional. Here's a SOAP message that lacks a header:
[soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"]
[soap:Body]
[GetQuote xmlns="http://tempuri.org/"]
[symbol]msft[/symbol]
[/GetQuote]
[/soap:Body]
And here's the same message with a header containing a user name and password:
[soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"]
[AuthHeader xmlns="http://tempuri.org/"]
[UserName]techie[/UserName]
[Password]imbest[/Password]
[/AuthHeader]
[/soap:Header]
[soap:Body]
[GetQuote xmlns="http://tempuri.org/"]
[symbol]msft[/symbol]
[/GetQuote]
[/soap:Body]
[/soap:Envelope]
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 Quotes.asmx define a custom header named AuthHeader:
public class AuthHeader : SoapHeader
{
public string UserName;
public string Password;
}
The statement
public AuthHeader Credentials;
declares an instance of AuthHeader named Credentials in QuoteService, and the statement
[SoapHeader ("Credentials", Required=true)]
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:
if (Credentials.UserName.ToLower () != "techie" && Credentials.Password.ToLower () != "imbest")
throw new SoapException ("Unauthorized", SoapException.ClientFaultCode);
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 III
<%@ WebService Language="C#" Class="QuoteService" %>
<%@ WebService Language="C#" Class="QuoteService" %>
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
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 int GetQuote (string symbol)
{
// Fail the call if the caller is not authorized
if (Credentials.UserName.ToLower () != "techie" && Credentials.Password.ToLower () != "imbest")
throw new SoapException ("Unauthorized",SoapException.ClientFaultCode);
// Process the request
if (symbol.ToLower () == "msft")
return 55;
else if (symbol.ToLower () == "intc")
return 32;
else
throw new SoapException ("Unrecognized symbol",SoapException.ClientFaultCode);
}}
public class AuthHeader : SoapHeader
{
public string UserName;
public string Password;
}
{
public string UserName;
public string Password;
}
To call this version of Quotes.asmx version of GetQuote, a client must include a SOAP header containing the user name "techie" and the password "imbest" as below.
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 ();
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 = "techie";
Credentials.Password = "imbest";
qs.AuthHeaderValue = Credentials;
int price = qs.GetQuote (args[0]);
Console.WriteLine ("The current price of " + args[0] + " is " + price);
}
}
Voila !! Our web service is secured.