Windows Identity Foundation – integracja z ASP .NET.

Autor: Piotr Zieliński

Wymagane oprogramowanie:

 

Wprowadzenie

Windows Identity Foundation jest narzędziem zapewniającym ujednolicony model zabezpieczeń. Aplikacja stworzona za pomocą dostarczonego API jest całkowicie niezależna od sposobu uwierzytelniania. Problemem aplikacji niewykorzystujących WIF często jest zbyt sztywne powiązanie modeli uwierzytelnia i autoryzacji z kodem aplikacji. Ponadto każdy sposób uwierzytelniania wiąże się z użyciem innego API. Dzięki WIF programista zastosuje te same API w aplikacjach wykorzystujących różne modele uwierzytelniania. Zmiana modelu, opartego na loginie i haśle, na np. CardSpace jest bardzo prosta i nie wymaga modyfikacji kodu aplikacji.

Rysunek 1. Model oparty na STS.

WIF działa według modelu federacji. By uzyskać dostęp do zasobów, użytkownik posługuje się tokenem, a nie bezpośrednio danymi uwierzytelniającymi (np. loginem i hasłem). Strony chronione (tzw. Relying Party) w celu weryfikacji tokena łączą się z Security Token Service.

Model bezpieczeństwa WIF oparty jest również na tzw. żądaniach (Claims). Użytkownik wysyła do STS listę żądań, a serwer weryfikuje je przed wydaniem tokena. Żądaniem może być jakakolwiek informacja o użytkowniku: grupa, rola, imię, nazwisko, e-mail itp. Innymi słowy, żądania to po prostu zweryfikowane przez STS atrybuty użytkownika. Token wydany przez STS jest cyfrowo podpisany i wszelkie jego modyfikacje podczas transportu zostaną wykryte np. przez klienta.

Najpierw należy stworzyć aplikację ASP .NET z dwiema stronami: Default.aspx oraz SecurePage.aspx. Visual Studio musi być uruchomiony z uprawnieniami administratora.

Następnie można przejść do tworzenia STS służącego do wydawania tokenów:

  1. Klikamy prawym przyciskiem myszy na projekcie i z menu kontekstowego wybieramy Add STS reference.

    Rysunek 2. IDE wspiera tworzenie serwerów STS.

  2. W pierwszym oknie można ustawić ścieżkę do pliku konfiguracyjnego aplikacji oraz jej adres. Kreator sam powinien wykryć prawidłowe wartości. Jeśli jednak któreś z pól jest puste, należy wpisać ręcznie prawidłową ścieżkę do web.config oraz adres strony.

    Rysunek 3. Ścieżka do pliku konfiguracyjnego oraz adres strony WWW.

  3. Następnie w celu utworzenia STS wybieramy Create a new STS project in the current solution.

    Rysunek 4. Tworzenie STS.

  4. Po pojawieniu się okna podsumowującego klikamy na przycisk Finish. Do rozwiązania powinien zostać dodany nowy projekt zawierający STS.

    Rysunek 5. Na tym etapie rozwiązanie powinno składać się z aplikacji web oraz STS.

  5. Po uruchomieniu aplikacji web, WIF przekieruje nas do zewnętrznej strony logowania, czyli do STS.

    Rysunek 6. Autoryzacja odbywa się na zaufanym serwerze STS.

Dodawanie własnych żądań

Automatycznie wygenerowany STS zawiera tylko dwa żądania. Bardzo łatwo można jednak dodać własne:

  1. Otwieramy web.config aplikacji ASP .NET. Dodajemy dwa żądania wymagane przez aplikację kliencką:
<claimTypeRequired>
          <claimType type="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" optional="true" />
          <claimType type="https://schemas.microsoft.com/ws/2008/06/identity/claims/role" optional="true" />
          <claimType type="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth" optional="true"/>
</claimTypeRequired>
  1. W poprzednim kroku określiliśmy, jakich żądań wymaga aplikacja kliencka. Teraz należy zmodyfikować STS tak, aby wydawał te żądania. W tym celu otwieramy plik APP_CODE\\CustomSecurityTokenService.cs.
  2. Odnajdujemy metodę GetOutputClaimsIdentity, która służy do wydawania żądań. Po modyfikacji powinna wyglądać następująco:
protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope )
    {
        if ( null == principal )
        {
            throw new ArgumentNullException( "principal" );
        }

        ClaimsIdentity outputIdentity = new ClaimsIdentity();

        outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name ) );
        outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Manager" ) );
        outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.DateOfBirth, "5/5/1905"));

        return outputIdentity;
    }

Wykorzystywanie żądań

