.NET [PL] Architecture Aurora Backend Functionalities Get Noticed 2016 Security

Logowanie i kontrola dostępu użytkowników z JWT Bearer Token cz 1. (serwer)

Witam, witam i o zdrowie pytam !

A to akurat mnie nie powinno śmieszyć po przeleżeniu w łóżku dwóch dni przypominając warzywo. Pierwsza rada wieczoru, nie zamawiać pizzy po dwudniowych świętach. Dziś jednak w końcu mogę wrócić do jakże pasjonującego blogowania w świetle blasku i sławy…taa… Dobra, teraz bez beki. Ostatnio udało nam się zrobić całkiem zgrabnie rejestrację użytkownika, dziś zajmiemy się logowaniem oraz no właśnie… autoryzacją, autentykacją, uwierzytelnianiem, a może kontrolą dostępu? Czy to jedno i to samo? Rozpocznijmy od wyjaśnienia tych pojęć.

Na szeroko pojętą kontrolę dostępu składają się trzy etapy:

  • Identyfikacja (ang. indentification) – jest to deklaracja przez użytkownika swojej tożsamości.
  • Uwierzytelnianie (ang. authentication)- jest to weryfikacja deklaracji użytkownika poprzez jakiś wewnętrzny mechanizm. Tu właśnie ten proces często nazywa się autentykacją, co z punktu widzenia języka polskiego jest formą niepoprawną (sam dostałem za to „baty” na uczelni).
  • Autoryzacja (ang. authorization) – jest to etap, w którym użytkownik został poprawnie „rozpoznany” jednak sprawdzane są jego uprawnienia do uzyskania żądanego zasobu.

Tu jeszcze małe dopowiedzenie w .NET-owym świetle. Uwierzytelnianie użytkowników odbywa się na hoście czyli IIS, natomiast autoryzacja przed wywołaniem akcji kontrolera. Tu mała wizualizacja pipeline-u:

 

webapi_auth01
http://www.asp.net/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api

 

Przejdźmy do naszego dzisiejszego zadania. Powiem szczerze, że kiedy zabierałem się za ten temat pomyślałem „easy, robiłem to już tyle razy, że 30 minutek mi styknie” . No właśnie…myślałem. Dla tych, którzy nie są obeznani w temacie, w dotychczasowej wersji ASP.NET podczas tworzenia projektu generowana była klasa o nazwie OAuthProvider, która zajmowała się przyznawaniem użytkownikom tokenów. Strzelaliśmy pod endpoint {host}/token i tyle. Mamy token. W ASP.NET Core sprawa nie wygląda już tak kolorowo, ponieważ tej klasy nie ma, a wszystko musimy pisać sami. Na szczęście my, jako zawodowi stackoverflow developers po ok. 10 sekundach mamy wszystko wyłożone na tacy. Użyjemy do tego celu tytułowego JWT Bearer Token.

Pierwszą rzeczą będzie zainstalowanie paczki. Do naszego project.json dopisujemy:

 


"Microsoft.AspNet.Authentication.JwtBearer": "1.0.0-rc1-final"


 

Następnie w warstwie Web utworzymy sobie katalog Auth, który będzie przechowywał wszystko to co związane z naszym tokenem. Jego zawartość prezentuje się następująco:

 

auth_folder

 

Omówmy pokrótce za co odpowiedzialna jest każda z klas:

  • OAuthBearerAuthenicationRegistration – zawiera konfiguracje tokena, która odbywa się podczas uruchamiania aplikacji.
  • OAuthService – serwis dostarczający użytkownikowi token. Jako jedyny posiada interfejs, gdyż będzie on wstrzykiwany poprzez Autofac do AccountsControler
  • TokenAuthOptions – klasa, która posłuży nam przy konfiguracji tokena. Posiada jedynie trzy property.

 

Dobra przejdźmy do kodu, implementacja TokenAuthOptions:

 


public class TokenAuthOptions
   {
       public string Audience { get; set; }
       public string Issuer { get; set; }
       public SigningCredentials SigningCredentials { get; set; }
   }

 

Dalej OAuthBearerAuthenicationRegistration:

 


public static class OAuthBearerAuthenticationRegistration
    {
        private const string _audience = "Aurora";
        private const string _issuer = "self";
   
        public static void RegisterBearerPolicy(this IServiceCollection services)
        {
            services.AddAuthorization(auth =>
            {
                auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
                    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                    .RequireAuthenticatedUser().Build());
            });
        }
 
        public static void RegisterTokenAuthorizationOptions(this IServiceCollection services)
        {
            var key = new RsaSecurityKey(GetNewRSAKey());
 
            var tokenOptions = new TokenAuthOptions
            {
                Audience = _audience,
                Issuer = _issuer,
                SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature)
            };
 
            services.AddInstance(tokenOptions);
        }
 
        public static void RegisterBearerAuthentication(this IApplicationBuilder app, TokenAuthOptions tokenAuthOptions)
        { 
            app.UseJwtBearerAuthentication(options =>
            {
                options.TokenValidationParameters.IssuerSigningKey = tokenAuthOptions.SigningCredentials.Key;
                options.TokenValidationParameters.ValidAudience = tokenAuthOptions.Audience;
                options.TokenValidationParameters.ValidIssuer = tokenAuthOptions.Issuer;
                options.TokenValidationParameters.ValidateSignature = true;
                options.TokenValidationParameters.ValidateLifetime = true;
                options.TokenValidationParameters.ClockSkew = TimeSpan.Zero;
            });
        }
 
        private static RSAParameters GetNewRSAKey()
        {
            using (var rsa = new RSACryptoServiceProvider(2048))
            {
                try
                {
                    return rsa.ExportParameters(true);
                }
                finally
                {
                    rsa.PersistKeyInCsp = false;
                }
            }
        }
    }

 

Tu zatrzymajmy się na chwilę w celu omówienia. Metoda rozszerzająca RegisterBearerPolicy ma na celu zarejestrowanie w domyślnym kontenerze ASP.NET Core, polityki którą będziemy stosowali w naszych kontrolerach Web API. Druga metoda (również rozszerzająca IServiceCollection) rejestruje instancję klasy TokenAuthOptions. Instancja zostaje wcześniej uzupełniona dwoma stringami (nie znalazłem dobrych, polskich odpowiedników na te property) oraz kluczem RSA, który posłuży nam do szyfrowania oraz deszyfrowania tokenów. Nasza tożsamość nie będzie przecież latać po sieci niezabezpieczona 😉 Dla tych, którzy nie słyszeli o RSA, jest to kryptograficzny algorytm asymetryczny, którego siła i fundamenty oparte są na generowaniu bardzo dużych liczb pierwszych. Tu zapodam fajny artykuł, który to omawia. Klucz zostaje wygenerowany prywatną metodą GetNewRSAKey. Generalnie to rozwiązanie ma małą wadę. W przypadku restartu aplikacji, na starcie zostanie wygenerowany nowy klucz, a co za tym idzie, wszyscy użytkownicy aktualnie zalogowani będą otrzymywać z serwera 401 (Unaothorized) dopóki nie zalogują się ponownie 😀 Z tego co widziałem doradzano, aby klucz trzymać w pliku XML i deseralizować w celu jego wydobycia. Mało to bezpieczne, dlatego próbowałem schować to do sekretów (nowy bajer w ASP), ale zserializowany klucz chyba był za długi i dostawałem error :/ Jak coś wykombinuję to na pewno się z Wami podzielę. Ostatnią metodą jest RegisterBearerAuthentication, w której rejestrujemy wszelkie opcje związane z naszym tokenem. Wywołanie tych trzech metod odbywa się w klasach odpowiedzialnych za rejestrację klas w standardowym kontenerze IoC oraz Autofac w folderze DependencyInjection:

 


public class Registration : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterModule(new DomainProxy.DependencyInjection.Registration());
            builder.RegisterType<OAuthService>().As<IOAuthService>();            
        }
    }


public static class AspNetRegistration
    {
        public static void Register(IServiceCollection services)
        {
            DomainProxy.DependencyInjection.AspNetRegistration.Register(services);
            services.RegisterBearerPolicy();
            services.RegisterTokenAuthorizationOptions();
        }
    }

 

Jeśli nie wiecie skąd te klasy się wzięły to odsyłam do starszego posta o konfiguracji Autofac w ASP.NET Core 😉 Dobra przedstawię Wam jeszcze implementację OAuthService:

 


public sealed class OAuthService : IOAuthService
    {
        private readonly TokenAuthOptions _tokenAuthOptions;
 
        public OAuthService(TokenAuthOptions tokenAuthOptions)
        {
            _tokenAuthOptions = tokenAuthOptions;
        }
 
        public string GetUserAuthToken(string userName, string userId)
        {
            var handler = new JwtSecurityTokenHandler();
 
            var identity = new ClaimsIdentity(new GenericIdentity(userName, "TokenAuth"), new[] { new Claim("UserId", userId, ClaimValueTypes.String) });
 
            var securityToken = handler.CreateToken(
                _tokenAuthOptions.Issuer,
                _tokenAuthOptions.Audience,
                signingCredentials: _tokenAuthOptions.SigningCredentials,
                subject: identity,
                expires: DateTime.UtcNow.AddDays(14));
 
            return handler.WriteToken(securityToken);
        }
    }

 

