How to implement Windows Forms Based Authentication in ASP.Net

This is an updated version of an article I wrote in March 2003 for ASPAlliance. I corrected some minor errors and updated the code samples a bit. C# and VB.Net samples are both attached at the bottom of the page.

Introduction

The Windows authentication prompt can often be an intimidating dialog for users. It asks for two or three things: username, password, and sometimes domain. Users may (and should) know their network username and password combination, but how many of them know the name of the domain their account is kept on? To make matters more complex, depending on the operating system and browser version, the domain entry box isn’t shown. In this case, users need to prefix their username with DomainName\ or enter their UPN.

Another solution to this is to use a standard WebForm in conjunction with a Windows API, LogonUser. This is the method used by Microsoft Exchange Outlook Web Access Forms Based Authentication. The LogonUser API is a function found in the advapi32.dll library on all Windows NT, 2000, 2003 and newer based servers and workstations.

The LogonUser API

The LogonUser function is defined in the Microsoft Platform SDK as follows:

BOOL LogonUser(
  LPTSTR lpszUsername,
  LPTSTR lpszDomain,
  LPTSTR lpszPassword,
  DWORD dwLogonType,
  DWORD dwLogonProvider,
  PHANDLE phToken
);

This function takes five input parameters, and has a sixth output parameter, a security token. The .Net version of the API declaration is as follows (there are multiple ways to do this, all of which are functionally equivalent):

<DllImport("advapi32.dll", SetLastError:=True)> _

Private Function LogonUser( _
    ByVal lpszUsername As String, _
    ByVal lpszDomain As String, _
    ByVal lpszPassword As String, _
    ByVal dwLogonType As Integer, _
    ByVal dwLogonProvider As Integer, _
    ByRef phToken As IntPtr) As Integer
End Function

The reason I’ve chosen to declare the function as an integer is because in the event an error code is returned, it can be accessed this way, and in turn looked up in the Platform SDK.

There are two input parameters which LogonUser requires that are of specific interest: dwLogonType, and dwLogonProvider. The two of these control the type of logon that is initiated, and the way the credentials are passed, respectively.

There are only a couple of LogonTypes were really apply to this context: LOGON32_LOGON_NETWORK and LOGON32_LOGON_INTERACTIVE. If your need is solely to validate the supplied credentials, then LOGON32_LOGON_NETWORK is your best choice. It is the fastest, and does not cache the logins on the server. However, if you plan to impersonate the user whose credentials were supplied, then LOGON32_LOGON_INTERACTIVE is necessary. It outputs the proper type of security token for use with the WindowsImpersonationConext class in the .Net Framework.

dwLogonProvider dictates the method in which the webserver will pass the user’s credentials to the domain. There are four possible values:

LOGON32_PROVIDER_DEFAULT
LOGON32_PROVIDER_WINNT35
LOGON32_PROVIDER_WINNT40
LOGON32_PROVIDER_WINNT50

The first of the four will use the default logon protocol for the system. By default on Windows 2000, this is NTLM, which will work with Windows NT 4.0 and newer domains. If one or more of the domain controllers you’ll be authenticating against is still running Windows NT 3.51, you’ll need to use LOGON32_PROVIDER_WINNT35. If the web server is running Windows XP or Windows Server 2003, and you have a Windows NT4 or NT351 based domain, you’ll need to use the appropriate provider specific to that type of domain, because the default provider is Negotiate, which is not supported by Windows NT4.

One final parameter which has some special cases is the lpszDomain parameter. To authenticate against a domain, specify the NetBIOS name of the domain in which the account resides (this is the name selected in the dropdown of a Windows Control + Alt + Del when you login to your computer). If a “.” Is specified, the LogonUser API will attempt to login to the machine executing the code. The name of another workstation or server on the network may also be specified for lpszDomain. If the lpszUsername parameter is the user’s UPN (universal principal name), then the lpszDomain parameter should be null. The UPN is a name for the user which is unique within the Active Directory forest. In many organizations this is also the user’s email address.

Code

With all that said and done, it’s time for some code! The download for this article contains a sample webform and the associated code in Visual Basic.Net and C#. All the code shown in the article is written in Visual Basic.Net.

