I put together a custom authentication and authorization solution this week for a customer that I wanted to share, both because it works pretty well, and also to see if anyone can spot any gaping security holes in the approach that I didn’t think of before we go prime time with it. It is basically a way to mix ASP.NET Forms Authentication with Windows Authentication and Authorization without requiring elevated privilege on the ASPNET worker process account. The solution involves using an .NET Enterprise Service (COM+) component to do the actual checks against the Windows accounts.
The scenario is this. The customer has an ASP.NET web application with some unique requirements regarding authentication and authorization. They need explicit control over the login/logout process because they need to restrict session length and number of concurrent logins for some accounts differently than others. They require that the accounts being used for auth be Windows accounts, and that the auth decisions are based on the Windows users and groups. They are also running on Win2K server. They also need to support Linux clients running Mozilla. At least they made it an easy scenario
The need for the accounts to be Windows based makes Windows auth sound like the ticket, but there was no way to satisfy the login/logout/session/concurrent login limitations in the requirements because your code is pretty much out of the loop with the built in Windows auth, so Forms auth sounds good. But then you have to use Windows account information for the actual auth decisions. Fine, that is what the LogonUser API is for.
Ah yes, but then there is that little hitch that they are running on Win2K, which requires the SETCBNAME (Act as part of operating system) privilege to call LogonUser, which is not something you want to give to the ASPNET account if you want a secure system. So what to do…?
What I came up with was to create a .NET Enterprise Services component that runs in a Server application. The application is configured to run under a special Windows account that is just a normal user, but has the required SETCBNAME privilege assigned to be able to call LogonUser. The account is also configured so that interactive logins are disallowed. The component exposes an Authenticate method that takes the credentials and returns a bool indicating success or failure, and returns the token that LogonUser gives you to represent the user.
The ASP.NET app includes a custom principal class that encapsulates the user identity provided by the login process through Forms authentication, and also holds onto the user token returned after calling Authenticate on the ES component. I tried using the token directly within the ASP.NET app to create a WindowsIndentity, but could not get it to work because of the token coming from another process. So I added an IsInRole method to the ES component that takes the desired role name and the user token. This method uses the token to create a WindowsIdentity and WindowsPrincipal, and calls IsInRole against that. The IsInRole method of the custom principal just calls that corresponding method in the ES component, passing in the role and the user token it cached after calling Authenticate. The user token is also stored in session so that it can be used to recreate and set the custom principal on subsequent requests.
Using this approach, the login form just calls Authenticate, and if successful, stores the user token in session and uses FormsAuthentication to issue the login session cookie. In the PreRequestHandlerExecute event handler in the application class, the custom principal is recreated and set as the principal on the context as long as the request is authenticated (managed by Forms auth). Then the rest of the app can just call IsInRole on the User property like normal, and the custom principal’s implementation of that gets called. This is also the place where all the unique session length/concurrent sessions are enforced.
Obviously going cross process on every request for role checks is not such a great idea, so we also cache the authenticated roles after they have been checked once through the ES component, and time out that cache using a configurable period so that if someone’s group membership has been changed and they are running a long session (the app has a self-refresh status page), those changes will be picked up.
To add some additional security, we use COM+ role based security to make it so that only the local machine ASPNET account can call into the component.
It may sound a little complicated, and it was to get it all implemented and working correctly the first time. But it has the advantage that it is very easy for the ASP.NET app to integrate, makes the custom mechanisms being used transparent to the app other than the call to Authenticate and the call to attach the custom principal. And the security provided by ES gives a way to get the privileged role out of the ASPNET process. We could have used a Windows Service to do this as well, but then we would need to call into the service somehow and protect against unauthorized callers, and all that is built into ES.
Anyone see any holes here? Was there a better way to do this that escaped me?