Pro reprezentaci Windows identity nám v .NET slouží objekt WindowsIdentity. Pomoci něj se například můžeme dostat k aktuální autentizované identitě ve windows pomoci volání: (interně se použije current thread token)
var windowsIdentity = System.Security.Principal.WindowsIdentity.GetCurrent();
Nás ale v tomto článku bude zajímat jiný scénář. Někdy může být v aplikaci vyžadováno ověření Windows uživatelským účtem, který je jiný než ten, pod kterým je zrovna uživatel na počítači přihlášen. Nebo se může jednat o webovou ASP.NET aplikaci, kde k požadované Windows identitě uživatele nemusíme mít přístup (například když aplikace používá Anonymous a Forms Authentication).
Pro přihlášení pomoci přihlašovacího jména a hesla Windows slouží Win32 API funkce LogonUser. Překvapivé je to, že v .NETu nemáme pro její volání žádnou wrapper metodu (jedno volání je ve WIF v System.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler metodě ValidateToken, tam ale není možné předat parametry, které můžeme potřebovat). Nezbývá tedy nic jiného, než použít přímo tuto API funkci a wrapper pro ní si napsat sami. Já jsem implementaci umístil do samostatné třídy WindowsAuthenticationManager a v ní do metody LogonWindowsUser.
Než si třídu ukážeme, všimněte si ještě parametrů, které se na vstupu funkci LogonUser předávají:
bool LogonUser([In] string lpszUserName, [In] string lpszDomain, [In] string lpszPassword, [In] uint dwLogonType, [In] uint dwLogonProvider, out SafeCloseHandle phToken)
Budou nás teď zajímat parametry lpszUserName a lpszDomain. Parametr lpszUserName může být buď:
- Přihlašovací jméno uživatelského účtu nazývané SAM account name (přihlašovací jméno pro systémy starší než Windows 2000), pak parametrem lpszDomain určíme doménu uživatelského účtu. Pokud pro SAM account name doménu neurčíme, použije se výchozí, pokud jako doménu zadáme “.”, použije se lokální databáze účtů (doména počítače).
- Druhou možností je parametr lpszUserName předat ve formátu User Principal Name (UPN), jedná se o formát ve tvaru User@DNSDomainName a v takovém případě již parametr lpszDomain nezadáváme.
DNSDomainName přitom nemusí odpovídat jménu domény, jména můžou být různá. Nejčastěji se nastavuje tak, aby bylo stejné jako má uživatel emailovou adresu, např. můj účet jan.holan v doméně netdomain.local má UPN [email protected].
Protože je zvykem, že uživatel zadává své přihlašovací jméno do jednoho pole dohromady s doménou ve tvaru Doména\Přihlašovací jméno, zavedeme kromě metody LogonWindowsUser(string user, string domain, string password, WindowsAuthenticationLogonType logonType) i variantu pouze s parametry LogonWindowsUser(string userName, string password, WindowsAuthenticationLogonType logonType), ve které doménu ze zadaného přihlašovacího jména extrahujeme.
Kompletní třída WindowsAuthenticationManager a obě varianty metody LogonWindowsUser vypadají takto:
using System;
using System.Security;
using System.Runtime.InteropServices;
using System.Runtime.ConstrainedExecution;
using System.Security.Principal;
using System.Security.Claims;
namespace WindowsAuthentication
{
#region SafeCloseHandle class
internal sealed class SafeCloseHandle : Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid
{
private SafeCloseHandle() : base(true)
{
}
internal SafeCloseHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle)
{
base.SetHandle(handle);
}
[SuppressUnmanagedCodeSecurity, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
private static extern bool CloseHandle(IntPtr handle);
protected override bool ReleaseHandle()
{
return CloseHandle(base.handle);
}
}
#endregion
public enum WindowsAuthenticationLogonType
{
Interactive = 2,
Network = 3,
Batch = 4,
Service = 5,
Unlock = 7,
NetworkCleartext = 8,
NewCredentials = 9
}
public static class WindowsAuthenticationManager
{
[SuppressUnmanagedCodeSecurity]
private static class NativeMethods
{
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool LogonUser([In] string lpszUserName, [In] string lpszDomain, [In] string lpszPassword, [In] uint dwLogonType, [In] uint dwLogonProvider, out SafeCloseHandle phToken);
}
private const uint LOGON32_PROVIDER_DEFAULT = 0;
private const uint LOGON32_PROVIDER_WINNT40 = 2; //NTLM
private const uint LOGON32_PROVIDER_WINNT50 = 3; //Negotiate (NTLM, Kerberos or other SSP (Security Support Provider))
private const uint LOGON32_LOGON_INTERACTIVE = 2;
private const uint LOGON32_LOGON_NETWORK = 3;
private const uint LOGON32_LOGON_BATCH = 4;
private const uint LOGON32_LOGON_SERVICE = 5;
private const uint LOGON32_LOGON_UNLOCK = 7;
private const uint LOGON32_LOGON_NETWORK_CLEARTEXT = 8;
private const uint LOGON32_LOGON_NEW_CREDENTIALS = 9;
#region action methods
public static WindowsPrincipal LogonWindowsUser(string userName, string password, WindowsAuthenticationLogonType logonType = WindowsAuthenticationLogonType.Interactive)
{
//userName can be in form 'Domain\SAMAccountName' or only SAMAccountName or UserPrincipalName (User@DNSDomainName)
//Extract domain if userName is in form 'Domain\SAMAccountName', domain can be '.' for local accounts
string domain = null;
char[] separator = new char[] { '\\' };
string[] userNameParts = userName.Split(separator);
if (userNameParts.Length != 1)
{
if (userNameParts.Length != 2 || string.IsNullOrEmpty(userNameParts[0]))
{
throw new ArgumentException("The username format is not valid. The username format must be in the form of 'username' or 'domain\\username'.", "userName");
}
userName = userNameParts[1];
domain = userNameParts[0];
}
return LogonWindowsUser(userName, domain, password, logonType);
}
public static WindowsPrincipal LogonWindowsUser(string user, string domain, string password, WindowsAuthenticationLogonType logonType = WindowsAuthenticationLogonType.Interactive)
{
//Code base on System.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler ValidateToken
SafeCloseHandle userToken = null;
try
{
if (!NativeMethods.LogonUser(user, domain, password, (uint)logonType, LOGON32_PROVIDER_DEFAULT, out userToken))
{
int error = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(error);
}
var windowsIdentity = new WindowsIdentity(userToken.DangerousGetHandle(), "Password", WindowsAccountType.Normal, true);
windowsIdentity.AddClaim(new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant", System.Xml.XmlConvert.ToString(DateTime.UtcNow, "yyyy-MM-ddTHH:mm:ss.fffZ"), "http://www.w3.org/2001/XMLSchema#dateTime"));
windowsIdentity.AddClaim(new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", "http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password"));
return new WindowsPrincipal(windowsIdentity);
}
finally
{
if (userToken != null)
{
userToken.Close();
}
}
}
#endregion
}
}
Metoda LogonWindowsUser kromě zavolání funkce LogonUser dále použije získaný handler a vytvoří objekt WindowsIdentity. Ten jak asi víte v .NET 4.5 již používá claims, a to nám umožňuje si do něj přidat ještě libovolné vlastní hodnoty.
Následující příklad volání autentizace uživatele ještě dále ukazuje přidání do identity celého jména z AD atributů vytaženého pomoci System.DirectoryServices.AccountManagement:
public static class AuthenticationManager
{
#region action methods
public static WindowsPrincipal AuthenticateWindowsUser(string userName, string password, WindowsAuthenticationLogonType logonType = WindowsAuthenticationLogonType.Interactive)
{
var principal = WindowsAuthenticationManager.LogonWindowsUser(userName, password, logonType);
var identity = (WindowsIdentity)principal.Identity;
//Get Windows user DisplayName and add it to Claims
string displayName = GetUserDisplayName(identity.Name, identity.User); //In identity.Name is NTAccountName ('Domain\SAMAccountName'), in identity.User is user SID
identity.AddClaim(new Claim("DisplayName", displayName));
return new WindowsPrincipal(identity);
}
#endregion
#region private member functions
private static string GetUserDisplayName(string NTAccountName, SecurityIdentifier userSID)
{
using (var context = GetPrincipalContext(NTAccountName))
{
using (var userPrincipal = UserPrincipal.FindByIdentity(context, userSID.ToString()))
{
if (userPrincipal != null)
{
return string.IsNullOrEmpty(userPrincipal.DisplayName) ? userPrincipal.Name : userPrincipal.DisplayName;
}
}
}
return null;
}
private static PrincipalContext GetPrincipalContext(string NTAccountName)
{
string domain = ExtractDomain(NTAccountName);
ContextType type = ContextType.Domain;
if (Environment.MachineName.Equals(domain, StringComparison.OrdinalIgnoreCase))
{
type = ContextType.Machine;
}
return new PrincipalContext(type, domain);
}
private static string ExtractDomain(string NTAccountName)
{
int index = NTAccountName.IndexOf('\\');
if (index != -1)
{
return NTAccountName.Substring(0, index);
}
return null;
}
#endregion
}