Ausweiss Kontrolle, czyli co to ten ClaimsPrincipal

Ausweiss Kontrolle, czyli co to ten ClaimsPrincipal

Wstęp

Gdy zaczynasz przygodę z mechanizmem Identity albo uwierzytelnianiem w .NET, możesz mieć problem ze zrozumieniem czym jest ClaimsPrincipal, claimsy i wszystko co z tym związane. W tym artykule rozwiewam wszelkie wątpliwości. Temat jest dość prosty, a więc artykuł będzie dość krótki.

Dokumenty do kontroli

Krótko mówiąc, ClaimsPrincipal to zbiór danych, który przechowuje informacje na temat zalogowanego użytkownika. Pewnie chcesz zadać pytanie – czy to nie może być moja super klasa User? Może, zwłaszcza jeśli chcesz walczyć z materią zamiast programować 🙂 ClaimsPrincipal to pewien standardowy sposób przechowywania i przesyłania danych. Poza tym w pewnych sytuacjach naprawdę jest dużo wygodniejszy. Chociaż będziesz tworzył swoją super klasę User na podstawie ClaimsPrincipal, to jednak to właśnie jest podstawowy sposób trzymania danych o zalogowanym użytkowniku.

Scenariusz

Wyobraź sobie, że Twoja firma wysyła Cię do innej dużej firmy w ramach jakiejś współpracy. Nikt Cię tam nie zna, a musisz mieć pewne uprawnienia (np. możliwość wejścia do sali konferencyjnej). Podchodzisz do strażnika i pokazujesz mu swój dowód osobisty – logujesz się swoimi poświadczeniami (w tym przypadku dowód osobisty to Twój login i hasło)

Strażnik sprawdza dane i widzi, że faktycznie miałeś przyjść i masz w tej firmie jakąś rolę. Wydaje Ci więc coś w rodzaju dowodu tożsamości – to może być identyfikator, karta wstępu, cokolwiek. Załóżmy, że to będzie tymczasowa karta wstępu z paskiem magnetycznym.

Ta karta wstępu to ClaimsIdentity (tożsamość). Dane zawarte na tej karcie (imię, nazwisko, rola) – to są Claimsy. A Ty jako posiadacz takiego dokumentu jesteś ClaimsPrincipal.

Gdybyś chodził po firmie tylko z dowodem osobistym, musiałbyś pokazywać go na każdym kroku i miałbyś utrudnione poruszanie się po budynku. Natomiast taka karta wstępu uwierzytelnia Cię i automatycznie daje Ci dostęp do pewnych pomieszczeń (np. sali konferencyjnej).

Podsumowując:

  • ClaimsPrincipal – osoba (użytkownik, system), posiadająca przynajmniej jeden dowód tożsamości
  • ClaimsIdentity – dowód tożsamości tej osoby, może ich być kilka (tak jak w życiu możesz posiadać dowód osobisty, prawo jazdy, paszport…)
  • Claims – dane z tego dowodu

Tworzenie ClaimsPrincipal

W mechanizmie uwierzytelniania, który opisałem tutaj, ręcznie musisz zalogować użytkownika, tworząc ClaimsPrincipal. Microsoft Identity robi to automatycznie (Ty tylko musisz ewentualnie odczytać pewne dane).

Aby utworzyć ClaimsPrincipal, musisz sobie najpierw odpowiedzieć na pytanie – jakie dane chcesz mieć w nim dostępne. To pytanie właściwie odnosi się do rodzaju dokumentu tożsamości, jaki będzie dodany do ClaimsPrincipal.

Tworzenie Claim

Na początku powinieneś utworzyć listę Claimów. Claim w mechanizmie uwierzytelniania pełni kluczową rolę.

Każdy Claim ma swój typ i wartość. Oczywiście jest kilka konstruktorów i dodatkowych właściwości Claima, ale skupimy się na tych podstawowych – typ i wartość. Reszta, jak np. wydawca sama się opisuje. Jeśli chcesz się wczytać bardziej technicznie w Claim, zobacz ten artykuł w Microsoft.

Typ claima to string określający co jest jego zawartością (zazwyczaj w formie URI). I w zasadzie możesz sobie podać tam co Ci się żywnie podoba, np:

Claim shoeSizeClaim = new Claim("rozmiar-buta", "46");