Serwis posiada jedną zależność, która zostaje wstrzyknięta w konstruktorze, a jest nią wcześniej wspomniana klasa TokenAuthOptions. Metoda GetUserAuthToken na podstawie nazwy użytkownika oraz jago identyfikatora zwraca token w postaci stringa. Na początku tworzymy ClaimsIdentity użytkownika po czym przechodzimy do samego tokena, podając Issuer-a, Audience, klucz RSA, nasze wygenerowane identity, oraz czas wygaśnięcia tokena. Jeżeli jakieś pole nie będzie zgodne z tym co zostało zadeklarowane w metodzie RegisterBearerAuthentication wówczas otrzymamy piękne 401 🙂

Żeby wszystko zaczęło działać musimy oznaczyć nasz kontroler odpowiednim atrybutem. Żeby jednak tego procesu nie powielać za każdym razem, utworzymy sobie kontroler bazowy po którym dziedziczyć będzie reszta. Póki co wygląda tak:


[Authorize("Bearer")]
    public class BaseController : Controller
    {        
    }

 

Przejdźmy do akcji logowania użytkowników w kontrolerze AccountsControler:

 


        [HttpPost("Login"), AllowAnonymous]
        public async Task<string> LoginUserAsync([FromBody] UserLoginDto userLoginDto)
        {
            var user = await _userAuthDomainServiceProxy.GetUserLoginInfoAsync(userLoginDto.UserName);
 
            if (user == null || !user.IsActive)
            {
                throw new OperationException("User not found");
            }
            if (user.IsLocked)
            {
                throw new OperationException("User is locked");
            }
 
            var signInResult = await _userAuthDomainServiceProxy.PasswordSignInAsync(userLoginDto);
 
            if (!signInResult.Succeeded)
            {
                throw new OperationException("Sign in failed");
            }
 
            var userToken = _oAuthService.GetUserAuthToken(userLoginDto.UserName, user.Id);
 
            return userToken;
        }

 

Metoda LoginUserAsync przyjmuje DTO, które zawiera trzy pola:

  • nazwę uytkownika
  • hasło
  • flagę RememberMe, która determinuje żywotność sesji użytkownika

Następnie pobieramy z domeny niezbędne informacje o użytkowniku tj. Id, informację czy jest on zablokowany oraz czy został usunięty. Sprawdzamy warunki, a w przypadku nieprawidłowości rzucamy wyjątkiem. Następnie przechodzimy do wywołania metody, która utworzy sesję użytkownika na serwerze. Zobaczmy jak wygląda jej implementacja w warstwie Domain:

 


public sealed class UserAuthDomainService : IUserAuthDomainService
   {
       private readonly UserManager<UserEntity> _userManager;
       private readonly RoleManager<IdentityRole> _roleManager; 
       private readonly SignInManager<UserEntity> _signInManager;
 
       public UserAuthDomainService(UserManager<UserEntity> userManager, SignInManager<UserEntity> signInManager, RoleManager<IdentityRole> roleManager)
       {
           _userManager = userManager;
           _signInManager = signInManager;
           _roleManager = roleManager;
       } 
 
       public async Task<SignInResult> PasswordSignInAsync(UserLoginDomainObject userLoginDomainObject)
       {          
           return await _signInManager.PasswordSignInAsync(userLoginDomainObject.UserName, userLoginDomainObject.Password, userLoginDomainObject.RememberMe,false);
       } 

       //inne metody
   }

 

Jak widać, jest to wywołanie metody domyślnego SignInManager-a ASP.NET Identity. Nothing special 😛 Wracając do kontrolera. Po otrzymaniu rezultatu z domeny w postaci obiektu SignInResult sprawdzamy czy operacja logowania użytkownika powiodła się. Jeżeli tak, zwracamy użytkownikowi token, który pozyskujemy poprzez omawiany OAuthService. Dobra, czas zobaczyć czy to działa 😀 Odpalamy postmana i wysyłamy nasze dane:

 

postman

 

 

Dobra nasza, działa ! Teraz użyjmy tokena do wywołania akcji kontrolera, który jest już opatrzony atrybutem Authorize. Robimy to poprzez dodnie do naszego żądania nagłówka Authorization z Value równym Bearer {token}. Wygląda to tak:

 

request_with_token

 

Natomiast gdy nie podamy tokena, rezultat to 401:

 

request_without_token

 

Na zakończenie rozszerzymy sobie funkcjonalność naszego kontrolera bazowego dodając mu metodę wyciągającą Id zalogowanego użytkownika z sesji :

 


protected string GetUserId()
        {
            var claimsIdentoty = (ClaimsIdentity) User.Identity;
            var userClaim = claimsIdentoty.Claims.FirstOrDefault(c => c.Type == "UserId");
 
            return userClaim?.Value;
        }

 

To wszystko na dziś. W weekend pojawi się post dotyczący strony klienta. Stworzymy klasę odpowiedzialną za przechowywanie tokena w przeglądarce, oraz stworzymy widok logowania wraz z view modelem i serwisem. Przypominam także, o śledzeni mojego twittera, aby być na bieżąco z wpisami oraz o githubie projektu.

Do następnego !