Benutzer Authentifizierung ist im Web Umfeld ein gängiges Geschäft. Zunächst stellt sich die Frage welches Schema zum Einsatz kommen soll. Da die ASP.NET Forms Authentifizierung naturemäß für Services ungeeignet ist, bleibt eigentlich nur Basic. Windows scheidet wegen der Clients aus, OAuth ist für viele Fälle zu komplex.
Als weitere Frage ist zu klären, wer der die Benutzerdaten hält. Wenn man beim IIS einfach das Basic Authentifzierungsmodul aktiviert, setzt dieses Windows AD voraus. Es ist eher ungeschickt Benutzerdaten mehr oder weniger fest ins Hostsystem zu verdrahten.
Das ASP.NET Membership System hat sich seit vielen Jahren, in Verbindung mit der Forms Authentifizierung, als passende Lösung erwiesen. Die Benutzerdaten liegen abhängig vom Provider in einer Datenbank. Der bis ASP.NET 3.5 Standard Provider ist der ASPNETSQLMembershipprovider. Der Datenbank Connection String lautet dazu LocalSQLServer. In der Standardinstallation wird Systemweit per machine.config eine lokale ASPNETDB im app_data Verzeichnis jeder Web Application vorausgesetzt.
Das ist natürlich eher unwahrscheinlich, das jede Website einen eigene Benutzerverwaltung hat. Deshalb wird der Connection String im folgenden auf eine Datenbank in einen SQL Server umgebogen.
1: <connectionStrings>
2: <remove name="LocalSqlServer" />
3: <add name="LocalSqlServer" connectionString="Data Source=localhost;Initial
Catalog=aspnetdb;Integrated Security=True"
4: providerName="System.Data.SqlClient" />
In der Praxis werden Sie nicht mit integrated Security authentifizieren, sondern mit speziellen Credentials die man zb im SQL Server verwalten kann.
Weiters muss man wissen, das mit ASP.NET 4 ein neuer default Membershipprovider existiert, der wohl ein anderes Datenbank Schema nutzt. Da ich hier eine bestehende Web Anwendung erweitern möchte wird der Provider wiederum in der web.config umgestellt.
1: <membership defaultProvider="AspNetSQLMembershipProvider">
2: </membership>
Als nächstes soll ein Modul in die IIS Pipeline gehängt werden, das sich um die Basic Authentifizierung kümmert.
1: <modules>
2: <add name="myBasicAuth" type="hateoas1.BasicAuthHttpModule" />
3: </modules>
4: </system.webServer>
Um sicherzustellen das nur dieses Modul und niemand anderer auch noch Authentifizierung versucht fehlt noch ein Eintrag in der web.config.
1: <authentication mode="None" />
Soweit die Konfiguration. Nun braucht es noch die Logik, also die Klasse die das Modul BasicAuthHttpModule beinhaltet.
Die Basic Authentifzierung sendet Username und Passwort im Header mit. Dabei wird diese User:Passwort Kombination nach Base64 codiert um Sonderzeichen transportieren zu können. Unsere Logik decodiert dies und ruft die Membership Validierung auf.
Ein ganz wesentlicher Bestandteil der Kommunikation zwischen Browser und Web Server ist, das der Webserver bei einem nicht authentifizierten Zugriff mit 401 antwortet und dabei das gewünschten Authentifzierungsverfahren mitschickt.
1: Imports System.Threading
2: Imports System.Security.Principal
3: Imports System.Net.Http.Headers
4:
5:
6: Public Class BasicAuthHttpModule
7: Implements IHttpModule
8:
9: Private Const Realm As String = "MyRealm"
10:
11: Public Sub Init(context As HttpApplication) Implements IHttpModule.Init
12: AddHandler context.AuthenticateRequest, AddressOf OnApplicationAuthenticateRequest
13: AddHandler context.EndRequest, AddressOf OnApplicationEndRequest
14: End Sub
15:
16: Private Shared Sub SetPrincipal(principal As IPrincipal)
17: Thread.CurrentPrincipal = principal
18: If HttpContext.Current IsNot Nothing Then
19: HttpContext.Current.User = principal
20: End If
21: End Sub
22:
23:
24:
25:
26: Private Shared Function AuthenticateUser(credentials As String) As Boolean
27:
28: Try
29: Dim decoded As String = Encoding.UTF8.GetString(Convert.FromBase64String(credentials))
30: Dim user As String = Left(decoded, decoded.IndexOf(":"))
31: Dim password As String = decoded.Substring(decoded.IndexOf(":") + 1)
32: If Membership.ValidateUser(user, password) Then
33: Dim identity = New GenericIdentity(user)
34: SetPrincipal(New GenericPrincipal(identity, Nothing))
35: Return True
36: End If
37: Catch generatedExceptionName As FormatException
38: ' böse
39:
40:
41: End Try
42: Return False
43: End Function
44:
45: Private Shared Sub OnApplicationAuthenticateRequest(sender As Object, e As EventArgs)
46: Dim authHeader = HttpContext.Current.Request.Headers("Authorization")
47: If authHeader IsNot Nothing Then
48: Dim authHeaderVal = AuthenticationHeaderValue.Parse(authHeader)
49: ' RFC 2617 sec 1.2, "scheme" name is case-insensitive
50: If authHeaderVal.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase)
AndAlso authHeaderVal.Parameter IsNot Nothing Then
51: AuthenticateUser(authHeaderVal.Parameter)
52: End If
53: End If
54: End Sub
55:
56: Private Shared Sub OnApplicationEndRequest(sender As Object, e As EventArgs)
57: Dim response = HttpContext.Current.Response
58: If response.StatusCode = 401 Then
59: response.Headers.Add("WWW-Authenticate", String.Format("Basic realm=""{0}""", Realm))
60: End If
61: End Sub
62:
63: Public Sub Dispose() Implements IHttpModule.Dispose
64: End Sub
65:
66:
67: End Class
68:
Ein kleines Stück fehlt nun noch. Der Zugriff auf eine ASPX Seite muss noch reglementiert werden. Dies kann in der Web.Config geschehen. Die Syntax mit dem ? steht für anonymous. * schliesst alle ein.
<location path="webform2.aspx">
<system.web>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</location>
Ruft nun der Browser diese Seite auf kann man folgenden Netzwerk Traffic in Fiddler beobachten.
Dabei zeigt der Browser nach erhalt der 401 HTTP Statusmeldung den Login Dialog an. Der Benutzer füllt diesen aus und erzeugt damit den zweiten Request.
Umgemünzt auf einen Service request, erweist sich ein Browser Login Dialog als eher hinderlich. Der Client wird in diesem Fall gleich den Authorization Header mitschicken oder er erhält eine Exception.
Außerdem wird vermutlich der Entwickler und nicht der Admin festlegen wollen, welche Methode nun nur durch Autorisierte Requests erfolgen darf. Entsprechend sollte im Code festgelegt werden ob man auch Anonym auf die Ressource zugreifen darf.
Am sinnigsten erscheint mir der Weg über ein Methodenattribut in der ASPX Codebind Methode.
1: <PrincipalPermission(SecurityAction.Demand, Authenticated:=True)>
Natürlich kann man auch im Code User.Identity.IsAuthenticated abfragen.
Das ganze Konzept lässt sich auch sehr einfach auf einen Token basierten Ansatz erweitern. Aber das ist wieder eine andere Geschichte.