Autoryzacja użytkownika poprzez role w ASP.NET Core

Witam serdecznie !

Jak pewnie część  z Was zauważyła, zmieniłem nieco layout bloga. Powodów było kilka, ale najważniejszym była zdecydowanie czcionka. Według mnie była ona nieco koślawa i mała, przez co czytanie wpisów (szczególnie tych dłuższych) mogło okazać się męczące. Mam nadzieję, że aktualna wersja przypadnie Wam do gustu bo nad ogarnięciem samego menu spędziłem dobre 30 min 😀 Dziś jednak nie o tym. Jakiś czas temu zaimplementowaliśmy mechanizm kontroli dostępu użytkowników z JWT Bearer Token. Jeżeli ktoś tego wpisu nie czytał, gorąco zachęcam do zapoznania się z nim zanim przejdziemy dalej, ponieważ częściowo będziemy modyfikowali zamieszczony tam kod. Link macie tu. Czy w takim razie coś poszło nie tak, musimy coś poprawić? I tak, i nie 😉 Aktualnie posiadamy mechanizm przyznawania access tokena zalogowanym użytkownikom, który dołączamy do żądań AJAX-owych wszędzie tam gdzie serwer go wymaga. Dzięki temu anonimowi użytkownicy nie posiadają dostępu np. do danych naszego konta, ponieważ każda próba ubiegania się po nie zakończy się zwróceniem przez serwer kodu HTTP 401 (Unauthorized). Wszystko wydaje się więc być w porządku. Problem pojawia się jednak kiedy dochodzimy do hierarchii użytkowników w systemie. Zazwyczaj buduje się ją poprzez system ról, który jasno rozgranicza funkcjonalności dostępne dla poszczególnych użytkowników. Przykładowo, użytkownik w roli administratora oprócz „standardowych” akcji ma możliwość zarządzania userami w systemie (o tym wkrótce ;)). Pytanie które nasuwa się automatycznie brzmi „no dobrze, ale aktualnie system wie czy użytkownik jest zalogowany czy nie, skąd zatem ma wiedzieć czy jego rola zezwala na wykonanie pewnych akcji”. I to właśnie jest przedmiotem dzisiejszego wpisu, będziemy autoryzować role!

 

Pierwszą rzeczą jaką zrobimy będzie dołączenie do naszego access tokena, wszystkich roli do których użytkownik jest przypisany. Wróćmy zatem do klasy OAuthService, którą jakiś czas temu implementowaliśmy:

 

public sealed class OAuthService : IOAuthService
{
    private readonly TokenAuthOptions _tokenAuthOptions;

    public OAuthService(TokenAuthOptions tokenAuthOptions)
    {
        _tokenAuthOptions = tokenAuthOptions;
    }

    public string GetUserAuthToken(string userName, string userId, string[] roles)
    {
        var handler = new JwtSecurityTokenHandler();

        var identity = new ClaimsIdentity(new GenericIdentity(userName, "TokenAuth"), new[]
        {
            new Claim("UserId", userId, ClaimValueTypes.String)
        });

        foreach (var role in roles)
        {
            identity.AddClaim(new Claim("role",role));
        }

        var securityToken = handler.CreateToken(
            _tokenAuthOptions.Issuer,
            _tokenAuthOptions.Audience,
            signingCredentials: _tokenAuthOptions.SigningCredentials,
            subject: identity,
            expires: DateTime.UtcNow.AddDays(14));

        return handler.WriteToken(securityToken);
    }
}

 

Jak widać od pierwotnej wersji nie różni się za wiele. Zmiana polega na tym, że metoda GetUserAuthToken przyjmuje teraz trzeci parametr tj. tablica nazw ról użytkownika. Następnie po zbudowaniu identity dołączamy claimy, które przechowują o nich informacje. Wiemy już zatem jakie role posiada użytkownik ubiegający się po zasoby z serwera. Teraz potrzebujemy czegoś co pozwoli nam na weryfikację delikwenta tuż przed wywołaniem akcji w kontrolerze. W poprzednich wersjach ASP.NET mogliśmy w tym celu napisać własny atrybut, który dziedziczył z klasy AuthorizeAttribute. Nowa wersja oferuje inne rozwiązanie. Przyjrzyjmy się metodzie RegisterBearerPolicy, w której rejestrowaliśmy politykę wykorzystywaną do autoryzowania użytkowników:

 

