Přihlášení pomoci Windows Authentizace

Jan Holan       17. 4. 2015       Bezpečnost, .NET, WIF       4065 zobrazení

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 jan.holan@h2net.cz.

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
}

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback