Introduction
I created a simple MCF Service for a client application I was working on. I wanted the communication to use SSL and to use the ASP membership provider as the authentication source. It all seemed easy enough, and all was great until I changed the wsHttpBinding
security message clientCredentialType
to Username
. Then, started seven hours of the same very unhelpful error: "The request for security token has invalid or malformed elements."
Background
After seven hours of Googling "The request for security token has invalid or malformed elements", I came across an article that talked about svcTraceviewer.exe. For the life of me, I could not figure out where to get this magical application. Finally, I found and downloaded the Windows Vista SDK which includes the .NET 3.0 SDK. Why not just a .NET 3.0 SDK I am not sure, but I certainly wasn't looking for the Vista SDK to install on my XP development machine. Anyways, I added some lines to enable logging on both the client and the MCF Service. I thought then that I was getting some where. The error at this point was the same: "The request for security token has invalid or malformed elements." OK, so now what? I now knew everything works when clientCredentialType
="windows
", but when I change it to clientCredentialType
="UserName
", the error would come back. I knew that Microsoft would not allow the username and password to be sent in clear from an MSDN post, and it had to do with Client Validation of the SSL certificate, but even when I tried to use my own customCertificateValidator
on the client, the code never got called. My problem was that in all the articles I read, I never saw the client enpointBehaviors
serverCertificate
authentication certificateValidationMode
attribute set. As soon as I set this to PeerOrChainTrust
, I started to get errors that meant something. Errors like: x509 Certificate is not trusted and Identity Mismatch try changing "www.domain.com" to "domain.com". Now with errors that meant something, I could figure out what to fix.
Using the code
The first thing you need to do is create a test certificate using the makecert executable. I used:
C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0>makecert.exe
-sr LocalMachine -ss My -a sha1 -n CN=localhost -sky exchange -pe
I found How to Grant Access to the ASPNET account to the Certificates at http://www.eggheadcafe.com/articles/20021231.asp
In order for WSE to obtain the X.509 private key from the local computer certificate store, it must have permission to do so. By default, only the owner and the System account can access the private key of a certificate. Also, by default, the ASP.NET service runs under the ASPNET account, and that account does not have access to the private key.
To give the ASPNET account access to the private key, give the account under which ASP.NET is running Full Control access to the files containing the keys WSE will need to retrieve in the following folder:
C:\Documents and Settings\All Users\Application Data\
Microsoft\Crypto\RSA\MachineKeys
The account the ASP.NET worker process runs under is controlled by the <processModel>
element in the Machine.config file. Set the userName
attribute of the <processModel>
element to specify the account ASP.NET runs under. By default, the userName
attribute is set to the special machine account, which maps to the low-privileged ASPNET user account created when the .NET Framework SDK is installed.
- Open Windows Explorer.
- Navigate to the C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys folder.
- Select the files containing the keys that WSE will need to retrieve.
- From the File menu, select Properties.
- On the Security tab, add the ASPNET account and select the Full Control option.
Note: Determining which key file in the MachineKeys folder is associated with a certificate can be difficult. One easy method is to note the creation date and time when creating a new certificate. When you view the files in the MachineKeys directory, check the Date Modified field for the corresponding date and time.
Now, run MMC and add the certificate plug-in twice. I added the first one as my user account, and added the second as Computer. With this MMC plug-in, you can see what certificates are installed as personal in the local computer, plus cut and paste them between folders. What you are looking for is in the Local Computer personal certificates if a certificate exists with a friendly name that matches your findvalue
attribute in serviceBehaviors serverCredentials serviceCertificate
. It should be noted that I was also able to use a signed certificate from godaddy that I was using in IIS for a real domain even though the DNS name didn't match the certname by editing the client identity.
This is my web.config:
<system.serviceModel>
<diagnostics>
<messageLogging
logEntireMessage="true"
logMalformedMessages="true"
logMessagesAtServiceLevel="true"
logMessagesAtTransportLevel="false"
maxMessagesToLog="30000"
maxSizeOfMessageToLog="200000"/>
</diagnostics>
<bindings>
<wsHttpBinding>
<binding name="AuroraSyncService">
<security mode="Message">
<transport clientCredentialType="None" />
<message clientCredentialType="UserName" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<services>
<service behaviorConfiguration="AuroraSyncServiceBehavior"
name="AuroraSyncService">
<endpoint address="" binding="wsHttpBinding"
bindingConfiguration="AuroraSyncService"
contract="IAuroraSyncService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding"
contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="AuroraSyncServiceBehavior">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
<serviceAuthorization principalPermissionMode="UseAspNetRoles" />
<serviceCredentials>
<serviceCertificate findValue="localhost"
x509FindType="FindBySubjectName"
storeLocation="LocalMachine"
storeName="My" />
<userNameAuthentication
userNamePasswordValidationMode="MembershipProvider"
membershipProviderName="MembershipSqlProvider" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
<system.diagnostics>
<sources>
<source name="System.ServiceModel.MessageLogging">
<listeners>
<add name="messages"
type="System.Diagnostics.XmlWriterTraceListener"
initializeData="c:\logs\servermessages.svclog" />
</listeners>
</source>
</sources>
</system.diagnostics>
This is my app.config:
<system.diagnostics>
<sources>
<source name="System.ServiceModel.MessageLogging">
<listeners>
<add name="messages"
type="System.Diagnostics.XmlWriterTraceListener"
initializeData="c:\logs\clientmessages.svclog" />
</listeners>
</source>
</sources>
</system.diagnostics>
<system.serviceModel>
<diagnostics>
<messageLogging
logEntireMessage="true"
logMalformedMessages="true"
logMessagesAtServiceLevel="true"
logMessagesAtTransportLevel="false"
maxMessagesToLog="30000"
maxSizeOfMessageToLog="200000"/>
</diagnostics>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_IAuroraSyncService"
closeTimeout="00:01:00"
openTimeout="00:01:00"
receiveTimeout="00:10:00"
sendTimeout="00:01:00"
bypassProxyOnLocal="false"
transactionFlow="false"
hostNameComparisonMode="StrongWildcard"
maxBufferPoolSize="524288"
maxReceivedMessageSize="1000000000"
messageEncoding="Text"
textEncoding="utf-8"
useDefaultWebProxy="true"
allowCookies="True">
<readerQuotas maxDepth="32"
maxStringContentLength="8192"
maxArrayLength="16384"
maxBytesPerRead="4096"
maxNameTableCharCount="16384" />
<reliableSession ordered="true"
inactivityTimeout="00:10:00"
enabled="false" />
<security mode="Message">
<transport clientCredentialType="None"
proxyCredentialType="None"/>
<message clientCredentialType="UserName" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<client>
<endpoint
address="http://localhost:4358/FamilyOrganizer.Aurora.
Sync.WCFService/AuroraSyncService.svc"
binding="wsHttpBinding"
bindingConfiguration="WSHttpBinding_IAuroraSyncService"
contract="FamilyOrganizer.Aurora.Sync.WCFService.IAuroraSyncService"
name="WSHttpBinding_IAuroraSyncService"
behaviorConfiguration="myClientBehavior">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
</client>
<behaviors>
<endpointBehaviors>
<behavior name="myClientBehavior">
<clientCredentials>
<serviceCertificate>
<authentication certificateValidationMode="PeerOrChainTrust" />
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
Your application will need to create an instance of the MCFclient
and set the username and password like this:
_WebServerSyncService = New WCFService.SyncServiceClient
_WebServerSyncService.ClientCredentials.UserName.UserName = myUserName
_WebServerSyncService.ClientCredentials.UserName.Password = myPassword
Points of interest
The very vague errors about the security channel annoyed me. Why not give an error like: sorry the client is not set to trust this certificate, try changing this attribute, like all the other errors MCF throws.
History
- Added the ASPNET Permissions section
- Fixed some spelling
- Formatted XML to prevent scrolling