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:
Znaczenie | Pole w ClaimTypes | Uwagi |
---|---|---|
Adres e-mail | ClaimTypes.Email | |
Nazwa użytkownika | ClaimTypes.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żytkownika | ClaimTypes.NameIdentifier | Pamiętaj, że to jest id użytkownika w postaci stringu. To może być zarówno int jak i GUID |
Imię | ClaimTypes.GivenName | |
Nazwisko | ClaimTypes.Surname | |
Wspólna nazwa użytkownika | ClaimTypes.CommonName | To 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żytkownika | ClaimTypes.Role | Rola 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 sesji | ClaimTypes.Sid | To 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