Basic authentication (ASP.NET Web API)
Tworzenie serwisu REST z wykorzystaniem ASP.NET Web API jest stosunkowo proste. Gdy nasz serwis jest już wystawiony na świat, prawdopodobnym stanie się fakt odpowiedniego jego zabezpieczenia. Jednym z najprostszych i najbardziej standardowym sposobem jest zabezpieczenie takiego serwisu z wykorzystaniem basic authentication.
Basic authentication jest najprostszym i najłatwiejszym mechanizmem autentykacji poprzez zastosowanie pary "username/password" w "plaintext".
Basic authentication działa w następujący sposób:
- Jeżeli żądanie "request" wymaga autentykacji, serwer zwraca status code "401" (unauthorized). Odpowiedź taka zawiere "WWW-uthenticate header", które mówi, że serwer wspiera "basic authentication".
- Klient wysyła kolejne żądanie ("request") ale tym razem już z odpowiednimi "credentials" umieszczonymi w "WWW-Authenticate header". Charakteryzują je następujące właściwości:
- sformatowane jako string: "name:password"
- base64-encoded
- brak szyfrowania
Basic authentication wykonywany jest w kontekście pewnej domeny ("realm"). Serwer dodaje nazwę tej domeny do nagłówka WWW-Authenticate header.
WAŻNE:
- Ponieważ "credentials", które przesyłane są od klienta na serwer nie są szyfrowane, stosowanie basic authentication jest sensowne i bezpieczne jedynie jeśli komunikacja jest bezpieczna i przebiega przez szyfrowaną wersję protokołu HTTP czyli HTTPS.
- Basic authentication nie jest odporne na ataki CSRF (cross-site request forgery)
Niniejszy post przedstawia jak zaimplementować taką autentykację w naszym ASP.NET Web API (post nie dotyczy ASP.NET Web API 2, które jest prostsze i zawiera już zaimplementowane techniki autentykacji serwisu bez konieczności pisania dodatkowego kodu).
AuthorizeAttribute
Implementacja basic authentication będzie polegała na stworzeniu własnego atrybutu do autoryzacji. W pierwszej kolejności jednak musimy napisać metodę pomocniczą, która wyłuska nam credentials (nazwę użytkownika i hasło) z wiadomości, która przychodzi do serwera. Metoda taka może wyglądać następująco:
private Credentials ParseAuthorizationHeader(string authHeader)
{
string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(authHeader)).Split(new[] { ':' });
if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[1]))
return null;
return new Credentials() { Username = credentials[0], Password = credentials[1], };
}
Teraz wystarczy napisać naszą klasę, która będzie dziedziczyła po "AuthorizeAttribute":
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
private const string BasicAuthResponseHeader = "WWW-Authenticate";
private const string BasicAuthResponseHeaderValue = "Basic";
protected IPrincipal CurrentUser
{
get { return Thread.CurrentPrincipal as IPrincipal; }
set { Thread.CurrentPrincipal = value as IPrincipal; }
}
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
try
{
AuthenticationHeaderValue authValue = actionContext.Request.Headers.Authorization;
Credentials parsedCredentials = ParseAuthorizationHeader(authValue.Parameter);
if (parsedCredentials != null)
{
this.CurrentUser = new DummyPrincipalProvider().CreatePrincipal(parsedCredentials.Username, parsedCredentials.Password);
if (!CurrentUser.Identity.IsAuthenticated)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Forbidden);
actionContext.Response.Headers.Add(BasicAuthResponseHeader, BasicAuthResponseHeaderValue);
return;
}
}
}
catch (Exception ex)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
actionContext.Response.Headers.Add(BasicAuthResponseHeader, BasicAuthResponseHeaderValue);
return;
}
}
}
W powyższej implementacji korzystam z klasy "DummyPrincipalProvider", która jest dziedziczy z IPrincipalProvider i jest moją "fake'ową" implemetacją dostarczyciela tożsamości. Wygląda następująco:
public class DummyPrincipalProvider : IProvidePrincipal
{
private const string Username = "username";
private const string Password = "password";
public IPrincipal CreatePrincipal(string username, string password)
{
if (username != Username || password != Password)
{
return null;
}
var identity = new GenericIdentity(Username);
IPrincipal principal = new GenericPrincipal(identity, new[] { "User" });
return principal;
}
}
W tym miejscu jednak powinna być implemetacja odpowiedniego dortarczyciela tożsamości, np. jeśli dane użytkowników przechowywane są bazie to wtedy najlepszym rozwiązaniem będzie użycie gołego ADO.NET lub z wykorzystaniem EntityFramework do pobrania takich danych z bazy danych.
Mając tak napisany atrybut, wystarczy teraz, że klasę lub metodę, którą chcemy zabezpieczyć udekorujemy tym atrybutem:
[CustomAuthorizeAttribute]
[HttpGet, HttpHead]
public Product Get(int id)
{
var product = productRepository.Get(id);
if (product == null)
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
return product;
}