JEDNAK są pewne zdefiniowane typy, którymi powinieneś się posługiwać. Są dostępne z poziomu klasy ClaimTypes. Poniżej prezentuję te, które uważam za najważniejsze. Jeśli sądzisz, że ta lista powinna być rozszerzona – koniecznie daj znać w komentarzu:

ZnaczeniePole w ClaimTypesUwagi
Adres e-mailClaimTypes.Email
Nazwa użytkownikaClaimTypes.UserName.NET Identity używa tego jako nazwy użytkownika. Ale inni wystawcy mogą trzymać tu imię i nazwisko albo jakiś własny ciąg dla customowych danych
Id użytkownikaClaimTypes.NameIdentifierPamiętaj, że to jest id użytkownika w postaci stringu. To może być zarówno int jak i GUID
ImięClaimTypes.GivenName
NazwiskoClaimTypes.Surname
Wspólna nazwa użytkownikaClaimTypes.CommonNameTo jest nazwa użytkownika, która powinna być taka sama we wszystkich systemach. Załóżmy, że użytkownik ma login na Facebooku „PanWłodek”, natomiast na google „PaniWiesia”. Wspólna nazwa powinna określać ten sam nick na Facebooku i Google, np: „Janek123”. Oczywiście jeśli inne systemy pozwalają na taką dodatkową daną. To coś jak numer SKU w systemach magazynowych.
Rola użytkownikaClaimTypes.RoleRola użytkownika w systemie (np. admin, edytor itd). Oczywiście może być kilka claimów tego typu, ponieważ użytkownik może mieć wiele ról.
Unikalny identyfikator sesjiClaimTypes.SidTo jest unikalny identyfikator sesji dla użytkownika na danym urządzeniu.

Utwórzmy teraz przykładową listę claimów:

List<Claim> claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
    new Claim(ClaimTypes.Email, user.Email),
    new Claim(ClaimTypes.Role, "Writer"),
    new Claim(ClaimTypes.Role, "Moderator"),
    new Claim("numer-buta", user.ShoeNo)
};

Załóżmy, że user to jakiś użytkownik, którego próbujemy zalogować. Role Writer i Moderator to jakieś role w Twoim systemie.

Mając listę claim’ów, możemy teraz utworzyć dowód tożsamości – ClaimsIdentity:

ClaimsIdentity identity = new ClaimsIdentity(claims);

Mając ClaimsIdentity, możesz utworzyć ClaimsPrincipal:

ClaimsPrincipal principal = new ClaimsPrincipal(identity);

Jak tego używać?

Przede wszystkim, jeśli używasz mechanizmu Identity, to nie musisz tworzyć ClaimsPrincipal. To robi mechanizm. Jeśli używasz czystego uwierzytelniania, tak jak opisałem tutaj, podczas logowania musisz utworzyć ten obiekt i przekazać go dalej (np. do ciastka logowania).

Natomiast ważna rzecz – mechanizm uwierzytelniania (z którego korzysta też Identity) podczas ładowania strony (czy też endpointa w WebApi) automatycznie tworzy ten obiekt na podstawie danych, które otrzyma (z ciasteczka, tokenu, czy innego schematu). To być może brzmi niezbyt jasno. Lepiej to opisałem w artykule o uwierzytelnianiu.

W każdym razie pamiętaj, że w HttpContext.User masz w pełni gotowy obiekt ClaimsPrincipal, który możesz wykorzystywać.

Sprawdzenie, czy użytkownik posiada Claim

Sprawdźmy, czy użytkownik ma zapisany numer buta. Można to zrobić na dwa sposoby. Albo użyjesz LINQ i zrobisz to wygodniej, albo ręcznie sprawdzisz wszystkie Claimy 🙂

if(principal.HasClaim(c => c.Type == "numer-buta"))
  //posiada
else
  //nie posiada

Ta instrukcja pod spodem sprawdzi WSZYSTKIE ClaimsIdentity (tożsamości), które posiada użytkownik. Jeśli któryś z nich ma taką daną jak „numer-buta”, HasClaim zwróci true.

Pobranie wartości konkretnego Claim

Spróbujmy teraz pobrać Id użytkownika. Jak pisałem wyżej – powinno to być zapisane jako NameIdentifier:

Claim idClaim = principal.FindFirst(ClaimTypes.NameIdentifier);