public static void RegisterBearerPolicy(this IServiceCollection services)
{
    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("User", new AuthorizationPolicyBuilder()
            .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
            .RequireAuthenticatedUser().Build());        
    });
}

 

Kiedy poszukamy trochę dokładniej zauważymy, że klasa AuthorizationPolicyBuilder zawiera ciekawą metodę rozszerzającą:

 

extension_method

 

Wygląda na to, że polityka może przyjąć pewne wymagania/ restrykcje, które będą egzekwowane podczas procesu autoryzacji użytkownika. Wystarczy klasa z implementacją interfejsu IAuthorizationRequirement. Żeby nie wprowadzać zbędnego napięcia, poniżej przedstawienie zaimplementowanej klasy:

 

public class OnlyRoleRequirement : AuthorizationHandler<OnlyRoleRequirement>, IAuthorizationRequirement
    {
        private readonly string _roleName;
 
        public OnlyRoleRequirement(string roleName)
        {
            _roleName = roleName;
        }
 
        protected override void Handle(AuthorizationContext context, OnlyRoleRequirement requirement)
        {
            if (context.User.IsInRole(_roleName))
            {
                context.Succeed(requirement);
                return;
            }
 
            context.Fail();
        }
    }

 

Kod wydaje się dość prosty jednak mały komentarz się należy. W konstruktorze, przyjęta zostaje nazwa roli użytkownika którą zapisujemy do prywatnego pola. Dzięki temu klasa będzie uniwersalna i będzie w stanie obsłużyć każdą rolę. W przeciwnym przypadku jesteśmy skazani na implementację per rola. Metoda Handle jest metodą abstrakcyjną dostarczoną przez klasę AuthorizationHandler wobec czego obligatoryjnie ją implementujemy. Jednym z jej parametrów jest obiekt klasy AuthorizationContext. To właśnie stąd wyciągamy naszego użytkownika po czym sprawdzamy czy jest w zadanej roli. Jeżeli tak, „przepuszczamy” request dalej. W przeciwnym przypadku go odrzucamy. Kiedy dokonaliśmy enkapsulacji naszych wymagań możemy dołączyć je do polityki. Ponieważ jednak chcemy, aby część kontrolerów wymagała jedynie access tokena, dodamy drugą politykę:

 

auth.AddPolicy("Admin", new AuthorizationPolicyBuilder()
    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
    .AddRequirements(new OnlyRoleRequirement(RoleNames.Admin))
    .RequireAuthenticatedUser().Build());

 

Jak widać w środku metody AddRequirements tworzymy instancję naszych wymagań przekazując do konstruktora nazwę roli (w tym przypadku admin). Użycie polityki „Admin” jest identyczne jak dotychczas. Nasz kontroler musimy opatrzyć atrybutem:

 


[Authorize("Admin")]

 

To wszystko! Zobaczmy jak to działa. W tym celu uruchomimy klienta Postman i wykonam dwa żądania na serwer. Pierwsze będzie posiadało token administratora. Drugie, użytkownika który nie posiada żadnej roli. Oba żądania zostaną skierowane na kontroler AdminController, który posiada wcześniej przedstawiony atrybut autoryzacyjny. Zerknijmy na rezultat:

 

sample

 

Wygląda na to, że jest ok. W przypadku „podawania” się za zwykłego użytkownika serwer zwraca 403 – Forbidden, czyli mówi nam kolokwialnie „wiem kim jesteś, ale nie masz praw do tego zasobu” 😛

 

No to chyba wszystko. Mam nadzieje, że komuś się to przyda 😉 Przypominam Wam, że cały kod macie dostępny na githubie. Wpadnijcie też na mojego twittera, gdzie zawsze wrzucam nowe posty i dzielę się co ciekawszymi rzeczami związanymi z Web devem.

Trzymajcie się !

 

You may also like...