Platforma WIF - autoryzacja oparta na oświadczeniach, cz. II Udostępnij na: Facebook

Konfiguracja modelu tożsamości

W celu włączenia w usługach WCF autoryzacji opartej na oświadczeniach za pomocą platformy WIF należy zainicjować wystąpienie ServiceHost dla federacji. Cel ten można zrealizować programowo, wywołując metodę ConfigureServiceHost udostępnioną przez typ FederatedServiceCredentials w następujący sposób:

ServiceHost host = new ServiceHost(typeof(TodoList.TodoListService));
FederatedServiceCredentials.ConfigureServiceHost(host);
host.Open();

Ten sam wynik można uzyskać w sposób deklaratywny za pomocą rozszerzenia zachowania ConfigurationServiceHostBehaviorExtension:

<serviceBehaviors>
  <behavior name="fedBehavior" > 
    <federatedServiceHostConfiguration/>
    <serviceMetadata />
  </behavior>
</serviceBehaviors>

W obu przypadkach do funkcji ServiceHost przypisywane jest wystąpienie typu FederatedServiceCredentials sterujące autoryzacją opartą na oświadczeniach w ramach usługi. Ten typ można zainicjować programowo lub w sekcji konfiguracji modelu microsoft.identityModel dla usługi. Ustawienia modelu tożsamości są powiązane z platformą WIF i zawierają ustawienia autoryzacji opartej na oświadczeniach w aplikacjach ASP.NET i WCF, co zostało w większości podsumowane na rysunku 5.

Rysunek 5. Podsumowanie podstawowych elementów modelu microsoft.identityModel.

W przypadku usług WCF korzystających z platformy WIF nie trzeba inicjować funkcji ServiceHost z typowymi zachowaniami uwierzytelniania i autoryzacji WCF. Platforma WIF zastępuje te mechanizmy i udostępnia ogólnie wygodniejszy sposób konfigurowania zabezpieczeń. (Rozwiązanie to jest przydatne nie tylko w przypadku scenariuszy opartych na oświadczeniach i federacji). Na rysunku 6 przedstawiono ustawienia modelu tożsamości używane w aplikacji TodoListService.

Rysunek 6. Ustawienia modelu tożsamości często stosowane w usługach WCF

<microsoft.identityModel>
  <service>
    <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.
      ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, 
      Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
      <trustedIssuers>
        <add name="https://localhost:8010/rpsts" thumbprint=
"c3 95 cd 4a 74 09 a7 77 d4 e3 de 46 d7 08 49 86 76 1a 99 50"/>
      </trustedIssuers>
    </issuerNameRegistry>
    <serviceCertificate>
      <certificateReference findValue="CN=RP" storeLocation="LocalMachine" 
         storeName="My" x509FindType="FindBySubjectDistinguishedName"/>
    </serviceCertificate>
    <audienceUris mode="Always">
      <add value="https://localhost:8000/TodoService"/>
    </audienceUris>
    <certificateValidation certificateValidationMode="PeerTrust" />         
    <securityTokenHandlers>
      <remove type="Microsoft.IdentityModel.Tokens.Saml11.
         Saml11SecurityTokenHandler, Microsoft.IdentityModel, 
         Version=1.0.0.0, Culture=neutral, 
         PublicKeyToken=31bf3856ad364e35"/>
      <add type="Microsoft.IdentityModel.Tokens.Saml11.
         Saml11SecurityTokenHandler, Microsoft.IdentityModel, 
         Version=1.0.0.0, Culture=neutral, 
         PublicKeyToken=31bf3856ad364e35">
        <samlSecurityTokenRequirement >
          <roleClaimType 
            value="urn:TodoListApp/2009/06/claims/permission"/>
        </samlSecurityTokenRequirement>
      </add>
    </securityTokenHandlers>
    <claimsAuthorizationManager 
      type="TodoList.CustomClaimsAuthorizationManager, TodoList"/>
  </service>
</microsoft.identityModel>

Ustawienie issuerNameRegistry jest używane do określania zaufanych wystawców certyfikatów. Jeśli ustawienie ConfigurationBasedIssuerNameRegistry zostanie użyte w sposób przedstawiony na rysunku 6, należy podać listę zaufanych wystawców certyfikatów, określając ich unikatowe identyfikatory. Podczas uruchomienia typ ConfigurationBasedIssuerNameRegistry sprawdza tokeny zabezpieczeń X509 pod względem tej listy i odrzuca tokeny z identyfikatorami, których nie ma na liście. Typu SimpleIssuerNameRegistry można użyć do akceptacji dowolnych tokenów X509 lub RSA, ale prawdopodobnie warto użyć niestandardowego typu IssuerNameRegistry w celu sprawdzania tokenów na podstawie własnej heurystyki, jeśli typ ConfigurationBasedIssuerNameRegistry jest nieodpowiedni.

Konfiguracja przedstawiona na rysunku 6 odrzuca wszystkie tokeny, które nie zostały podpisane przez usługę RP-STS (za pomocą unikatowego identyfikatora certyfikatu CN=RPSTS). Poniższa konfiguracja określa niestandardowy typ IssuerNameRegistry, czyli TrustedIssuerNameRegistry:

<issuerNameRegistry type="TodoListHost.TrustedIssuerNameRegistry, TodoListHost"/>

Implementacja typu TrustedIssuerNameRegistry jest używana w celu uzyskania takiego samego wyniku — odrzucenia tokenów niepodpisanych identyfikatorem CN=RPSTS poprzez sprawdzenie nazw tokenów przychodzących:

public class TrustedIssuerNameRegistry : IssuerNameRegistry
{
    public override string GetIssuerName(SecurityToken securityToken)
    {
        X509SecurityToken x509Token = securityToken as
            X509SecurityToken;
        if (x509Token != null)
        {
            if (String.Equals(x509Token.Certificate.SubjectName.Name,
                "CN=RPSTS"))
            {
                return x509Token.Certificate.SubjectName.Name;
            }
        }

        throw new SecurityTokenException("Niezaufany wystawca.");
    }
}

Ustawienie serviceCertificate na rysunku 6 wskazuje certyfikat, który ma być używany do odszyfrowywania przychodzących tokenów zabezpieczeń, o ile zostały one zaszyfrowane dla domeny RP przez usługę STS wystawcy. W przypadku aplikacji Todo List usługa RP-STS szyfruje tokeny przy użyciu klucza publicznego domeny RP (CN=RP).

Zazwyczaj token SAML zawiera element URI odbiorców, który jest sprawdzany w domenie RP w celu określenia właściciela tokenu. Istnieje możliwość jawnego odrzucania tokenów, które nie były przeznaczone dla domeny RP. Domyślnie tryb audienceUris ma wartość „Always” (Zawsze), co oznacza, że należy podać co najmniej jeden identyfikator URI na potrzeby weryfikacji przychodzących tokenów. Konfiguracja przedstawiona na rysunku 6 zezwala wyłącznie na tokeny SAML zawierające identyfikator URI odbiorców zgodny z adresem usługi TodoListService. Mimo że ogólnie jest to niezalecane, można ustawić tryb audienceUris na wartość „Never” (Nigdy), aby uniemożliwić ocenę warunku ograniczenia odbiorców dla przychodzących tokenów SAML:

<audienceUris mode="Never"/>

Należy pamiętać, że gdy klient wysyła żądanie RST do usługi STS, zwykle zawiera ono ustawienie AppliesTo wskazujące na odpowiedniego właściciela tokenu, czyli domenę RP. Usługa STS może użyć tych informacji do wprowadzenia identyfikatora URI odbiorców tokenu SAML.

Ustawienie certificateValidation steruje sposobem sprawdzania poprawności przychodzących tokenów X509 używanych na przykład w sygnaturach tokenów. Na rysunku6 tryb certificateValidationMode ma wartość „PeerTrust”, co oznacza, że certyfikaty są prawidłowe tylko wtedy, gdy powiązany certyfikat znajduje się w magazynie TrustedPeople. W przypadku sprawdzania poprawności wystawcy tokenu ustawienie to jest bardziej odpowiednie niż domyślne ustawienie PeerOrChainTrust, ponieważ wymaga jawnego zainstalowania zaufanego certyfikatu w magazynie certyfikatów. Ustawienie PeerOrChainTrust oznacza, że sygnatury są autoryzowane także wtedy, gdy główny urząd certyfikacji (CA) jest zaufany, a w przypadku wielu komputerów lista zaufanych urzędów certyfikacji może być długa.

Warto pokrótce przeanalizować inne ustawienia przedstawione na rysunkach rysunkach5 i6. W kontekście inicjalizacji usługi WIF należy zwrócić uwagę na fakt, że wystąpienie typu FederatedServiceCredentials można zainicjować programowo i przekazać do funkcji ConfigureServiceHost — nie trzeba inicjować go w sekcji microsoft.identityModel. Odpowiednią ilustracją jest następujący fragment kodu:

ServiceHost host = new ServiceHost(typeof(TodoList.TodoListService));

ServiceConfiguration fedConfig = new ServiceConfiguration();
fedConfig.IssuerNameRegistry = new TrustedIssuerNameRegistry();
fedConfig.AudienceRestriction.AudienceMode = AudienceUriMode.Always;
fedConfig.AudienceRestriction.AllowedAudienceUris.Add(new 
Uri("https://localhost:8000/TodoListService"));
fedConfig.CertificateValidationMode = 
X509CertificateValidationMode.PeerTrust;
fedConfig.ServiceCertificate = CertificateUtil.GetCertificate(
StoreName.My, StoreLocation.LocalMachine, "CN=RP");

FederatedServiceCredentials fedCreds = 
new FederatedServiceCredentials(fedConfig);

FederatedServiceCredentials.ConfigureServiceHost(host,fedConfig);
host.Open();

Inicjalizacja programowa jest szczególnie przydatna podczas inicjowania typu ServiceHost na podstawie ustawień bazy danych dotyczących całej farmy serwerów.

Architektura składników WIF

Podczas stosowania zachowań WIF do typu ServiceHost inicjowanych jest kilka składników usługi WIF umożliwiających autoryzację opartą na oświadczeniach — wiele z nich to rozszerzenia platformy WCF. Powoduje do dołączenie do wątku żądania składnika ClaimsPrincipal, który obsługuje autoryzację opartą na oświadczeniach. Narysunku 7 przedstawiono podstawowe relacje między składnikami usługi WIF i typem ServiceHost.


Rysunek 7. Podstawowe składniki instalowane wraz z platformą WIF

Typ FederatedServiceCredentials zastępuje domyślne zachowanie ServiceCredentials, a składnik IdentityModelServiceAuthorizationManager (instalowany podczas inicjalizacji typu FederatedServiceCredentials) zastępuje domyślne zachowanie ServiceAuthorizationBehavior. Typ FederatedServiceCredentials tworzy również wystąpienie FederatedSecurityTokenManager. Typy te sterują uwierzytelnianiem i autoryzacją w ramach poszczególnych żądań za pomocą składników ClaimsAuthenticationManager i ClaimsAuthorizationManager oraz składnika obsługi SecurityTokenHandler dotyczącego konkretnego żądania.

Na rysunku 8 przedstawiono schemat komunikacji między składnikami, która prowadzi do utworzenia podmiotu zabezpieczeń w wątku żądania — w tym przypadku typu ClaimsPrincipal — oraz możliwości autoryzacji na podstawie tego podmiotu.


Rysunek 8. Składniki umożliwiające utworzenie typu ClaimsPrincipal i autoryzację na jego podstawie.

Składnik FederatedSecurityTokenManager zwraca odpowiednie dojście tokenu dla żądania —w tym przypadku Saml11SecurityTokenHandler — dostarczając odwołanie do składnika ClaimsAuthenticationManager. Dojście tokenu tworzy element ClaimsIdentity na podstawie przychodzącego tokenu oraz element ClaimsPrincipal (za pośrednictwem klasy otoki) i przekazuje go do metody ValidateToken na potrzeby składnika ClaimsAuthenticationManager. Umożliwia to zmodyfikowanie lub zastąpienie typu ClaimsPrincipal dołączanego do wątku żądania. Implementacja domyślna po prostu zwraca ten sam podany typ ClaimsPrincipal:

