In-Depth
Drilldown on WSE 3.0
Take advantage of WSE 3.0 to set up Web service authentication with a username and password using only one server certificate.
- By Martin Kulov
- 02/01/2007
Technology Toolbox: C#, XML, WSE
Microsoft's Web Service Enhancements (WSE) standard gives developers a robust infrastructure for securing Web services at the client level or at the server by using server X.509 certificates, Kerberos, or username tokens.
WSE 3.0 is now available, and you can use it to create custom assertions and provide additional functionality at any point in processing messages on the client or at the server. You can also use it to customize the security authorization provider and select between Active Directory, LDAP, or custom database. Be sure to check Microsoft's Patterns and Practices page for WSE 3.0 (go here).
My recent article, "Simplify Authentication With WSE 3.0" (go here) explained how to secure a Web service using Web Services Enhancements 3.0. That's a useful first step, but there's much more you can accomplish with this technology.
For a given Internet environment, you want to authenticate each user of a Web service with a username and password. This approach is one of the simplest ways to handle authentication because you don't need to use a certificate to authenticate every Web service client. You can accomplish this by setting up Web service authentication with a username and password that relies on only one server certificate, while at the same time preserving a high level of message encryption. I'll also explain various techniques for diagnosing security-related problems, which can be difficult to sort through on your own.
On top of being flexible and extensible, one of the best parts about WSE 3 is that it can also be easy to use. For example, you can use the usernameForCertificateSecurity turnkey assertion to simplify authentication in Web services, which is an important aspect of mastering Microsoft's recently released WSE 3.0.
While you don't have to provide a certificate using this approach, nothing prevents you from encrypting the request to the Web service using an X.509 certificate located on the server. If you adopt this approach, the client should hold the public key of the server certificate before it initiates a request for credentials. You then use the public key to encrypt a symmetric key that encrypts the communication channel to send the username and password, as well as other requests.
You can secure a simple Web service with the WSE Settings 3.0 tool and a few lines of code:
using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace =
"http://academy.devbg.org/wse30")]
[WebServiceBinding(ConformsTo =
WsiProfiles.BasicProfile1_1)]
public class RemoteDisk :
System.Web.Services.WebService
{
public RemoteDisk ()
{
}
[WebMethod]
public string[] GetFolders()
{
string[] folders =
{ "/", "/Users", "/Docs" };
return folders;
}
}
Adopting this approach requires that you use the usernameForCertificateSecurity assertion built into WSE 3.0. You take advantage of this assertion by opening the solution and starting the WSE 3.0 Settings tool by right clicking on the project.
The next step is to add the necessary configuration elements to the web.config file, which enables you to process WSE requests and responses. Do this by enabling the checkboxes "Enable this project for Web Services Enhancements" and "Enable Microsoft Web Services Enhancements Soap Protocol Factory."
Enable Policy
Once you can processs WSE requests and responses, you need to click on the Policy tab and check the "Enable Policy" checkbox, click on the Add button, and enter "MyPolicy" for the policy name to bring up the WSE Security Settings Wizard.
Click on "Next" in the wizard's first dialog, then select "Secure a service application" in the Authentication Settings dialog. Select Username for the Client Authentication Method, then click on "Next" to go to the Users and Roles dialog. Be sure to leave the Perform Authorization check box unchecked because you won't use role-based security in this article's sample Web service. Uncheck Establish Secure Session on the next page and leave all other settings at their default settings.
Protection Order is set to Sign and Encrypt by default. You can also select Sign, Encrypt, or Encrypt Signature if you need to keep the signature private, otherwise accept the default values for this option.
An important step is to select the certificate you'll use on the server in the Server Certificate dialog. The client uses the public part of this certificate to encrypt the requests sent to the server. The server then uses the private part of the certificate to decrypt the message. Click on Select Certificate and choose the WSE2QuickStartServer certificate. You can find more information on how to install the sample certificates in the Readme.htm file located in the Samples folder of the WSE 3.0 installation folder. The next dialog lets you review the security settings. Click on Finish, then click OK on the WSE Settings tool to create the policy file in the folder for your Web service. The WSE Settings tool assigns the policy file the name wse3policyCache.config by default.
Once you run the wizard and create your policy file, you're ready to examine the structure of the file to see how WSE has implemented the usernameForCertificateSecurity assertion (see Listing 1).
You can find the full name for the assembly that implements this assertion in the extension tag called usernameForCertificate-Security. The policy element follows immediately after the extension element. You can create multiple policies by adding new policy elements with different names. You apply the policy created with the Microsoft.Web.Services3.PolicyAttribute attribute on your Web service class. Do this by specifying the name of the policy your Web service will use:
[Policy("MyPolicy")]
public class RemoteDisk :
System.Web.Services.WebService
{
...
}
You configure the assertion itself through the attributes applied to the element <usernameForCertificateSecurity> in the policy element. You disabled the use of Secure Sessions in the wizard, so you can see the attribute establishSecurityContext is set to false. This means that the authentication is based on an asymmetric algorithm using the public/private key pair included in the server certificate for every single request to the Web service. Security context requests are outside the scope of this article, but look for more information on this topic in VSM in the near future.
Encrypt Your Messge
Another important setting for the <usernameForCertificate-Security> assertion is messageProtectionOrder, the value of which is set to SignBeforeEncrypt. This value ensures that the message is first signed and then encrypted, keeping the signature out of the encryption process. You can use SignBeforeEncryptAnd-EncryptSignature if you want to encrypt the signature also. The third possible value, EncryptBeforeSign, allows you to encrypt the message first, and then sign the complete encrypted message.
The next element, <usernameForCertificateSecurity>, includes two defined child elements: <serviceToken> and <protection>. The <serviceToken> element specifies the location of the certificate that the server uses for decrypting and encrypting every single request and response. WSE stores the X.509 certificate you selected with the wizard in your LocalMachine location. Specifically, the file is located in the My storeName and named WSE2QuickStartServer:
<x509 storeLocation="LocalMachine"
storeName="My"
findValue="CN=WSE2QuickStartServer"
findType=
"FindBySubjectDistinguishedName"
/>
The Certificate location, LocalMachine, serves as the location for all certificates that users and services such as IIS can access. Installing your certificate as described in the Readme.htm file places your server certificate in LocalMachine.
One tool that deserves mention is the Certificates Tool, which you find in the Microsoft WSE 3.0 folder. Start the tool, select Local Computer from the Certificate Location dropdown, and Personal from the Store Name list (see Figure 1). Click on the Open Certificates button and you should see the WSE2QuickStartServer certificate. If the certificate is missing, you must return to the Readme.htm file again and make sure that you have installed your certificates as described. Select the WSE2QuickStartServer certificate, and you will see the name and key identifiers for this certificate.
Pay special attention to the View Private Key File Properties button. This button gives you the Windows Explorer file properties for the selected certificate. This is an extremely convenient way to see the actual file location and NTFS permissions for this certificate. If you have configured your IIS to use accounts other than ASPNET in Windows XP or NETWORK SERVICE in Windows 2003, you might need to give the new account read access to the certificate. Failing to do this will prompt this vaguely worded error at the client:
System.InvalidOperationException: Security requirements are not satisfied because the security header is not present in the incoming message.
Unfortunately, every security error that occurs on the server side will prompt the same, vague error message at the client. This means you can find yourself in deep trouble, and spending endless nights debugging such problems, if you don't know the real cause for this error. You still have a chance, however. You can see the specific exception that the server threw by enabling Message Tracing in the Diagnostics tab of the WSE Settings 3.0 tool. Be sure to enable both the input and output files. You can find the real exception data at the end of the InputTrace.webinfo file in the last processing step:
Exception thrown: WSE600:
Unable to unwrap a symmetric key
using the private key of
an X.509 certificate.
Please check if the account 'xxx'
has permissions to read the private
key of certificate with subject name
'CN=WSE2QuickStartServer' and thumbprint
'87D92CCAB714C30DB0E0BAE6B9B0B65B5D6DF7CF'.
The account xxx refers to the one that your IIS is running under in cases where you have changed the default ASPNET or NETWORK SERVICE. You need to give this account read permission to the certificate that uses View Private Key File Properties in the Certificates Tool. A word of advice: Enable the Message Tracing feature for both your client and server applications, and remember to review all four log files in case you encounter any exceptions.
Inside the Config File
With those caveats out of the way, turn your attention back to the wse3policyCache.config file. The <protection> element is among the most significant elements of this file. It lets you specify which parts of the message are taken into account when the signature is generated and whether you would like to encrypt the body of the message. This element also enables you to set different values for its request, response, and fault messages.
Depending on your message types, you probably want to sign only the parts of the message that you don't have to change using intermediate services. Note that changing any part of the message that is included in the signature results in a signature validation failure and causes your entire message to be discarded. You can find the full list of message parts that can be included in the signature on the SignatureOptions Enumeration page (see additional resources). There is one tricky part you need to pay special attention to. You cannot encrypt custom message headers using the standard policy features in WSE 3.0. You can, however, create a custom SecurityPolicyAssertion that can handle the encryption process.
So far, you have the service ready and secured. The next step is to use the simple console client:
using System;
using System.Collections.Generic;
using System.Text;
namespace RemoteDiskClient
{
class Program
{
static void Main(string[] args)
{
// Underscore is an illegal line break.
// Remove the space and underscore below
// and concatenate that line with line below.
RemoteDiskWS.RemoteDiskWse service =
new RemoteDiskClient. _
RemoteDiskWS.RemoteDiskWse();
string[] folders =
service.GetFolders();
foreach (string folder in folders)
{
Console.WriteLine(folder);
}
}
}
}
Before you call the service, you need to add support for the usernameForCertificateSecurity assertion, just as you did for the service. Failure to do this yields an unhandled exception System.-
Web.Services.Protocols.SoapHeaderException:
Unhandled Exception:
System.Web.Services.Protocols.SoapHeaderException:
System.Web.Services.Protocols.SoapHeaderException:
Server unavailable, please try later --->
System.ApplicationException: WSE841:
An error occured processing an outgoing fault response. ---
> System.Web.Services.Protocols.SoapHeaderException:
Microsoft.Web.Services3.Security.SecurityFault:
WSE2008: UsernameToken is expected but not present in
the security header of the incoming message.
You enable the usernameForCertificateSecurity assertion by opening the WSE Settings tool, selecting the Policy tab, and checking Enable Policy. Next, click on Add, enter MyPolicy as a name for the policy, and click on OK. The WSE Security Settings Wizard pops up again. Click on Next to go to the Authentication Settings dialog box. Once there, select the "Secure a client application with authentication" method and choose the Username option for authentication. Go to the next dialog box and leave the option "Specify Username Token in code" checked. This enables you to set the username and password used for calling the service when the user enters them. Continue with the wizard. On the Message Protection dialog, uncheck the "Establish Secure Session" check box. Next, go to the Server Certificate dialog. Here you should select the certificate you want to use to encrypt the messages sent to the server.
The encryption process requires only a public key. You use the private key only for decrypting or signing messages, which is why that key is available only on the server. Select the WSE2QuickStartServer certificate, click on Next, then click on Finish in the last dialog to complete the wizard. You're almost done with this stage. Click on OK in the WSE Settings tool to create a new wse3policyCache.config file that contains all the settings for your policy. If you compare the client and server policy files, the only differences you'll see are in the certificates' store locations. All other settings should be identical.
All that remains is to tell the proxy that calls the Web service to use the policy you've just created and pass it user credentials:
RemoteDiskWS.RemoteDiskWse service =
// Underscore is an illegal line break.
// Remove the space and underscore below
// and concatenate that line with line below.
new RemoteDiskClient. _
RemoteDiskWS.RemoteDiskWse();
// TODO: ask user to specify credentials
string username = "mike";
string password = "123";
service.SetPolicy("MyPolicy");
UsernameToken token =
new UsernameToken(username, password,
PasswordOption.SendPlainText);
service.SetClientCredential(token);
string[] folders = service.GetFolders();
foreach (string folder in folders)
{
Console.WriteLine(folder);
}
Create the UsernameToken Class
You need to take special care in how you create the Username-Token class. If you create an instance of UsernameToken by passing only a username and password to the constructor, it sets the PasswordOption to SendHashed value by default. If you then try to call the service with this option, you get a highly dubious and non-pertinent exception message.
You can avoid this error by making sure that you create Username-Token with the PasswordOption set to SendPlainText value:
protected virtual string
AuthenticateToken(UsernameToken token)
{
if (token.PasswordOption !=
PasswordOption.SendPlainText)
{
return null;
}
WindowsPrincipal principal1 =
UsernameTokenManager.LogonUser(
token.Username, token.Password);
if (principal1 != null)
{
token.Principal = principal1;
}
else
{
this.OnLogonUserFailed(token);
}
return token.Password;
}
By default, UsernameToken validates the user against Active Directory. The reason you see this behavior is because the UsernameTokenManager.AuthenticateToken() method returns a null value if you don't set the PasswordOption to PasswordOption.-
SendPlainText; more simply, the authentication doesn't succeed. In this case, you need to provide a custom authentication mechanism. When you use PasswordOption.SendPlainText, the method tries to authenticate the user against Active Directory and returns a WindowsPrincipal object. You can enable the Authorization feature in the server policy file by replacing the existing server policy file with a new one that has this feature turned on. The server then impersonates the server thread with the credentials that contain this WindowsPrincipal object. You can also perform impersonation checks in your code:
if (this.User.IsInRole(
"Administrators"))
{
// do something important
}
If you have done everything correctly to this point, you should be able to call your Web service in a secure manner now. Calling it displays the string array returned from the GetFolders method:
using System;
using System.Web;
using System.Collections;
using System.Web.Services;
using System.Web.Services.Protocols;
using Microsoft.Web.Services3;
[WebService(Namespace =
"http://academy.devbg.org/wse30")]
[WebServiceBinding(ConformsTo =
WsiProfiles.BasicProfile1_1)]
public class RemoteDiskInternal :
System.Web.Services.WebService
{
public RemoteDiskInternal()
{
}
[WebMethod]
public string[] GetFolders()
{
string[] folders =
{ "/", "/C$", "/Users", "/Docs" };
return folders;
}
}
The next step is to turn on WSE's diagnose feature and see what an encrypted message looks like.
Go to the WSE Settings tool for the client project again and select the Diagnostics tab. In the Message Tracing panel, check the Enable Message Trace option. Run the client again, and you will find InputTrace.webinfo and OutputTrace.webinfo files in the bin subfolder of the client project. You can also see these files when you select the project in Visual Studio 2005 and in the Solution Explorer when you click on the Show All Files button. Navigate to the bin subfolder and open the input and output files. The input file logs every message that the client receives, while the output file contains every message sent to the service. You can also enable the Diagnostics feature for the service project to get the corresponding log files. The good thing when you open these files with Visual Studio 2005 is that you get syntax highlighting and the option to reload the file whenever it has been changed. There is also a restriction on downloading files with the extension .webinfo, so you can be sure that your server logs won't be compromised. For example, attempting to download InputTrace.webinfo prompts this error: "This type of page is not served."
Open the OutputTrace.webinfo file and take a look at its contents. There is an outputMessage element that contains the result of every processing step performed. For example, the outputMessage element contains the raw SOAP packet before any transformation:
<processingStep
description="Unprocessed message">
You can also see what the unencrypted message looks like and compare it to the result provided by the encryption process:
<processingStep
description="Processed message">
Space prohibits covering the details of every element in the encrypted message, but two of these elements are worth covering here. The first of these is the soap:Envelope/soap:Header/
Signature/ SignatureValue element. It contains the signature of the entire message. The second one is the soap:Envelope/ soap:Body/ xenc:EncryptedData/ xenc:CipherData/ xenc:CipherValue element, which holds the encrypted data. Examining this element reveals that all the data from the original message, including the name of the request GetFolders, is encrypted.
More Information
- Find more information about implementing direct authentication with the UsernameToken in WSE 3.0
here.
- See the full list of message parts that can be included in the signature on the SignatureOptions Enumeration page here.
-Learn more about public key infrastructure here.
- Learn more about X.509 certificates here.