Pamiętaj tylko, że jeśli użytkownik nie ma Claima tego typu, FindFirst zwróci null. Dlatego też powinieneś się zabezpieczyć przed takim scenariuszem. Oczywiście NameIdentifier powinien zawsze być obecny, jeśli użytkownik jest zalogowany.

Zawsze możesz też stworzyć rozszerzenie (extension class), które pomoże Ci pobierać odpowiednie wartości, np:

public static class ClaimsPrincipalExtensions
{
    public static Guid GetUserId(this ClaimsPrincipal cp)
    {
        Claim idClaim = cp.FindFirst(ClaimTypes.NameIdentifier);
        return idClaim == null ? Guid.Empty : Guid.Parse(idClaim.Value);
    }

    public static int GetShoeSize(this ClaimsPrincipal cp)
    {
        Claim claim = cp.FindFirst("rozmiar-buta");
        if (claim == null)
            return 0;

        int result = 0;
        if (!int.TryParse(claim.Value, out result))
            return 0;
        else
            return result;
    }
}

Utworzenie takiej klasy to dobra praktyka, jeśli używasz Claimów trochę bardziej niż w najprostszej aplikacji.

Czy użytkownik jest zalogowany?

Czasem zachodzi potrzeba sprawdzenia, czy użytkownik jest zalogowany – np. z poziomu RazorPage. Chociaż częściej będziesz się posługiwał atrybutem Authorize, to jednak możesz sprawdzić to w kodzie.

Obiekt User w HttpContext będzie obecny zawsze. Zatem sprawdzenie, czy jest nullem nie ma żadnego sensu, bo taki warunek nigdy nie będzie spełniony. Natomiast możesz na kilka sposobów sprawdzić, czy użytkownik jest zalogowany (poniżej pokazuję Ci przykład kodu, który możesz umieścić w swoim extension class):

public static bool IsLoggedIn(this ClaimsPrincipal cp)
{
    if (cp.Identity == null)
        return false;
    return cp.Identity.IsAuthenticated;
}

Jako że ClaimsPrincipal może mieć kilka ClaimsIdentity, właściwość Identity zwraca Ci pierwsze ClaimsIdentity z listy. Oczywiście wcale nie musi być żadnej tożsamości.

Każde ClaimsIdentity posiada właściwość IsAuthenticated, która jedyne co robi, to sprawdza, czy właściwość AuthenticationType ma jakąś wartość.

AuthenticationType to nazwa schematu, którym dana tożsamość (ClaimsIdentity) została uwierzytelniona. Więcej o tym w artykule o uwierzytelnianiu. To może być np. „cookie”, „bearer” itd.

Oczywiście możesz sprawdzić konkretne ClaimsIdentity, np:

public static bool IsLoggedIn(this ClaimsPrincipal cp)
{
    ClaimsIdentity? ci = cp.Identities.FirstOrDefault(id => id.AuthenticationType == "facebook");
    return ci != null;            
}

Tutaj sprawdzam, czy istnieje ClaimsIdentity o odpowiedniej wartości AuthenticationType. Nie sprawdzam już, co zwraca właściwość IsAuthenticated, bo jak napisałem wyżej – ona sprawdza tylko czy wartość AuthenticationType nie jest pusta. Więc jeśli na liście tożsamości jest tożsamość o zadanym AuthenticationType, znaczy to że użytkownik jest zalogowany.

Czy użytkownik ma odpowiednią rolę

To też możesz sprawdzić, używając ClaimsPrinciple:

if(User.IsInRole("moderator"))
    //ma rolę
else
    //nie ma

Metoda IsInRole przeleci wszystkie Claimy o type Role.

Uwaga, tutaj standardową techniką jest też posłużenie się atrybutem Authorize z odpowiednią rolą, ale czasem chcesz sprawdzić to w kodzie.


To chyba wszystko, co Ci potrzebne, żeby zacząć świadomie działać z ClaimsPrincipal. Dziękuję Ci za przeczytanie tego artykułu. Jeśli masz jakieś pytania, czegoś nie rozumiesz lub znalazłeś błąd, koniecznie daj znać w komentarzu 🙂

Obraz wyróżniający: Makieta pliki psd utworzone przez Xvect intern – pl.freepik.com

Podziel się artykułem na: