Introduction
Microsoft IssueVision is a developer sample application that demonstrates the best practices for building Smart Client applications. It was developed specifically for the DevDays 2004 Smart Client track, and it provides a sample help desk management application. The original source code can be downloaded here.
In this article, I describe an implementation of adding WS-SecureConversation to the Microsoft IssueVision sample application using WSE 2.0.
The WS-SecureConversation specification allows clients and Web Services to establish a token-based, secure conversation for the duration of a session. It is analogous to the Secure Sockets Layer (SSL) protocol that provides on-demand, secure communications over the HTTP transport channel. Secure conversation is based on security tokens that are procured by a service token provider. This process involves an initial amount of overhead, but once the channel is established, the client and service exchange a lightweight, signed security context token, which optimizes message delivery time compared with using regular security tokens. The security context token enables the same signing and encryption features as other security tokens, like UsernameToken or X509SecurityToken.
System Requirements
- Microsoft� Windows 2000, Microsoft� Windows XP Professional or Microsoft� Windows Server� 2003
- Microsoft� Internet Information Services (IIS) 5.1, or 6.0
- Microsoft� Visual Studio .NET 2003
- Microsoft� .NET Framework 1.1
- Microsoft� SQL Server 2000 (requires SQL Server Authentication enabled)
- Microsoft� WSE 2.0 SP3
Installation
After copying the contents of the source zip file to a location on your local disk, we must complete the following setup steps before we can compile and run the application.
1. Installing the Server Certificate
This application uses the sample certificates included in WSE 2.0. Follow the procedures below to install the server certificate. (Note: you should not use these sample certificates in a production environment. Instead, contact a certificate authority, and request your own certificate.)
- Open an MMC console by pressing Start, press Run, type mmc, and then click OK.
- On the File menu, click Add/Remove Snap-in.
- Click Add, under Snap-in, double-click Certificates.
- Click My user account to add the certificates for the current user. Click Finish.
- Click Add, under Snap-in, double-click Certificates.
- Click Computer account for the local machine's certificates. Click Finish.
- Close the dialog boxes.
- In the console tree, under Certificates (Local Computer)\Personal, click Certificates.
- Open the Certificate Import wizard by selecting Action | All Tasks and choose Import.
- Follow the wizard. When asked for the file to import, specify: C:\Program Files\Microsoft WSE\v2.0\Samples\Sample Test Certificates\Server Private.pfx. When asked for the private key password, specify: wse2qs. Finish the wizard.
- Your MMC window should now look something like this:
Note: this certificate will be used to encrypt messages between the applications. The client application will use the public key to encrypt the message and the service will use the private key to decrypt the message. The client needs to have the public portion of the certificate available in the Current User store.
- In the console tree, under Certificates - Current User\Other People, click Certificates.
Note: if you don't have an Other People store under Current User, open Internet Explorer, select Tools, Internet Options, Content, and press the Certificates button. You should see an Other People tab in the Certificates dialog. You can import the certificate here through this interface or you can return to MMC and refresh the Current User tree and Other People should now show up.
- Open the Certificate Import wizard by selecting Action | All Tasks and choose Import.
- Follow the wizard. When asked for the file to import, specify: C:\Program Files\Microsoft WSE\v2.0\Samples\Sample Test Certificates\Server Public.cer. Finish the wizard.
- Your MMC window should now look something like this:
- On Windows 2000 machines, the file Server Public.cer should be installed under Certificates - Current User\Personal instead. Your MMC window should now look something like this:
Note: this certificate only contains the public portion of Server Private.pfx. The client will use this to encrypt messages and the server will use the private key installed in the Local Machine store to decrypt the messages.
- Launch the WSE 2.0 X.509 Certificate Tool. Change the Certificate Location to Local Computer and Store Name to Personal and press the Open Certificate button. Select the WSE2QuickStartServer certificate and press OK. Then, press the View Private Key File Properties� button. Navigate to the Security tab and give the local machine's ASPNET account read access to the private key using the Add� button. Press OK to close the dialog.
2. Installing the IssueVision Sample Database
To install the sample database, please run DataReset.cmd included in the source zip file. The IssueVision XML Web Service requires SQL Server authentication (mixed mode) to be enabled on the SQL Server 2000 server. Follow the procedures below to verify:
- Click Start, Programs, Microsoft SQL Server and click SQL Enterprise Manager to run SQL Enterprise Manager from the Microsoft SQL Server program group.
- Select the server you want to work with, then from the Tools menu, select SQL Server Configuration Properties, and choose the Security page.
- Your SQL Server Properties window should look something like this:
3. Creating the Required Virtual Directory
Finally, run the CreateSampleVdir.vbs script included in the source zip file to automatically create the required virtual directory. (You can later uninstall the virtual directory using the DeleteSampleVdir.vbs script.)
Code Description
A secure conversation is initiated by a client that requires an on-demand secure communication session with a Web Service, and it consists of the following three steps:
1. The client issues a signed request to the security token service provider for a security context token
The client initiates the secure conversion by issuing a signed request to the secure token service (STS) provider for a security context token. The client uses UsernameToken to sign the security token request, and uses X509SecurityToken to encrypt the SOAP message sender's entropy value. If the request is successful, the client caches the security context token for further communication. The following function RequestSCTByUsername()
implements this:
public static SecurityContextToken RequestSCTByUsername(String username,
String password)
{
SecurityContextToken sct = null;
if (m_sctData.m_username == string.Empty ||
m_sctData.m_password == string.Empty ||
m_sctData.m_username != username || m_sctData.m_password !=
password || m_sctData.m_sct.IsExpired)
{
SecurityToken token = new UsernameToken(username,
password, PasswordOption.SendPlainText);
SecurityToken issuerToken = GetServerToken(true);
String secureConvEndpoint = ConfigurationSettings.AppSettings["tokenIssuer"];
SecurityContextTokenServiceClient STSClient = new
SecurityContextTokenServiceClient(new Uri(secureConvEndpoint));
sct = STSClient.IssueSecurityContextTokenAuthenticated(token, issuerToken);
m_sctData = new SctData(username, password, sct);
}
return m_sctData.m_sct;
}
2. The security token service provider verifies the request and issues a security context token back to the client
On the server side, in order to validate digital signatures for incoming SOAP messages created using a UsernameToken, we override the AuthenticateToken
method of the UsernameTokenManager
class. The username and password are verified by checking whether the user exists in the database and by comparing the password hash value stored in the IssueVision database:
[SecurityPermissionAttribute(SecurityAction.Demand,
Flags=SecurityPermissionFlag.UnmanagedCode)]
public class CustomUsernameTokenManager : UsernameTokenManager
{
protected override String AuthenticateToken( UsernameToken token )
{
DataSet dataSet = new DataSet();
string dbPasswordHash;
try
{
SqlConnection conn = new SqlConnection(Common.ConnectionString);
SqlCommand cmd = new SqlCommand("GetUser", conn);
cmd.Parameters.Add("@UserName", token.Username);
cmd.CommandType = CommandType.StoredProcedure;
SqlDataAdapter da = new SqlDataAdapter(cmd);
da.Fill(dataSet);
}
catch (Exception ex)
{
EventLogHelper.LogFailureAudit(string.Format("The GetUser" +
" stored procedure encounted a problem: {0}",
ex.ToString()));
throw new SoapException(string.Empty,
SoapException.ServerFaultCode, "Database");
}
if (dataSet.Tables[0].Rows.Count == 0)
{
EventLogHelper.LogFailureAudit(string.Format("The username" +
" {0} does not exist.", token.Username));
throw new SoapException(string.Empty,
SoapException.ClientFaultCode, "Security");
}
else
{
DataRow dataRow = dataSet.Tables[0].Rows[0];
dbPasswordHash = (string)dataRow["PasswordHash"];
string dbPasswordSalt = (string)dataRow["PasswordSalt"];
string passwordHash = HashString(dbPasswordSalt + token.Password);
if (string.Compare(dbPasswordHash, passwordHash) != 0)
{
EventLogHelper.LogFailureAudit(string.Format("The password" +
" for the username {0} was incorrect.",
token.Username));
throw new SoapException(string.Empty,
SoapException.ClientFaultCode, "Security");
}
else
{
return token.Password;
}
}
}
private static string HashString(string textToHash)
{
SHA1CryptoServiceProvider SHA1 = new SHA1CryptoServiceProvider();
byte[] byteValue = System.Text.Encoding.UTF8.GetBytes(textToHash);
byte[] byteHash = SHA1.ComputeHash(byteValue);
SHA1.Clear();
return Convert.ToBase64String(byteHash);
}
}
3. The client and the Web Service use the security context token for secure communication
From the client side, every call to the Web Service starts by requesting the security context token cached from the above steps. We then use the security context token to sign and encrypt the SOAP request message:
public static IVDataSet SendReceiveIssues(DataSet changedIssues,
DateTime lastAccessed)
{
IVDataSet data = null;
IssueVisionServicesWse dataService = GetWebServiceReference();
try
{
SecurityContextToken sct =
Wse2HelperClient.RequestSCTByUsername(UserSettings.Instance.Username,
UserSettings.Instance.Password);
SoapContext requestContext = dataService.RequestSoapContext;
requestContext.Security.Tokens.Add(sct);
requestContext.Security.Elements.Add( new MessageSignature( sct ) );
requestContext.Security.Elements.Add( new EncryptedData( sct ) );
data = dataService.SendReceiveIssues(changedIssues, lastAccessed);
}
catch (WebException)
{
HandleWebServicesException(WebServicesExceptionType.WebException);
}
catch (SoapException soapEx)
{
if (soapEx.Actor == "Security")
{
HandleWebServicesException(WebServicesExceptionType.SoapException);
}
else
{
HandleWebServicesException(WebServicesExceptionType.WebException);
}
}
catch (Exception)
{
HandleWebServicesException(WebServicesExceptionType.Exception);
}
return data;
}
On the server side, every incoming request is first verified whether it is signed and encrypted. Also, we check whether it is signed by a security context token. When all these conditions are met, we use the same security context token to sign and encrypt the response SOAP message:
[WebMethod(Description="Synchronize data by" +
" send and recieving from the remote client.")]
public IVDataSet SendReceiveIssues(DataSet changedIssues, DateTime lastAccessed)
{
SoapContext requestContext = RequestSoapContext.Current;
Wse2HelperServer.VerifyMessageParts(requestContext);
Wse2HelperServer.VerifyMessageSignature(requestContext);
Wse2HelperServer.VerifyMessageEncryption(requestContext);
SecurityContextToken sct =
Wse2HelperServer.GetSigningToken(requestContext)
as SecurityContextToken;
if (sct == null)
{
throw new SoapException("The request is not signed with an SCT.",
SoapException.ServerFaultCode, "Security");
}
SoapContext responseContext = ResponseSoapContext.Current;
responseContext.Security.Tokens.Add(sct);
responseContext.Security.Elements.Add(new MessageSignature(sct));
responseContext.Security.Elements.Add(new EncryptedData(sct));
return new IVData().SendReceiveIssues(changedIssues, lastAccessed);
}
To monitor the actual encrypted and signed SOAP messages sent between the client and the web server, we can check the input and output message trace files InputTrace.webinfo and OutputTrace.webinfo on either the client or the web server side.
Further Work
- For more security, it is better to use SHA-256, SHA-384, or SHA-512 instead of SHA1 in function
HashString()
to calculate password hash values. This also requires changing the password hash stored in the IssueVision sample database. Check this link for more information.
History
- June 23rd, 2005 - Initial release.
- September 12th, 2005 - Added support for Windows 2000.