Sunday, August 15, 2010

Custom Authentication in WCF

I'm building an HTML5 iPhone application that accesses a WCF RESTful web service hosted by IIS. The stumbling block for me has been authentication. I decided to go with Basic Authentication over HTTPS as a simple and secure enough approach for authenticating my users. But as with most Microsoft tech it doesn't pay to wander too far from the track. If you want to authenticate Windows users then I suspect everything would more or less just work. I'm working with an existing credential store in a SQL Server database so I needed something a little different. I'm not going to go into detail about how I eventually got it sorted - this is really just to acknowledge Dominick Baier over at www.leastprivilege.com for shining a light on the process.

A few things to note for future reference. WCF doesn't use HttpContext.Current.User to identify the current user (although this can be enabled). Instead use ServiceSecurityContext.Current.PrimaryIdentity.Name. If following along with Dominick's example and setting up the HttpContextPrincipalPolicy it's quite valid to keep using the Thread.CurrentPrincipal to identify the user. And if the identity of the user is not reflected in Thread.CurrentPrincipal then something else is configured incorrectly. In my case the policy wasn't being loaded because I had it defined under the default behavior and not the MunqServiceBehavior I set up in an earlier post, see config section below:

1 <behaviors>
2   <serviceBehaviors>
3      <behavior name="MunqServiceBehavior">
4         <serviceMetadata httpGetEnabled="true" />
5         <serviceDebug includeExceptionDetailInFaults="false" />
6         <MunqInstanceProvider />
7         <serviceAuthorization principalPermissionMode="Custom"> <!-- set to Custom to enable auth policies below -->
8           <authorizationPolicies>
9           <!--
10             sync ServiceSecurityContext.PrimaryIdentity with Context.User.Identity and Thread.CurrentPrincipal with Context.User
11         -->
12         <add policyType="CustomAuthenticationModule.HttpContextIdentityPolicy, CustomAuthenticationModule" />
13         <add policyType="CustomAuthenticationModule.HttpContextPrincipalPolicy, CustomAuthenticationModule" />
14        </authorizationPolicies>
15       </serviceAuthorization>
16     </behavior>
17     <behavior name="">
18        <serviceMetadata httpGetEnabled="true" />
19        <serviceDebug includeExceptionDetailInFaults="false" />
20     </behavior>
21    </serviceBehaviors>
22 </behaviors>

Then I couldn't figure out why the authentication module itself wasn't loading. Turns out it's a known issue with Cassini, the Visual Studio web server. For IIS7 we're supposed to be using the <system.webServer> section but Cassini uses the old IIS6 syntax. The work around is to define the module in both locations:

1 <system.web>
2   <compilation debug="true" targetFramework="4.0" />
3   <httpModules>
4     <!-- added here and in system.webServer to load module using Cassini -->
5     <add name="CustomAuthenticationModule"
6         type="CustomAuthenticationModule.CustomAuthentication, CustomAuthenticationModule"/>
7   </httpModules>
8 </system.web>
9
10 <system.webServer>
11   <validation validateIntegratedModeConfiguration="false"/>
12   <modules runAllManagedModulesForAllRequests="true">
13     <remove name="CustomAuthenticationModule"/>
14     <add name="UrlRoutingModule"
15         type="System.Web.Routing.UrlRoutingModule, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
16     <add name="CustomAuthenticationModule"
17         type="CustomAuthenticationModule.CustomAuthentication, CustomAuthenticationModule"/>
18   </modules>
19 </system.webServer>
20

So getting that sorted helped but the next issue was particularly annoying. I set breakpoints in my code and I could see the custom authentication taking place but after successfully authenticating the user and exiting my AuthenticateRequest handler it would immediately hit the EndRequest handler with a 401 - Unauthorized error. Apparently it pays to turn off security in the standard endpoint in order to use a custom handler. Presumably after executing my handler it was trundling off to a standard handler which wasn't then able to authenticate the user trying to access the service and returning access denied.

1 <standardEndpoints>
2   <webHttpEndpoint>
3      <!--
4       Configure the WCF REST service base address via the global.asax.cs file and the default endpoint
5       via the attributes on the <standardEndpoint> element below
6     -->
7     <standardEndpoint name="" helpEnabled="true" automaticFormatSelectionEnabled="true">
8       <security mode="None"/> <!-- turn off security to use custom module -->
9     </standardEndpoint>
10  </webHttpEndpoint>
11 </standardEndpoints>
12

As a postscript and after working for more than a few years developing an Internet Banking site I'm extremely wary of roll-your-own authentication schemes. It's very difficult to get authentication right and the amount of effort required is typically a reflection of the overall risk of getting it wrong. For a bank that can include not just the risk of exposing a customer's accounts or even losing actual money. Banks have for a number of years implemented rules to minimise those losses by restricting the amount of money that can be transferred online and ensuring inter-bank transfers have a built-in delay period. The risk to a bank is also about the public perception of safety and bad press around online banking can result in substantial loss of custom. I walked away from a job with a bank where I felt strongly that inadequate steps were taken to ensure the security of the online application I was developing. I still think I made the right decision and in case anyone is wondering, it most definitely wasn't the PSIS.

No comments: