Integration in Windows Security mit .NET 2.0 - Teil 2

Veröffentlicht: 17. Jan 2006
Von Dominick Baier

Im ersten Teil dieser Artikelserie haben Sie gesehen, wie Sie den Windows Token eines Benutzers inspizieren können, um z.B. dessen Namen und Gruppenzugehörigkeiten in Erfahrung zu bringen. Bis jetzt wissen wir allerdings nur, wie man auf den Token des aktuell angemeldeten Benutzers zugreifen kann. Dieser Artikel gibt einen Überblick über verschiedene andere Möglichkeiten, um an einen Token zu gelangen.

Auf dieser Seite

 LogonUser
 Protocol Transition
 ASP.NET
 Wo stehen wir?
 Der Autor

LogonUser

Der Win32 API LogonUser erwartet den Namen eines Benutzers, eine Autorität (z.B. Machine oder Domäne) sowie ein Passwort und liefert einen Pointer auf ein unmanaged Token-Handle zurück. Danach kann der Token benutzt werden, um ein WindowsIdentity Objekt zu erzeugen. Dies Methode wird oft verwendet, um das Passwort eines Benutzers zu überprüfen, z.B. in ASP.NET Forms Authentication: kommt ein null-pointer von LogonUser zurück, sind die Credentials ungültig. Andere Anwendungs-Gebiete sind, wenn Sie die Gruppenzugehörigkeiten eines anderen Benutzers in Erfahrung bringen bzw. temporär dessen Identität annehmen möchten. Dieser Vorgang wird Impersonierung genannt und in einem der nächsten Teile dieser Artikel-Serie genauer beleuchtet.

Wie bereits erwähnt, ist LogonUser ein Win32 API und kann einfach mittels Deklaration mit [DllImport] in Ihrem Programm verfügbar gemacht werden.

internal class NativeMethods
{
    [DllImport("advapi32.dll")]
    internal static extern int LogonUser(
            string lpszUsername, 
            string lpszDomain, 
            string lpszPassword, 
            LogonType dwLogonType, 
            ProviderType dwLogonProvider, 
            out IntPtr phToken);

    [DllImport("kernel32.dll")]
    internal static extern bool CloseHandle(IntPtr phToken);
}

enum LogonType
{
    LOGON32_LOGON_INTERACTIVE = 2,
    LOGON32_LOGON_NETWORK = 3,
    LOGON32_LOGON_BATCH = 4,
    LOGON32_LOGON_SERVICE = 5,
    LOGON32_LOGON_UNLOCK = 7,
    LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
    LOGON32_LOGON_NEW_CREDENTIALS = 9,
}

enum ProviderType
{
    LOGON32_PROVIDER_DEFAULT = 0,
    LOGON32_PROVIDER_WINNT35 = 1,
    LOGON32_PROVIDER_WINNT40 = 2,
    LOGON32_PROVIDER_WINNT50 = 3
}

Der LogonType enum bestimmt, welche Art von Logon durchgeführt werden soll. Am häufigsten wird hier wohl INTERACTIVE und NETWORK zum Einsatz kommen, was einer lokalen bzw. Netzwerk-Anmeldung entspricht. Die Sicherheits-Richtlinie der Maschine muss dies dem entsprechenden Benutzer auch gestatten, sonst schlägt LogonUser fehl.

Um z.B. die Credentials eines Benutzer zu prüfen, kann folgender Code verwendet werden:

public static bool ValidateUser(
string username, string password, string domain)
{
    // pointer auf standard wert initialisieren
    IntPtr token = IntPtr.Zero;

    try
    {
        NativeMethods.LogonUser(
            username, 
            domain, 
            password, 
            LogonType.LOGON32_LOGON_NETWORK, ProviderType.LOGON32_PROVIDER_DEFAULT, 
            out token);
     
        // wenn pointer nicht 0 ist, waren die credentials gültig
        return token != IntPtr.Zero;
    }
    finally
    {
        // wichtig: unmanaged resourcen wieder aufräumen
        NativeMethods.CloseHandle(token);
    }
}

Für den domain Parameter muss man entweder den Namen der Domäne oder der Maschine angeben, in der der Benutzer-Account existiert. Um die lokale Maschine anzusprechen (auf der der Code ausgeführt wird), kann man ein "." als Platzhalter verwenden. Folgende Hilfsroutine erleichtert den Umgang mit dem domain Parameter:

public static bool ValidateUser(string username, string password)
{
    // überprüfen ob benutzer im autorität\benutzername format 
    // übergeben wurde
    Regex rex = new Regex(@"^([\w]+)\\([\w]+)$");
    Match m = rex.Match(username);

    if (m.Success)
    {
        string domain = m.Groups[1].Value;
        string user = m.Groups[2].Value;
        return ValidateUser(user, password, domain);
    }
    else
    {
        // benutzen eine standard autorität, z.B. die lokale maschine
        return ValidateUser(username, password, ".");
    }
}

Um nun aus dem Token ein WindowsIdentity Objekt zu erzeugen, kann man einen der zahlreichen Konstruktoren verwenden:

WindowsIdentity id = new WindowsIdentity(
      token, 
      "LogonUser", 
      WindowsAccountType.Normal, 
      true);

Und danach ein WindowsPrincipal Objekt erzeugen, um die IsInRole Methode verwenden zu können.

WindowsPrincipal p = new WindowsPrincipal(id);

