Beginning this month, The XML Files will run under the name Service Station. We have made this change so that the column can discuss broader topics such as Web services, service-oriented architecture, and the like.
In the August 2004 issue, I gave you an introductory tour of the various new and improved features in Web Services Enhancements for Microsoft® .NET (WSE) 2.0. This month I'll take you on a tour of the WSE 2.0 security features and show you how to automate security code with declarative policies.
Simply installing WSE 2.0 does not make your ASMX endpoints secure. Configuring your ASMX projects with the WSE SoapExtension makes them capable of processing security elements found in the messages (such as verifying signatures, decrypting elements, and so forth), but these security elements aren't required by the endpoint. You must take action to require such security elements by either writing code or applying a policy. You must also either write code or apply a policy to generate messages that meet the security requirements of a secure endpoint.
Let's start with a simple unsecured ASMX operation: [WebMethod]
public MortgagePayments CalcMortgage(
double amount, double interest, double years,
double annualTax, double annualIns)
{
MortgageCalc mc = new MortgageCalc();
return mc.CalculateMortgage(amount, years, interest,
annualTax, annualIns);
}
As it stands, the CalcMortgage operation is open to a variety of attacks. First, the operation can be invoked by anyone. There's no way to identify the sender and verify that it actually has rights to perform the given operation. Second, there's no way to verify the integrity of incoming messages. Malicious parties can intercept incoming messages, tamper with them, and forward them to the endpoint without being detected. And finally, the messages are available for anyone to read since they're sent in plain text.
I'll show you how to tackle these problems one at a time. First, you need to require the sender to provide a UsernameToken for identification, authentication, and authorization purposes.
Requiring UsernameTokens
Assuming WSE 2.0 is installed and configured properly (note that you must enable the WSE SoapExtension in the service), you can add some code to CalcMortgage to require the presence of a UsernameToken in incoming messages. You can inspect the WS-Security elements found in incoming messages through the SoapContext object. You can look through the SoapContext to determine if a UsernameToken was supplied by the sender.
I've added a new class to the Web service project called WseHelpers, which contains a single method called GetUsernameToken. This method expects the caller to supply it with a SoapContext object that it will inspect for a UsernameToken element, which it returns to the caller when found. The method throws an exception if it doesn't find a UsernameToken (see Figure 1).
You can now use this method to require a UsernameToken in all messages entering CalcMortage by simply calling it at the beginning of the method, as shown here: [WebMethod]
public MortgagePayments CalcMortgage(
double amount, double interest, double years,
double annualTax, double annualIns)
{
WseHelpers.GetUsernameToken(RequestSoapContext.Current);
MortgageCalc mc = new MortgageCalc();
return mc.CalculateMortgage(amount, years, interest,
annualTax, annualIns);
}
After making this change, you won't be able to invoke the operation without supplying a UsernameToken. To test this, I've provided a sample Windows® Forms application (available in the code download) that is currently configured to invoke this operation without doing anything related to security. Now when you run the client you'll get the "Missing security token" exception.
Now that the CalcMortgage operation requires a UsernameToken, you need to update the client application to provide one. WSE 2.0 provides a UsernameToken class that represents the <wsse:UsernameToken> element. You only have to instantiate a UsernameToken object and add it to the proxy's SoapContext object, as illustrated here: // retrieve loan values from form
MortgageCalculatorWse mc = new MortgageCalculatorWse();
// instantiate UsernameToken and add to SoapContext
UsernameToken tok = new UsernameToken("mike", "ekim",
PasswordOption.SendPlainText);
mc.RequestSoapContext.Security.Tokens.Add(tok);
MortgagePayments pay = mc.CalcMortgage(amount, interest,
year, annualTax, annualIns);
Then, when you invoke any operations through the Web service proxy class, the WSE runtime will automatically add the appropriate security tokens to the SOAP message.
In this case, the password is sent in plain text. You can specify how you want the password to be sent when you instantiate the UsernameToken object. I used the PasswordOptions.SendPlainText option in this example. The other two options allow you to send a hashed password or no password at all. It's obviously not a good idea to send the password in plain text unless you plan to send the SOAP message over a secure channel or to encrypt the UsernameToken element (more on this shortly).
Now when you run the client application supplying a UsernameToken, you won't get the "Missing security token" exception anymore. You must, however, supply the credentials of a valid user account or you'll get the following exception: "The security token could not be authenticated or authorized."
Authentication and Authorization
One of the benefits of sending passwords in plain text is that Windows can automatically authenticate the incoming message by calling the Win32® LogonUser API. If the call fails, WSE 2.0 throws an exception and the request is not allowed through to the ASMX operation. On the other hand, if the call is successful, WSE 2.0 initializes the UsernameToken's Principal property with the authenticated user. The Principal object can be used to authorize access to the operation.
This is accomplished through the IsInRole method, as illustrated in Figure 2. Here we're verifying that the authenticated user is a member of the Broker group on the machine hosting the service. With this code in place, the CalcMortage endpoint knows that only authenticated users belonging to the Broker role will be allowed to access the service.
Although this is certainly a step in the right direction, the endpoint cannot completely rest since it still has the plain text password to deal with. Not addressing this issue could severely compromise the system since someone could sniff the messages and steal the password. Also, since we're not supplying a signature, someone could modify the credentials en route to the endpoint.
Implementing a UsernameTokenManager
There are a few ways to avoid sending the password in plain text, but they all require you to implement a custom token manager. A custom token manager is a class you write and configure on the service to process incoming UsernameToken elements. You can use a UsernameTokenManager to implement a custom authentication scheme where user credentials are stored in a SQL Server™ database (a very common scenario in Web applications today).
You implement a token manager by deriving a new class from UsernameTokenManager and overriding the AuthenticateToken method. Within AuthenticateToken, your job is to look up the password for the supplied user name and return it to the caller, which in this case is the WSE infrastructure. WSE will use the password to authenticate the user by making sure they match (in the plain text case) or by verifying any password-based hashes/signatures. Before returning from AuthenticateToken, you can also create and associate a Principal object with the UsernameToken object that specifies role membership. The Principal object can be used later in the processing pipeline for authorization purposes, as illustrated in the previous section.
Figure 3 shows a sample UsernameTokenManager implementation. For pedagogical reasons, I've hardcoded the passwords for each user, but this is obviously not something you'd want to do in practice. The implementation associates a GenericPrincipal object with each token specifying membership in the Broker role.
You have to configure WSE to use your UsernameTokenManager class. You can do this by adding the <securityTokenManager> element to the <microsoft.web.services2> section of the service's Web.config file, as shown here: <configuration>
•••
<microsoft.web.services2>
<security>
<securityTokenManager
type="SecureInvoiceServiceA.MyUsernameTokenManager,
SecureInvoiceServiceA" xmlns:wsse="http://docs.oasis-
open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-
1.0.xsd" qname="wsse:UsernameToken" />
</security>
</microsoft.web.services2>
With the UsernameTokenManager class configured in Web.config, the WSE runtime will call into your implementation of AuthenticateToken each time it receives a UsernameToken element. And since you're looking up the password manually and providing it to WSE, you no longer need to have Windows accounts set up on the machine hosting the service. Instead, they can be stored in your location of choice (SQL Server, an XML file, and so on).
The other main benefit of using a UsernameTokenManager is that the sender no longer has to supply the password in plain text. Instead, the sender can send a hashed version of the password. When WSE receives the message and retrieves the password from our UsernameTokenManager, it can use the same algorithm to generate a hash of the value returned by Authenticatetoken and verify that it matches the one sent in the message. When they match, the sender indeed sent the correct password for the user. When they don't match, the sender sent an incorrect password or the token hash has been tampered, which means authentication fails. WSE takes care of all of this for you if you've provided a UsernameTokenManager implementation to supply a correct password.
The following example illustrates how to send a hashed password from the client (again, I've hardcoded the password but only for ease of explanation): // retrieve loan values from form
MortgageCalculatorWse mc = new MortgageCalculatorWse();
UsernameToken tok = new UsernameToken("mike", "ekim",
PasswordOption.SendHashed);
mc.RequestSoapContext.Security.Tokens.Add(tok);
This solved the password problem, but you still don't have protection against message tampering. This is what signatures are designed to prevent.
Requiring Signatures
You can require that incoming messages contain signatures by adding more code to inspect the SoapContext object, very much like you did to require UsernameToken elements. In Figure 4 you'll see that I've added a CheckForSignature method to the WseHelpers class shown in Figure 1. This method illustrates how to traverse the SoapContext looking for MessageSignature objects. The presence of a MessageSignature object indicates that at least part of the message was signed. You'd have to extend this method to require signatures for specific elements.
Adding a call to CheckForSignature at the beginning of CalcMortgage requires signatures on the incoming messages. If you run the client application without adding a signature, you'll get a "Missing signature" exception.
Signing messages from the sending applications is very straightforward with WSE. You can sign messages with a variety of different token types, including UsernameTokens. You sign a message by instantiating a MessageSignature object based on a particular security token and then adding it to the SoapContext, as illustrated in the following lines of code: MortgageCalculatorWse mc = new MortgageCalculatorWse();
UsernameToken tok = new UsernameToken("aaron", "noraa",
PasswordOption.SendHashed);
mc.RequestSoapContext.Security.Tokens.Add(tok);
MessageSignature sig = new MessageSignature(tok);
mc.RequestSoapContext.Security.Elements.Add(sig);
By default, WSE signs the body and most of the headers, although you can control which elements you want to sign. When WSE receives this message on the server, it will recalculate the signature value and verify that it matches the one supplied in the message. As such, the message signature can be used as proof that the sender knows the password (known as proof of possession). This makes is possible for the sender to stop sending the password altogether, as illustrated here: MortgageCalculatorWse mc = new MortgageCalculatorWse();
UsernameToken tok = new UsernameToken("aaron", "noraa",
PasswordOption.SendNone);
mc.RequestSoapContext.Security.Tokens.Add(tok);
// sign the message with the UsernameToken
MessageSignature sig = new MessageSignature(tok);
mc.RequestSoapContext.Security.Elements.Add(sig);
In this case, WSE will automatically verify the signature and, by so doing, verify the password. Supplying an invalid password causes an exception ("The signature or decryption was invalid") even though the password was not supplied in the message.
You can also tell WSE to sign the response SOAP messages returned by CalcMortgage. One way would be to use the UsernameToken supplied by the sender to sign the response SOAP message, adding the appropriate objects to the current ResponseSoapContext. However, signing the response using the same username token from the request is not commonly used since only the user should use its token to sign a message. A service should not use it to sign anything. Instead, a service should have its own token, normally an X.509 security token (the details of which I'll discuss shortly) to sign the response.
Using DerivedKeyTokens
One of the problems with using a raw UsernameToken to sign messages is that it becomes less secure with each message sent. Signing repeatedly with the same key might expose the system to ciphertext-only attacks that could result in exposing the secret data you're encrypting. To overcome this, by default the WSE runtime adds some randomness to the username token before it is sent so that the keys for different messages are actually different.
WS-SecureConversation also addresses this by defining a new token type called DerivedKeyToken. A DerivedKeyToken is a security token that's derived from another token and then used to sign (or encrypt) the message. A new DerivedKeyToken can be generated from the original shared secret for each message, making it less likely for ciphertext-only attacks to succeed. The following sample illustrates how to modify the sending code to generate a DerivedKeyToken from the original UsernameToken, which it then uses to sign the message: MortgageCalculatorWse mc = new MortgageCalculatorWse();
UsernameToken tok = new UsernameToken("aaron", "noraa",
PasswordOption.SendNone);
mc.RequestSoapContext.Security.Tokens.Add(tok);
// derive a key from the UsernameToken and use it to sign
DerivedKeyToken dk = new DerivedKeyToken(tok);
mc.RequestSoapContext.Security.Tokens.Add(dk);
MessageSignature sig = new MessageSignature(dk);
mc.RequestSoapContext.Security.Elements.Add(sig);
Requiring Encryption
Encryption is the last feature you may want the service to require. Encryption prevents eavesdropping and guarantees the privacy of sensitive data. Figure 5 shows how to write a method to check for EncryptedData elements in the SoapContext object. Like CheckSignature, the CheckforEncryption method will return true as long as at least one element was encrypted in the message. You'd have to extend this method if you wanted to require that specific elements are encrypted. Now, if the client application tries to invoke CalcMortgage, with a call to CheckForEncryption in place, it will get the "Encryption required" exception.
As with signatures, it is easy to encrypt the messages sent by the client. You can encrypt the message with a raw UsernameToken (like you originally did with signatures) but you should really use a DerivedKeyToken instead. The code in Figure 6 illustrates how to encrypt with a DerivedKeyToken.
WSE only encrypts the body of the SOAP message by default, but you can specify exactly which elements you'd like to encrypt. You can also modify the WebMethod to encrypt the response messages like you did for signatures.
Signing and Encrypting with X.509 Tokens
The examples I've shown thus far have used UsernameTokens to authenticate, sign, and encrypt SOAP messages. WS-Security allows for a variety of security token types besides UsernameTokens, including X.509 certificates, Kerberos tickets, and custom tokens.
The code in Figure 7 illustrates how to use X.509 tokens to sign and encrypt the message when invoking the CalcMortgage operation. GetX509Token retrieves the certificate from the machine's certificate store. In this example, you're using the client's certificate to sign the message (with the private key) and the server's certificate to encrypt the message (with the public key). When the server receives the message, it will verify the signature and automatically decrypt the message using its private key. WSE 2.0 ships with some sample certificates that you can use to experiment with this type of code (these are the certificates I'm using).
In order to illustrate how to encrypt a specific portion of the message, let's look at how to encrypt a UsernameToken element with an X.509 token. You can do this by adding a new EncryptedData object to the SoapContext, specifying the ID of the element you want to encrypt. The following sample illustrates how to do this for the UsernameToken element: UsernameToken tok = new UsernameToken("aaron", "noraa",
PasswordOption.SendPlainText);
...
// Encrypt the body
mc.RequestSoapContext.Security.Elements.Add(
new EncryptedData(serverToken));
// Encrypt the UsernameToken element
mc.RequestSoapContext.Security.Elements.Add(
new EncryptedData(serverToken, string.Format("#{0}", tok.Id)));
This scenario allows you to safely use a plain text password in the UsernameToken element since it will be encrypted before crossing the wire. When WSE receives this message on the server, it will decrypt the UsernameToken header before proceeding with the normal authentication process described earlier.
Automating Security with Policy
As you can see, it's not hard to programmatically require security features in a service or to programmatically add security features to messages sent by a client. However, it still requires you to write code that mixes the security infrastructure with business logic. One of the most compelling features of WSE 2.0 is that you can completely automate everything that I've shown here through declarative policies.
Web Services Enhancements 2.0 provides support for WS-Policy, WS-PolicyAssertions, and WS-SecurityPolicy. Policies are used to require security elements when receiving messages and to suggest the need for security elements when sending messages. All you have to do is add an element to your configuration file indicating which policy file to use, as shown in the following lines of code: <configuration>
<microsoft.web.services2>
<policy>
<cache name="policyCache.config" />
</policy>
WSE 2.0 provides a wizard integrated with Visual Studio® .NET that makes it easy to generate policies that describe the most common security features. You can launch the wizard from the Policy tab in the WSE Settings Tool. When the wizard appears, select "Secure a service application" and press Next. The next step allows you to specify the security elements that you want to require in the request and response messages. Here you can require signatures and encryption for both the request and response messages.
On the next step you select the type of token the service will require for client authentication. Choose UsernameToken since that's what you've been using throughout this column. Next you specify users or roles that should have access to the operation. In this example, you've been restricting access to users in the Broker role so that's what you'll specify in this step. And finally, you have to specify the server certificate that will be used for request encryption and response signing. You can then finish the wizard and the policy file will be generated and configured in the project's Web.config file (see Figure 8).
Figure 8 WSE Security Settings Tool
In this case, the generated policy states that incoming messages must contain a UsernameToken that is associated with the SKON-COMPAQ\Broker role, a signature based on a DerivedKeyToken (derived from the UsernameToken), and a body encrypted with the server certificate selected in the wizard. This policy will be automatically enforced by WSE. As a result, you can comment out all of the code that you added to the service in order to require the different security elements.
You can also use a policy on the client side to influence how the messages are secured. You just need to run the wizard again, select "Secure a client application," and choose the same settings used for the service. Using a policy on the client makes it possible to automate the code used to add tokens, signatures, and encryption. All you have to do is tell WSE where to find the UsernameToken that it needs to add to the message. You can do this with the PolicyEnforcementSecurityTokenCache, as illustrated here: MortgageCalculatorWse mc = new MortgageCalculatorWse();
UsernameToken tok = new UsernameToken("mike", "ekim",
PasswordOption.SendHashed);
// add the UsernameToken to the global cache, it will be
// added to the message automatically by WSE
PolicyEnforcementSecurityTokenCache.GlobalCache.Add(tok);
You can now remove all of the code that you added to the client application throughout this column (except for the last line of code you just saw) since WSE will take care of securing the messages for you. These policies automate all of the security features that were previously implemented manually.
The cool thing about declarative policies is that you can tweak them to control all sorts of things. For example, you can specify that the UsernameToken element needs to be encrypted in the request (when you want to use plain text passwords) by simply adding a reference to the <MessageParts> element. Or you can modify the policy to turn on WS-SecureConversation. This is actually just a checkbox in the very first step of the security wizard. Policy makes it possible to control all of your Web services security needs without any code.
Note on the UsernameTokenManager
When you implement custom authentication using a UsernameTokenManager, your implementation of AuthenticateToken must return the same secret (for example, password) used on the client side to generate the hash/signature, depending on which option you use. This is necessary because WSE needs to recalculate the hash/signature to verify that it's the same. So when the client side uses clear text passwords, UsernameTokenManager must have access to the clear text passwords of all the users in the system. This approach makes security experts cringe because if the machine is compromised, so are all of the passwords. And since passwords are mostly used by humans, and humans typically use the same password everywhere, compromising the machine can have a far-reaching effect. The only way around this is to have the sender and receiver agree on an algorithm for generating a new secret from the original password that it can use instead. Hence, the client would supply the generated secret when instantiating the UsernameToken, and the machine would store the generated password somewhere on the machine. The rest of the process would work the same. This approach doesn't protect the server if it's compromised, but the real passwords are safe.
Where Are We?
WSE 2.0 provides a powerful and flexible security framework that makes it possible to secure incoming and outgoing messages within the context of SOAP. WSE 2.0 accomplishes this by allowing you to attach security tokens that can be used to identify and authorize senders as well as to sign and encrypt (portions of) the messages. You can add these security features to your code manually or through the declarative policy framework that automates everything. WSE 2.0 is a major breakthrough because it's the first available Microsoft Web services framework that makes it possible to secure Web services endpoints without requiring you to become a complete security expert.