Basic Authentication selbst gehäkelt

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.

image

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.

Pingbacks and trackbacks (1)+

Kommentare sind geschlossen