As I’ve said in previous posts, I’m a big fan of WCF in general – recently I have had the opportunity to work with a less popular, but extremely powerful feature: the WCF Routing Service (https://msdn.microsoft.com/en-us/library/ee517423(v=vs.110).aspx)
The MSDN documentation is confusing. The existing blog articles are confusing or broken. Examples of hosting it in IIS using simplified configuration are pretty much missing…
The routing service allows you to create proxy service endpoints. In my case, I needed to expose some internal web services to the internet through a VPN connection in Azure. Also, I needed to implement security on these publicly exposed endpoints.
The internal web services are hosted by a non-microsoft service bus, use HTTP with no authentication.
The external endpoints need to run over HTTPS (SSL) and require authentication.
Yes, you can do this with the Routing Service. No, it does not require a bunch of code.
The routing service is capable of using xpath matching on the message header or message itself to determine the endpoint routing. You could use the SOAP Action for example. Or… a much simpler approach is to use URL-pattern based routing.
In fact, you can do 95% of it in the web.config only. The only place you will need code is to apply authorization (what users can access the proxy), but it is very simple.
Suppose you have three SOAP endpoints you would like to proxy.
http://mailserver/sendservice
http://customermanagement/customerretrieval
http://customermanagement/customerupdate
Suppose you want to serve these up through you service as
https://proxyservice.example.org/sendmail
https://proxyservice.example.org/getcustomer
https://proxyservice.example.org/updatecustomer
You want Basic Authentication over SSL, using Windows for credentials. Finally, you want a means of limiting which authenticated users can access the proxy.
So, step by step.
1. Create a WCF Project
1.1 Setup IIS to host your project
Create a site in IIS pointing to the folder containing Service1.svc. Disable anonymous auth, enable basic auth, set it up for SSL.
(I recommend using a real ssl cert with a hosts entry pointing back at localhost. Easier than getting self signed certs working…)
2. Delete the IService1.cs; expand the Service1.svc and delete the code behind (Service1.svc.cs).
3. Rt click the Service1.svc and edit markup. Replace it with the following:
<%@ ServiceHost Language="C#" Debug="true" Service="System.ServiceModel.Routing.RoutingService,System.ServiceModel.Routing, version=4.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35" %>
This will cause your Service1.svc to invoke the routing service. No code required (yet) – you do the rest in the web.config.
4. Update your web.config with the following.
<?xml version="1.0"?> <configuration> <configSections> </configSections> <system.web> <compilation debug="true" targetFramework="4.5"/> </system.web> <system.serviceModel> <client> <!--Define named internal (private-side) endpoints here. Defined like any client endpoint except the contract doesn't need to be specified. Define binding as needed --> <endpoint name="ep_sendmail" address="http://mailserver/sendservice" binding="basicHttpBinding" contract="*"/> <endpoint name="ep_getcustomer" address="http://customermanagement/customerretrieval" binding="basicHttpBinding" contract="*"/> <endpoint name="ep_updatecustomer" address="http://customermanagement/customerupdate" binding="basicHttpBinding" contract="*"/> </client> <routing> <filters> <!--Define named filters that will be applied to requests coming into the (public side of) the router. (hostname is ignored - anything can be used - protocol and path/query strings must match identically)--> <filter name="f_sendmail" filterType="EndpointAddress" filterData="https://host/Service1.svc/SendMail"/> <filter name="f_getcustomer" filterType="EndpointAddress" filterData="https://host/Service1.svc/GetCustomer"/> <filter name="f_updatecustomer" filterType="EndpointAddress" filterData="https://host/Service1.svc/UpdateCustomer"/> </filters> <filterTables> <filterTable name="filterTable1"> <!--Define the mapping between the filter match and endpoint. I'm using a 1:1:1 mapping--> <add filterName="f_sendmail" endpointName="ep_sendmail"/> <add filterName="f_getcustomer" endpointName="ep_getcustomer"/> <add filterName="f_updatecustomer" endpointName="ep_updatecustomer"/> </filterTable> </filterTables> </routing> <services> <service behaviorConfiguration="RoutingBehavior" name="System.ServiceModel.Routing.RoutingService"> <!--Server endpoint must be defined with the appropriate contract (Most likely IRequestReplyRouter) and associated with its own binding--> <endpoint address="" binding="basicHttpBinding" bindingConfiguration="externalBinding" name="RouterEndpoint1" contract="System.ServiceModel.Routing.IRequestReplyRouter"/> </service> </services> <bindings> <basicHttpBinding> <!--For this example, the default or internal binding are just using the defaults--> <binding name="internalBinding"/> <!--Binding configuration for the router's "endpoint" - configures it to expect Basic Authentication over SSL.--> <binding name="externalBinding"> <security mode="Transport"> <transport clientCredentialType="Basic"/> </security> </binding> </basicHttpBinding> </bindings> <behaviors> <serviceBehaviors> <behavior> <!--These behaviors are likely unused--> <serviceMetadata httpGetEnabled="true"/> <serviceDebug includeExceptionDetailInFaults="true"/> </behavior> <behavior name="RoutingBehavior"> <!--The behaviors applied specifically to the routing service - must specify the filtertable name --> <routing routeOnHeadersOnly="true" filterTableName="filterTable1"/> <serviceDebug includeExceptionDetailInFaults="true"/> <!--The router's provided metadata is pretty much useless - it is the IRequestReply contract--> <serviceMetadata httpGetEnabled="false"/> <!--This tells the router to use Windows for establishing identities, and to use our custom class for determining permission--> <serviceAuthorization principalPermissionMode="UseWindowsGroups" serviceAuthorizationManagerType="ExampleNamespace.ExampleAuthorizationManager, ExampleAssemblyName"/> </behavior> </serviceBehaviors> </behaviors> <!--Very important - there is a bug of sorts in the router when using basicHttpBinding with asp compatibility. Just disable it.--> <serviceHostingEnvironment aspNetCompatibilityEnabled="false" multipleSiteBindingsEnabled="true"/> </system.serviceModel> </configuration>
If you comment out the serviceAuthorization tag, your project should build and run – so long as you have ssl and basic auth working…
You should update the “ExampleNamespace.ExampleAuthorizationManager, ExampleAssemblyName” with the Namespace, class name and assembly name that you actually use
(often the namespace and assembly name are the same)
If you simply want a router – with no auth and no ssl remove the enternalBinding’s security node AND the serviceAuthorization node
5. Implement the ExampleAuthorizationManager
namespace ExampleNamespace { public class ExampleAuthorizationManager : ServiceAuthorizationManager { protected override bool CheckAccessCore(OperationContext operationContext) { //check that the user is allowed and return true to allow, false to deny return true; //return base.CheckAccessCore(operationContext); } } }
6. Update clients
Since there is no WSDL, you will need build your clients against the internal endpoints’ WSDLs, then update them to use the external endpoint. Additionally you will need to update the binding configuration to use Transport security with Basic Authentication (unless you have turned this off).
—
You now have a secure proxy for WCF services without having to reimplement the services in WCF – adding new services requires 3 lines of configuration. I’m not sure if Microsoft could have made this any easier… except perhaps by documenting it better 😛