Let’s assume you have a web site which is exposed to the internet (i.e. to the public) but the site itself works with windows accounts internally. If you have problems visualizing this scenario, think about a web mail interface for a mail server system which offers mail services for user accounts in active directory. Users inside your corporate network can use integrated Windows authentication to access this site, so they don’t really have a problem. But when they want to access the interface from outside the corporate network, or from a machine/device which doesn’t understand integrated Windows authentication, you’ll realize that it doesn’t just work like that.
So you’ll want to do authentication based on the credentials the user enters in a simple form on a web page. The challenge now is that the rest of the site (i.e. apart from the one form which does the initial authentication) will likely still work with the HttpContext.Current.User
property to determine which user is currently authenticated and provide actions and data based on that identity, because you don’t want to re-implement the entire logic. The good news is, you can do that! The bad news is, you don’t just get it for free. But that’s why you came here, and that’s why I’ll try to help you with this.
Let’s first arrange a few things on the web site facing the public — you don’t necessarily need to change this on your internal site. First, the login page will need to be accessible for anyone, i.e. anonymous access must be turned on on the site. Second, from ASP.net’s point of view, all the pages which require the user to be authenticated should be in/under the same directory. If you don’t want to do this, you’ll simply have to add a
WebSiteRoot/ +--Default.aspx Could redirect to /ActualSite/Default.aspx +--ActualSite/ +--Default.aspx The actual home page with all its logic +--Auth/ +--Login.aspx The login page
So all the logic you had in the site’s root directly would be under ActualSite or whatever name you chose.
Updating Web.config
Now for the above scenario with different directories, the web.config in the site’s root could look as follows. Please note that for the pages under ActualSite we are simply disabling anonymous access, while for everything else, we allow it. Also, in the authentication element we’re setting the mode to None because we’re not going to use any predefined authentication mechanism as is.
<configuration> <location path="ActualSite"> <system.web> <authorization> <deny users="?"/> </authorization> </system.web> </location> <system.web> <authentication mode="None"> <forms loginUrl="~/Auth/Login.aspx" defaultUrl="~/ActualSite/Default.aspx" /> </authentication> <authorization> <allow users="*" /> </authorization> </system.web> </configuration>
But of course that’s not all yet. You’ll see that if now you wanted to look at the actual site, you’ll get a 401 because you’re not authenticated. So let’s take a look at that.
Validating credentials
Next, let’s create the login page which the user will use to enter his credentials. Fortunately, ASP.net offers the Login control which does almost everything we need. So add that one to your login page plus add an event handler for the OnAuthenticate event of that control. Alternatively, you can also derive your own control from the Login control and override the OnAuthenticate method. This event handler is where we’ll do our custom logic to check that the credentials really map to an existing Windows user. Below’s the code which does that.
protected override void OnAuthenticate(AuthenticateEventArgs args) { string[] parts = UserName.Split('\\'); string password = Password; string username; string domain = null; args.Authenticated = false; if (parts.Length == 1) { username = parts[0]; } else if (parts.Length == 2) { domain = parts[0]; username = parts[1]; } else { return; } if (WebAuthenticationModule.AuthenticateUser(username, domain, password)) { string userData = String.Format("{0}\n{1}\n{2}", username, domain, password); FormsAuthenticationTicket ticket = new FormsAuthenticationTicket( 2 /* version */, username, DateTime.Now /* issueDate */, DateTime.Now.AddMinutes(30) /* expiration */, true /* isPersistent */, userData, FormsAuthentication.FormsCookiePath); HttpCookie ticketCookie = new HttpCookie( FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket)); Context.Response.Cookies.Add(ticketCookie); Context.Response.Redirect( FormsAuthentication.GetRedirectUrl(username, false), false); } }
The AuthenticateUser method from WebAuthenticationModule is a wrapper on the LogonUser function from the Win32 API which will return true if the user could be logged on. So if the credentials are valid, we’re going to pass them in to the FormsAuthenticationTicket in the UserData property so later on, we’ll be able to use them again. At least, we don’t want the consumers of the site have to enter credentials for every request their making, right? Also, we’re encrypting the entire ticket because we’re going to send it over the wire. The Encrypt method from the FormsAuthentication class does this. However, you’ll have to make sure that the protection attribute of the forms element in the web.config is set to All which actually is the default (but it can be inherited, so watch out!).
What you see is that we’re heavily using the functionality offered by the FormsAuthentication class and related classes to handle the tickets, encryption, settings, etc. This is not mandatory but it helps a lot. Plus it’s better anyway than coming with your own ticketing and encryption mechanisms; unless you have a degree in maths and/or cryptography, chances are that that’s not so secure as you think it is.
Authenticating users
Then, we need to authenticate the user for all the requests he makes after providing the credentials. Thus, we need to add some custom logic to the AuthenticateRequest event of the HttpApplication. There’s multiple ways to do that:
- Add a file called global.asax to your site’s root
- Create and register a new HttpModule by implementing the System.Web.IHttpModule interface and adding an entry in the httpModules section in your root’s web.config
Personally, I like the approach with the custom module more, but adding this stuff to the global.asax can be done a little bit faster. In either case, make sure you can handle the AuthenticateRequest event. My code proposal for the handler is given below. I’m intentionally omitting most error handling code here.
private static void OnAuthenticateRequest(object sender, EventArgs args) { HttpApplication application = sender as HttpApplication; HttpContext context = application.Context; HttpRequest request = context.Request; HttpResponse response = context.Response; if (!request.IsAuthenticated && !context.SkipAuthorization) { if (request.CurrentExecutionFilePath.Equals(FormsAuthentication.LoginUrl, StringComparison.OrdinalIgnoreCase) || request.CurrentExecutionFilePath.EndsWith(".axd")) { context.SkipAuthorization = true; } else { HttpCookie cookie = request.Cookies.Get(FormsAuthentication.FormsCookieName); FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value); if (!ticket.Expired) { IntPtr hToken = LogonUserFromTicket(ticket); WindowsIdentity identity = new WindowsIdentity( hToken, "Win/Forms", WindowsAccountType.Normal, true); context.User = new WindowsPrincipal(identity); if (FormsAuthentication.SlidingExpiration) { ticket = FormsAuthentication.RenewTicketIfOld(ticket); cookie.Value = FormsAuthentication.Encrypt(ticket); response.Cookies.Set(cookie); } return; } FormsAuthentication.RedirectToLoginPage(); } } }
So let’s go through the important things here. First we check (lines 9/10) if the request is already authenticated or authorization is to be skipped completely. If either of those is true, we’re out of the picture already. Else it’s our job to do the authentication. Lines 12-17 are used to prevent authentication on the login page as well as the *.axd handlers, which are typically used to return resources like scripts for ASP.net components. Lines 20-42 do the actual authentication: we first retrieve the cookie from the previous credential validation and decrypt it to get the ticket. If the ticket has not expired, we log the user on (the call to LogonUserFromTicket is again only a wrapper for the LogonUser function from the Win32 API; it uses the data from the UserData property of the ticket) to get the logon token which we’ll pass in to the WindowsIdentity constructor to get the WindowsIdentity object all the code in ActualSite will use to determine which user is making the request. Then of course we need to update the context with the new identity. If sliding expiration is turned on in the web.config, we renew the ticket. And if the ticket has expired, of course we don’t authenticate the user but instead we redirect him to the login page.
Cleanup
Finally, we still have a tiny problem here. We have used the LogonUser function, but according to its documentation, we should call the CloseHandle function from the Win32 API. For the method AuthenticateUser I have already done that; there we don’t need the token/handle anymore when we have verified the credentials. But what about the authentication we’re doing in AuthenticateRequest? We’re setting the newly created WindowsIdentity (which is here used to represent the user token) to the current HttpContext because we’ll need it there to ultimately handle the request. But once the request handler is done, we don’t need it anymore. Luckily, there’s also an event for this purpose. It’s the last event that there is and it’s called EndRequest. So let’s add the following code in the handler for that event.
private static void OnEndRequest(object sender, EventArgs args) { HttpApplication application = sender as HttpApplication; HttpContext context = application.Context; if (null != context.User) { WindowsIdentity identity = context.User.Identity as WindowsIdentity; if (null != identity) { NativeAuth.CloseHandle(identity.Token); } } }
Basically all it does, if the request was given a WindowsIdentity, it’ll call the CloseHandle function from the Win32 API on that identity’s token handle. That should do the trick and we shouldn’t leak handles anymore.
Summary
I have shown here how you can make use of the forms authentication mechanisms which come with ASP.net to do Windows authentication behind the scenes. This can be very useful in cases when not all users have access to systems which know how to do Windows authentication.
Please note that I do not claim that this solution is going to work for every challenge you may be facing. The solution shown here is neither claimed to be complete nor suitable for every web application.