WIF zapewnia bardzo łatwy dostęp do żądań:

  1. W aplikacji web otwieramy plik źródłowy Default.aspx.cs.
  2. Niezbędne są dwie dodatkowe przestrzenie nazw:
using System.Threading;
using Microsoft.IdentityModel.Claims;
  1. W celu wyświetlenia żądania wystarczy wywołać:
IClaimsIdentity claimsIdentity = ((IClaimsPrincipal)(Thread.CurrentPrincipal)).Identities[0];

DateTime birthDay = DateTime.Parse(
(from c in claimsIdentity.Claims
where c.ClaimType == Microsoft.IdentityModel.Claims.ClaimTypes.DateOfBirth
select c.Value).FirstOrDefault());

Label1.Text = birthDay.ToString();

Interfejs IIdentity reprezentuje użytkownika. Z kolei IPrincipal określa relację między użytkownikiem a daną grupą lub rolą. Za pomocą IPrincipal można zatem sprawdzić, czy użytkownik należy do danej grupy (metoda IsInRole).

WIF wykorzystuje rozszerzone interfejsy IClaimsPrincipal oraz IClaimsIdentity umożliwiające dostęp do żądań:

Rysunek 7. Rozszerzenie podstawowych interfejsów, źródło: WIF SDK.

Wyświetlenie nazwy użytkownika wygląda identycznie jak w przypadku klasycznego ASP .NET Membership, a mianowicie:

this.txtName.Text = User.Identity.Name;

Analogicznie można ograniczyć dostęp do strony, by mieli go tylko użytkownicy należący do wskazanej grupy:

<location path="SecretPage.aspx">
          <system.web>
              <authorization>
                  <allow roles="Manager"/>
                  <deny users="*"/>
              </authorization>
          </system.web>
      </location>

Podany kod ma postać identyczną jak w aplikacji wykorzystującej ASP .NET Membership.

Aby się przekonać, że autoryzacja działa, wystarczy się zalogować, używając standardowych danych (login: Adam Carter). Strona Default.aspx wyświetli żądanie zawierające datę urodzin użytkownika. Prawo dostępu do SecretPage również zostanie nadane, ponieważ użytkownik Adam Carter należy do grupy Manager. Przetestujemy zachowanie aplikacji w przypadku, gdy użytkownik nie należy do grupy Manager. W tym celu należy zmienić nazwę nadawanej grupy w metodzie GetOutputClaimsIdentity (plik CustomSecurityTokenService.cs):

protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope )
    {
        if ( null == principal )
        {
            throw new ArgumentNullException( "principal" );
        }

        ClaimsIdentity outputIdentity = new ClaimsIdentity();

        outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name ) );
        //outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Manager" ) );
        outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Sales"));
        outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.DateOfBirth, "5/5/1955"));

        return outputIdentity;
    }

Po uruchomieniu aplikacji i zalogowaniu się, przekonamy się, że dostęp nie został przyznany:

Rysunek 8. Dostęp do SecretPage przyznawany jest wyłącznie użytkownikom należącym do grupy Manager.

Własny moduł autoryzacji

Jak już pokazano, dostępem do poszczególnych stron można zarządzać tak samo jak w ASP .NET Membership (kwestia kilku wpisów w pliku konfiguracyjnym). Ponadto przyznawanie dostępu na podstawie żądań łatwo realizować ręcznie w kodzie.

WIF wprowadza możliwość definiowania własnych polityk bezpieczeństwa. Wystarczy stworzyć klasę dziedziczącą po ClaimsAuthorizationManager. Następnie zasady przyznawania praw należy umieścić w metodzie CheckAccess. Istnieje również możliwość definiowania praw w pliku konfiguracyjnym aplikacji. Ogromnie ułatwia to pracę, ponieważ konkretne reguły zmodyfikujemy w pliku bez rekompilowania aplikacji.

W poprzednich krokach zostało już zdefiniowanie żądanie zawierające datę urodzin użytkownika. Spróbujmy zatem zaimplementować własny menedżer żądań przyznający prawa do różnych stron na podstawie wieku odwiedzającego oraz reguł zawartych w pliku konfiguracyjnym:

  1. Tworzymy nową klasę służącą do autoryzacji użytkowników (źródło Windows Identity Foundation SDK):
