Using HTTP Module to Authenticate Users in WCF - Part 2: Windows Authentication
In a previous post I addressed the issue of using HTTP module based authentication in WCF. The presented solution worked in most cases but failed completely with Windows authentication. In this post I'll describe the necessary changes to make this work as well.
Let's first see what goes wrong with the existing solution and why. To configure WCF for Windows authentication, the following changes are required in web.config
:
<system.serviceModel>
<!-- ... -->
<bindings>
<basicHttpBinding>
<binding name="HttpWindowsBinding"
maxReceivedMessageSize="2147483647">
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Windows" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="WcfAuthentication.Service">
<endpoint address="windows"
binding="basicHttpBinding"
bindingConfiguration="HttpWindowsBinding"
contract="WcfAuthentication.IService" />
</service>
</services>
</system.serviceModel>
Of course the settings have to be matched in IIS: Windows authentication should be enabled for the application while anonymous authentication should be disabled, as well as all the other types of authentication.
After setting all this up any calls to our service will throw a MessageSecurityException
:
The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was 'Negotiate,NTLM'.
If you try searching the web for solutions, you'll notice the same error pops up in many different situations not related to our case. So what's going on here?
The problem is being caused by the following method in HttpAuthenticationModule
:
void context_AuthenticateRequest(object sender, EventArgs e)
{
HttpContext.Current.User = ProcessAuthentication();
}
Setting the user in the current HttpContext
to a custom IPrincipal
implementation confuses WCF which expects a WindowsPrincipal
as configured. The only way to make it work is to pass through the original user information in this case:
void context_AuthenticateRequest(object sender, EventArgs e)
{
if (!(HttpContext.Current.User is WindowsPrincipal) &&
HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath
.EndsWith(".svc"))
HttpContext.Current.User = ProcessAuthentication();
}
The extension based filtering is there so that the authentication will still work for the rest of our web application. This change alone is not enough, of course. We still need to do the authentication somewhere for the WCF case. HttpContextAuthorizationPolicy
is the right spot for it. Evaluate method should be modified as follows:
public bool Evaluate(EvaluationContext evaluationContext, ref object state)
{
HttpContext context = HttpContext.Current;
if (context != null)
{
if (context.User is WindowsPrincipal)
{
IPrincipal principal = HttpAuthenticationModule.ProcessAuthentication();
evaluationContext.Properties["Principal"] = principal;
evaluationContext.Properties["Identities"] = new List<IIdentity>
{
principal.Identity
};
}
else
{
evaluationContext.Properties["Principal"] = context.User;
evaluationContext.Properties["Identities"] = new List<IIdentity>
{
context.User.Identity
};
}
}
return true;
}
Keep in mind that calling a static method in HttpAuthenticationModule to authenticate the user is just a shortcut to make this sample work and is not suggested practice. In production code you'll want to have your authentication logic implemented somewhere in the business layer and call it from both HttpAuthenticationModule
and HttpContextAuthorizationPolicy
.