To use custom user name/password style credentials with an IIS hosted Http service the following config is used:
<wsHttpBinding>
<binding name="UserNameHttpBindingConfig"> <security mode="TransportWithMessageCredential"> <message clientCredentialType="UserName" /> </security> </binding>
</wsHttpBinding>
NOTE: The way the credentials are passed to the service are distinctly different to Http Basic credentials - this is not Basic Auth. What is actually happening is the credentials are passed as part of the SOAP envelope.
<?xml version="1.0" encoding="utf-8" ?> <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> <s:Header> <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT</a:Action> <a:MessageID>urn:uuid:1a54b138-c3aa-46a5-b80c-1581aa6432b8</a:MessageID> <a:ReplyTo> <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> </a:ReplyTo> <a:To s:mustUnderstand="1">https://localhost/WcfUsernameOverWsHttpWithTransport/Service1.svc</a:To> <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> <u:Timestamp u:Id="_0"> <u:Created>2013-05-14T21:17:03.746Z</u:Created> <u:Expires>2013-05-14T21:22:03.746Z</u:Expires> </u:Timestamp> <o:UsernameToken u:Id="uuid-8f7509e1-b84b-4291-adb5-23ae3f83d2cd-1"> <o:Username> <!-- Removed--> </o:Username> <o:Password> <!-- Removed--> </o:Password> </o:UsernameToken> </o:Security> </s:Header> <s:Body> <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"> <t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType> <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType> <t:Entropy> <!-- Removed--> </t:Entropy> <t:KeySize>256</t:KeySize> </t:RequestSecurityToken> </s:Body> </s:Envelope>
You'll get this error if you are using a self-signed cert:
SecurityNegotiationException
To fix it you'll need to add an event handler to change the default service certificate validation on the client. THIS IS DEFINITELY NOT RECOMMENDED IN PRODUCTION CODE! It basically tells the client to ignore all certificate problems. But when using self-signed certs in dev, it is useful.
// Ignore any certificate errors - results from using self-signed certs. System.Net.ServicePointManager.ServerCertificateValidationCallback += (se, cert, chain, sslerror) => true;
You will get a strange exception when authentication fails:
MessageSecurityException
This is the expected response you'll get when the password is incorrect and any Exception is thrown from the custom validator (including Fault Exceptions). This is very misleading, because if you search for this, you'll get a great deal of solutions talking about differences in service and client clocks.
Service Contract:
namespace WcfUsernameOverWsHttpWithTransport { [ServiceContract] public interface IService1 { [OperationContract] bool DoSomething(string param); } }
Service Implementation
<%@ ServiceHost Language="C#" Debug="true" Service="WcfUsernameOverWsHttpWithTransport.Service1" CodeBehind="Service1.svc.cs" %>
namespace WcfUsernameOverWsHttpWithTransport { public class Service1 : IService1 { public bool DoSomething(string param) { return param == "Foo bar"; } } }
Service web.config:
Note: targeting .Net 4.0 or 4.5 does not impact this example, either works fine.
<?xml version="1.0"?> <configuration> <system.web> <compilation debug="true" targetFramework="4.0"/> <httpRuntime/> </system.web> <system.serviceModel> <bindings> <wsHttpBinding> <binding name="HttpBindingConfig"> <security mode="TransportWithMessageCredential"> <message clientCredentialType="UserName"/> </security> </binding> </wsHttpBinding> </bindings> <services> <service name="WcfUsernameOverWsHttpWithTransport.Service1"> <endpoint address="" binding="wsHttpBinding" contract="WcfUsernameOverWsHttpWithTransport.IService1" bindingConfiguration="HttpBindingConfig"/> </service> </services> <behaviors> <serviceBehaviors> <behavior> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/> <serviceDebug includeExceptionDetailInFaults="true"/> <serviceCredentials> <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="WcfUsernameOverWsHttpWithTransport.UserNameValidator,WcfUsernameOverWsHttpWithTransport"/> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/> </system.serviceModel> <system.webServer> <modules runAllManagedModulesForAllRequests="true"/> <directoryBrowse enabled="true"/> </system.webServer> </configuration>
Client App.Config
<?xml version="1.0"?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> </startup> <system.serviceModel> <bindings> <wsHttpBinding> <binding name="WSHttpBinding_IService1"> <security mode="TransportWithMessageCredential"> <transport clientCredentialType="None" /> <message clientCredentialType="UserName" /> </security> </binding> </wsHttpBinding> </bindings> <client> <endpoint address="https://localhost/WcfUsernameOverWsHttpWithTransport/Service1.svc" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1" contract="ServiceReference1.IService1" name="WSHttpBinding_IService1" /> </client> </system.serviceModel> </configuration>
Client Proxy Code:
namespace ClientConsole { class Program { static void Main(string[] args) { Thread.Sleep(1000); // Ignore any certificate errors - results from using self-signed certs. System.Net.ServicePointManager.ServerCertificateValidationCallback += (se, cert, chain, sslerror) => true; var ep = new EndpointAddress(new Uri("https://localhost/WcfUsernameOverWsHttpWithTransport/Service1.svc")); var binding = new WSHttpBinding(SecurityMode.TransportWithMessageCredential) { SendTimeout = TimeSpan.FromSeconds(70), BypassProxyOnLocal = false }; binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName; var proxy = new ServiceReference1.Service1Client(binding, ep); proxy.ClientCredentials.UserName.UserName = "test"; proxy.ClientCredentials.UserName.Password = "pass99"; // pass99 object response = string.Empty; try { response = proxy.DoSomething("Foo bar"); Console.WriteLine("Authenticated."); } catch (MessageSecurityException) { Console.WriteLine("Your password is wrong."); } Console.WriteLine(response.ToString()); } } }
References
- MSDN: How to: Use a Custom User Name and Password Validator
- Code Project: WCF Service over HTTPS with custom username and password validator in IIS
No comments:
Post a Comment