The first step is to declare the necessary API call, and the constants associated with it:

<DllImport("advapi32.dll", SetLastError:=True)> _

Private Function LogonUser( _
    ByVal lpszUsername As String, _
    ByVal lpszDomain As String, _
    ByVal lpszPassword As String, _
    ByVal dwLogonType As Integer, _
    ByVal dwLogonProvider As Integer, _
    ByRef phToken As IntPtr) As Integer
End Function

Const LOGON32_LOGON_INTERACTIVE As Long = 2
Const LOGON32_LOGON_NETWORK As Long = 3
Const LOGON32_PROVIDER_DEFAULT As Long = 0
Const LOGON32_PROVIDER_WINNT50 As Long = 3
Const LOGON32_PROVIDER_WINNT40 As Long = 2
Const LOGON32_PROVIDER_WINNT35 As Long = 1

Step two, actually use the API that has been defined:

Private Function ValidateLogin( _
    ByVal Username As String, _
    ByVal Password As String, _
    ByVal Domain As String) As Boolean

    Dim token As IntPtr

    ' The Windows Error Code for success is 0. Therefore,
    ' if the function returns 0, the supplied credentials are valid
    If Not LogonUser(Username, _
        Domain, _
        Password, _
        LOGON32_LOGON_INTERACTIVE, _
        LOGON32_PROVIDER_DEFAULT, token) = 0 Then

        Return True
    Else
        Return False
    End If
End Function

In this example, I’m using interactive login, to allow for easy use of the access token which is returned (and stored in the IntPtr variable “token”) if the API call is successful.

Handling Errors

If the API Call returns false, use this line of code to get the Win32 error code back for debugging:

System.Runtime.InteropServices.Marshal.GetLastWin32Error()

A listing of Win32 error codes can be found online at: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/debug/base/system_error_codes.asp

Server Setup

This section applies only if you’re running a Windows 2000 server. Windows XP, and Windows Server 2003 handle this behavior automatically. The ASPNET account, or whatever account your server is configured to run the ASP.Net worker process account as requires the “Act as Part of the Operating System” user right. This is also known by the operating system, and in error messages as SE_TCB_NAME. The following steps detail how to grant this right:

  1. Logon to the server as an administrator
  2. Open up the machines local security policy (start>run>secpol.msc)
  3. Expand Security Settings, Local Policies, User Rights Assignment
  4. Double click Act as part of the operating system
  5. Click Add User or Group, enter the ASP.Net worker process’ account name and click OK, and OK again

In order for this change to become effective, the web server will have to be rebooted. This setting is applied when the start dialog reads “Applying Security Settings”.

In Summary

For all the details of how to work with the LogonUser API call, it’s actually very simple to use. A few things to consider:

When deciding which logon provider to use, you must be sure that all of the domain controllers on the network support the protocol you’re using. This means that if all but one of the domain controllers in your network is running Windows 2000, and this lone NT4 domain controller is located halfway across the globe, you still need to use the NT40 logon provider. This is because your application could theoretically contact this NT4 domain controller, and fail to communicate the given credentials.

If you have no plans to impersonate the logged in user, use the LOGON32_LOGON_NETWORK login type. It is faster, and consumes less network resources.

If you have one account domain, I’d recommend that you store it in the appSettings area of your web.config file. That way, if your network admins ever rename the domain, you won’t have to recompile the application. Obviously, if there are multiple account domains, your users or your application will have to decide which one to query. Standardizing on the use of the user principal name (UPN) for logons will alleviate the need to determine the domain containing each user’s account.

From the Frontline

I’ve used the LogonUser API in conjunction with numerous web applications and it has worked well. Microsoft Exchange 2003 Outlook Web Access (specifically the Forms Based Authentication feature) is a very popular example of this API in action as well. When I originally wrote this article, I was targeting a diverse user base on a variety of operating systems. By standardizing on the forms based interface, I was able to deliver a common look and feel to my user population which was not intimidating and required no knowledge of the backend system (e.g. the domain name).


Share this post: email it! | digg it! | bookmark it! | live it!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>