Esse artigo foi escrito a fim de disponibilizar uma solução genérica e completa interagindo com Identity do ASP.NET CORE usando autenticação Cookies em uma aplicação Blazor.
O que iremos fazer é algo flexível e genérico que possibilite usufruir autenticação e autorização do ASP.NET CORE mas sem usar todos os métodos pré-existentes.
Essa é para você que já tem o seu próprio cadastro de usuários (banco de dados ou serviço) e quer usar o Identity do ASP.NET CORE.
Algumas informações importantes para quem está chegando agora. Se você é um desenvolvedor .NET mas nunca viu nada de .NET Core, sugiro você ler antes o artigo .NET Core para DesenvolvedoreS .NET.
Dica: Não é necessário para esse artigo, mas se você não sabe como configurar o .NET Core no IIS, veja IIS: Como Hospedar Aplicação .NET Core em 10 Passos.
Se você tem preferência em usar MAC ou Linux, também vai conseguir acompanhar, já que toda a solução é em .NET Core.
Diversas classes e interfaces usadas nesse artigo, também foram usadas no outro artigo sobre autenticação com Identity, o JWT: Customizando o Identity no ASP.NET CORE.
A partir desse ponto, a leitura do artigo é de 10 minutos, e sua aplicação leva 10 minutos.
# Cookies – O que faremos?
1 – Aplicação Blazor em ASP.NET Core C#
Criação de uma aplicação web Blazor Server App em .NET Core na linguagem de programação C# com objetivo de interagir com regras de negócio e retornar informações do Identity.
2 – Endpoint público de login
Criação de um endpoint público para LOGIN, ou seja, a partir de login/senha, autenticar o usuário gerando Cookies.
3 – Página privada
– Criação de uma página privada que só funcionará se houver um Cookies válido, ou seja, só funcionará com o usuário autenticado.
4 – Classes e Interfaces
Criação das interfaces e classes genéricas para compor a solução completa.
5 – Configuração do Identity
Configuração do Identity para usar essa solução customizada.
Os Design Patterns usados nesse artigo podem ser encontrados no meu eBook Design Patterns Vol. 1 Programação no Mundo Real.
# Cookies – Workflow
Por experiência, eu prefiro começar de trás para frente, ou seja, primeiro criar um endpoint público de Login para autenticar um usuário.
Pois, se começarmos a partir da configuração do Identity vai dar um nó na cabeça.
O Workflow é o seguinte:
Os sufixos Service, Provider e Configuration são apenas para padronizar nomenclatura, mas você poderia chamá-los do jeito que quiser.
A partir de uma Action GET, LoginPageModel irá receber login/senha, descriptografá-los através de ICryptographyProvider e enviá-los ao método Authorize de IAuthorizationService.
Se a autorização do usuário/senha for realizada com sucesso, LoginPageModel acionará o método AuthenticateAsync de IAuthenticationService autenticando o usuário gerando assim um Cookie válido.
# Cookies – Criando a aplicação Blazor
Para criar uma aplicação Blazor no Visual Studio 2019, vá em File, New, Project.
Na janela que será aberta, escolha Blazor App e clique em Next.
Coloque FSL.BlazorCookies no nome do projeto e clique em Create.
Agora, escolha ASP.NET Core, template Blazor Server App e desmarque a opção HTTPS.
Por fim, deixe sem autenticação e clique em Create.
Se você precisar baixar a versão ASP.NET CORE, visite o site asp.et
Após a criação da aplicação Blazor em ASP.NET CORE, aperte F5 para executá-la.
Dessa forma será exibido o resultado de uma tela de demonstração.
# Cookies – Criando componente de Login
Antes de tudo é necessário criar um componente de Login que exibirá os campos de Login e Senha.
LoginControl.razor:
@using FSL.BlazorCookies.Provider @page "/loginControl" @inject ICryptographyProvider CryptographyProvider @inject NavigationManager NavigationManager <AuthorizeView> <Authorized> <b>Hello, @context.User.Identity.Name!</b> <a class="ml-md-auto btn btn-primary" href="logout" target="_top">Log out</a> </Authorized> <NotAuthorized> <input type="text" placeholder="Email or Login" @bind="@Username" /> <input type="password" placeholder="Password" @bind="@Password" /> <a class="ml-md-auto btn btn-primary" href="@(MontarUrlLogin())" target="_top">Log in</a> </NotAuthorized> </AuthorizeView> @code { string Username = ""; string Password = ""; string url = ""; protected override void OnInitialized() { url = NavigationManager.Uri.ToString(); } private string MontarUrlLogin() { if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password)) { return ""; } return string.Format( "login?emailOrLogin={0}&password={1}&returnUrl={2}", CryptographyProvider.Encrypt(Username), CryptographyProvider.Encrypt(Password), url); } }
Esse componente de Login, que chamei de LoginControl, também pode ser chamado através de uma URL “/logincontrol” ou como uma tag HTML “LoginControl”.
São injetados duas funcionalidades ICryptographyProvider e NavigationManager, uma para criptografar login/senha e outra para capturar a URL atual.
Se o usuário não estiver autenticado será exibido campos de login e senha, mas se o usuário já estiver autenticado, será exibido o nome dele.
Para saber mais sobre componentes no Blazor, consulte meu artigo Blazor: Muito mais que um WebForms de Luxo.
O método MontarUrlLogin criptografa login/senha e monta a URL com endpoint “/login“.
O endpoint “/login” é o nosso core. É ele que receberá os dados de login criptografadas e realizará a autorização/autenticação do usuário.
# Cookies – Criando endpoint Login e interfaces
Após ter construído o frontend para capturar o login/senha, vamos criar o endpoint de Login.
Conforme desenho de arquitetura e workflow, o endpoint de Login ficou assim:
Login.cshtml:
@page @model FSL.BlazorCookies.Pages.LoginPageModel
Login.cshtml.cs:
[AllowAnonymous] public class LoginPageModel : PageModel { private readonly IAuthenticationService _authenticationService; private readonly Service.IAuthorizationService _authorizationService; private readonly ICryptographyProvider _cryptographyProvider; private readonly string _loginControlName; public LoginPageModel( IAuthenticationService authenticationService, Service.IAuthorizationService authorizationService, ICryptographyProvider cryptographyProvider) { _authenticationService = authenticationService; _authorizationService = authorizationService; _cryptographyProvider = cryptographyProvider; _loginControlName = typeof(LoginControl).Name.ToString(); } public async Task<IActionResult> OnGetAsync( string emailOrLogin, string password, string returnUrl = null) { emailOrLogin = _cryptographyProvider.DeCrypt(emailOrLogin); password = _cryptographyProvider.DeCrypt(password); var loginUser = new LoginUser { LoginOrEmail = emailOrLogin, Password = password }; var authorization = await _authorizationService.AuthorizeAsync(loginUser); if (!authorization.Success) { return RedirectToLogin(); } var authentication = await _authenticationService.AuthenticateAsync(authorization.Data); if (!authentication.Success) { return RedirectToLogin(); } return RedirectTo(returnUrl); } private IActionResult RedirectTo( string returnUrl) { if (!string.IsNullOrEmpty(returnUrl)) { if (returnUrl.Contains(_loginControlName)) { returnUrl = Url.Content("~/"); } else { returnUrl ??= Url.Content("~/"); } } return Redirect(returnUrl); } private IActionResult RedirectToLogin() { return RedirectTo(Url.Content($"~/{_loginControlName}")); } }
Imagine que a classe LoginPageModel dentro do arquivo Login.cshtml.cs como um Controller de uma API.
Se autorizado/autenticado, o usuário irá ser redirecionado a URL que ele estava, caso contrário ele será redirecionado para URL de login.
As instâncias que implementam as interfaces IAuthenticationService e IAuthorizationService serão injetadas no construtor de LoginPageModel. Esse Design Pattern é chamado de Dependency Injection.
Para que tudo isso funcione ainda é necessário criar uma classe para cada uma dessas interfaces e fazer uma configuração no arquivo Startup.cs da aplicação web. Veremos isso mais a frente.
As classes e interfaces envolvidas no LoginPageModel são:
public interface ICryptographyProvider { string Encrypt( string info); string DeCrypt( string info); } public interface IAuthorizationService { Task<BaseResult<IUser>> AuthorizeAsync( LoginUser loginUser); } public interface IAuthenticationService { Task<AuthenticationResult> AuthenticateAsync( IUser user); string GetAuthenticationSchema(); Task LogoutAsync(); } public sealed class AuthenticationResult : BaseResult<object> { public bool Authenticated { get; set; } public string Created { get; set; } public string Expiration { get; set; } } public class BaseResult<T> { public string Message { get; set; } public bool Success { get; set; } public T Data { get; set; } } public interface IUser { string Id { get; set; } string Name { get; set; } } public sealed class LoginUser { public string LoginOrEmail { get; set; } public string Password { get; set; } } public sealed class MyLoggedUser : IUser { public string Id { get; set; } public string Name { get; set; } public string Credentials { get; set; } public bool IsAdmin { get; set; } } public sealed class MySettingsConfiguration { public int ExpirationInSeconds { get; set; } public string CryptographicKey { get; set; } public string CookieName { get; set; } }
Todas essas interfaces e classes são genéricas, assim poderiam estar em uma DLL separada para serem reutilizadas em outros projetos.
# Cookies – Criando as classes Fakes
No item anterior desenvolvemos toda a lógica do LoginPageModel, usando as interfaces sem precisar ter as classes que implementam essas interfaces. Esse conceito é chamado de Inversion of Control.
Mas para testar a page LoginPageModel, precisamos das classes que implementam IAuthenticationService, IAuthorizationService e ICryptographyProvider.
Vamos inicialmente criar classes “Fake” para simular a entrada/saída de dados.
public sealed class FakeAuthorizationService : IAuthorizationService { public async Task<BaseResult<IUser>> AuthorizeAsync( LoginUser loginUser) { var loginOrEmail = loginUser?.LoginOrEmail ?? ""; var password = loginUser?.Password ?? ""; var result = new BaseResult<IUser>(); if (loginOrEmail == "fsl" && password == "1234") { result.Success = true; result.Message = "User authorized!"; result.Data = new MyLoggedUser { Id = Guid.NewGuid().ToString(), Name = "Name test", Credentials = "01|02|09", IsAdmin = false }; } else { result.Success = false; result.Message = "Not authorized!"; } return await Task.FromResult(result); } }
public sealed class FakeAuthenticationService : IAuthenticationService { public async Task<AuthenticationResult> AuthenticateAsync( IUser user) { var dateFormat = "yyyy-MM-dd HH:mm:ss"; var result = new AuthenticationResult() { Success = true, Authenticated = true, Created = DateTime.UtcNow.ToString(dateFormat), Expiration = DateTime.UtcNow.AddHours(2).ToString(dateFormat), Message = "OK" }; return await Task.FromResult(result); } }
A classe que implementa ICryptographyProvider, responsável por criptografar/descriptografar ficou assim:
public sealed class DESKeyCryptographyProvider : ICryptographyProvider { private readonly MySettingsConfiguration _configuration; private readonly byte[] _iv = { 12, 34, 56, 78, 90, 102, 114, 126 }; public DESKeyCryptographyProvider( MySettingsConfiguration configuration) { _configuration = configuration; } public string DeCrypt( string info) { DESCryptoServiceProvider des; MemoryStream ms; CryptoStream cs; byte[] input; try { des = new DESCryptoServiceProvider(); ms = new MemoryStream(); var conteudo = info.Replace(" ", "+").Replace("[i]", "=").Replace("[e]", "&").Replace("[m]", "+").Replace("[n]", "-"); input = new byte[conteudo.Length]; input = Convert.FromBase64String(conteudo); var key = Encoding.UTF8.GetBytes(_configuration.CryptographicKey.Substring(0, 8)); cs = new CryptoStream(ms, des.CreateDecryptor(key, _iv), CryptoStreamMode.Write); cs.Write(input, 0, input.Length); cs.FlushFinalBlock(); return Encoding.UTF8.GetString(ms.ToArray()); } catch (Exception ex) { throw ex; } } public string Encrypt( string info) { DESCryptoServiceProvider des; MemoryStream ms; CryptoStream cs; byte[] input; try { des = new DESCryptoServiceProvider(); ms = new MemoryStream(); input = Encoding.UTF8.GetBytes(info); var key = Encoding.UTF8.GetBytes(_configuration.CryptographicKey.Substring(0, 8)); cs = new CryptoStream(ms, des.CreateEncryptor(key, _iv), CryptoStreamMode.Write); cs.Write(input, 0, input.Length); cs.FlushFinalBlock(); return Convert.ToBase64String(ms.ToArray()).Replace("=", "[i]").Replace("&", "[e]").Replace("+", "[m]").Replace("-", "[n]"); } catch (Exception ex) { throw ex; } } }
Por fim, com as classes e interfaces de autorização, autenticação e criptografia criadas, precisamos configurar o Dependency Injection do ASP.NET CORE através do arquivo Startup.cs.
public void ConfigureServices( IServiceCollection services) { services.AddConfiguration<MySettingsConfiguration>(Configuration); services.AddHttpContextAccessor(); services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton<WeatherForecastService>(); services.AddSingleton<Provider.ICryptographyProvider, Provider.DESKeyCryptographyProvider>(); // here services.AddSingleton<Service.IAuthorizationService, Service.FakeAuthorizationService>(); // here services.AddSingleton<Service.IAuthenticationService, Service.FakeAuthenticationService>(); // here }
Repare que existe uma classe MySettingsConfiguration na primeira linha do método ConfigureServices acima.
Essa classe é utilizada para guardar as configurações existentes no arquivo AppSettings.JSON de uma aplicação ASP.NET CORE.
Para saber mais sobre como funciona o arquivo AppSettings.JSON, consulte meu artigo AppSettings: 6 Formas de Ler o Config no ASP.NET CORE 3.0.
# Cookies – Criando Cookies Service
A primeira versão da classe de autenticação que irá autenticar o usuário via Cookies é:
public sealed class CookiesIdentityAuthenticationService : IAuthenticationService { private readonly IHttpContextAccessor _httpContextAccessor; private readonly MySettingsConfiguration _configuration; public CookiesIdentityAuthenticationService( IHttpContextAccessor httpContextAccessor, MySettingsConfiguration configuration) { _httpContextAccessor = httpContextAccessor; _configuration = configuration; } public async Task<AuthenticationResult> AuthenticateAsync( IUser user) { await LogoutAsync(); var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Name), new Claim("Data", user.ToJson()) }; var claimsIdentity = new ClaimsIdentity( claims, GetAuthenticationSchema()); var created = DateTime.UtcNow; var expiration = created + TimeSpan.FromSeconds(_configuration.ExpirationInSeconds); var authProperties = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = expiration }; var dateFormat = "yyyy-MM-dd HH:mm:ss"; var result = new AuthenticationResult { Success = true, Authenticated = true, Created = created.ToString(dateFormat), Expiration = expiration.ToString(dateFormat), Message = "OK", Data = new CookiesAuthentication { ClaimsIdentity = claimsIdentity, AuthProperties = authProperties } }; var cookieAuthentication = result.Data as CookiesAuthentication; cookieAuthentication.AuthProperties.RedirectUri = _httpContextAccessor.HttpContext.Request.Host.Value; await LogOnAsync( cookieAuthentication.ClaimsIdentity, cookieAuthentication.AuthProperties); return result; } public string GetAuthenticationSchema() { return CookieAuthenticationDefaults.AuthenticationScheme; } public async Task LogoutAsync() { try { await _httpContextAccessor.HttpContext.SignOutAsync(GetAuthenticationSchema()); } catch { } } private async Task LogOnAsync( ClaimsIdentity claimsIdentity, AuthenticationProperties authProperties) { try { await _httpContextAccessor.HttpContext.SignInAsync( GetAuthenticationSchema(), new ClaimsPrincipal(claimsIdentity), authProperties); } catch { } } }
Antes de testar, precisamos informar a Dependency Injection no Startup.cs, trocando a classe FakeAuthenticationService por CookiesIdentityAuthenticationService.
public void ConfigureServices( IServiceCollection services) { services.AddConfiguration<MySettingsConfiguration>(Configuration); services.AddHttpContextAccessor(); services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton<WeatherForecastService>(); services.AddSingleton<Provider.ICryptographyProvider, Provider.DESKeyCryptographyProvider>(); // here services.AddSingleton<Service.IAuthorizationService, Service.FakeAuthorizationService>(); // here services.AddSingleton<Service.IAuthenticationService, Service.CookiesIdentityAuthenticationService>(); // here }
# Cookies – Configurando Autorização
Antes de fazer a configuração no Blazor ASP.NET CORE para autorizar pages usando autenticação por Cookies, vamos ver como ficar uma page privada de exemplo PrivatePage.razor:
@page "/private-page" @attribute [Authorize] <h2>Private Content</h2>
Para deixar uma page ou component privado, basta usar o atributo [Authorize] do próprio ASP.NET CORE.
Então, no caso acima só irá aparecer o título “Private Content” após o usuário estar autenticado na aplicação Blazor.
Para todo esse mecanismo funcionar corretamente abrindo a tela de login quando entrar na page privada, é necessário configurar o arquivo Startup.cs do ASP.NET CORE para Autorização via Cookies.
public void ConfigureServices( IServiceCollection services) { services.AddConfiguration<MySettingsConfiguration>(Configuration); services.AddCookiesAuthentication(Configuration); // here services.AddHttpContextAccessor(); services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton<WeatherForecastService>(); services.UseCookiesAuthentication(); // here services.AddSingleton<Provider.ICryptographyProvider, Provider.DESKeyCryptographyProvider>(); services.AddSingleton<Service.IAuthorizationService, Service.FakeAuthorizationService>(); } public void Configure( IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseCookiesAuthentication(); // here app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); }); }
As linhas de código acima marcadas como “// here” são Extensions Methods para facilitar a manutenção do código fonte:
public static class BlazorExtension { public static IServiceCollection AddCookiesAuthentication( this IServiceCollection services, IConfiguration configuration) { services.Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services .AddAuthentication(options => { var scheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = scheme; options.DefaultSignInScheme = scheme; options.DefaultChallengeScheme = scheme; }) .AddCookie(opt => { opt.Cookie.Name = configuration.GetValue<string>($"{typeof(MySettingsConfiguration).Name}:CookieName"); }); return services; } public static IServiceCollection UseCookiesAuthentication( this IServiceCollection services) { services.AddSingleton<IAuthenticationService, CookiesIdentityAuthenticationService>(); return services; } public static IApplicationBuilder UseCookiesAuthentication( this IApplicationBuilder app) { var cookiePolicyOptions = new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax, }; app.UseCookiePolicy(cookiePolicyOptions); app.UseAuthentication(); app.UseAuthorization(); return app; } }
Outros Extension Methods utilizados na solução:
public static class ConfigurationExtension { public static void AddConfiguration<T>( this IServiceCollection services, IConfiguration configuration, string configurationTag = null) where T : class { if (string.IsNullOrEmpty(configurationTag)) { configurationTag = typeof(T).Name; } var instance = Activator.CreateInstance<T>(); new ConfigureFromConfigurationOptions<T>(configuration.GetSection(configurationTag)).Configure(instance); services.AddSingleton(instance); } }
public static class StringExtension { public static T FromJson<T>( this string json) { if (json == null || json.Length == 0) { return default; } return JsonConvert.DeserializeObject<T>( json, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); } public static string ToJson<T>( this T obj) { if (obj == null) { return null; } return JsonConvert.SerializeObject(obj, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); } }
# Cookies – Doces ou Travessuras?
Recapitulando, criamos uma page de LoginControl com campos login/senha que enviam os dados criptografados ao endpoint LoginPageModel, que recebe esses dados e faz a autorização/autenticação usando Identity com Cookies Authentication na aplicação web Blazor ASP.NET CORE.
Nesse workflow todo, usei Dependency Injection a partir de interfaces, Extension Methods para encapsular configurações gerais para uso no Startup.cs e criei uma classe para guardar as configurações do arquivo AppSettings.JSON.
Mas… há um ponto de atenção que passa despercebido.
Toda essa lógica de autentição e autorização era pra funcionar, porém nesse caso, precisamos adaptar o arquivo App.razor, vide:
@using FSL.BlazorCookies.Pages <Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> <h1>Sorry!</h1> <p>Log in to access this page.</p> <LoginControl></LoginControl> </NotAuthorized> <Authorizing> <h1>Authentication in progress</h1> <p>Only visible while authentication is in progress.</p> </Authorizing> </AuthorizeRouteView> </Found> <NotFound> <CascadingAuthenticationState> <LayoutView Layout="@typeof(MainLayout)"> <h1>Sorry!</h1> <p>Something is wrong :(</p> </LayoutView> </CascadingAuthenticationState> </NotFound> </Router>
O Blazor se comunica com a autenticação/autorização nativa do ASP.NET CORE através desses componentes AuthorizeView, AuthorizeRouteView e CascadingAuthenticationState.
Para um detalhamento maior, acesse o site do blazor clicando aqui.
# Cookies – Considerações finais
Vendo assim o passo a passo pode parecer confuso, mas não é, depois que você passa entender como funciona tudo fica mais simples.
Além do Identity e Cookies, nesse artigo foi demonstrado várias técnicas e Design Patterns.
Essa é uma solução para customizar Identity usando Cookies no Blazor.
Se você conhece outras, compartilhe e comente.
Muito obrigado.
Créditos e materiais de apoio:
Renato Groffe | Maccoratti | Tahir Naushad | Microsoft
Artigos sobre ASP.NET CORE e Blazor:
Crie seu Framework em ASP.NET CORE 3 e Blazor
Blazor: Muito mais que um WebForms de Luxo
Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0
AppSettings: 6 Formas de Ler o Config no ASP.NET CORE 3.0
Crie um Gerenciador de Arquivos do Zero em .NET Core e VueJS
IIS: Como Hospedar Aplicação .NET Core em 10 Passos
.NET Core para Desenvolvedores .NET
Blazor: O Começo do Fim do Javascript?
Faça download completo do código fonte no github. |