pátek 22. července 2011
Emulation of Azure needs registered ASP.NET 4
Here is a good blog which describes hot to diagnose this error:
http://dunnry.com/blog/2011/07/14/HowToDiagnoseWindowsAzureErrorAttachingDebuggerErrors.aspx
At the core the issue was:
Handler "PageHandlerFactory-Integrated" has a bad module "ManagedPipelineHandler" in its module list
I quickly found out what was the issue. Later to perform some testing I have uninstalled ASP.NET 4 extensions from my IIS and kept just the 3.5 versions (actually 2.0, because 3.5 were just framework extensions). So a quick solution aspnet_regiis.exe -i in 4 version folder will fix it.
úterý 19. července 2011
Provide JSONP with your WCF services (using .NET 3.5)
This example works except the case when you are returning a raw JSON, that is you are not returning object which is serialized in to JSON, but rather returning a Stream which represents this JSON.
The exception which you might obtain will be:
Encountered invalid root element name 'Binary'. 'root' is the only allowed root element name.
About JSONP
JSON with Padding is a transport format, which uses the ability of SCRIPT tag to execute scripts from different domains to overcome the cross-domain access issue. Generally the returned JSON is wrapped by JavaScript function which can be executed cross-domain.
So before we start - JSONP support is already added to .NET 4 so the services can be configured to use JSONP only by adding the CrossDomainScriptAccessEnabled attribute.
When the problem occurs
However I am stuck with NET 3.5 - so I needed to provide JSONP manually. Actually that is not that hard because MS provides this functionality in the WCF-WF example package (Downloadable here).The problem is, that this example is not complete. To be more specific: It works only when the service returns .NET objects which are serialized to JSON by WCF. However in some cases you might be serving the JSON which is already prepared. In this case your service returns a Stream. And in this case the example provided by MS will not work.
To understand the problem, we have to take a look at what exactly does the example of MS code. Well to start you can simply look at this blog.
So basically to enable JSONP you just need to add JSONPBehavior attribute to your service. In fact this behavior uses JSONPEncoderFactory class, which defines an encoder (JSONPEncoder) which converts the messages to JSONP. The encoding takes place in the override WriteMessage method. Let's take a look at the method provided in the MS example.
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) { MemoryStream stream = new MemoryStream(); StreamWriter sw = new StreamWriter(stream); string methodName = null; if (message.Properties.ContainsKey(JSONPMessageProperty.Name)) methodName = ((JSONPMessageProperty)(message.Properties[JSONPMessageProperty.Name])).MethodName; if (methodName != null) { sw.Write(methodName + "( "); sw.Flush(); } XmlWriter writer = JsonReaderWriterFactory.CreateJsonWriter(stream); message.WriteMessage(writer); writer.Flush(); if (methodName != null) { sw.Write(" );"); sw.Flush(); } byte[] messageBytes = stream.GetBuffer(); int messageLength = (int)stream.Position; int totalLength = messageLength + messageOffset; byte[] totalBytes = bufferManager.TakeBuffer(totalLength); Array.Copy(messageBytes, 0, totalBytes, messageOffset, messageLength); ArraySegment<byte> byteArray = new ArraySegment<byte>(totalBytes, messageOffset, messageLength); writer.Close(); return byteArray; }
So what is happening here: the Message object contains the object which returns your method. The WriteMessage method will take this object and write it to a Stream which is passed to it in argument. In the method the passed stream is a JsonWriter. The problem is that JsonWriter expects the structure of the message to be object represented by XML, which it will convert to JSON.
Now you can see that before we are actually writing the content of the message, we write "methodName(" and after ");". Generally this is the wrapping by JavaScript function. The result will be something like "methodName({JSONOBject});".
The resulted Stream is than just converted to byte array.
This works, but the problem is that when you are returning raw JSON, in other words, that your method returns Stream, than you cannot use JsonWriter, because the Message.WriteMessage will push to the writer XML of different structure, than it expects.
To be specific the XML will have a form of <binary>asdqwetasfd</Binary> and JsonWriter will not be able to create reasonable Json object.
Solution
The solution to the problem is following:- Check the format of the message (if it Json or Raw Stream)
- If it is a Raw Stream, than just convert the Stream to array of bytes
- If it is Json, than use the same procedure as before
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) { WebContentFormat messageFormat = this.GetMessageContentFormat(message); MemoryStream stream = new MemoryStream(); StreamWriter sw = new StreamWriter(stream); string methodName = null; if (message.Properties.ContainsKey(JSONPMessageProperty.Name)) methodName = ((JSONPMessageProperty)(message.Properties[JSONPMessageProperty.Name])).MethodName; if (methodName != null) { sw.Write(methodName + "( "); sw.Flush(); } XmlWriter writer = null; if (messageFormat == WebContentFormat.Json) { writer = JsonReaderWriterFactory.CreateJsonWriter(stream); message.WriteMessage(writer); writer.Flush(); //writer.Close(); } else if (messageFormat == WebContentFormat.Raw) { String messageBody = ReadRawBody(ref message); sw.Write(messageBody); sw.Flush(); } if (methodName != null) { sw.Write(" );"); sw.Flush(); } byte[] messageBytes = stream.GetBuffer(); int messageLength = (int)stream.Position; int totalLength = messageLength + messageOffset; byte[] totalBytes = bufferManager.TakeBuffer(totalLength); Array.Copy(messageBytes, 0, totalBytes, messageOffset, messageLength); ArraySegment<byte> byteArray = new ArraySegment<byte>(totalBytes, messageOffset, messageLength); stream.Close(); return byteArray; }
You can see that I am using two additional methods: GetMessageContentFormat and ReadRawBody. I did not came up with these methods, instead I have borrowed them from the blog of Carlos Figueira
In his blog, he describes how to use these methods when Inspecting messages. That is not the same scenario, but actually Inspecting outgoing methods or creating own MessageEncoder are just two ways to achieve the same thing.
I will add the definitions of the methods here, but the above mentioned blog post is a great source of information regarding customization of WCF Service, definitely worth checking.
private WebContentFormat GetMessageContentFormat(Message message) { WebContentFormat format = WebContentFormat.Default; if (message.Properties.ContainsKey(WebBodyFormatMessageProperty.Name)) { WebBodyFormatMessageProperty bodyFormat; bodyFormat = (WebBodyFormatMessageProperty)message.Properties[WebBodyFormatMessageProperty.Name]; format = bodyFormat.Format; } return format; } private String ReadRawBody(ref Message message) { XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents(); bodyReader.ReadStartElement("Binary"); byte[] bodyBytes = bodyReader.ReadContentAsBase64(); string messageBody = Encoding.UTF8.GetString(bodyBytes); // Now to recreate the message MemoryStream ms = new MemoryStream(); XmlDictionaryWriter writer = XmlDictionaryWriter.CreateBinaryWriter(ms); writer.WriteStartElement("Binary"); writer.WriteBase64(bodyBytes, 0, bodyBytes.Length); writer.WriteEndElement(); writer.Flush(); ms.Position = 0; XmlDictionaryReader reader = XmlDictionaryReader.CreateBinaryReader(ms, XmlDictionaryReaderQuotas.Max); Message newMessage = Message.CreateMessage(reader, int.MaxValue, message.Version); newMessage.Properties.CopyProperties(message.Properties); message = newMessage; //return bodyBytes; return messageBody; }
pátek 8. července 2011
Consuming WCF Services with Java Client
The prerequisites for this post are some basic knowledge of WCF (bindings, services, endpoints) and some knowledge of Java (I am using Axis to generate the clients...for the first time).
To make it a bit more complicated: I was using FormsAuthentication on the backend side, since these services are hosted by IIS 7.
Here I want to describe how to configure WCF services to be consumed by JAVA clients.
The second part which describes how to keep using Forms Authentication is described in my other post
To expose the services for JAVA client, we have two options:
- Expose the services using SOAP protocol
- Expose the services using REST approach
Here I will describe in details how to expose the services using SOAP protocol and in the end I will give a brief description of what to do to expose these services using REST approach.
Changing WCF configuration
The first step is to start changing WCF configuration which is presented in "web.config" file (at least in the case of service hosted in IIS).If you let Visual Studio configure your service, you will see that it creates for each services it's own binding - event though the services can share binding configuration.
Also - if you consume service by Silverlight client, than VS chooses to define binnaryMessageEncoding as a transport format. Because both - the backend and the client are .NET applications, WCF can be configured to transfer the objects over the wires in binary format (because both the client and the server know how to serialize/deserialize) the data in this format. To consume the service by a Java application you will need to use a traditional basicHttpBinding - that is simple binding which uses standard WSDL specification in hand with XML serialization.
So first step is to locate your binding and service definition and change the binding to basicHttpBinding.
<binding name="BinaryOverHTTPBinding"> <binaryMessageEncoding /> <httpTransport /> </binding> <service name="Octo.Bank.Web.WCFServices.WCFUserService" behaviorConfiguration="NeutralBehavior"> <endpoint address="" binding="BinaryOverHTTPBinding" contract="MyProject.WCFUserService"/> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service>Replace the binding configuration in the endpoint definition.
<endpoint address="" binding="basicHttpBinding" contract="MyProject.WCFUserService"/>
Just to completed the image here, the service is configured to user "NeutralBehavior".
<behavior name="NeutralBehavior"> <serviceMetadata httpGetEnabled="true"/> <serviceDebug includeExceptionDetailInFaults="false"/> </behavior>
What is important is, that the httpGetEnabled set to true in the combination with the mex endpoint will ensure that the WSDL definition of this service will be exposed (the url of the WSDL definition will be simply http://server/myService?wsdl).
Now that is the bare minimum to be able to connect to this WCFUserService with Java client.
Defining the namespaces and ports
While WCF Client, or Silverlight Client do not have a problem to generate a stub client for the defined service, when you will try to generate the client in java, you will obtain an exception saying that one of the port bindings was not properly defined. The cause is that you need to define different namespace and name in your ServiceContract and ServiceBehavior. These are two attributes which can be placed on top of your service class.
[ServiceContract(Namespace = "octo.users.service",Name="UserService")] [ServiceBehavior(Namespace = "octo.users.port", Name = "UserPort")] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class WCFUserService { }This completely changes the resulting WSDL file, which is describing the service. This is really important because if you do not make these changes, you will not be able to generate the client with Axis framework.
Creating the Java client
I am using Eclipse in combination with Axis framework to talk to my services. But first let's put out the 2 options that we have to access Web Services.
- Creating dynamically the client - This option is more complicated because we have to now a bit how the service is defined in the WSDL file but allows us to perform some more changes on the SOAP messages that we want to send (for example changing the SOAP headers).
- Using Axis to generate the client for us - This is much easier, however it gives us only a limited ability to process the received SOAP messages.
Accessing Web Service using Axis created client
Before we start, we need to generated the client, either you can use the build in tool in Eclipse ("New -> Other -> Web Service Client") or you can use the commander line "WSDLtoJava" utility. In both cases you have to enter just the URL of the WSDL.
When the client is ready, you can see that there is quite a lot of code(10kLines) generated for you.MyServiceLocator locator = new MyServiceLocator(); AuthService client = locator.getBasicHttpBinding_AuthService(); String cookie = client.LoginCookie("login","password");
I am calling the method defined before which gives me the authentication cookie. Remember that this "Authentication Service" stays open, so anybody can call the methods. Now when we have the cookie, we can use it to make calls to other already protected services.
MyServiceLocator locator = new MyServiceLocator(); WCFUserService client = locator.getBasicHttpBinding_WCFUserService(); ((Stub)client)._setProperty(Call.SESSION_MAINTAIN_PROPERTY,new Boolean(true)); ((Stub)client)._setProperty(HTTPConstants.HEADER_COOKIE, ".ASPXAUTH=" + cookie); Object data = client.GetSecuredData(myParam);
The generated client does not allow you to add cookies, but you can convert the client to org.appache.axis.client.Stub which allows you to call _setProperty method a static HttpConstatns class provides the names of the headers which you can set.
Now notice the "ASPXAUTH=" that is the prefix(or in other words the name) of the cookie and it has to be there. It took me a while to find out in what exact form should I send the cookie, finally Fiddler came as help - I used the Silverlight client to see what exactly he is sending and I just did the same.Creating the client dynamically
The java.rmi namespace provides classes allowing the creation of web service client on the fly (without code generation). This has some advantages, specially that you can create a javax.rmi.xml.Service class permitting you to assign special handlers, which are executed during the "reception" and "sending" of SOAP messages. These handlers can allow you to alter the content of the message and thus provide possibility to do some additional tuning, or security checks.
When working with WCF or CXF framework, you have probably heard of Interceptors, which are equivalent to "Handlers".
Personally I thought, that I will be able to create my own handler to recuperate the authentication cookie send the standard way. But I did not manage to get the cookie from the SOAP message. I will provide here a conception of my solution - maybe someone will be able to finalize and obtain the cookie from the response of the authentication service.try { QName serviceName = new QName("http://mynamespace","AuthService"); URL wsdlLocation = new URL("http://localhost:49830/WCFServices/WCFUserService.svc?wsdl"); // Service ServiceFactory factory = ServiceFactory.newInstance(); Service service = factory.createService(wsdlLocation,serviceName); //Add the handler to the handler chain HandlerRegistry hr = service.getHandlerRegistry(); HandlerInfo hi = new HandlerInfo(); hi.setHandlerClass(SimpleHandler.class); handlerChain.add(hi); QName portName = new QName("http://localhost:49830/WCFServices/WCFUserService.svc?wsdl", "BasicHttpBinding_AuthService"); List handlerChain = hr.getHandlerChain(portName); QName operationName = new QName("http://localhost:49830/WCFServices/WCFUserService.svc?wsdl", "Login"); Call call = service.createCall(portName,operationName); //call the operation Object resp = call.invoke(new java.lang.Object[] {"login","pass"}); }To be able to call the web service dynamically, you will need to specify the name of the service, the port and the operations. You can find these easily in the WSDL definition file.
Here follows the definition of the SimpleHandler class which is added to the HTTP handler chain
public class SimpleHandler extends GenericHandler { HandlerInfo hi; public void init(HandlerInfo info) { hi = info; } public QName[] getHeaders() { return hi.getHeaders(); } public boolean handleResponse(MessageContext context) { try { //Iterate over all properties - did not find the cookie there :( Iterator properties = context.getPropertyNames(); while(properties.hasNext()){ Object property = properties.next(); System.out.println(property.toString()); } //examine the response header - did not find the cookie there either :( if(context.containsProperty("response")){ Object response = context.getProperty("response"); HttpResponse httpResponse = (HttpResponse)response; Header[] headers = httpResponse.getAllHeaders(); for(Header header:headers){ System.out.println(header.toString()); } } //here is how to get the SOAP headers - they do not serve - we need pure HTTP response // get the soap header SOAPMessageContext smc = (SOAPMessageContext) context; SOAPMessage message = smc.getMessage(); } catch (Exception e) { throw new JAXRPCException(e); } return true; } public boolean handleRequest(MessageContext context) { return true; } }
Securing services by SSL
In my other post I have described how to secure the web services by SSL, you can find the information which describes how to configure the JAVA client to connect to these secured services.
REST approach
To expose the service as RESTfull we will have to define another endpoint for the service.
<service behaviorConfiguration="NeutralBehavior" name="MyServic"> <endpoint address="json" binding="webHttpBinding" behaviorConfiguration="jsonBehavior" contract="Octo.Bank.Web.WCFServices.WCFAccountService" name="JsonEndpoint"/> <endpoint address="soap" binding="basicHttpBinding" .../> <endpoint address="mex" .../> </service>Notice that this endpoint uses webHttpBinding and a special behavior called jsonBehavior. This behavior as it's name says just defines JSON as the transport format.
<endpointBehaviors> <behavior name="jsonBehavior"> <webHttp defaultOutgoingResponseFormat="Json"/> </behavior> </endpointBehaviors>This is enough for the configuraiton. Now just some minor changes to the Service itself.
At the end I showed guidlines for exposing WCF services using the REST approach.
public class MyService { [OperationContract] [WebGet(UriTemplate="/accounts?id={id}", BodyStyle=WebMessageBodyStyle.Wrapped)] public IList<AccountDto> GetAccountsByCustomer(int id) { return AccountService.GetCustomerAccounts(id); } }It is the WebGet attribute which exposes the service for HTTP GET request. The UriTemplate defines which URL will invoke the service. Notice that the parameter of the service is extracted from the URL itself.
If we would have a method which posts data, it would be decorated with [WebInvoke] attribute.
This is just a slight intro, you can find more information on internet, here I wanted just to provide some basic information to make this post complete enough.
Summary
I have shown how to change the configuration to publish WCF Service using SOAP protocol and consume this services with JAVA client. At the end I just showed how to expose the service using REST approach.ASP.NET Forms Authentication and Java client
The prerequisites for this post are some basic knowledge of WCF (bindings, services, endpoints) and some knowledge of Java (I am using Axis to generate the clients...for the first time).
To make it a bit more complicated: I was using FormsAuthentication on the backend side, since these services are hosted by IIS 7.
Here I want to show what to do use Forms Authentication from Java application, mobile client or any other non-browser client.
The second part which describes how to enable WCF services to be consumed by JAVA client is described in my other post.
IIS 7 buid-in Authentication Service
I was using the build-in authentication service in order to authenticate the client, which is just a basic service, which offers methods such as Login, Logout etc.This service can be enabled on IIS server using the following configuration:
<system.web.extensions> <scripting> <webServices> <authenticationService enabled="true" requireSSL="false"/> </webServices> </scripting> </system.web.extensions>And we also need to expose this service:
<service behaviorConfiguration="NeutralBehavior" name="System.Web.ApplicationServices.AuthenticationService"> <endpoint address="" binding="basicHttpBinding" contract="System.Web.ApplicationServices.AuthenticationService" /> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> </service>Now that service works great from Silverlight client but I was not able to generate Java client for this service - I tried with different versions of Axis and settings - but it did not work for me.
So for the non-Silverlight client I needed to write my own Authentication service. That is actually prety easy using the FormsAuthentication static class.
[OperationContract] public Login(String login, String password) { //your way to auth the user againts DB or whatever var user = UserService.AuthenticateUser(login, password); if (user != null) { FormsAuthentication.SetAuthCookie(login, true); } return null; }After you check if the user is connected, you can just call the SetAuthCookie method. This method adds authentication token to the response which will go to server. Then the browser adds this token to any request which he will send to the server.
And here comes the problem: how to use this with non-browser based application?
Let me continue.
Services secured using PrincipalPermission
I use FormsAuthentication, because it allows me to secure all service just by adding the PrincipalPermission attribute over each Service method. So my WCFUserService can look like this:public class WCFTagService { public WCFTagService() { Thread.CurrentPrincipal = HttpContext.Current.User; } [OperationContract] [PrincipalPermission(SecurityAction.Demand, Authenticated = true)] public Object GetSecuredData(int param) { return MyDB.GetData(); } }In the constructor the CurrentPrincipal is set to the current user of the ASP.NET application (again we are hosting this service in IIS), than the [PrincipalPermission] attribute will be check even before the method is executed if the user is logged in.
And how is the HttpContext.Current.User determined?
Well simply by checking the authentication token which the browser adds to the request. IIS will automatically check this token and populated the User static class with the correct identity.
Adding one more authentication service for Java Clients
This is definitely not correct but it is the only way I was able to get it to work. Basically when I callFormsAuthentication.SetAuthCookie(login, true);the cookie is added to the response and I will have to get it on the client (Java) side. Actually I was not able to achieve that - I will describe the approach I took lower, but I just did not get the cookie from the response. So I decided to build one more service which will just return the authentication token (or cookie if you will).
[OperationContract] public String LoginCookie(String login, String password) { var user = UserService.AuthenticateUser(login, password); if (user != null) { var cookie = FormsAuthentication.GetAuthCookie(login, true); return cookie.Value; } return null; }Ok that's it, we are done. We can almost switch to JAVA.
Accessing Authentication Service using the Axis generated client
Before we start, we need to generated the client, either you can use the build in tool in Eclipse ("New -> Other -> Web Service Client") or you can use the commander line "WSDLtoJava" utility. In both cases you have to enter just the URL of the WSDL.When the client is ready, you can see that there is quite a lot of code(10kLines) generated for you.
MyServiceLocator locator = new MyServiceLocator(); AuthService client = locator.getBasicHttpBinding_AuthService(); String cookie = client.LoginCookie("login","password");That is quite simple, I am calling the method which I have defined before which gives me the authentication cookie. Remember that this "Authentication Service" stays open, so anybody can call the methods. Now when we have the cookie, we can use it to make calls to other already protected services.
MyServiceLocator locator = new MyServiceLocator(); WCFUserService client = locator.getBasicHttpBinding_WCFUserService(); ((Stub)client)._setProperty(Call.SESSION_MAINTAIN_PROPERTY,new Boolean(true)); ((Stub)client)._setProperty(HTTPConstants.HEADER_COOKIE, ".ASPXAUTH=" + cookie); Object data = client.GetSecuredData(myParam);The generated client does not allow you to add cookies, but you can convert the client to org.appache.axis.client.Stub which allows you to call _setProperty method a static HttpConstatns class provides the names of the headers which you can set.
Now notice the "ASPXAUTH=" that is the prefix(or in other words the name) of the cookie and it has to be there. It took me a while to find out in what exact form should I send the cookie, finally Fiddler came as help - I used the Silverlight client to see what exactly he is sending and I just did the same.
What is little bit said is the fact, that we have to create a special method to be called by the Java client which returns the authentication token directly and no as a cookie.
I was thinking - it could not be that hard, generate a client and get the cookie. This way I could have only one authentication method used by browser-based clients and Java clients. But I just did not managed to do that.
I will show an attempt which I did - but did not succeed.
Creating the client dynamically
The java.rmi namespace provides classes which will the creation of web service client on the fly (without generation). This has some advantages, specially that you can create a javax.rmi.xml.Service class which allows assignment of special handlers, which are executed during the "reception" and "sending" of SOAP messages, these handlers can allow you to alter the content of the message and thus provide possibility to do some additional tuning.Personally I thought, that I will be able to create my own handler to recuperate the authentication cookie send the standard way. But I did not manage to get the cookie from the SOAP message. Well that is actually normal, because the Cookie is not part of the SOAP message but instead part of the HTTP message (which wraps the SOAP message). But that is the problem I was not able to locate the cookie in the HTTP Response message, anyone knows how to do that?
I will provide here a conception of my solution - maybe someone will be able to finalize and obtain the cookie from the response of the authentication service.
try { QName serviceName = new QName("http://mynamespace","AuthService"); URL wsdlLocation = new URL("http://localhost:49830/WCFServices/WCFUserService.svc?wsdl"); // Service ServiceFactory factory = ServiceFactory.newInstance(); Service service = factory.createService(wsdlLocation,serviceName); //Add the handler to the handler chain HandlerRegistry hr = service.getHandlerRegistry(); HandlerInfo hi = new HandlerInfo(); hi.setHandlerClass(SimpleHandler.class); handlerChain.add(hi); QName portName = new QName("http://localhost:49830/WCFServices/WCFUserService.svc?wsdl", "BasicHttpBinding_AuthService"); List handlerChain = hr.getHandlerChain(portName); QName operationName = new QName("http://localhost:49830/WCFServices/WCFUserService.svc?wsdl", "Login"); Call call = service.createCall(portName,operationName); //call the operation Object resp = call.invoke(new java.lang.Object[] {"login","pass"}); }To be able to call the web service dynamically, you will need to specify the names of the service, the port and the operations, you can find these easily in the WSDL definition.
Here follows the definition of the SimpleHandler which is added to the handler chain
public class SimpleHandler extends GenericHandler { HandlerInfo hi; public void init(HandlerInfo info) { hi = info; } public QName[] getHeaders() { return hi.getHeaders(); } public boolean handleResponse(MessageContext context) { try { //Iterate over all properties - did not find the cookie there :( Iterator properties = context.getPropertyNames(); while(properties.hasNext()){ Object property = properties.next(); System.out.println(property.toString()); } //examine the response header - did not find the cookie there either :( if(context.containsProperty("response")){ Object response = context.getProperty("response"); HttpResponse httpResponse = (HttpResponse)response; Header[] headers = httpResponse.getAllHeaders(); for(Header header:headers){ System.out.println(header.toString()); } } //here is how to get the SOAP headers - they do not serve - we need pure HTTP response // get the soap header SOAPMessageContext smc = (SOAPMessageContext) context; SOAPMessage message = smc.getMessage(); } catch (Exception e) { throw new JAXRPCException(e); } return true; } public boolean handleRequest(MessageContext context) { return true; } }
Alternative approach using WCF Inspectors
When looking into this problem, I found one alternative approach that you can use when dealing with Security and WCF Service.The solution is basic:
- Give up on FormsAuthentication
- Define your own authentication tickets or just pass the login/pass combination on each request in the HTTP Header
- Define a Message InInspector on the Server which would read the message upon its reception and check the availability of the authentication token or the credentials in the message header
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, login, DateTime.Now, DateTime.Now.AddMinutes(30), false, login); string encryptedTicket = FormsAuthentication.Encrypt(ticket); HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);So you can create an Inspector class, which will do the reverse of this process:
public class TestInspector : IDispatchMessageInspector { public TestInspector() { } public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { var httpRequest = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name]; var cookie = httpRequest.Headers[HttpRequestHeader.Authorization]; if(cookie == null) { throw new SecurityException("Not authenticated!"); } var ticket = FormsAuthentication.Decrypt(cookie); if(ticket.IsExpired) { throw new SecurityException("Ticket expired"); } } public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { } }
Securing the servicing using SSL
When we pass the authentication token over the wire, we want to be sure, that no-one can intercept this token and act in name of the user against the services. To prevent this situation we can use SSL to secure the whole communication between client and server.The WCF configuration which is needed is quite simple, we just have to alter the standard basicHttpBinding by adding the security mode.
<basicHttpBinding> <binding name="SecurityByTransport"> <security mode="Transport"> <transport clientCredentialType="None"/> </security> </binding> </basicHttpBinding>
Than comes the infrastructure work:
- Be sure to publish the service on your local IIS server (you cannot use the build-in Visual Studio Server
- On the IIS server create a new certificate - for test purposes auto-signed
- Configure a new binding to application that you have deployed using the certificate, that you have created
java client unable to find valid certification path to requested target
That is because JVM maintains its list of trusted server. If he sees that the certificate is signed by Certification Authority, he will add it to its "keystore". Because for testing you usually use Self-Signed certificate, JVM will not add it do the keystore, it has to be done manually.
So: go back to the IIS 7 configuration and in the list of the certificates, select the certificate and on the "Details" tab page choose: "Copy to File".
You can leave the predefined option and just save the ".CER" wherever you want to.
Now to finish you have to run the following command in the JAVA-HOME\BIN directory:
keytool.exe -import -alias localhost -file C:\myCert.cer -keystore "c:\Program Files\Java\jre6\lib\security\cacerts"
- localhost - stands for the web server which holds the certificate (your local IIS).
- cacerts directory - is the store of trusted certificates.
- The password is "changeit".
Summary
I tried to connect to secured WCF services hosted on IIS server with Java client. During the process I found some issues, but at the end I was able to connect securely to the services. The main steps are:- Don't use IIS build-in Authentication Service
- Provide a service which will return the Authnentication Cookie to the Java client
- Pass this cookie along with any request which is sent to secured services
And at last I presented an approach which should be taken to replace FormsAuthentication with your own authentication scheme using WCF Message Inspectors.