Cookies: Identity no Blazor e ASP.NET CORE 3.0

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.

Cookies: Identity no Blazor e ASP.NET CORE 3.0

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.

JWT: Customizando o Identity no ASP.NET CORE

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.
Sobre o Autor:
Trabalha como arquiteto de soluções e desenvolvedor, tem mais de 18 anos de experiência em desenvolvimento de software em diversas plataformas sendo mais de 16 anos somente para o mercado de seguros.
Revisado por:
Apaixonado por tecnologia e sempre disposto a encarar novos desafios, atualmente trabalho focado em aplicações web e mobile com a plataforma .NET, e me aventurando nas diversas linguagens, desafios e experiências que a área nos proporciona.