Behold WSE 2.0: Removing Another Layer of WS-Pain (Cont.) Passing User Credentials To properly pass the user token, the client must create an instance of the UsernameToken type, populate it with the correct username and password, and somehow indicate to the client-side run time that it must be serialized to the outgoing message. You can manually add the token to the proxy's RequestSoapContext or configure a client-side policy to enforce behavior through the pipeline. The latter requires that you add the token to a global cache where the WSE run time looks to collect all tokens and signatures for serializing messages to fit policy. The same policy configuration interface shown in Figure 1 is used to define a policy cache for sending messages from the client. An application configuration file (app.config) is created by the tool to configure send and receive policy files for the client. Policy configuration files are also similar to Listing 1; client policy and service policy need only be complimentary, not identical. After setting up a send policy for the client, specifying the requirement for outgoing messages to support UsernameToken signatures, the WSE run time looks for an appropriate token in the global cache during serialization. This global cache is accessible through the SecurityTokenCache type in the Microsoft.Web.Services.Security.Tokens namespace. Its purpose is to store tokens for client applications that apply policies to outgoing messages. Using the global cache is an alternative to adding tokens directly to the Web service proxy class, which I'll use later when I need to deal with a more complicated security scenario. The following code creates a UsernameToken credential and adds it to the global cache: UsernameToken userToken = new UsernameToken( this.txtUsername.Text, this.txtPassword.Text, PasswordOption.SendHashed); SecurityTokenCache. GlobalCache.Add(userToken);
UsernameToken is an object representation of the wsse:UsernameToken type. A username token per the WS-Security specification may provide a plain-text password, a hashed password, or no password at all. The PasswordOption enumeration I used requests that the password be sent hashed, but values exist for the other two options as well, though not recommended for obvious reasons. Passing a username token without a password implies that the user has previously been authenticated within the trust domain of the service. This only works if all applications are part of the same domain, or if a single sign-on solution that handles username tokens is introduced. Sending a password in clear text, of course, has implications if the message is not encrypted. At this point, with two lines of code on the client, a single authentication method on the server, and some skillful pointing and clicking to configure policy and security, you have user authentication in place. Extensibility for Hashed Passwords Allowing the run time to validate the database-provided password against the information passed in the message has a flaw in its logic. Most secure databases actually store one-way hash versions of passwords that cannot be decoded for comparison. That means that either the client must pass a hash of their passwords using the same hashing algorithm, or you need a way to override the way WSE compares the password with the database hash. It's neither reasonable to require clients to hash passwords with a consistent algorithm, nor is it appropriate, as that publicly exposes the hashed value. So we must rely on the future extensibility of WSE to help us out in this department. Today it is not possible to override how the run time validates passwords. Controlling Message Privacy: Who Can Read the Message? If message parts are encrypted with a public key, only the holder of the (well guarded) private key can decrypt and process those parts. Once again using policy configuration, you can quickly add functionality to your Web services, leveraging the WSE run time to handle decryption. To configure TellerServices to require encryption, check the Require Encryption box (as shown in Figure 1), and select X509SecurityToken from the Token Type list. This adds a <wssp:Confidentiality> element to the policy WS-Policy configuration for the application shown in Listing 1. The security token used for encryption, in this case, is wsse:X509v3. The run time uses this policy setting to verify that the message body is encrypted using this type of token. Subsequently, the run time looks to the configured certificate store for a private key to match the public key token indicated by the message. Assuming the key is found, decryption is handled by the pipeline prior to deserializing the message. The security configuration interface (Figure 2) has an X.509 Certificate Settings section that specifies the certificate store, and further rules for certificate processing. Once again, a SoapHeaderException is raised if the message does not satisfy security policy, so you need to turn to the client to configure its policy and properly encrypt the message. Clients use a public key to encrypt messages, and the destination Web service must have access to the matching private key to successfully decrypt. The same configuration tool is used on the client side to specify an encryption policy for outgoing messages, which means that the run time on the client side also looks for a valid encryption token in the global cache. During message serialization, this token is retrieved from the configured certificate store on the client computer to encrypt the message. The default policy configuration sets <wssp:MessageParts> to wsp:Body(), indicating that the message body is the target of the encryption policy. This setting should match whatever the policy is for decryption on the service side. The following code creates an encryption token and adds it to the global cache: X509SecurityToken encToken = CertificateStoreUtil. GetCertificateToken( ConfigurationSettings.AppSettings ["serverpublickeyid"]); SecurityTokenCache.GlobalCache.Add( encToken);
CertificateStoreUtil.GetCertificateToken() encapsulates code to retrieve the public key from the certificate store on the local computer, based on a configured application setting. This assumes that the client has already retrieved the public key for the Web service, and placed it in the certificate store of the client computer. Encryption policy can be configured for inbound and outbound messages using the same policy configuration tool. In this case, a reverse set of keys are used. The service signs the response with the trusted source' public key and the client application uses its private key to decrypt the response. In situations where message data is highly sensitive, although this adds bloat to the message payload, it also guarantees that no intermediaries or non-trusted parties can read encrypted message parts. Verifying Message Integrity: Who Really Sent the Message? Even if a valid set of user credentials are supplied to invoke the WS, and the message is encrypted so that only the ultimate receiver can inspect messages, there still leaves the lack of certainty over the source of the message. Any client with the right public key can encrypt a message, and it is possible that a username and password combination could be hacked. If a limited set of trading partners or applications are considered to be valid Web service clients, it is possible to require each partner to use their private key to sign messages. When a message is signed, it not only can serve as proof of the message source, but also proof of message integrity. Signing a message part implies that an encrypted version of the message part is sent along with the message so that the receiver can verify that the part was not tampered with. If a message part is signed with a private key, the receiver can decrypt the signed message part using the public key, to verify that the decrypted part matches the message sent. Requiring Private Key Signatures The policy configuration tool supports many signing options, including asymmetric X.509 digital signatures. This type of signature is the one sure way to guarantee the sender of the message, without having to maintain a shared secret between sender and receiver (as in a symmetric authentication pattern like passing username and password tokens). So long as the service has access to the public key for the sender, the run time can automatically verify that the required message parts are correctly signed by a trusted source. On the client side, a similar policy is configured for sending encrypted messages, but in this case, the private key for the client application is retrieved and stored in the global cache: X509SecurityToken sigToken = Certificates.CertificateStoreUtil. GetCertificateToken( ConfigurationSettings.AppSettings ["clientprivatekeyid"]);
SecurityTokenCache.GlobalCache.Add(sigToken);
By default, the policy setting applies signatures to the body of the message only. The WS-Policy specification supports signing specific headers in addition to the body, but with WSE 2.0, this must be configured manually by editing the policy XML. | & | | Why We Need WS-Security
SSL was never an adequate security solution for Web services communication. Although SSL may have insured that the message was not tampered with en-route, it did not guarantee the source of the message, nor prevent message replay, and it indiscriminately added bloat to the message payload by encrypting the entire message, attachments included. SSL also fails to address the presence of Web service intermediaries that sit between the initial sender and ultimate receiver of the message. Intermediaries may not support SSL encryption, but worse, the sender doesn't likely have a trust relationship with intermediaries, yet may freely view message decrypted by an SSL exchange. In addition, the intermediary may not hold an encrypted exchange with the ultimate receiver and the message is exposed on the wire. |