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.
Table of Contents
# 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. |



