poniedziałek, 19 maja 2014

ASP.NET Web API - basic authentication

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:

  1. 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".
  2. 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: 
    1. sformatowane jako string: "name:password"
    2. base64-encoded
    3. 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;
}

Brak komentarzy:

Prześlij komentarz