Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Implementing WS-SecureConversation in Microsoft IssueVision

0.00/5 (No votes)
27 Sep 2005 1  
Adding secure communications to the Microsoft IssueVision sample application using WSE 2.0.

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:

server certificate

    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:

client certificate

  • 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:

client certificate

    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:

SQL Server mixed mode

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;

    // Request a new security context token

    // if one was not available from m_sctData

    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)
    {
        // Create a UsernameToken to use as the base

        // for the security context token

        SecurityToken token = new UsernameToken(username, 
                              password, PasswordOption.SendPlainText);

        // Retrieve the server certificate

        SecurityToken issuerToken = GetServerToken(true);

        // Create a SecurityContextTokenServiceClient

        // (STSClient) that will get the SecurityContextToken

        String secureConvEndpoint = ConfigurationSettings.AppSettings["tokenIssuer"];
        SecurityContextTokenServiceClient STSClient = new 
          SecurityContextTokenServiceClient(new Uri(secureConvEndpoint));

        // Request the security context token,

        // use the client's signing token as the base

        sct = STSClient.IssueSecurityContextTokenAuthenticated(token, issuerToken);

        // Cache the security context token in m_sctData

        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 )
    {
        // This method returns the password for the provided username

        // WSE will make the determination if they match

        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");
        }

        // does the user exist?

        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 
        {
            // we found the user, verify the password

            // hash by compare the Salt + PasswordHash

            DataRow dataRow = dataSet.Tables[0].Rows[0];
            dbPasswordHash = (string)dataRow["PasswordHash"];
            string dbPasswordSalt = (string)dataRow["PasswordSalt"];
            // create a hash based on the user's salt and the input password

            string passwordHash = HashString(dbPasswordSalt + token.Password);

            // does the computed hash match the database hash?

            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;
            }
        }
    }

    // generates a hash of the input plain text

    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
    {
        // Request the security context token

        SecurityContextToken sct = 
          Wse2HelperClient.RequestSCTByUsername(UserSettings.Instance.Username,
          UserSettings.Instance.Password);

        // Use the security context token to sign

        // and encrypt a request to the Web service

        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;

    // Reject any requests which are not valid SOAP requests

    Wse2HelperServer.VerifyMessageParts(requestContext);
    Wse2HelperServer.VerifyMessageSignature(requestContext);
    Wse2HelperServer.VerifyMessageEncryption(requestContext);

    // Check if the Soap Message is Signed with an SCT.

    SecurityContextToken sct = 
            Wse2HelperServer.GetSigningToken(requestContext) 
            as SecurityContextToken;
    if (sct == null)
    {
        throw new SoapException("The request is not signed with an SCT.", 
                              SoapException.ServerFaultCode, "Security");
    }

    // Use the SCT to sign and encrypt the response

    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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here