Vergessen Sie niemals den unmanaged Token Handle via CloseHandle zu schliessen. WindowsIdentity legt eine eigene Kopie des Handles im Konstruktor an, die ebenfalls zu gegebener Zeit wieder zerstört wird.

Hinweis: LogonUser benötigt unter Windows 2000/NT das "SeTcbPrivilege" (als Teil des Betriebssystems handeln). Dies ist ein sehr mächtiges Privileg und sollte normalerweise nicht an Server-Code vergeben werden. In Windows XP/2003 ist diese Limitierung aufgehoben.

Protocol Transition

Der große Nachteil der LogonUser Methode ist, das Sie das Passwort des Benutzers wissen müssen. Je weniger wir in Anwendungen mit Passwörtern zu tun haben (und diese eventuell sogar speichern müssen), desto besser. Protocol Transition ist ein Feature von Windows 2003, das es erlaubt, einen eingeschränkten Token eines Benutzers zu erlangen, ohne dessen Passwort kennen zu müssen. Dieser Token reicht vollkommen aus, um Gruppenprüfungen durchzuführen, ist aber nicht ohne weiteres impersonierbar, wie wir in der nächsten Folge dieses Artikels sehen werden.

Der Einsatz von Protocol Transition ist denkbar einfach. Ein spezieller Konstruktor von WindowsIdentity erwartet den Namen eines Benutzer im user@domain Format (auch UPN - User Principal Name genannt). Das resultierende WindowsIdentity Objekt kann wie gewohnt mit WindowsPrincipal gekoppelt werden.

WindowsIdentity ptId = new 
      WindowsIdentity("dominick@leastprivilege.home");
WindowsPrincipal p = new WindowsPrincipal(ptId);

if (p.IsInRole(@"LeastPrivilege\Developers"))
    Console.WriteLine("Developer, Developer, Developer");

Der Nachteil dieser Methode ist, dass die Voraussetzungen recht hoch sind. Der vorhergehende Code wird nur auf einer Windows Server 2003 Maschine, in einer Windows 2003 Funktionalitäts-Domäne und nur mit Domain-Accounts funktionieren. Sind diese Voraussetzungen gegeben, ist dies eine sehr eleganter Weg.

ASP.NET

In Intranet Web-Anwendungen, die auf Windows-Authentifizierung basieren, ist es auch möglich, auf den Token des authentifizierten Benutzers (sprich des Clients) zuzugreifen. Dafür sind einige vorbereitende Schritte notwendig:

  • Im IIS muss Windows-Authentifizierung für die Anwendung aktiviert sein

  • Die ASP.NET-Anwendung muss für Windows-Authentifizierung konfiguriert sein

Ist dies der Fall, übergibt IIS den Token, der das Resultat der Client Authentifizierung ist, an ASP.NET weiter und wird als User Property des Context Objekts abgelegt. Da ASP.NET mit verschiedenen Authentifizierungs-Mechanismen arbeiten kann, wurde ein allgemeiner Ansatz gewählt, und die Objekte müssen entsprechend gecastet werden. Das folgende Beispiel zeigt, wie man in ASP.NET an die WindowsIdentity gelangt:

protected void Page_Load(object sender, EventArgs e)
{
    WindowsPrincipal p = (WindowsPrincipal)Context.User;
    WindowsIdentity id = (WindowsIdentity)p.Identity;

    Response.Write(id.Name + "<br />");

    if (p.IsInRole(@"LeastPrivilege\Developers"))
        Response.Write("ASP.NET developer etc...");            
}

IIS ist übrigens "lazy by default", d.h. er authentifiziert nur, wenn dies benötigt ist. Es gibt zwei Möglichkeiten, die Authentifizierung für jeden Request zu erzwingen: durch Abschalten des anonymen Zugriffes in IIS oder das Verbot von anonymen Benutzern in ASP.NET mit dem <authorization> Element in web.config.

<authorization>
      <deny users="?" />
</authorization>

Wo stehen wir?

Der Windows Token enthält alle Security-relevanten Informationen über einen Windows Benutzer und ist z.B. die einzig zuverlässige Methode in komplexen Active Directory-Netzen, um Gruppenzugehörigkeiten zu ermitteln. Möchte man auf den Token des aktuell angemeldeten Benutzers zugreifen, benutzt man WindowsIdentity.GetCurrent. ASP.NET legt den Token des authentifizierten Clients in Context.User ab. LogonUser erlaubt es, einen Token zu erzeugen, wenn die Credentials (Benutzername und Passwort) bekannt sind, und Protocol Transition macht in Windows 2003 Domänen sogar das Passwort überflüssig. Nun kennen Sie einige Techniken, wie Sie die Windows-Benutzer- und Gruppenverwaltung in Ihre Anwendungen integrieren können. In den nächsten Teilen dieser Artikel-Serie schauen wir uns das Impersonierungs-/Delegations-Konzept an, das zusammen mit Kerberos auf diesen Tokens basiert.

Der Autor

Dominick Baier entwickelt ASP.NET- und Security-Kurse bei DevelopMentor, einer Entwickler-Schulungsfirma. Darüber hinaus unterstützt er Unternehmen beim Schreiben von sicheren verteilten Anwendungen. Als Referent kann man ihn auf diversen Konferenzen antreffen (BASTA, WinDev, DevWeek u.a.); Online findet man ihn auf seinem Blog unter www.leastprivilege.com.

Artikel-Serie " Integration in Windows Security mit .NET 2.0" im Überblick:


Anzeigen: