Web services are all about connecting businesses in a standard and secure manner.
For a real-life Web service, security is intrinsic to every facet of operation and no party would ever agree to interact with a non-secure Web service. Unfortunately, Web services security is still in its infancy; standards such as WS-I are just emerging and there is no built-in support in the development tools for them. That being said, there are quite a few programming techniques you can use today in .NET 1.1 to secure your Web services, and do so in a way that will ease the transition to future standards and protocols.
Who Needs Security?
You do, and you need to design security into your Web services from the ground up. Toy-like Web services you have seen at development conferences or used in tutorials have no place in today's business and services. Your Web service needs to authenticate callers, making sure they present a valid identity, and your authentication process should not compromise sensitive information, such as passwords. Once a Web service authenticates an identity, it can use that identity for a number of purposes, such as verifying that a caller is authorized to perform certain operations, or disallowing unauthorized access. Web services can use identities for billing, licensing and auditing, and even for run-time service customization.
.NET Web Services and Security
When you use .NET to build a Web service, you rely on the built-in security support in ASP.NET and Internet Information Services (IIS). While this support makes developing secure ASP.NET Web Forms a breeze, it may require some work to develop and consume secure Web services. The problem is that ASP.NET and IIS security assumes there is a user on the other side of the wire, and that the user can type a user name and password into a dialog. Of course, with Web services there is no user involved, because Web services connect a client (an object) to a remote object (the Web service). This means that client-side developers have to provide your Web service with security credentials either explicitly or implicitly. .NET offers two security options to Web service developers: rely on Windows security or provide custom authentication. This article describes these two options and their different flavors and provides a side-by-side comparison of the security techniques.
Windows-Based Security
Using Windows-based security requires that the calling client application provide the credentials of an account on the server (or on the domain server). As a result, Windows security is most appropriate for intranet applications that use Web services to interact across a well-administered corporate network. This is because typically you have relatively fewer clients in an intranet application than in an Internet application. However, if managing a large number of accounts is acceptable to you, you could use Windows security across the Internet as well, where the number of users of the service can be considerably larger.
To use Windows security, all you need to do is configure the Web service appropriately. Once you configure the Web server to use Windows-based authentication, all calls to all methods on the Web service are authenticated.
.NET offers the Web service developer two security options: rely on Windows security or provide custom security.
To configure your Web service to use Windows-based authentication, you need to set the authentication tag in the Web service configuration file to Windows:
<authentication mode="Windows" />
You also need to disable anonymous access to the Web service. In IIS, display the properties of the Web service and select the Directory Security tab. Click the Edit... button to bring up the Authentication Methods dialog box. Clear the Anonymous access check box (see Figure 1).
Then, you need to select from the available authentication options on the Authentication Methods dialog box: Basic, Digest, or Integrated. These three options differ in the transport protocols they require and in what they require on the client side. Since you have disabled anonymous access, if you do not select at least one of the authentication options, all calls to your Web service will fail.
Basic Windows Authentication
Basic authentication is an industry-standard authentication protocol (RFC 1945) that requires the caller to send credentials to the server. When you select the Basic authentication check box, IIS will deny access to incoming requests that do not include security credentials: domain, user name, and password. IIS will reply to requests lacking credentials with HTTP error 401. In the case of a Web page, using Basic authentication will cause the browser to display a dialog asking the user to enter network credentials. In the case of a Web service client, there is no browser, so it is up to the client to provide the credentials programmatically.
Consider, for example, the SecureCalculator Web service, defined as:
public class SecureCalculator
{
[WebMethod]
public int Add(int num1,int num2)
{
return num1+num2;
}
}
If you configure this Web service to rely on Basic authentication, the client needs to provide the account credentials. The client may or may not be a Windows or .NET client. However, if the client is written in .NET, then it can use the built-in support that the client-side wrapper class has for passing network credentials (even when interacting with a non-Windows service that relies on Basic authentication). If you develop a client in .NET, when you add a Web reference to the SecureCalculator Web service, Visual Studio .NET generates a client-side SecureCalculator wrapper class that derives indirectly from WebClientProtocol. WebClientProtocol has a public property called Credentials, of type ICredentials. You'll find ICredentials in the System.Net namespace (see Listing 1 for the ICredentials definition). By default, the Credentials property is not initialized and is set to null. Before invoking a method on a Web service that uses Basic authentication, the client needs to initialize the Credentials property of the wrapper class with a NetworkCredentials object and then call the method:
using System.Net;
SecureCalculator calc = new SecureCalculator();
ICredentials creds;
creds = new
NetworkCredential("UserName","Password","Domain");
calc.Credentials = creds;
//Optional:
calc.PreAuthenticate = true;
calc.Add(2,3);
The client can also set the PreAuthenticate property of the wrapper class to true. This will cause the wrapper class to send credentials to the server, even when not challenged for them. This is an optimization option that can save the round trip after receiving a 401 error.
It is important to note that Basic authentication sends credentials in clear text, so you should use HTTPS or SSL, especially when using Basic authentication over the Internet. If the credentials are static, that is, they are not a run-time parameter the client retrieves somehow, then the client can encapsulate setting the credentials in the constructor of the wrapper class:
public class SecureCalculator : SoapHttpClientProtocol
{
public SecureCalculator ()
{
ICredentials creds;
creds = new NetworkCredential("UserName",
"Password","Domain");
Credentials = creds;
Url = "http://<...>/SecureCalculator.asmx";
}
//Method wrappers...
}
SecureCalculator calc = new SecureCalculator();
Calc.Add(2,3);
Digest Windows Authentication
Digest authentication uses a special hashing algorithm (MD5) to avoid sending passwords in clear text over HTTP, and does not require HTTPS. Digest authentication is an industry standard (RFC 2617) supported natively by IIS. However, it does require using Active Directory to store the password. If you are not using Active Directory, the Digest authentication option will be disabled on the Authentication Methods dialog box in IIS (see Figure 1).
If you select to use Digest authentication, IIS will look in the request for the digest credentials and authenticate the user accordingly. There is nothing the Web service developer needs to do; it is up to the client to pass the digested credentials. If the client is a .NET client, it needs to initialize the Credentials property of the wrapper class with an object of type CredentialCache, as defined in Listing 2. CredentialCache is a collection of credentials that is polymorphic with ICredentials.
When using Windows security, there is nothing the Web service developer is required to do beyond configuring the Web service appropriately.
The client needs to construct individual credentials and add them to the cache, while specifying the type of the credentials using a non-type-safe string. Then, just as when using Basic authentication, the client needs to initialize the Credentials property of the wrapper class with the CredentialCache, optionally enable pre-authentication, and call the method:
SecureCalculator calc = new SecureCalculator();
ICredentials credsCache = new CredentialCache();
Uri uriPrefix = new Uri("<Service URL>");
ICredentials creds;
creds = new NetworkCredential("UserName",
"Password","Domain");
credsCache.Add(uriPrefix,"Digest",creds);
calc.Credentials = credsCache;
calc.PreAuthenticate = true;//Optional
calc.Add(2,3);
With static credentials, the client can also encapsulate these settings in the wrapper class constructor.
Integrated Windows Authentication
Integrated authentication is a Windows protocol that uses a proprietary password-hashing algorithm and requires both the client and the Web service to run on Windows stations. When using Integrated authentication, you can send credentials over HTTP, because they are not sent in clear text. Integrated authentication relies on the fact that Windows already knows the identity and credentials of the client (more precisely, the security token of the client's thread), which it can capture and provide as authentication credentials. To use Integrated authentication, the client needs to initialize the Credentials property of the wrapper class by assigning to it the credentials returned from the DefaultCredentials static property of CredentialsCache:
SecureCalculator calc = new SecureCalculator();
calc.Credentials = CredentialCache.
DefaultCredentials; calc.PreAuthenticate = true;
//Optional
calc.Add(2,3);
If the client's identity is not adequate and you need to explicitly provide other Windows credentials, then you should use Basic or Digest authentication.
Custom Authentication
When you use custom Web services authentication, you do not rely on IIS to authenticate the callers. Instead, you will need to obtain the caller's credentials somehow, and verify them against a custom repository, typically a database. The source files accompanying this article contain the UserManager helper class, defined as:
public class UserManager
{
public UserManager();
public bool Authenticate(string userName,
string password);
//Other members and methods
}
UserManager accesses a database and authenticates the caller against the entries in the database. You will see UserManager used in the custom authentication code samples.
If the callers provide valid credentials, they are allowed to call the Web method; otherwise, the Web service can throw an exception. Custom authentication is appropriate when the callers do not have accounts on the server and when the number of identities is considerably large. An important distinction between custom authentication and Windows authentication is that custom authentication offers a fine level of control over which calls to Web methods the Web service authenticates. For example, a Web method that returns current inventory status is less sensitive than a Web method that processes a purchase order. In the interest of throughput, you may choose to only authenticate purchasing calls.
When using custom authentication, you need to allow anonymous access to the service by selecting the Anonymous access check box in the IIS Authentication Mode dialog box, and by setting the authentication mode in the Web service configuration file to None:
<authentication mode="None" />
Using custom authentication you can use whatever custom mechanism you want, but in general, you will likely use a log-in method, SOAP headers, or SOAP extensions. Each of these mechanisms has its variations and you can come up with your own tweaks as well.
Log-in Method
The log-in method, as the name implies, involves exposing a dedicated method on the Web service that accepts the caller's credentials. Needless to say, you should use the log-in method only over a secure channel (HTTPS) because the credentials are transmitted in clear text. When the Web service authenticates a caller, it stores the caller's authentication in a session variable. In each Web method call that requires authentication, you need to explicitly verify that the caller's session state variable indicates an authenticated caller.
Custom authentication is appropriate when the callers do not have accounts on the server, and when the number of identities is considerably large.
Listing 3 shows the server-side code for security using the log-in method. The SecureCalculator Web service encapsulates and uses a Session variable to set the value of the Boolean IsAuthenticated property. Note that every Web method that deals with security has to enable session state support explicitly by setting the EnableSession property of the WebMethod attribute to true:
[WebMethod(EnableSession=true)]
The LogIn() method of the SecureCalculator shown in Listing 3 uses the UserManager helper class to verify that the caller provided valid credentials, and if so, it sets the IsAuthenticated property to true. Before doing anything else, every method must verify that the caller is authenticated by checking the value of IsAuthenticated. The Web server only authenticates the caller the first time the caller connects to the service; subsequent calls are not authenticated. The caller will be logged out once the session ends or when the cookie used to manage the session expires. For increased security you can also provide a LogOut() method that allows the client to explicitly log out.
A .NET client using the log-in method of authentication has to enable support for cookies in the Web service wrapper class by initializing the CookieContainer property, typically by modifying the wrapper class constructor:
public class SecureCalculator :
SoapHttpClientProtocol
{
public SecureCalculator()
{
CookieContainer = new
System.Net.CookieContainer();
Url = "http://<...>/SecureCalculator.asmx";
}
public bool LogIn(string userName,
string password);
public void LogOut();
public int Add(int num1,int num2);
//Other methods
}
The client, of course, needs to call LogIn() before using secure methods of the Web service:
SecureCalculator calc = new SecureCalculator();
calc.LogIn("UserName","Password");
calc.Add(2,3);
calc.LogOut();
calc.Add(2,3); //Throws exception
You can take the log-in method approach one step further by factoring it into a base class and adding declarative authentication support using a custom principal. A principal is an object that represents an identity and its role membership. Listing 4 shows the LogInWebService abstract class. Like the LogIn() method in Listing 3, LogInWebService uses a session variable to indicate whether the caller is authenticated and encapsulates the session variable in the IsAuthenticated property. LogInWebService also uses a session variable to store the user name and encapsulates the session variable in the UserName property.
It is always a good practice to encapsulate session variables and expose logical properties around them. This eases the task of maintenance and provides a single place to enforce business rules. For example, UserName only allows the user name to be set if the user is authenticated.
The LogIn() and LogOut() methods of LogInWebService are nearly the same as the methods of the same name shown in Listing 3. One difference is that in LogInWebService, LogIn() stores the user name in the UserName property if the caller is authenticated, while LogOut() resets the user name if the caller logs out. The main difference between the methods, however, is in the LogInWebService constructor. If the caller is authenticated (because it already called the LogIn() method), the constructor creates a generic identity around the user name, and provides it to a generic principal. Then, the constructor installs the generic principal as the principal of the current thread:
IIdentity identity= new GenericIdentity(UserName);
IPrincipal principal = new
GenericPrincipal(identity,null);
Thread.CurrentPrincipal = principal;
Both GenericIdentity and GenericPrincipal are defined in the System.Security.Principal namespace. The purpose of this is clear when you examine a Web service that derives from LogInWebService:
public class SecureCalculator : LogInWebService
{
[PrincipalPermission(SecurityAction.Demand,
Authenticated = true)]
[WebMethod(EnableSession=true)]
public int Add(int num1,int num2)
{
return num1+num2;
}
}
All the SecureCalculator needs to do is add the PrincipalPermission attribute to sensitive methods, and demand that the caller is authenticated. The PrincipalPermission attribute looks for the current principal associated with the current thread, GenericPrincipal in this case, and asks it if the user is authenticated. The GenericPrincipal installed by default returns false, but ours returns true. This makes developing secure Web services trivial, and it provides the benefits of declarative security.
SOAP Headers
SOAP headers allow the client of a Web service to pass additional contextual information to the Web service object, without invoking methods. The client adds the information to the SOAP payload and it is up to the Web service to make use of it. When you implement a .NET Web service, .NET performs all the work involved in processing the headers. You can use SOAP headers to pass caller credentials to a Web service, but if you do so you should use secure channels, because the header information is transported in clear text.
The System.Web.Services.Protocols namespace provides support for SOAP headers. To use SOAP headers, you need to derive a class from SoapHeader and add credentials, such as user name and password, as class members. The class members in a SOAP header class must be public and in the form of fields or properties only (see Listing 5). Next, you need to add a member variable to the Web service class of the header type (see AuthHeader in Listing 5). When .NET creates the Web Services Description Language (WSDL) associated with the service, the WSDL will contain the appropriate type information about the SOAP header member variable for the use of the clients.
You must decorate any Web method that accesses a SOAP header variable with the SoapHeader attribute, letting .NET know which member variable the method accesses:
[SoapHeader("AuthHeader")]
[WebMethod]
public int Add(int num1,int num2){...}
.NET will automatically initialize the AuthHeader member with the information provided by the client. All that is left for the Web method to do is to authenticate the caller (using UserManager again) in the sensitive methods. In Listing 5, this is done using the Authenticate() helper method, which simply extracts the credentials from the header member variable.
When a .NET client adds a Web reference to a service that uses SOAP headers, .NET will generate a definition of a client-side SoapHeader derived class, with public variables only (it will convert properties in the original header class). .NET will also add to the wrapper class a matching member variable. The type of that member will be the type of the client-side header class, and the name of the member will be the type name with a Value suffix, for example:
public class AuthenticationHeader : SoapHeader
{
public string UserName;
public string Password;
}
public class SecureCalculator : SoapHttpClientProtocol
{
public AuthenticationHeader AuthenticationHeaderValue;
//Method wrappers
}
The wrapper class header member variable is set to null by default.
To pass credentials, the client has to initialize the member and call the Web method:
SecureCalculator calc = new SecureCalculator();
calc.AuthenticationHeaderValue = new AuthenticationHeader();
calc.AuthenticationHeaderValue.UserName = "UserName";
calc.AuthenticationHeaderValue.Password = "Password";
calc.Add(2,3);
The headers will be forwarded to the Web service, where it will use them to authenticate the caller. If the credentials are static, the client can also encapsulate initialization of the SOAP header in the wrapper class constructor.
Using SOAP headers in this manner requires that the client provide credentials on every method call. Authentication on the server side can be a time-consuming operation. If one-time authentication is sufficient for your needs, you can optimize for throughput and use a session variable to record the fact that the client has already authenticated itself. Listing 6 shows this technique. When asked to authenticate, the Web service checks the value of the IsAuthenticated property, which encapsulates the session variable. If the caller is already authenticated, no further action is required. If not, the service uses UserManager to authenticate the caller.
Note that the client still must provide the credentials in the SOAP header on every call, but the service uses them only during the first call.
SOAP Extensions
Using SOAP extensions is an advanced way a Web service developer can intercept all calls coming into the service and perform custom pre- and post-call processing. The Web service client can also interact with the SOAP extension, and provide custom pre-and post-call processing. For example, you can use SOAP extensions to compress the SOAP payload and to encrypt the information. Note that using SOAP extensions for payload encryption raises some interesting challenges, such as key distribution. In general, relying on SSL is a lot easier than developing a SOAP extension, especially for a public service.
You can also use SOAP extensions to transport the caller's credentials. If you encrypt the payload as well, there is no need for secure channels. Developing a real-life SOAP extension involves a non-trivial amount of work. In practice, the use of SOAP extensions assumes a .NET client interacting with a .NET service, because other development platforms have very little or no support for it.
This article only provides a brief mention of SOAP extensions, as a full discussion of this topic would merit an article in its own right.
Comparing the Options
This article described three Windows-based and five custom authentication techniques for Web services. Obviously, there are many more permutations and variations on this theme. Table 1 lists the authentication options I discussed, and the main points to consider when choosing an authentication mechanism:
- Is the password sent in clear text and therefore requires HTTPS?
- What are the platform requirements on both the client and the server side?
- When does authentication take place, on the first call only, or on every call? What are the throughput implications of that?
My own recommendation is that whenever possible, you should choose Windows authentication over custom authentication to minimize the amount of work involved in developing the Web service. If you do need a custom solution, I would opt for the log-in method factored to a base class. It is trivial to apply, it results in clear declarative security for the Web methods, and it is intuitive and easy for the client to use. In addition, it is easier to call a method than to use a SOAP header or extension, especially if the client platform, such as legacy Visual Basic 6.0, does not have native support for SOAP headers or extensions.
Conclusion
No doubt, support for Web services security will become much more powerful and integrated in future versions of .NET, especially once the standards are finalized and adopted. However, you should not wait for that day to come. Armed with the techniques shown in this article, you can deploy and consume secure .NET Web services now.