Ich habe die letzten Tage überprüft, wie man mit der aktuellen Version von SignalR [1] (0.4-85) Security-Szenarien implementieren kann. Dabei habe ich die folgenden Möglichkeiten erfolgreich implementiert.
Hosting in ASP.NET + Zugriff über JavaScript aus derselben Site heraus
In diesem Szenario kann man Forms-based Security verwenden. Bei jedem Zugriff auf die in derselben Web-Site gehostete SignalR-Instanz wird das ausgestellte Security-Token als Cookie übertragen. Deswegen muss lediglich innerhalb der einzelnen Methoden der aktuelle Principal (System.Threading.Thred.CurrentPrincipal) geprüft werden. Diese Logik kann, wie weiter unten beschrieben, auch in der globalen Methode Application_AuthenticateRequest (global.asax) erfolgen.
Hosting in ASP.NET + Zugriff über C#-Client
In diesem Fall kann man sich entweder die in IIS implementierte HTTP Authentifizierung verlassen, oder eine benutzerdefinierte Art der Authentifizierung implementieren. Ersteres ist zwar einfach, veranlasst jedoch immer eine Prüfung gegen Active Directory-Benutzer. Mit benutzerdefinierten IIS-Modulen kann dieses Verhalten zwar angepasst werden, allerdings werden sich die meisten Admins weigern, benutzerdefinierte IIS-Module einzusetzen.
Bei der benutzerdefinierten Art der Authentifizierung ist zu beachten, dass es die aktuelle Version des C#-Clients nur bedingt erlaubt, Daten via HTTP zu übersenden. Angeboten wird die Übertragung von Credentials als Header sowie die Übertragung von Cookies. Benutzerdefinierte Header können derzeit leider noch nicht übertragen werden. Dies macht die Implementierung erweiterter Security-Szenarien, bspw. mittels OAuth, nicht möglich.
Das nachfolgende Listing zeigt, wie man HTTP BASIC auf diese Weise manuell implementieren kann. Somit kann gegen Benutzerpools jenseits von AD geprüft werden.
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
var path = this.Request.Path.ToLower();
if ( (path.StartsWith("/signalr/") || path.StartsWith("/raw/")) && !path.StartsWith("/signalr/hubs") ) OnAuthorization(this.Context);
}
public void OnAuthorization(HttpContext context)
{
var request = context.Request;
var response = context.Response;
var header = request.Headers["Authorization"];
// Optimierungsmöglichkeit: Gegen Session-Cookie prüfen
if (string.IsNullOrEmpty(header) || !header.Trim().ToLower().StartsWith("basic")) {
response.Headers["WWW-Authenticate"] = "Basic";
response.StatusCode = 401; //Unauthorized
return;
}
header = header.Trim();
header = header.Substring(5); // Basic wegschneiden ...
header = header.Trim();
header = Encoding.UTF8.GetString(Convert.FromBase64String(header));
var index = header.IndexOf(:);
if (index == -1)
{
response.Headers["WWW-Authenticate"] = "Basic";
response.StatusCode = 401; //Unauthorized
response.End();
return;
}
var user = header.Substring(0, index);
var password = header.Substring(index + 1);
if ((user != "Max" || password != "P@ssw0rd") && (user != "Susi" || password != "P@ssw0rd"))
{
response.Headers["WWW-Authenticate"] = "Basic";
response.StatusCode = 401; //Forbidden
response.End();
}
// Optimierungsmöglichkeit: Session-Cookie ausstellen
// Principal setzen
var principal = new GenericPrincipal(new GenericIdentity(user), new string[] { });
System.Threading.Thread.CurrentPrincipal = principal;
context.User = principal;
}
private static string RemovePrefix(string str, string prefix) {
if (str.StartsWith(prefix)) {
str = str.Substring(prefix.Length, str.Length - prefix.Length);
}
return str;
}
Das nachfolgende Listing zeigt den dazugehörigen Code, welcher den C#-Client verwendet. Es handelt sich dabei um eine für diesen Zweck erweiterte Version eines der mit SignalR mitgelieferten Beispiele.
var hubConnection = new HubConnection("http://localhost:40476/");
hubConnection.Credentials = new NetworkCredential("Max", "P@ssw0rd");
var demo = hubConnection.CreateProxy("SignalR.Samples.Hubs.DemoHub.DemoHub");
demo.On("invoke", i =>
{
Console.WriteLine("{0} client state index -> {1}", i, demo["index"]);
});
hubConnection.Start().Wait();
demo.Invoke("multipleCalls").ContinueWith(task => {
Console.WriteLine(task.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
Self-Hosting + Zugriff über C#-Client
In Self-Hosting-Szenarien kann die Callback-Methode OnProcessRequest zur Prüfung verwendet werden.
string url = "http://localhost:8081/";
var server = new Server(url);
server.MapHubs("/signalr");
server.OnProcessRequest = (ctx) =>
{
Console.WriteLine("OnProcessRequest");
if (ctx.Request.Cookies["Password"].Value != "P@ssw0rd")
throw new SecurityException();
};
server.Start();
Console.WriteLine("Server running on {0}", url);
Console.ReadKey();
In diesem Fall wird das Passwort als Cookie übergeben. Das ist notwendig, weil der C#-Client in der aktuellen Version noch keine ausgehenden Header unterstützt. Eine Betrachtung des Quellcodes von SignalR legt jedoch nahe, dass dies einfach zu implementieren ist, weswegen davon auszugehen ist, dass künftige Versionen von SignalR das Übersenden von Headern unterstützen werden.
Die Implementierung des Clients entspricht dabei der oben gezeigten, wobei das Passwort in diesem Fall als Cookie zu übergeben ist. Dabei ist darauf zu achten, dass als Domain-Name (hier localhost) jener, unter dem der Service läuft, übergeben wird. Ansonsten wird das Cookie nicht übertragen.
hubConnection.CookieContainer = new CookieContainer();
hubConnection.CookieContainer.Add(new Cookie("Password", "P@ssw0rd", "", "localhost"));