public class AgeThresholdClaimsAuthorizationManager : ClaimsAuthorizationManager
    {
        private static Dictionary<string, int> _policies = new Dictionary<string, int>();

        public AgeThresholdClaimsAuthorizationManager(object config)
        {
            XmlNodeList nodes = config as XmlNodeList;

            foreach (XmlNode node in nodes)
            {
                XmlTextReader rdr = new XmlTextReader(new StringReader(node.OuterXml));
                rdr.MoveToContent();

                string resource = rdr.GetAttribute("resource");

                rdr.Read();

                string claimType = rdr.GetAttribute("claimType");

                if (claimType.CompareTo(Microsoft.IdentityModel.Claims.ClaimTypes.DateOfBirth) != 0)
                    throw new NotSupportedException("Brak zdefiniowanego żądania DateOfBirth");

                string minAge = rdr.GetAttribute("minAge");

                _policies[resource] = int.Parse(minAge);
            }
        }

        public override bool CheckAccess(AuthorizationContext pec)
        {
            Uri webPage = new Uri(pec.Resource.First().Value);
            if (_policies.ContainsKey(webPage.PathAndQuery))
            {
                int minAge = _policies[webPage.PathAndQuery];
                string userBirthdate = pec.Principal.Identities[0].Claims
                    .Where(c => c.ClaimType == Microsoft.IdentityModel.Claims.ClaimTypes.DateOfBirth)
                    .First().Value;

                int userAge = DateTime.Now.Subtract(DateTime.Parse(userBirthdate)).Days / 365;

                if (userAge < minAge)
                {
                    return false;
                }
            }

            return true;
        }
    }

Słownik _policies zawiera reguły autoryzacji. Kluczem do niego jest nazwa zasobu (strony), a wartością − minimalny, podany w latach, wiek użytkownika, któremu zapewniamy dostęp do strony. Konstruktor klasy służy do wypełniania słownika – pobrania danych z pliku konfiguracyjnego web.config.

Metoda CheckAccess dokonuje autoryzacji użytkownika. Parametr wejściowy (AuthorizationContext) zawiera adres żądanego przez niego zasobu oraz dane aktualnie zalogowanej osoby. Na podstawie wcześniej wypełnionego słownika oraz parametrów wejściowych (nazwy zasobu, danych użytkownika) następuje przyznanie dostępu (zwracana jest wartość true) lub odrzucenie żądania (false).

  1. Otwieramy plik konfiguracyjny web.config. Usuwamy wcześniej zdefiniowaną regułę, przyznającą dostęp do SecretPage.aspx wyłącznie użytkownikom z grupy Manager:
<location path="SecretPage.aspx">
        <system.web>
            <authorization>
                <allow roles="Manager"/>
                <deny users="*"/>
            </authorization>
        </system.web>
    </location>
  1. Aby napisana klasa autoryzująca działała, należy dołączyć dodatkowy moduł http (ClaimsAuthorizationModule) w sekcji httpmodules (system.web) oraz modules (system.server):
<system.web>
    <httpModules>     
      <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
   </httpModules>
</system.web>



  <system.webServer>
    <modules>     
       <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </modules>
  </system.webServer>
  1. Następnie trzeba dołączyć zaimplementowaną klasę:
<microsoft.identityModel>
    <service>
        <claimsAuthorizationManager type="WebApplication1.AgeThresholdClaimsAuthorizationManager">
            <policy resource="/WebApplication1/SecretPage.aspx" action="GET">
                <claim claimType="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth" minAge="18" />
            </policy>
        </claimsAuthorizationManager>
    </service>
  </microsoft.identityModel>

Oprócz definicji klasy, fragment zawiera regułę nadawania dostępu do pliku SecretPage.aspx wyłącznie osobom, które ukończyły 18 lat.

  1. Uruchamiamy aplikację w celu jej przetestowania. Po zalogowaniu się uzyskujemy dostęp do SecretPage.aspx, ponieważ aktualnie zdefiniowana data urodzenia użytkownika to 5/5/1955:

    Rysunek 9. Dostęp do SecretPage został nadany.

  2. Otwieramy plik CustomSecurityTokenService.cs (projekt STS), a następnie edytujemy datę urodzenia w metodzie GetOutputClaimsIdentity:

protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope )
    {
        if ( null == principal )
        {
            throw new ArgumentNullException( "principal" );
        }

        ClaimsIdentity outputIdentity = new ClaimsIdentity();

        outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name ) );
        outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Sales"));
        outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.DateOfBirth, "5/5/2000"));

        return outputIdentity;
    }
  1. Po ponownym uruchomieniu SecretPage.aspx użytkownikowi nie zostaną już nadane prawa dostępu.

Uwagi końcowe

Visual Studio musi być uruchomiony w trybie administratora. W przypadku niespełnienia tego wymogu podczas testowania aplikacji pojawi się następujący komunikat:

Keyset does not exist

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Należy podkreślić, że WIF można zintegrować m.in. z tokenami wydawanymi przez Active Directory Federation Services lub Live ID. Warto również zainteresować się autoryzacją na podstawie CardSpace, gdyż jest znacznie bardziej bezpieczna niż klasyczny model, oparty na loginie i haśle.

Materiały dodatkowe: