Friday, February 29, 2008

Authenticating a Windows User in .NET

Authenticating a Windows User in .NET

When it comes to authenticating a windows user from within a .NET application, a developer has 3 options

  1. Querying Active Directory via LDAP (Lightweight Directory Access Protocol)
  2. Microsoft.Samples.Security.SSPI
  3. LogOnUser API (advapi32.dll)

Personally I prefer the third option. Let me explain why.

Querying the Active Directory is the most common way of performing authentication or at least it seems that way since when you google the subject most of the results point to this solution. An active directory is a directory structure used on Microsoft Windows based computers and servers to store information and data about networks and domains. It is primarily used for online information and was originally created in 1996 and first used with Windows 2000. An active directory (sometimes referred to as an AD) does a variety of functions including the ability to provide information on objects, helps organize these objects for easy retrieval and access, allows access by end users and administrators and allows the administrator to set security up for the directory. So its obvious that AD's sole purpose is not authentication. Hence the frustrated comments you WILL find on various blogs and other web pages on performing authentication using AD. In addition, the active directory feature should be installed on a server to make this work. If its not installed querying the AD amounts to no result.

I must admit that my knowledge on the second solution is much limited. But when going through some solutions in the web I saw that it required some fiddling with windows sockets involving some classes like Socket, TcpListener, etc. I could only imagine what sort of problems you may come across when you try that out in a firewalled environment.

So to be on the safe side I chose to go with the LogOnUser API. IMHO there is not enough documentation to be found in the internet on how to use the Windows LogOnUser API exposed by advapi.dll so I had to depend on trial and error and managed to create a managed wrapper consisting of a set of classes which I mention below. It is possible to perform the following functions with this wrapper.

  • Authenticating a local or domain windows user
  • See whether a windows user is in a specified windows user group
  • List all windows user groups that a certain windows user belongs to.
  • Impersonation may also be possible with some additional lines of coding.

Here's how I got it working,

The first class called 'WindowsAPIDeclarations' contains all the Windows API declarations.

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace libWinSecuritySubSystem
{
internal sealed class WindowsAPIDeclarations
{
internal static Int16 LOGON32_LOGON_NETWORK = 3;
internal static Int16 LOGON32_LOGON_INTERACTIVE = 2;
internal static Int16 LOGON32_PROVIDER_DEFAULT = 0;

internal WindowsAPIDeclarations() { }

[DllImport("advapi32.dll")]
internal static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
internal static extern bool CloseHandle(IntPtr handle);
}
}

The second class "AuthenticationService" contains the main AuthenticateUser() method. Remember to replace the string "<YOUR DOMAIN NAME HERE>" with your windows domain name in which you want to do the authentication.

using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Principal;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Collections;

namespace libWinSecuritySubSystem
{
public sealed class AuthenticationService
{
public static AuthenticationResult AuthenticateUser(string userName, string password)
{
IntPtr token = IntPtr.Zero;

try
{
string domainName = "<YOUR DOMAIN NAME HERE>";

if (!WindowsAPIDeclarations.LogonUser(userName, domainName, password, WindowsAPIDeclarations.LOGON32_LOGON_INTERACTIVE, WindowsAPIDeclarations.LOGON32_PROVIDER_DEFAULT, ref token))
{
int errID = Marshal.GetLastWin32Error();
throw new Win32Exception(5);
}

WindowsPrincipal principal = new WindowsPrincipal(new WindowsIdentity(token));
WindowsAPIDeclarations.CloseHandle(token);

return new AuthenticationResult(true, null, new AuthenticatedUser(principal));
}
catch (Exception ex)
{
try
{
WindowsAPIDeclarations.CloseHandle(token);
}
catch (Exception) { }
return new AuthenticationResult(false, ex, null);
}
}
}
}

The third class named "AuthenticationResult" contains information regarding the ultimate result of the AuthenticateUser() method.

using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Principal;
using System.ComponentModel;

namespace libWinSecuritySubSystem
{
public class AuthenticationResult
{
bool isAuthenticated;
Exception authenticationException;
AuthenticatedUser user;

internal AuthenticationResult(bool isAuthenticated, Exception authenticationException, AuthenticatedUser user)
{
this.isAuthenticated = isAuthenticated;
this.authenticationException = authenticationException;
this.user = user;
}

public AuthenticatedUser User
{
get { return user; }
}

public bool IsAuthenticated
{
get { return isAuthenticated; }
}

public Exception AuthenticationException
{
get { return authenticationException; }
}
}
}

Last but not least the "AuthenticatedUser" class which contains the functions to get info on the authenticated user.

using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Principal;
using System.Collections;

namespace libWinSecuritySubSystem
{
public class AuthenticatedUser
{
WindowsPrincipal principal;

internal AuthenticatedUser(WindowsPrincipal principal)
{
this.principal = principal;
}
public WindowsPrincipal Principal
{
get { return principal; }
}
public bool IsUserInGroup(string groupName)
{
try
{
return principal.IsInRole(groupName);
}
catch (Exception)
{
return false;
}
}

public bool IsUserInGroup(WindowsBuiltInRole builtInGroup)
{
try
{
return principal.IsInRole(builtInGroup);
}
catch (Exception)
{
return false;
}
}

public Dictionary<string, string> GetGroups()
{
Dictionary<string, string> groups = new Dictionary<string,string>();
IdentityReferenceCollection irc = ((WindowsIdentity)(principal.Identity)).Groups;
foreach (IdentityReference ir in irc)
groups.Add(ir.Value, ir.Translate(typeof(NTAccount)).Value);

return groups;
}

}
}

Additional functions like impersonation may also be introduced as necessary.