Windows Azure AppFabric – autoryzacja dostępu do operacji WCF za pomocą tokenów SWT 

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2011-04-11

Pobierz i uruchom

Wprowadzenie, zasada działania

ACS może służyć również do autoryzacji użytkowników chcących wywołać konkretne metody na usłudze WCF. Mechanizm ACS jest oparty na żądaniach (claim-based security). Użytkownik, chcąc uzyskać dostęp do danych zasobów, dostarcza pewne informacje do ACS (np. IssuerKey), a następnie otrzymuje token SWT (Simple Web Token) wraz z żądaniami, na podstawie którego może uzyskać dostęp do różnych zasobów. W liście żądań  można zawrzeć nazwy metod WCF, które użytkownik może wywoływać:

ACSAuthorizationManager jest klasą odpowiedzialną za parsowanie tokenu i przyznawanie lub odmawianie praw dostępu do RESTful WCF.  Dzięki prostej budowie tokeny SWT są doskonałym rozwiązaniem dla usług WCF opartych na architekturze REST.

Implementacja WCF

Podstawowym zadaniem jest oczywiście implementacja usługi WCF oraz klasy ACSAuthorizationManager:

1.     Tworzymy nowy, pusty projekt WCF Service Library.

Dodajemy klasę ACSAuthorizationManager, którą można znaleźć w WAPTK (Windows Azure Platform Training Kit):

public class ACSAuthorizationManager : ServiceAuthorizationManager
{
    private TokenValidator validator;
    private string requiredClaimType;
    public ACSAuthorizationManager(string issuerName, string trustedAudienceValue, byte[] trustedSigningKey, string requiredClaimType)
    {
        this.validator = new TokenValidator(issuerName, trustedAudienceValue, trustedSigningKey);
        this.requiredClaimType = requiredClaimType;
    }
    protected override bool CheckAccessCore(OperationContext operationContext)
    {
        // get the authorization header
        string authorizationHeader = WebOperationContext.Current.IncomingRequest.Headers[HttpRequestHeader.Authorization];
        if (string.IsNullOrEmpty(authorizationHeader))
        {
            SetUnauthorizedResponse();
            return false;
        }
        // check that it starts with 'WRAP'
        if (!authorizationHeader.StartsWith("WRAP "))
        {
            SetUnauthorizedResponse();
            return false;
        }
        string[] nameValuePair = authorizationHeader.Substring("WRAP ".Length).Split(new char[] { '=' }, 2);

        if (nameValuePair.Length != 2 ||
            nameValuePair[0] != "access_token" ||
            !nameValuePair[1].StartsWith("\"") ||
            !nameValuePair[1].EndsWith("\""))
        {
            SetUnauthorizedResponse();
            return false;
        }
        // trim the leading and trailing double-quotes
        string token = nameValuePair[1].Substring(1, nameValuePair[1].Length - 2);
        // validate the token
        if (!this.validator.Validate(token))
        {
            SetUnauthorizedResponse();
            return false;
        }
        // check for an action claim and get the value
        Dictionary<string, string> claims = this.validator.GetNameValues(token);

        string actionClaimValue;
        if (!claims.TryGetValue(this.requiredClaimType, out actionClaimValue))
        {
            SetUnauthorizedResponse();
            return false;
        }
        // check for the correct action claim value
        string requiredActionClaimValue = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.RelativePathSegments.First();
        string[] actions = actionClaimValue.Split(',');
        if (!actions.Any(a => requiredActionClaimValue.Equals(a, StringComparison.OrdinalIgnoreCase)))
        {
            SetUnauthorizedResponse();
            return false;
        }
        return true;
    }
    private static void SetUnauthorizedResponse()
    {
        WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Unauthorized;
        WebOperationContext.Current.OutgoingRequest.Headers.Add("WWW-Authenticate", "WRAP");
    }
}

W powyższym kodzie warto zwrócić uwagę na kilka fragmentów. Przyjrzyjmy się sposobowi inicjalizacji klasy:

public class ACSAuthorizationManager : ServiceAuthorizationManager
{
    private TokenValidator validator;
    private string requiredClaimType;

    public ACSAuthorizationManager(string issuerName, string trustedAudienceValue, byte[] trustedSigningKey, string requiredClaimType)
    {
        this.validator = new TokenValidator(issuerName, trustedAudienceValue, trustedSigningKey);
        this.requiredClaimType = requiredClaimType;
    }
}

