I had to ditch the basic WCF UserName/Pwd security and implement my own custom client credentials to hold some more info beyond what is provided by default.
I worked throughthis MSDN article, but I'm missing something because it doesn't work.
First, I have some custom ClientCredentials that provide a custom ClientCredentialsSecurityTokenManager:
public class CentralAuthCredentials : ClientCredentials
{
public override System.IdentityModel.Selectors.SecurityTokenManager CreateSecurityTokenManager()
{
return new CentralAuthTokenManager(this);
}
}
public class CentralAuthTokenManager : ClientCredentialsSecurityTokenManager
{
private CentralAuthCredentials credentials;
public CentralAuthTokenManager(CentralAuthCredentials creds) : base(creds)
{
this.credentials = creds;
}
public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement)
{
if (this.IsIssuedSecurityTokenRequirement(tokenRequirement) || tokenRequirement.TokenType == CentralAuthToken.TOKEN_TYPE)
return new CentralAuthTokenProvider(credentials.UserId, credentials.UserPassword, credentials.ImpersonateId, credentials.LoginType);
else
return base.CreateSecurityTokenProvider(tokenRequirement);
}
public override SecurityTokenAuthenticator CreateSecurityTokenAuthenticator(SecurityTokenRequirement tokenRequirement, out SecurityTokenResolver outOfBandTokenResolver)
{
outOfBandTokenResolver = null;
if (this.IsIssuedSecurityTokenRequirement(tokenRequirement) || tokenRequirement.TokenType == CentralAuthToken.TOKEN_TYPE)
return new CentralAuthTokenAuthenticator();
else
return base.CreateSecurityTokenAuthenticator(tokenRequirement, out outOfBandTokenResolver);
}
public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version)
{
return new CentralAuthTokenSerializer();
}
}
Now when I run the app, my custom credentials and token manager do get created. However, in the method:
CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement)
{
...
}
The tokenRequirement.TokenType comes across as something other than my custom token. That brings up my 1st question: How the heck does WCF know what the token requirements are?
Also, the method:
public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version)
{
return new CentralAuthTokenSerializer();
}
Does get called once by the client, but none of the methods on the returned token serializer are ever called. This indicates to me that the custom token is never being sent across the wire. I assume this is because the call to CreateSecurityTokenProvider() never returned my custom token provider, since the SecurityTokenRequirement is never passed in indicating my custom token is needed.
On the client side, I have:
public class CentralAuthorizationManagerClient : ClientBase<ICentralAuthorizationManager>, ICentralAuthorizationManager, IDisposable
{
public PFPrincipal GenerateToken()
{
if (!this.ChannelFactory.Endpoint.Behaviors.Contains(typeof(CentralAuthCredentials)))
throw new ArgumentException("Must set CentralAuthCredentials before calling this method.");
return base.Channel.GenerateToken();
}
public PFPrincipal GenerateToken(CentralAuthToken token)
{
this.ChannelFactory.Endpoint.Behaviors.Remove<ClientCredentials>();
this.ChannelFactory.Endpoint.Behaviors.Add(new CentralAuthCredentials(token));
return this.GenerateToken();
}
These methods are basically supposed to remove the default credentials from the endpoint and attach a new instance of my custom CentralAuthCredentials. (I grabbed this Remove/Add combo from an MSDN article somewhere).
In the configuration:
<behaviors>
<endpointBehaviors>
<behavior name="Server2ServerEndpointBehavior">
<clientCredentials type="MyApp.Security.CentralAuthCredentials, MyApp">
<clientCertificate findValue="localhost" x509FindType="FindBySubjectName" storeLocation="CurrentUser" storeName="My" />
<serviceCertificate>
<authentication certificateValidationMode="None" revocationMode="NoCheck" />
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
<bindings>
<wsHttpBinding>
<binding name="wsHttpServer2Server">
<security mode="Message">
<message clientCredentialType="UserName" />
</security>
</binding>
</wsHttpBinding>
</bindings>
Note that the behavior's clientCredentials type is set to my custom client credentials. However, at the moment I still have the binding's clientCredentialType set to "UserName". This brings up my 2nd question: What the heck should clientCredentialType="?" be set to if I'm using custom credentials? According to MSDN, the available values for Message security are: None, Windows, UserName, Certificate, and IssuedToken.
Any ideas? Hopefully I'm just missing something simple? There are like 6 more classes to the entire implementation, but I tried to only include the bits needed to understand the situation...
UPDATE #1:
I've been working on this all day, and thanks to a few sources, I realized that part of what I was missing was the very last step on this page, which is adding the TokenParameters to the binding, so that the binding knows what the token looks like. That is the answer to my original 1st question; "what the heck sets up the token requirements?" Answer: the TokenParameters assigned to the binding.
So now I added the following extension which sets the TokenParameters on the binding:
public sealed class CentralAuthTokenBindingExtension : BindingElementExtensionElement
{
public CentralAuthTokenBindingExtension()
: base()
{
}
public override Type BindingElementType
{
get { return typeof(SymmetricSecurityBindingElement); }
}
protected override System.ServiceModel.Channels.BindingElement CreateBindingElement()
{
X509SecurityTokenParameters protectionParams = new X509SecurityTokenParameters();
protectionParams.InclusionMode = SecurityTokenInclusionMode.Never;
SymmetricSecurityBindingElement innerBindingElement = new SymmetricSecurityBindingElement();
innerBindingElement.EndpointSupportingTokenParameters.SignedEncrypted.Add(new CentralAuthTokenParameters());
//innerBindingElement.MessageProtectionOrder = MessageProtectionOrder.SignBeforeEncrypt;
innerBindingElement.ProtectionTokenParameters = protectionParams;
return innerBindingElement;
}
}
<extensions>
<bindingElementExtensions>
<add name="CentralAuthCreds" type="MyApp.Security.Configuration.CentralAuthTokenBindingExtension, MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</bindingElementExtensions>
</extensions>
<bindings>
<customBinding>
<binding name="wsHttpServer2Server">
<CentralAuthCreds />
<binaryMessageEncoding />
<httpTransport />
</binding>
</customBinding>
</bindings>
Well that gets me a step further. Now I get a new exception on the server:
"The security token manager cannot create a token authenticator for requirement ..."
It looks like WCF is using some default token manager to try to deal with my custom token, instead of my custom token handler (my custom token handler's constructor is never called). I think this is happening because for the client, I have this config:
<endpointBehaviors>
<behavior name="Server2ServerEndpointBehavior">
<clientCredentials type="MyApp.Security.CentralAuthCredentials, MyApp">
But on the server I don't have any equivalent to let it know about the custom client credentials. So, new question: Where in the config for the server do I tell it what the custom ClientCredentials are?
Update #2:
Well, I finally figured out a bit more of the puzzle. I had only implemented a ClientCredentials implementation, thinking that the client sends creds, and thats it. The client doesnt authenticate the service, so I don't need custom ServiceCredentials. Well, I was wrong. The specified ServiceCredentials authenticates the token from the ClientCredentials, and vice-versa. So I just had to add a custom ServiceCredentials implementation that passed the same TokenSerializer and TokenAuthenticator classes.
On to the next issue: WCF is now ignoring my x509 certs specified in config that were working fine with UserName auth. I'm going to open a whole new question for this one!
I ran in to similar issues with an application I'm working on, unfortunately I gave up as I couldn't get the custom credentials working. I'm now using username/password (client credentials) and certificate (service credentials) with custom encrypted soap headers added to service calls to pass around additional info e.g UserID etc.