UsernameToken Profile vs. WCF
The WS-Security UsernameToken Profile 1.0 defines how the username and password can be encapsulated into a security token.
The profile defines two different ways to transmit the password (or password equivalent):
· wsse:PasswordText: the password is sent in clear-text
· wsse:PasswordDigest: a digest derived from the password is sent
As documented on MSDN, only the first option is supported by WCF’s built-in UsernameToken. This token is used in scenarios where client credential is set to Username.
The second option, wsse:PasswordDigest, is the subject of this post.
Besides WCF’s predecessor – Web Services Enhancements –, other 3rd-party SOAP protocol stacks (e.g. Axis) support the digest version, too. This leads to interoperability problems with WCF, and we regularly get Support Requests from our customers to get help on this interoperability issue.
Security tokens are most of the time not easy matter, but probably the UsernameToken is one of the simpler tokens. So after I began writing a custom token that supports the wsse:PasswordDigest, this indeed proved to be doable.
Usual Disclaimer
Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object code form of the Sample Code, provided that. You agree: (i) to not use Our name, logo, or trademarks to market Your software product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code.
You can download the sample from here.
Layout of the custom UsernameToken
The custom token builds on the CustomToken sample in the Windows SDK’s WCF Samples section. This lays out the skeleton of a custom token by implementing a simple credit card token. Actually, the MSDN documentation for WCF security classes often showcases parts from this credit card sample, where applicable.
Like this stock sample, our UsernameToken token won’t support most of the advanced concepts, simply because they are not possible with this type of token. One of such would be, for example, to use this token for encryption.
This also means that the implementation’s most interesting part is generating the password digest. The rest of the code is plumbing that makes integration with WCF seamless.
The sample Visual Studio 2010 solution consists of 5 projects:
Client |
Sample client using the new token |
Common |
A few common types that client and service share |
Service |
Sample IIS-hosted service using the new token |
UsernameToken |
The new token’s implementation |
Wse3InteropTest |
A test project that demonstrates WSE3 interoperability on token-level |
Here’s a list of the most important classes and their role.
UsernameClientCredentials |
Allows using the new token by providing username and password to the client |
UsernameClientCredentialsSecurityTokenManager |
Manages the new tokens on client-side |
UsernameInfo |
Simply holds username and password |
UsernamePasswordProvider |
Retrieves password for a given username at the service side for validation |
UsernameSecurityTokenSerializer |
Serializes and deserializes the token |
UsernameServiceCredentials |
Allows using the new token on service-side |
UsernameToken |
Core token class dealing with cryptography |
UsernameTokenAuthenticator |
Authenticates the new tokens |
UserNameTokenAuthorizationPolicy |
Issues claims on the new token |
UsernameTokenParameters |
Describes the new token’s capabilities |
UsernameTokenProvider |
Creates the new token |
UsernameToken class at the core
This central type deals with cryptography to produce the password digest according to the requirements of the WS-S UsernameToken Profile:
Password_Digest = Base64 ( SHA-1 ( nonce + created + password ) )
This is done in GetPasswordDigestAsBase64 as follows:
public string GetPasswordDigestAsBase64()
{
// generate a cryptographically strong random value
RandomNumberGenerator rndGenerator = new RNGCryptoServiceProvider();
rndGenerator.GetBytes(_nonce);
// get other operands to the right format
byte[] time = Encoding.UTF8.GetBytes(GetCreatedAsString());
byte[] pwd = Encoding.UTF8.GetBytes(_usernameInfo.Password);
byte[] operand = new byte[_nonce.Length + time.Length + pwd.Length];
Array.Copy(_nonce, operand, _nonce.Length);
Array.Copy(time, 0, operand, _nonce.Length, time.Length);
Array.Copy(pwd, 0, operand, _nonce.Length + time.Length, pwd.Length);
// create the hash
SHA1 sha1 = SHA1.Create();
return Convert.ToBase64String(sha1.ComputeHash(operand));
}
Fortunately, this isn’t too complicated thanks to the rich .NET class library available to us:
1. Create a cryptographically strong random value, which will be the nonce. This can be used to provide defense against replay-attacks with the combination of a nonce-cache in the service (not part of the sampe).
2. Take the time the token was created at, and transform it to yyyy-MM-ddTHH:mm:ssZ format. Encode it in UTF8 byte array.
Note that knowing the time is useful if we don’t want the service’s nonce-cache grow unlimited.
3. Encode in a UTF8 byte array.
4. Allocate a byte array large enough to hold all of this data, and concatenate them.
5. Create the SHA-1 hash.
6. Return the base64 representation.
Validating an incoming token takes simply re-building the hash from the password associated with the username (service must know this), and the time and nonce already stored in the incoming token:
public bool ValidateToken(string password)
{
byte[] pwd = Encoding.UTF8.GetBytes(password);
byte[] createdBytes = Encoding.UTF8.GetBytes(GetCreatedAsString());
byte[] operand = new byte[_nonce.Length + createdBytes.Length + pwd.Length];
Array.Copy(_nonce, operand, _nonce.Length);
Array.Copy(createdBytes, 0, operand, _nonce.Length, createdBytes.Length);
Array.Copy(pwd, 0, operand, _nonce.Length + createdBytes.Length, pwd.Length);
SHA1 sha1 = SHA1.Create();
string trueDigest = Convert.ToBase64String(sha1.ComputeHash(operand));
return String.Compare(trueDigest, _usernameInfo.Password) == 0;
}
Deployment
To test the sample, build it with Visual Studio 2010 (please ignore source control warnings, this solutio.n is part of a Team Foundation Server project), and create a virtual application on local IIS that points to the service. The client is configured to use https://localhost/servicemodelsamples/service.svc.
Interoperability with Axis
In the actual scenario I wrote this custom token for, the Java-based service didn’t like the addressing headers that WCF generated. In particular, the wsa:Action header had mustUnderstand set to true, and we got the error message that “Must Understand check failed for header https://www.w3.org/2005/08/addressing : Action”.
WCF doesn’t allow removing mustUnderstand from the addressing headers (unless you remove it from the serialized message). Therefore, in the sample I simply ask WCF to not add the addressing headers by setting MessageVersion to Soap11.
Security note
WS-S UsernameToken Profile 1.0 warns that digested password should be sent over secure transport.
Obviously, using an unsecure transport doesn’t protect against capturing or modifying the message payload, but I’m not sure why the password would not be protected against attacks. Anyway, I’m not a security expert, so I advise following the recommendation and use a secure transport.
Comments
- Anonymous
January 24, 2011
in BindingHelper.cstransportSecurity.AllowInsecureTransport = true;there is no 'AllowInsecureTransport ' property...in my assembly.how could I do? - Anonymous
January 24, 2011
I can't build/debug the sample.'AllowInsecureTransport' property doesn't exist in my assembly.in BindingHelper.csI use visual studio 2010, and windows7 , and I installed WSE3.0.. - Anonymous
April 13, 2011
Hello kay,SecurityBindingElement.AllowInsecureTransport got introduced with build 3.0.4506.4135 - you need to update your libraries to this version, e.g. by installing support.microsoft.com/.../971831.Note that Windows7 should already come with a build that contains this feature. - Anonymous
April 21, 2011
Akos,Absolutely awesome!!After hours of struggling with the WSS UsernameToken in a WCF client I found this post that solved my problems. Just had to replace the http for https at the BindingHelper class.Excelent work. :D - Anonymous
April 28, 2011
Thanks Jose! :)