public virtual IClaimsPrincipal Authenticate(string resourceName, IClaimsPrincipal incomingPrincipal)
{
    return incomingPrincipal;
}

Warto rozważyć dostarczenie niestandardowego składnika ClaimsAuthenticationManager w celu przekształcenia przychodzących oświadczeń z tokenu zabezpieczeń na oświadczenia, które mogą zostać użyte w domenie RP do autoryzacji dostępu. W tym przykładzie token SAML zawiera jednak odpowiednie oświadczenia domeny RP wydane przez usługę RP-STS, dzięki czemu typ ClaimsPrincipal utworzony na podstawie tych oświadczeń obsługuje autoryzację.

Następnie składnik IdentityModelServiceAuthorizationManager odwołujący się do składnika ClaimsAuthorizationManager wywołuje metodę CheckAccess, co pozwala na dostosowanie sposobu kontrolowania dostępu. Implementacja domyślna nie ogranicza dostępu:

public virtual bool CheckAccess(AuthorizationContext context)
{
    return true;
}

Parametr AuthorizationContext zapewnia dostęp do typu ClaimsPrincipal i skojarzonych z nim oświadczeń, zbioru akcji odpowiednich dla żądania (takich jak identyfikator URI wskazujący wywoływaną operację usługi) oraz informacji na temat zasobu powiązanego żądaniem (na przykład identyfikatora URI usługi). Może to być przydatne w celu sprecyzowania wywołań wielu usług przechodzących przez tę samą ścieżkę autoryzacji. Aby wdrożyć scentralizowany mechanizm autoryzacji, można podać niestandardowy parametr ClaimsAuthorizationManager.Odpowiedni przykład zostanie opisany podczas analizy technik autoryzacji.

Zabezpieczenia oparte na rolach w architekturze .NET Framework wymagają dołączenia do poszczególnych wątków podmiotów zabezpieczeń zgodnych z typem IPrincipal. Taki podmiot zabezpieczeń otacza tożsamość uwierzytelnionego użytkownika w implementacji typu IIdentity. W przypadku braku platformy WIF usługa WCF dołącza podmiot zabezpieczeń do każdego wątku żądania na podstawie konfiguracji modelu system.serviceModel dla uwierzytelniania i autoryzacji. Typ IIdentity jest tworzony zgodnie z typem poświadczeń przedstawionych podczas uwierzytelniania. Na przykład poświadczenia systemu Windows będą sprawdzane za pomocą typu WindowsIdentity, certyfikat X.509 zostanie sprawdzony przy użyciu typu X509Identity, natomiast token UserName zostanie zweryfikowany za pomocą typu GenericIdentity. Zachowanie ServiceAuthorizationBehavior steruje typem otoki IPrincipal tożsamości. Na przykład w przypadku autoryzacji w systemie Windows tworzony jest typ WindowsPrincipal, a dla dostawcy członkowstwa ASP.NET generowany jest typ RoleProviderPrincipal. W przeciwnym wypadku wybrany obiekt IPrincipal jest tworzony na podstawie niestandardowej polityki autoryzacji. Obiekt IPrincipal udostępnia metodę IsInRole, którą można wywoływać bezpośrednio lub pośrednio za pomocą wymogów uprawnień w celu kontrolowania dostępu do funkcji.

Platforma WIF rozszerza ten model dzięki typom ClaimsPrincipal i ClaimsIdentity opartym na typach IClaimsPrincipal oraz IClaimsIdentity, które są pochodnymi obiektów IPrincipal i IIdentity. Wszystkie tokeny są mapowane na element ClaimsIdentity za pośrednictwem usługi WIF. Podczas sprawdzania poprawności tokenu zabezpieczeń skojarzony z nim typ SecurityTokenHandler tworzy obiekt ClaimsIdentity i dostarcza do niego odpowiednie oświadczenia. Obiekt ClaimsIdentity jest zawarty w zbiorze ClaimsIdentityCollection (na wypadek, gdyby token wygenerował wiele wystąpień obiektu ClaimsIdentity), który jest ukryty w typie ClaimsPrincipal i dołączony do wątku żądania. Typ ClaimsPrincipal jest podstawą mechanizmu autoryzacji WIF w ramach usług WCF.

Autoryzacja oparta na oświadczeniach

W przypadku usług WCF zastosowane podejście do autoryzacji będzie zapewne uwzględniać jedną z następujących technik:

  • Użycie typu ClaimsPrincipal w celu dynamicznego sprawdzania wymogów IsInRole.
  • Użycie typu PrincipalPermission w celu stosowania dynamicznych wymogów uprawnień.
  • Użycie typu PrincipalPermissionAttribute w celu dostarczania deklaratywnych wymogów uprawnień w ramach poszczególnych operacji.
  • Podanie niestandardowego składnika ClaimsAuthorizationManager na potrzeby centralizacji kontroli dostępu w ramach pojedynczego składnika.

Pierwsze trzy możliwości są oparte na metodzie IsInRole udostępnionej przez typ ClaimsPrincipal. Nie oznacza to wdrożenia zabezpieczeń opartych na rolach — po prostu wybierany jest typ roli-oświadczenia, dzięki czemu prawidłowe oświadczenia są sprawdzane pod względem żądanych oświadczeń przekazanych do metody IsInRole. W przypadku usług WIF domyślny typ roli-oświadczenia został zdefiniowany na stronie sieci Web pod adresem schemas.microsoft.com/ws/2008/06/identity/claims/role. Jeśli usługa STS skojarzona ze scenariuszem federacji wydaje ten typ oświadczeń, opcjonalnie można kontrolować dostęp na podstawie tego typu oświadczenia. W przypadku scenariusza dla aplikacji Todo List wspominano, że na potrzeby autoryzacji stosowany jest niestandardowy typ oświadczeń o uprawnieniach, zatem konfiguracja modelu tożsamości musi wskazywać typ roi-oświadczenia, aby możliwe było sprawdzanie na podstawie metody IsInRole.

Użytkownik przekazuje typ oświadczenia-roli do dojścia SecurityTokenHandler oczekiwanego typu tokenu — w tym wypadku Saml11SecurityTokenHandler.Jak przedstawiono na rysunku 6, konfigurację domyślną dojścia SecurityTokenHandler można zmodyfikować, usuwając ją i dodając ponownie wraz z określonymi preferowanymi ustawieniami właściwości. Dojścia tokenów SAML zawierają sekcję samlSecurityTokenRequirement, w której można zdefiniować ustawienia nazwy lub typu roli-oświadczenia wraz z innymi parametrami związanymi ze sprawdzaniem poprawności certyfikatów i tokenów systemu Windows. W tym scenariuszu podano niestandardowy typ roli-oświadczenia:

<samlSecurityTokenRequirement >
  <roleClaimType value= "urn:TodoListApp/2009/06/claims/permission"/>
</samlSecurityTokenRequirement>

Oznacza to, że przy każdym wywołaniu metody IsInRole dla typu ClaimsPrincipal następuje sprawdzenie poprawności oświadczenia o uprawnieniach. Jednym ze sposobów zrealizowania tego celu jest jawne wywołanie metody IsInRole przed wykonaniem sekcji kodu wymagającej konkretnego oświadczenia. Dostęp do bieżącego podmiotu można uzyskać za pomocą właściwości Thread.CurrentPrincipal w następujący sposób:

if (!Thread.CurrentPrincipal.
IsInRole("urn:TodoListApp/2009/06/claims/permission/delete"))
  throw new SecurityException("Odmowa dostępu.");

Poza jednoznacznym sprawdzeniem metody IsInRole podczas uruchomienia można także utworzyć tradycyjne wymogi uprawnień opartych na rolach za pomocą typu PrincipalPermission. Należy zainicjować typ z wymaganym oświadczeniem roli (drugi parametr konstruktora), a po wywołaniu obiektu Demand nastąpi wywołanie metody IsInRole bieżącego podmiotu. Jeśli oświadczenie nie zostanie znalezione, wystąpi wyjątek:

PrincipalPermission p = new PrincipalPermission("", "urn:TodoListApp/2009/06/claims/permission/delete");
p.Demand();

Można także utworzyć zestaw PermissionSet w celu zebrania i sprawdzenia kilku oświadczeń:

PermissionSet ps = new PermissionSet(PermissionState.Unrestricted);
ps.AddPermission(new PrincipalPermission("", "urn:TodoListApp/2009/06/claims/permission/create"));
ps.AddPermission(new PrincipalPermission("", "urn:TodoListApp/2009/06/claims/permission/read"));
ps.Demand();

Jeśli sprawdzanie dostępu dotyczy całej usługi, można zastosować typ PrincipalPermissionAttribute, który ułatwia deklaratywne skojarzenie wymaganych oświadczeń z wywoływaną operacją. Atrybuty te można również połączyć w celu sprawdzenia wielu oświadczeń:

[PrincipalPermission(SecurityAction.Demand, Role = Constants.Permissions.Create)]
[PrincipalPermission(SecurityAction.Demand, Role = Constants.Permissions.Read)]
public string CreateItem(TodoItem item)

W określonych sytuacjach warto scentralizować autoryzację w ramach pojedynczego składnika, czyli podać niestandardowy składnik ClaimsAuthorizationManager, który będzie przeprowadzał operacje sprawdzania. Na rysunku 6 przedstawiono sposób konfiguracji składnika ClaimsAuthorizationManager a jego implementacja na potrzeby usługi TodoListService jest widoczna na rysunku 9 (kod został częściowo skrócony w celu poprawy czytelności).

Rysunek 9. Niestandardowa implementacja składnika ClaimsAuthorizationManager.

class CustomClaimsAuthorizationManager : ClaimsAuthorizationManager
{
    public CustomClaimsAuthorizationManager()
    {
    }

    public override bool CheckAccess(AuthorizationContext context)
    {
        
        if (context.Resource.Where(x=> x.ClaimType == 
            System.IdentityModel.Claims.ClaimTypes.Name && x.Value == 
            "https://localhost:8000/TodoListService").Count() > 0)
        {
            if (context.Action.Where(x=> x.ClaimType == 
                System.IdentityModel.Claims.ClaimTypes.Name && x.Value == 
                Constants.Actions.GetItems).Count() > 0)
            {
                return
                    context.Principal.IsInRole(
                       Constants.Permissions.Read);
            }

        // other action checks for TodoListService
        }
        return false;
    }  
}

Składnik ClaimsAuthorizationManager zastępuje typ CheckAccess, który pobiera parametr AuthorizationContext z odwołaniem do zasobu (w tym przypadku identyfikatora URI usługi), zbiór akcji (w tym przykładzie pojedyncza akcja wskazująca na identyfikator URI operacji) oraz typ ClaimsPrincipal, który nie został jeszcze dołączony do wątku żądania. Zasób można sprawdzić, jeśli składnik jest udostępniony w wielu usługach, tak jak w bieżącym przykładzie. Przede wszystkim należy sprawdzić akcję pod względem listy identyfikatorów URI operacji usługi oraz przeprowadzić weryfikację za pomocą metody IsInRole zgodnie z wymaganiami.

Ogólnie nie zaleca się oddzielania mechanizmu sprawdzania autoryzacji od operacji chronionej lub bloku kodu. Obsługa kodu zadeklarowanego w kontekście odpowiednim dla danego działania jest znacznie łatwiejsza.

Ciąg dalszy nastąpi

Dotychczas udało się względnie szczegółowo przedstawić sposób konfiguracji scenariusza federacji aktywnej za pomocą usług WCF i WIF oraz informacje dotyczące powiązań federacyjnych usługi WCF i semantyki generowania serwera proxy, a także konfigurowania funkcji WIF w ramach usługi oraz implementowania różnych technik autoryzacji opartej na oświadczeniach. Następna część artykułu będzie poświęcona federacji pasywnej za pomocą technologii ASP.NET i WIF.