W konstruktorze inicjalizujemy m.in. TokenValidator, który jest odpowiedzialny za weryfikację otrzymanego tokena (czy został wysłany w prawidłowym formacie oraz czy po drodze nie został zmodyfikowany np. przez hakera). Implementację TokenValidator można znaleźć również w WAPTK – nie jest to standardowa klasa i należy ją skopiować z SDK. Konstruktor walidatora przyjmuje następujące parametry:

  • trustedTokenIssuer (issuerName) –nazwa identyfikująca encję odpowiedzialną za wydanie przetwarzanego tokenu. W przypadku tokenów wydawanych przez ACS będzie to ‘https://{0}.accesscontrol.windows.net/’ ,  gdzie {0} oznacza Service namespace,
  • trustedAudienceValue – adres np. usługi, dla której token został nadany. Zmiana tej wartości może sugerować, że pakiet został po drodze zmodyfikowany i powinien zostać odrzucony,
  • trustedSigningKey – klucz (w tym przypadku uzyskany od AppFabric ACS), którym przychodzący token jest podpisany.

Z kolei requiredClaimType zawiera typ żądania, na którym chcemy pracować w danym ACSAuthorizationManager. Konstruktor więc po prostu inicjalizuje TokenValidator oraz ustawia  requiredClaimType.

Najważniejsza metodą jest CheckAccessCore, która odpowiedzialna jest za przyznawanie lub odmawianie dostępu do operacji WCF:

protected override bool CheckAccessCore(OperationContext operationContext)
    {
        // get the authorization header
        string authorizationHeader = WebOperationContext.Current.IncomingRequest.Headers[HttpRequestHeader.Authorization];
        if (string.IsNullOrEmpty(authorizationHeader))
        {
            SetUnauthorizedResponse();
            return false;
        }

        // check that it starts with 'WRAP'
        if (!authorizationHeader.StartsWith("WRAP "))
        {
            SetUnauthorizedResponse();
            return false;
        }

        string[] nameValuePair = authorizationHeader.Substring("WRAP ".Length).Split(new char[] { '=' }, 2);

        if (nameValuePair.Length != 2 ||
            nameValuePair[0] != "access_token" ||
            !nameValuePair[1].StartsWith("\"") ||
            !nameValuePair[1].EndsWith("\""))
        {
            SetUnauthorizedResponse();
            return false;
        }

        // trim the leading and trailing double-quotes
        string token = nameValuePair[1].Substring(1, nameValuePair[1].Length - 2);

        // validate the token
        if (!this.validator.Validate(token))
        {
            SetUnauthorizedResponse();
            return false;
        }

        // check for an action claim and get the value
        Dictionary<string, string> claims = this.validator.GetNameValues(token);

        string actionClaimValue;
        if (!claims.TryGetValue(this.requiredClaimType, out actionClaimValue))
        {
            SetUnauthorizedResponse();
            return false;
        }

        // check for the correct action claim value
        string requiredActionClaimValue = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.RelativePathSegments.First();
        string[] actions = actionClaimValue.Split(',');
        if (!actions.Any(a => requiredActionClaimValue.Equals(a, StringComparison.OrdinalIgnoreCase)))
        {
            SetUnauthorizedResponse();
            return false;
        }

        return true;
    }

W skrócie wykonuje ona następujące operacje:

  1. pobiera nagłówek zawierający dane odpowiedzialne za autoryzację i sprawdza, czy jest on zgodny z protokołem WRAP,
  2. waliduje token pobrany z nagłówka,
  3. jeśli token jest poprawny, pobiera wszystkie żądania (claim) i sprawdza, czy wśród nich znajduje się aktualnie wywoływana metoda (operacja). Jeśli na liście żądań nie ma aktualnie wywoływanej metody, prawo dostępu jest odmawiane (za pomocą metody pomocniczej SetUnauthorizedResponse), w przeciwnym wypadku metoda zwraca true i tym samym dostęp zostaje nadany.

Pozostało już tylko zdefiniowanie samej usługi. Dla celów testowych należy stworzyć dwie metody – o nazwach Read oraz Write, do których dostęp będzie ograniczany na podstawie przychodzącego tokenu:

public class DataBaseRepository : IDatabaseRepository
{
        public string Read()
        {
            return "OK";
        }
        public void Write(string data)
        {
            //
        }
    }

Hosting WCF

Po implementacji można przejść do hostingu WCF. Proces niewiele będzie się jednak różnił od hostowania klasycznej usługi. Zdefiniujmy najpierw jednak stałe, które będziemy wykorzystywać do konfiguracji usługi:

   private const string TokenPolicyKey = “";
private const string Audience = "https://localhost/Service"
private const string RequiredClaimType = "action";

RequiredClaimType ma wartość „action” – żądania o nazwie „action” będą właśnie przechowywały listę dozwolonych operacji. Audience to z kolei adres usługi, dla której będzie wydawany token.  TokenPolicyKey zostanie zdefiniowany wkrótce podczas konfiguracji ACS.

Następnie przechodzimy do samego hostingu:

   WebHttpBinding binding = new WebHttpBinding(WebHttpSecurityMode.None);
WebServiceHost host = new WebServiceHost(typeof(DataBaseRepository));
host.AddServiceEndpoint(typeof(IDatabaseRepository), binding, Audience);

host.Authorization.ServiceAuthorizationManager = new
ACSAuthorizationManager(string.Format(https://{0}.accesscontrol.windows.net/,”<wstaw prawidlowy Service Namespace”),
Audience,
Convert.FromBase64String(TokenPolicyKey), RequiredClaimType);

host.Open();

Pogrubioną czcionką zaznaczono fragment kodu, który się zmienia w stosunku do standardowego hostingu pokazanego w poprzednich artykułach. Jak widać, dodano wyłącznie inicjalizację ACSAuthorizationManager, która odpowiada za autoryzację WCF.

Konfiguracja ACS

Do konfiguracji ACS wykorzystamy narzędzie AcmBrowser.exe (dostępne w WAPTK):

  1. Po uruchomieniu wprowadzamy Service Namespace oraz Management Key (klucz dostępny jest w panelu webowych AppFabric)

  2. Klikamy na przycisku Load From Cloud, aby załadować konfigurację z ACS.

  3. Teraz najpierw tworzymy politykę tokena (Token Policy), która definiuje zbiór reguł odpowiedzialnych za autoryzację. Każdy Service Namespace może zawierać kilka takich polityk. Zaznaczamy więc węzeł Token Policies i wybieramy Create:

  4. Następnie w analogiczny sposób tworzymy Scope, który odpowiedzialny jest za skojarzenie wywoływanego URI z regułami autoryzacyjnymi:

  5. W kolejnym kroku tworzymy Issuer, czyli użytkownika usługi WCF. Na podstawie podanych parametrów (nazwa, klucz) będziemy rozpoznawać, kto się tak naprawdę łączy z usługą i jakie prawa powinniśmy mu nadać. Wprowadzone poniższe dane będą potrzebne aplikacji klienckiej. W ramach artykułu stworzymy dwa wpisy w Issuers – jeden dla klienta posiadającego prawa do zapisu i odczytu oraz jeden dla klienta wyłącznie z prawami odczytu (będzie mógł wywoływać wyłącznie metodę Read).

  6. Pozostało już tylko zdefiniowanie reguł dla danego scope. Dla reguły nadającej prawa wywołania wyłącznie metody Read konfiguracja powinna wyglądać następująco:

 

Gdy przyjdzie zapytanie do serwera z dopasowanymi żądaniami Input Claim, wtedy zostaną mu nadane żądania Output Claim. Innymi słowy, gdy klient identyfikujący się jako Issuer o nazwie ReadOnly oraz żądanie typu Issuer posiada wartość ReadOnly, wtedy zostanie mu nadane żądanie typu action o wartości Read. Żądanie Issuer jest zawsze zdefiniowane przez ACS i przyjmuje jako wartość po prostu nazwę danego Issuer. Z kolei po żądaniu action usługa rozpoznaje, czy klient ma odpowiednie prawa.

Konfiguracja dla użytkownika posiadającego prawa do zapisu i odczytu powinna być już w pełni zrozumiała – tworzymy dwie osobne reguły, które nadają kolejno prawa odczytu i zapisu:

  1. Klikamy Save to Cloud, aby zapisać zmiany w ACS.
  2. Musimy jeszcze uzyskać klucz tokena. W tym celu klikamy na Token Policies i kopiujemy do schowka zaznaczony klucz:

 

Następnie ustawiamy wartość stałej TokenPolicyKey (została zdefiniowana w poprzedniej sekcji) na powyższy klucz.

Aplikacja kliencka

Pozostało już tylko przetestować zaimplementowane rozwiązanie, tworząc odpowiedniego klienta:

  1. Tworzymy nową, pustą aplikację konsolową.

  2. Definiujemy następujące stałe:

       private const string ServiceNamespace = "wstaw prawidlowa wartosc";
    private const string IssuerName = "ReadOnly";
    private const string IssuerKey = " wstaw prawidlowa wartosc ";
    private const string AcsHostName = "accesscontrol.windows.net";
    private const string Audience = "https://localhost/Service";

    Wartość ServiceNamespace z pewnością jest już znana, np. z panelu Azure. Wartości IssuerName oraz IssuerKey tym razem bierzemy z wartości zdefiniowanych w poprzedniej sekcji podczas tworzenia Issuer. Jeśli chcemy przetestować klienta, który posiada prawa wyłącznie odczytu, jako IssuerName podajemy ReadOnly, a IssuerKey przypisujemy odpowiedni klucz z AcmBrowser. Audience powinno zawierać wartość adresu usługi, dla którego wydawany jest token.

  3. Jak pamiętamy z wprowadzenia, aplikacja kliencka najpierw musi uzyskać token od ACS:

       private static string GetACSToken()
    {
    WebClient client = new WebClient();
    client.BaseAddress = string.Format("https://{0}.{1}", ServiceNamespace, AcsHostName);
    NameValueCollection values = new NameValueCollection(); values.Add("wrap_name", IssuerName);
    values.Add("wrap_password", IssuerKey);
    values.Add("wrap_scope", Audience);
    byte[] responseBytes = client.UploadValues("WRAPv0.9", "POST", values);
    string response = Encoding.UTF8.GetString(responseBytes);
    return response.Split('&').Single(value => value.StartsWith("wrap_access_token=",, StringComparison.OrdinalIgnoreCase))
                         .Split('=')[1];
    }

Pobranie tokenu za pomocą protokołu WRAP jest bardzo proste. Wystarczy wysłać na odpowiedni adres (jeśli ServiceNamespace nazywa się msdnpoland, wtedy będzie to https://msdnpoland. accesscontrol.windows.net) zapytanie typu POST z odpowiednimi parametrami.

Pozostało już tylko wywołać metodę WCF:

   WebHttpBinding binding = new WebHttpBinding(WebHttpSecurityMode.None);
WebChannelFactory<IDataBaseRepositoryChannel> channelFactory = new WebChannelFactory<IDataBaseRepositoryChannel>(binding,new Uri(Audience));
IDataBaseRepositoryChannel channel = channelFactory.CreateChannel();
using (new OperationContextScope(channel as IContextChannel))
{
string authHeaderValue = string.Format("WRAP access_token=\"{0}\"", HttpUtility.UrlDecode(GetACSToken()));
WebOperationContext.Current.OutgoingRequest.Headers.Add("authorization",     authHeaderValue);
   string text=channel.Read();
  }

Na dodatkowy komentarz zasługuje pogrubiony fragment kodu. Przed wysłaniem wiadomości do usługi należy dołączyć odpowiedzialny za autoryzację nagłówek zawierający token SWT. Klient najpierw pobiera token z ACS, a następnie przesyła go w nagłówku WRAP podczas wysyłania wiadomości do WCF. Usługa wtedy pobierze nagłówek, sprawdzi poprawność tokena i nada bądź odmówi dostępu do metody, bazując na żądaniach (klasa ACSAuthorizationManager).

Zakończenie

Przedstawiony scenariusz oparty został na prostych tokenach SWT oraz protokole WRAP. W prawdziwym środowisku warto pokusić się o wykorzystanie również Service Bus, a nie komunikację bezpośrednią między klientem a WCF. Za pomocą żądań można zaimplementować różną logikę autoryzacyjną – dostęp do metod to tylko przykładowy scenariusz. ACS ofertuje interfejs zgodny z architekturą REST, który pozwala na uzyskiwanie tokenów SWT – wystarczy wysłać odpowiednie wartości WRAP.

 


          

Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.