In our current project we did not incorporate any audit trail for user tracking till very recently. For the same we decided to use Log4Net to log the credentials - user name and IP address - of any user accessing any application developed by our team. Log4Net has a set of recognized pattern layouts that evaluate to information regarding the application events in which the logging is implemented.
For example:
%date - Outputs the date using the local time zone information.
Now, specific to our logging requirements, we were looking into the following three properties:
%identity - This is the user name of the current user using the Principal.Identity.Name method.
%username - This outputs the value of the WindowsIdentity property.
%aspnet-request{key} - This outputs request parameters in the case of an ASP.Net request
There are two different windows users here - the first one is the application user and the second is the user (or Windows account) under which the ASP.NET application (or the application pool from the perspective of IIS) is running. WindowsIdentity will typically return the second reference.
As a result %username
always logs the name of the account under which the app pool containing our application was running.
%identity
logs the actual user making the request to the hosted application.
To log the client IP address we used %aspnet-request{REMOTE_ADDR}
.
And we were set. Everything worked as expected. A few weeks went by - uneventful. Then one morning we made a strange observation. None of our hosted WCF services were getting the %identity
or %aspnet-request{REMOTE_ADDR}
. The first was blank and the second came up "NOT AVAILABLE".
Now, I noticed that Anonymous Authentication for the WCF service was enabled in IIS along with Windows Authentication. It's important to note that whenever Anonymous Authentication is enabled for an application, the browser does not send any credentials or user info with the request to the application. As a result %identity
was empty.
On unchecking Anonymous Authentication in IIS I received the following error:
The authentication schemes configured on the host ('IntegratedWindowsAuthentication') do not allow those configured on the binding 'BasicHttpBinding' ('Anonymous').
In .Net 4.0+, Simplified WCF configuration uses the 'anonymous' configurations when configurations are not explicitly set on a per-services basis in the <services>
section. So removing Anonymous Authentication results in the above error.
I realised I had to add a named binding under the <basichttpbinding>
(within the <bindings>
section in Web.config) as such:
<binding name="XYZServiceBinding">
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Windows" />
</security>
</binding>
Next, I had to create a <serviceBehaviours>
tag for the service under <behaviours>
:
<serviceBehaviors>
<behavior name="XYZServiceBehavior">
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="false" httpsGetUrl="" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
Now, I created a <service>
tag under the <services>
tag like:
<service name="XYZService" behaviorConfiguration="XYZServiceBehavior">
<endpoint address="" binding="basicHttpBinding" bindingConfiguration="XYZServiceBinding" contract="IXYZService" />
Had the <security mode>
in the binding been set to "Transport" we would have received the following error:
Could not find a base address that matches scheme https for the endpoint with binding CustomBinding. Registered base address schemes are [http].
This is because "Transport" can only be used for https endpoints. WCF does not permit sending of the clear password over unsecured transport.
In such cases, using of HTTP and basic clear text can be done if following the TransportCredentialOnly security mode.
Once all the above steps were completed, we tested the service and checked the logs. The %identity
property now had the requesting user's ID. However, the client IP address (%aspnet-request{REMOTE_ADDR}
) was still showing "NOT AVAILABLE".
I guessed that Log4Net tries to get all %aspnet-request
values from the current HTTPContext. Somehow, the HTTPContext was not available to us. On reading up a little, I realised our folly. A WCF service by default does not have an HTTPContext. From MSDN:
The ASP.NET HTTP runtime handles ASP.NET requests but does not participate in the processing of requests destined for WCF services, even though these services are hosted in the same AppDomain as is the ASP.NET content. Instead, the WCF Service Model intercepts messages addressed to WCF services and routes them through the WCF transport/channel stack.
WCF’s ASP.NET compatibility mode is suitable for scenarios that do not require the ability to host outside of IIS or to communicate over protocols other than HTTP, but that use all of features of the ASP.NET Web application platform.
Unlike the default side-by-side configuration, where the WCF hosting infrastructure intercepts WCF messages and routes them out of the HTTP pipeline, WCF services running in ASP.NET Compatibility Mode participate fully in the ASP.NET HTTP request lifecycle. In compatibility mode, WCF services use the HTTP pipeline through an IHttpHandler implementation, similar to the way requests for ASPX pages and ASMX Web services are handled.
So I set aspNetCompatibilityEnabled="true"
under the <serviceHostingEnvironment>
tag.
New error message:
The service cannot be activated because it does not support ASP.NET compatibility. ASP.NET compatibility is enabled for this application. Turn off ASP.NET compatibility mode in the web.config or add the AspNetCompatibilityRequirements attribute to the service type with RequirementsMode setting as 'Allowed' or 'Required'.
The reason is the change in the default ASP.NET compatibility mode value in 4.5. When you set the aspNetCompabitilityEnabled="true" in your Web.config without setting any compatibility setting on the service definition, then:
On machines with v4.5 runtime: Individual service definitions automatically get RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed as the default. So your service will activate successfully.
On machines with v4.0 runtime: Individual service definitions automatically get RequirementsMode = AspNetCompatibilityRequirementsMode.NotAllowed as the default. So your service will fail to activate with the above error.
Our service was compiled in v4.0. This meant that I had to add the following attribute to my service implementation:
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class XYZService : IXYZService
{
//Implementation code
}
Finally, with all of this in place, we re-deployed our service DLL. And voila!