Using HTTP Module to Authenticate Users in WCF
WCF has great built-in support for most types of authentication so there aren't many good reasons to use HTTP module based authentication with it. Having an existing ASP.NET application already using such authentication certainly is one of them. Finding resources on how to do it might be a challenge though. I managed to stumble upon an article by Microsoft patterns & practices team which helped a lot. In a way this post is its abridged and more practical version.
From here on I assume you already have an IHttpModule
in your application (ProcessAuthentication()
being the method implementing the actual authentication of the user):
public class HttpAuthenticationModule : IHttpModule
{
public void Dispose()
{ }
public void Init(HttpApplication context)
{
context.AuthenticateRequest += context_AuthenticateRequest;
}
void context_AuthenticateRequest(object sender, EventArgs e)
{
HttpContext.Current.User = ProcessAuthentication();
}
private static IPrincipal ProcessAuthentication()
{
// implement your authentication here
IIdentity identity = new GenericIdentity("Authenticated User");
return new GenericPrincipal(identity), null);
}
}
The module should also already be registered in web.config
:
<system.webServer>
<!-- ... -->
<modules>
<add name="HttpAuthenticationModule"
type="WcfAuthentication.HttpAuthenticationModule"/>
</modules>
</system.webServer>
The above snippet works with integrated managed pipeline. If you happen to still use classic managed pipeline, registration will be a bit different:
<system.web>
<!-- ... -->
<httpModules>
<add name="HttpAuthenticationModule"
type="WcfAuthentication.HttpAuthenticationModule"/>
</httpModules>
</system.web>
The goal is of course getting access to the authenticated user (i.e. IPrincipal
instance) in WCF service through ServiceSecurityContext
. The following test method is a great way for testing that:
public string GetUser()
{
if (ServiceSecurityContext.Current != null)
return ServiceSecurityContext.Current.PrimaryIdentity.Name;
else
return null;
}
IAuthorizationPolicy
is the interface to implement custom authorization in WCF with. In our case the authenticated user can be accessed through current HttpContext
:
public class HttpContextAuthorizationPolicy : IAuthorizationPolicy
{
public bool Evaluate(EvaluationContext evaluationContext, ref object state)
{
HttpContext context = HttpContext.Current;
if (context != null)
{
evaluationContext.Properties["Principal"] = context.User;
evaluationContext.Properties["Identities"] = new List<IIdentity>()
{
context.User.Identity
};
}
return true;
}
public System.IdentityModel.Claims.ClaimSet Issuer
{
get { return ClaimSet.System; }
}
public string Id
{
get { return "HttpContextAuthorizationPolicy"; }
}
}
Of course the class should be registered in web.config
so that our service will use it:
<system.serviceModel>
<!-- ... -->
<behaviors>
<serviceBehaviors>
<behavior>
<!-- ... -->
<serviceAuthorization>
<authorizationPolicies>
<add policyType="
WcfAuthentication.HttpContextAuthorizationPolicy,
WcfAuthentication, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null"/>
</authorizationPolicies>
</serviceAuthorization>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
There is still one thing missing. If you try out the above code, you will realize that HttpContext.Current
is always null
even if authorization in our HTTP module was successful. To get access to it you need to enable ASP.NET compatibility:
<system.serviceModel>
<!-- ... -->
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"
aspNetCompatibilityEnabled="true"/>
</system.serviceModel>
To make your WCF service work in this mode you need decorate it with AspNetCompatibilityRequirementsAttribute
:
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
public class Service : IService
{
// ...
}
Finally, we're done. If you've implemented all of the above correctly, our test method GetUser()
should return the user who was authenticated in the HTTP module. Unless you're trying to use Windows authentication which still doesn't work in this setup. That's already a subject for another post, though.