Blazor WebAssembly - API com Autenticação JWT e Identity - I
Neste artigo veremos implementar a autenticação JWT em uma aplicação Blazor WebAssembly para acesso a uma Web API com Identity com EF Core usando a abordagem Code-First. |
Se você esta chegando agora e não sabe o que é o Blazor leia o artigo ASP .NET Core - Iniciando com o Blazor - Macoratti; se você já conhece e quer saber mais pode fazer o meu curso de Blazor Essencial.
Este artigo é uma adaptação do artigo original de Chris Sainty onde eu realizei alguns ajustes e usei o ambiente .NET Core SDK 5.0.
Neste artigo iremos usar conceitos do Identity, Migrations do EF Core e da autenticação usando o Json Web Tokens(JWT). Se você precisar conhecer mais esses conceitos veja os artigos publicados no site nas referências do artigo.
Recursos usados:
Criando um projeto Blazor Web Assembly
Abra o VS 2019 Community e selecione a opção Create a New Project;
A seguir selecione a opção Blazor WebAssembly App e clique em next;
Informe o nome da solução BlazorAppAgenda e clique em Next;
Defina as configurações conforme mostrado na figura abaixo e clique em Create;
Note que marcamos a opção para criar o projeto usando a hospedagem ASP .NET Core.
A final teremos a solução e os projetos criados em uma arquitetura monolítica de projeto único com a seguinte estrutura exibida na janela Solution Explorer:
Temos 3 projetos e vamos iniciar configurando o projeto Server onde vamos definir a configuração para implementar a autenticação JWT , a configuração do Identity , a implementação dos controladores e da API AgendaController que iremos usar para testar a implementação da autenticação.
Configurando o projeto Server : Jwt, Identity e APIs
Vamos iniciar incluindo no projeto Server os pacotes que serão necessários para realizar toda a configuração que precisamos para realizar as implementações.
"Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.7"
"Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.6"
"Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.7"
"Microsoft.AspNetCore.Identity.UI" Version="5.0.7"
"Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.7"
"Microsoft.EntityFrameworkCore.Tools" Version="5.0.7
"Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2" />
Para incluir estes pacotes com as versões usadas você pode usar o comando :
No arquivo appsettings.json vamos incluir uma seção 'ConnectionStrings' onde vamos definir a string de conexão e a seção 'Jwt' onde vamos definir a chave secreta, o emissor, o destinatário e o tempo de expiração do token JWT que iremos usar para realizar a autenticação :
{
"ConnectionStrings": {
"DefaultConnection": "Sua string de conexão"
},
"Jwt": {
"JwtSecurityKey": "abracadabra#simsalabim@2021",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
"JwtExpiryInDays": 1
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
|
Na definição da string de conexão você deve informar o nome do banco de dados que será criado pelo EF Core Migrations.
Como vamos executar localmente nossa aplicação definimos o emissor e a audiência como localhost. Mas em um aplicativo de produção, o Issuer deverá ser definido ara o domínio em que a API está sendo executada e a Audience para o domínio em que o aplicativo cliente está sendo executado.
Agora vamos criar uma pasta Data no projeto Server e nesta pasta criar a classe AppDbContext que vai herdar de IdentityDbContext e que representa o contexto da aplicação com o EF Core:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace BlazorAppAgenda.Server.Data
{
public class AppDbContext : IdentityDbContext
{
public AppDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<Agenda> Agendamentos { get; set; } } } |
Como estamos usando o Identity, que precisa armazenar informações em um banco de dados, não herdamos de DbContext, mas sim de IdentityDbContext que contém todas as configurações de que o EF Core precisa para gerenciar as tabelas do Identity.
Nesta classe definimos o mapeamento da entidade Agenda para a tabela Agendamentos que será criada no banco de dados SQL Server quando aplicarmos o Migrations. Vamos criar a classe Agenda no projeto Shared. (Isso é opcional se você não quiser gerenciar os dados da agenda no SQL Server ou junto com as tabelas do Identity)
Precisamos agora registrar o serviço do contexto no método ConfigureServices da classe Startup do projeto Server.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<AppDbContext>();
services.AddControllersWithViews();
services.AddRazorPages();
}
|
Neste código estamos incluindo o arquivo de contexto AppDbContext na coleção de serviços e registrando os serviços do Identity informando-o para usar o EF Core como o meio de armazenamento via AppDbContext.
Também definimos o provedor para o usar o SQL Server e estamos obtendo a string de conexão definida no arquivo appsettings.json.
Antes de aplicar o Migrations vamos criar no projeto Shared a classe Agenda :
public class Agenda
{
public int Id { get; set; }
public string Evento { get; set; }
public DateTime Data { get; set; }
}
|
Agora podemos aplicar o Migrations para criar o banco de dados e as tabelas do Identity e a tabela Agendamentos no SQL Server.
Para isso execute os comandos na seguinte ordem: (Estou usando o Package Manager Console)
Add-Migration Inicial -o Data/Migrations
Este comando irá criar a pasta Migrations dentro da pasta Data no projeto Server e criará o arquivo de script contendo as instruções para criar as tabelas do Identity e a tabela Agendamentos.
Para aplicar o script no banco de dados SQL Server digite o comando:
update-database
Ao final teremos o banco e as tabelas criadas e prontas para serem usadas no projeto.
Configurando a autenticação JWT
A próxima etapa é habilitar a autenticação JWT no projeto que iremos usar em nossa API para isso vamos incluir o código abaixo no método ConfigureServices() do arquivo Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<AppDbContext>();
var secretKey = Configuration["Jwt:JwtSecurityKey"];
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:JwtIssuer"],
ValidAudience = Configuration["Jwt:JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey))
};
});
services.AddControllersWithViews();
services.AddRazorPages();
}
|
Neste código invocamos o middleware AddAuthentication() e especificamos o esquema do portador JWT para ser o esquema de autenticação. Também especificamos várias opções para o esquema do portador(bearer) JWT.
Se você observar o objeto TokenValidationParameters, verá que ele indica se o emissor, a audiência, a vida útil e a chave de assinatura devem ser validados ou não. No exemplo usamos definindo como true.
Além disso, também especificamos um emissor válido, um público-alvo válido e uma chave de assinatura válida. Esses valores são recuperados do arquivo de configuração appsettings.json onde definimos os valores para chave secreta, emissor e destinatário.
É muito importante que a JwtSecurityKey seja mantida em segredo, pois é ela que é usada para assinar os tokens produzidos pela API. Se isso for comprometido, seu aplicativo não estará mais seguro.
Não podemos esquecer de habilitar a autenticação incluindo no método Configure da classe Startup as instruções destacadas em azul:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
|
Agora vamos iniciar a etapa de definição do registro e do login do usuário mas antes precisamos criar as seguintes entidades no projeto Shared:
1-UserModel
public class UserModel
{
public string Email { get; set; }
public bool IsAuthenticated { get; set; }
}
|
2-LoginModel
using System.ComponentModel.DataAnnotations;
namespace BlazorAppAgenda.Shared
{
public class LoginModel
{
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
public bool RememberMe { get; set; }
}
}
|
3-LoginResult
public class LoginResult
{
public bool Successful { get; set; }
public string Error { get; set; }
public string Token { get; set; }
}
|
4-RegisterModel
using System.ComponentModel.DataAnnotations;
namespace BlazorAppAgenda.Shared
[Required]
[DataType(DataType.Password)] |
5-RegisterResult
using System.Collections.Generic;
namespace BlazorAppAgenda.Shared
{
public class RegisterResult
{
public bool Successful { get; set; }
public IEnumerable<string> Errors { get; set; }
}
}
|
Temos assim as classes que iremos usar a seguir para definir o Login e o Registro do usuário.
Definindo o Registro : Criando o controller AccountController
Para permitir que os usuários possam se autenticar vamos criar o controlador AccountController na pasta Controllers do projeto Server que será responsável por criar novas contas de usuário ou seja registrar um novo usuário.
using BlazorAppAgenda.Shared;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;
namespace BlazorAppAgenda.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private static UserModel LoggedOutUser = new UserModel { IsAuthenticated = false };
private readonly UserManager<IdentityUser> _userManager;
public AccountController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] RegisterModel model)
{
var newUser = new IdentityUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(newUser, model.Password);
if (!result.Succeeded)
{
var errors = result.Errors.Select(x => x.Description);
return Ok(new RegisterResult { Successful = false, Errors = errors });
}
return Ok(new RegisterResult { Successful = true });
}
}
}
|
Neste controlador estamos criando uma instância de UserModel definindo o usuário como não autenticado.
A seguir estamos injetando no construtor uma instância de UserManager<T> a partir da qual iremos usar o método CreateAsync() para criar um novo usuário no método Action Post do controlador.
Definindo o Login : Criando o controller LoginController
Vamos agora criar o controlador que vai permitir realizar a autenticação de usuário existente fazendo o login. Se o login for realizado com sucesso iremos criar o token JWT para retornar com LoginResult.
using BlazorAppAgenda.Shared; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Threading.Tasks;
namespace BlazorAppAgenda.Server.Controllers
[HttpPost]
if (!result.Succeeded) return BadRequest(new
LoginResult
var claims = new[]
var key
= new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:JwtSecurityKey"]));
var token = new JwtSecurityToken(
return Ok(new LoginResult |
No construtor do controlador estamos injetando instâncias de SignInManager e Configuration que iremos usar para validar o login do usuário e para obter informações do arquivo de configuração appsetings.json.
Após validar o usuário criamos uma nova SymmetricSecurityKey com base na chave secreta.
O objeto
SigningCredentials é gerado com base no
SymmetricSecurityKey. Observe que usamos o
algoritmo HS256 para gerar a assinatura digital.
Agora podemos seguir em frente e criar um token JWT. Isso é feito usando a
classe JwtSecurityToken. Passamos o emissor,
o audience(público-alvo), e data de expiração para o token e as
credenciais de assinatura obtidas do arquivo appsettings.json.
Queremos o JWT em um formato de string para que possa ser facilmente enviado ao
cliente. Isso é feito usando a classe
JwtSecurityTokenHandler. O método WriteToken()
aceita um JwtSecurityToken criado anteriormente e o
retorna como uma string de formato JSON compactada.
Com isso a parte da configuração para autenticação JWT esta pronta. Vamos criar agora a nossa API AgendaController que iremos usar para testar a autenticação.
Na pasta Controllers crie o controlador AgendaController com o código abaixo:
using BlazorAppAgenda.Shared;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace BlazorAppAgenda.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AgendaController : ControllerBase
{
[HttpGet, Authorize]
public IActionResult GetAgendamentos()
{
List<Agenda> agenda = new List<Agenda>();
agenda.Add(new Agenda()
{
Id = 1,
Evento = "Curso DDD na prática",
Data = System.DateTime.Now
});
agenda.Add(new Agenda()
{
Id = 1,
Evento = "Evento - Design Patterns",
Data = System.DateTime.Now.AddDays(15)
});
agenda.Add(new Agenda()
{
Id = 1,
Evento = "Palestra - Os recursos do .NET 6.0",
Data = System.DateTime.Now.AddDays(30)
});
return new ObjectResult(agenda);
}
}
}
|
Definimos o método HttpGet - GetAgendamentos() - que retorna uma lista de agendamentos.
Observe que usamos o atributo [Authorize] no endpoint da API e assim somente usuários autenticados poderão acessar esse endpoint.
Dessa forma temos
toda a infraestrutura para realizar a autenticação JWT
preparada vamos agora configurar o projeto Client.
Na próxima parte do artigo vamos configurar o projeto Client e criar os
componentes.
Referências:
ASP .NET Core - Implementando a segurança com
ASP.NET Core MVC - Criando um Dashboard .
C# - Gerando QRCode - Macoratti
ASP .NET - Gerando QRCode com a API do Google
ASP .NET Core 2.1 - Como customizar o Identity
Usando o ASP .NET Core Identity - Macoratti
ASP .NET Core - Apresentando o IdentityServer4
ASP .NET Core 3.1 - Usando Identity de cabo a rabo