
O uso de JSON Web Tokens (JWT) constitui uma das práticas mais comuns para garantir um acesso seguro a APIs REST. Partindo de tokens criptografados com um tempo de validade pré-definido, esta técnica é suportada pelas principais plataformas de desenvolvimento da atualidade.
Já abordei, inclusive, em um artigo anterior, a utilização de JWT como mecanismo de autenticação em APIs REST construídas com o ASP.NET Core 2.0. O artigo em questão fazia uso de um banco de dados contendo usuários e chaves de acesso, além de demonstrar os passos necessários para a geração de tokens e como proteger o acesso a recursos disponibilizados por uma API:
ASP.NET Core 2.0: autenticação em APIs utilizando JWT (JSON Web Tokens)
O banco de dados mencionado corresponde a um controle customizado para gerenciamento de usuários e credenciais de acesso. Outras opções similares podem ser empregadas e o ASP.NET Core Identity constitui um bom exemplo neste sentido.
Fazendo uso de bases relacionais e do Entity Framework Core como tecnologia de acesso a dados, o ASP.NET Core Identity conta com funcionalidades e recursos que simplificam a implementação de controles de segurança em aplicações Web construídas com o .NET Core.
Este novo artigo foca no uso de JWT em conjunto com o ASP.NET Identity Core, a fim de garantir o acesso seguro a APIs REST baseadas no ASP.NET Core 2.0.
Aproveito este artigo para deixar aqui também um convite. No dia 29/01 (segunda) às 22h00 — horário de Brasília — farei uma apresentação no Canal .NET sobre as novidades do C# 7.2, juntamente com o MVP André Secco e utilizando para isto o release mais recente do Visual Studio 2017 (Update 15.5).
Para efetuar a sua inscrição, acesse a página do evento no Facebook ou então o Meetup. A transmissão acontecerá via YouTube, em um link a ser divulgado em breve.
Habilitando o uso do Identity em projetos ASP.NET: uma visão geral
Ao criar um novo projeto do tipo Web Application ou Web Application (Model-View-Controller) a partir do Visual Studio 2017, podemos definir como será a autenticação através da opção Change Authentication:

Selecionando então o modo Individual User Accounts e, na sequência, a opção Store user accounts in-app:

A confirmação destes procedimentos resultará na criação de tipos como ApplicationDbContext e ApplicationUser, além de outras classes, Controllers e Views empregados no gerenciamento de usuários e controle de acesso:

Já no caso de projetos Web API, não contamos com a opção Store user accounts in-app ao empregar o modo Individual User Accounts:

Esta característica trará a necessidade de implementar manualmente em nossas APIs algumas estruturas e instruções para integração com o ASP.NET Core Identity. As próximas seções descrevem em detalhes os ajustes necessários para se atingir tal objetivo.
Alguns detalhes sobre a aplicação de exemplo
A API apresentada neste artigo já foi detalhada no primeiro artigo que publiquei sobre JWT, fornecendo uma funcionalidade para a conversão de alturas em pés (unidade comumente utilizada na aviação) para o equivalente em metros.
Os fontes do novo projeto que combina o uso de JWT com o ASP.NET Core Identity já se encontram no GitHub (esta aplicação foi criada empregando o ASP.NET Core 2.0):
Implementando a integração com o ASP.NET Core Identity
No arquivo appsettings.json será incluída a string de conexão para acesso ao banco BaseIdentity (que conterá as tabelas das quais o ASP.NET Core Identity depende), bem como configurações para a geração do token (Audience, Issuer e tempo de duração em segundos):
{
"ConnectionStrings": {
"BaseIdentity": "Data Source=.\\MSSQLSERVER2016;Initial Catalog=BaseIdentity;Integrated Security=SSPI;"
},
"TokenConfigurations": {
"Audience": "ExemploAudience",
"Issuer": "ExemploIssuer",
"Seconds": 120
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}
A classe ApplicationUser herda do tipo IdentityUser (namespace Microsoft.AspNetCore.Identity), correspondendo à implementação dos dados de um usuário registrado em uma base configurada para uso do ASP.NET Core Identity:
using Microsoft.AspNetCore.Identity;
namespace APIAlturas.Models
{
public class ApplicationUser : IdentityUser
{
}
}
Na próxima listagem está a implementação das seguintes estruturas:
- User: classe empregada na manipulação de credenciais de usuários;
- Roles: tipo estático contendo o nome da role de acesso (Acesso-APIAlturas) à API de conversão de alturas;
- TokenConfigurations: classe que conterá configurações (Audience, Issuer – emissor, Seconds – tempo de validade em segundos) empregadas na geração de tokens. Estas definições serão carregadas a partir do arquivo appsettings.json.
namespace APIAlturas
{
public class User
{
public string UserID { get; set; }
public string Password { get; set; }
}
public static class Roles
{
public const string ROLE_API_ALTURAS = "Acesso-APIAlturas";
}
public class TokenConfigurations
{
public string Audience { get; set; }
public string Issuer { get; set; }
public int Seconds { get; set; }
}
}
Já a classe de contexto ApplicationDbContext implementa o tipo genérico IdentityDbContext (namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore), referenciando também ApplicationUser em sua definição. Esta construção baseada no Entity Framework Core acessará o banco BaseIdentity, sendo empregada pelas demais estruturas do ASP.NET Identity Core para gerenciar dados de usuários e permissões:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using APIAlturas.Models;
namespace APIAlturas.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}
}
A classe IdentityInitializer será responsável pela inicialização dos dados para utilização do ASP.NET Identity Core:
- O construtor de IdentityInitializer receberá instâncias dos tipos ApplicationDbContext, UserManager (namespace Microsoft.AspNetCore.Identity) e RoleManager (namespace Microsoft.AspNetCore.Identity);
- O método Initialize criará, caso ainda não existam, as estruturas de dados utilizadas pelo Identity (o que inclui o próprio banco de dados, indicado em appsettings.json pelo nome BaseIdentity). Isto acontecerá através de uma chamada ao método EnsureCreated do objeto Database vinculado a ApplicationDbContext;
- Já a instância do tipo RoleManager será empregada na criação da role Acesso-APIAlturas (se a mesma ainda não existir);
- As chamadas ao método CreateUser registrarão os usuários admin_apialturas (com permissão de acesso à API de conversão de alturas) e usrinvalido_apialturas (sem permissão), utilizando para tanto a instância da classe UserManager.
using System;
using Microsoft.AspNetCore.Identity;
using APIAlturas.Models;
namespace APIAlturas.Data
{
public class IdentityInitializer
{
private readonly ApplicationDbContext _context;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public IdentityInitializer(
ApplicationDbContext context,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager)
{
_context = context;
_userManager = userManager;
_roleManager = roleManager;
}
public void Initialize()
{
if (_context.Database.EnsureCreated())
{
if (!_roleManager.RoleExistsAsync(Roles.ROLE_API_ALTURAS).Result)
{
var resultado = _roleManager.CreateAsync(
new IdentityRole(Roles.ROLE_API_ALTURAS)).Result;
if (!resultado.Succeeded)
{
throw new Exception(
quot;Erro durante a criação da role {Roles.ROLE_API_ALTURAS}.");
}
}
CreateUser(
new ApplicationUser()
{
UserName = "admin_apialturas",
Email = "admin-apialturas@teste.com.br",
EmailConfirmed = true
}, "AdminAPIAlturas01!", Roles.ROLE_API_ALTURAS);
CreateUser(
new ApplicationUser()
{
UserName = "usrinvalido_apialturas",
Email = "usrinvalido-apialturas@teste.com.br",
EmailConfirmed = true
}, "UsrInvAPIAlturas01!");
}
}
private void CreateUser(
ApplicationUser user,
string password,
string initialRole = null)
{
if (_userManager.FindByNameAsync(user.UserName).Result == null)
{
var resultado = _userManager
.CreateAsync(user, password).Result;
if (resultado.Succeeded &&
!String.IsNullOrWhiteSpace(initialRole))
{
_userManager.AddToRoleAsync(user, initialRole).Wait();
}
}
}
}
}
O tipo SigningConfigurations já foi detalhado no artigo anterior e não passará por mudanças. É responsabilidade desta estrutura a geração da chave e da assinatura empregadas na criação de tokens:
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
namespace APIAlturas
{
public class SigningConfigurations
{
public SecurityKey Key { get; }
public SigningCredentials SigningCredentials { get; }
public SigningConfigurations()
{
using (var provider = new RSACryptoServiceProvider(2048))
{
Key = new RsaSecurityKey(provider.ExportParameters(true));
}
SigningCredentials = new SigningCredentials(
Key, SecurityAlgorithms.RsaSha256Signature);
}
}
}
Ajustes serão realizados também na classe Startup, a fim de possibilitar o uso do ASP.NET Identity Core (as instruções necessárias para a utilização de tokens já foram descritas no primeiro artigo sobre JWT, o qual foi mencionado no início deste artigo):
No método ConfigureServices serão invocados os métodos AddDbContext (configurando o uso da classe ApplicationDbContext) e AddIdentity (habilitando a utilização das estruturas do Identity na API via injeção de dependências);
A chamada ao método Initialize de IdentityInitializer na operação Configure permitirá a criação de estruturas, usuários e permissões na base designada para uso do ASP.NET Identity Core (caso tais elementos ainda não existam). Em um cenário ideal, esta instrução seria evitada, já que o gerenciamento de usuários aconteceria separadamente da API REST aqui descrita (em uma aplicação Web isolada que acessa um banco de dados pré-existente, por exemplo).
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using APIAlturas.Data;
using APIAlturas.Models;
namespace APIAlturas
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Configurando o uso da classe de contexto para
// acesso às tabelas do ASP.NET Identity Core
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("BaseIdentity")));
// Ativando a utilização do ASP.NET Identity, a fim de
// permitir a recuperação de seus objetos via injeção de
// dependências
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var signingConfigurations = new SigningConfigurations();
services.AddSingleton(signingConfigurations);
var tokenConfigurations = new TokenConfigurations();
new ConfigureFromConfigurationOptions<TokenConfigurations>(
Configuration.GetSection("TokenConfigurations"))
.Configure(tokenConfigurations);
services.AddSingleton(tokenConfigurations);
services.AddAuthentication(authOptions =>
{
authOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
authOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(bearerOptions =>
{
var paramsValidation = bearerOptions.TokenValidationParameters;
paramsValidation.IssuerSigningKey = signingConfigurations.Key;
paramsValidation.ValidAudience = tokenConfigurations.Audience;
paramsValidation.ValidIssuer = tokenConfigurations.Issuer;
// Valida a assinatura de um token recebido
paramsValidation.ValidateIssuerSigningKey = true;
// Verifica se um token recebido ainda é válido
paramsValidation.ValidateLifetime = true;
// Tempo de tolerância para a expiração de um token (utilizado
// caso haja problemas de sincronismo de horário entre diferentes
// computadores envolvidos no processo de comunicação)
paramsValidation.ClockSkew = TimeSpan.Zero;
});
// Ativa o uso do token como forma de autorizar o acesso
// a recursos deste projeto
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ApplicationDbContext context,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Criação de estruturas, usuários e permissões
// na base do ASP.NET Identity Core (caso ainda não
// existam)
new IdentityInitializer(context, userManager, roleManager)
.Initialize();
app.UseMvc();
}
}
}
O tipo LoginController passará pelas seguintes alterações (novamente foram omitidas as explicações quanto à geração de tokens, descritas no artigo anterior sobre JWT):
- Instâncias dos tipos UserManager e SignInManager (namespace Microsoft.AspNetCore.Identity) serão recebidas via injeção de dependências no método Post;
- A referência de UserManager será empregada para validar a existência de um usuário (cujas credenciais foram informadas na instância do tipo User);
- Já o objeto do tipo SignInManager validará a senha informada, utilizando para isto o método CheckPasswordSignInAsync.
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Identity;
using APIAlturas.Models;
namespace APIAlturas.Controllers
{
[Route("api/[controller]")]
public class LoginController : Controller
{
[AllowAnonymous]
[HttpPost]
public object Post(
[FromBody]User usuario,
[FromServices]UserManager<ApplicationUser> userManager,
[FromServices]SignInManager<ApplicationUser> signInManager,
[FromServices]SigningConfigurations signingConfigurations,
[FromServices]TokenConfigurations tokenConfigurations)
{
bool credenciaisValidas = false;
if (usuario != null && !String.IsNullOrWhiteSpace(usuario.UserID))
{
// Verifica a existência do usuário nas tabelas do
// ASP.NET Core Identity
var userIdentity = userManager
.FindByNameAsync(usuario.UserID).Result;
if (userIdentity != null)
{
// Efetua o login com base no Id do usuário e sua senha
var resultadoLogin = signInManager
.CheckPasswordSignInAsync(userIdentity, usuario.Password, false)
.Result;
if (resultadoLogin.Succeeded)
{
// Verifica se o usuário em questão possui
// a role Acesso-APIAlturas
credenciaisValidas = userManager.IsInRoleAsync(
userIdentity, Roles.ROLE_API_ALTURAS).Result;
}
}
}
if (credenciaisValidas)
{
ClaimsIdentity identity = new ClaimsIdentity(
new GenericIdentity(usuario.UserID, "Login"),
new[] {
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
new Claim(JwtRegisteredClaimNames.UniqueName, usuario.UserID)
}
);
DateTime dataCriacao = DateTime.Now;
DateTime dataExpiracao = dataCriacao +
TimeSpan.FromSeconds(tokenConfigurations.Seconds);
var handler = new JwtSecurityTokenHandler();
var securityToken = handler.CreateToken(new SecurityTokenDescriptor
{
Issuer = tokenConfigurations.Issuer,
Audience = tokenConfigurations.Audience,
SigningCredentials = signingConfigurations.SigningCredentials,
Subject = identity,
NotBefore = dataCriacao,
Expires = dataExpiracao
});
var token = handler.WriteToken(securityToken);
return new
{
authenticated = true,
created = dataCriacao.ToString("yyyy-MM-dd HH:mm:ss"),
expiration = dataExpiracao.ToString("yyyy-MM-dd HH:mm:ss"),
accessToken = token,
message = "OK"
};
}
else
{
return new
{
authenticated = false,
message = "Falha ao autenticar"
};
}
}
}
}
A classe ConversorAlturasController permanecerá inalterada, tendo por função a conversão de alturas em pés para o equivalente em metros (o acesso à Action Get será permitido apenas se as requisições recebidas conterem um token válido):
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace APIAlturas.Controllers
{
[Route("api/[controller]")]
public class ConversorAlturasController : Controller
{
[Authorize("Bearer")]
[HttpGet("PesMetros/{alturaPes}")]
public object Get(double alturaPes)
{
return new
{
AlturaPes = alturaPes,
AlturaMetros = Math.Round(alturaPes * 0.3048, 4)
};
}
}
}
Testes
Ao acionar a execução do projeto APIAlturas, serão criadas as estruturas esperadas para o banco BaseIdentity (e eventualmente o próprio) caso tais construções ainda não existam. A consulta a seguir mostra que foram populadas as tabelas dbo.AspNetUsers, dbo.AspNetRoles e dbo.AspNetUserRoles:

Testes de acesso à API serão realizados então via Postman. Uma primeira simulação será feita por meio da URL http://localhost:56435/api/login, informando no corpo desta solicitação do tipo POST uma string JSON contendo o usuário admin-apialturas (userID) e sua respectiva senha (password). No retorno produzido pela API constará o token de acesso:
Uma requisição GET para a conversão de uma altura de 100 pés será enviada agora, utilizando o token obtido no passo anterior. Esta solicitação terá como URL o valor http://localhost:56435/api/conversoralturas/pesmetros/100, sendo que o resultado da mesma pode ser observado na próxima imagem:

Um teste com o usuário usrinvalido_apialturas indicará que o mesmo não possui acesso à API de conversão de alturas:

Caso necessite consumir APIs REST que utilizam JWT em .NET Core, acesse o seguinte artigo:
E para concluir este artigo, não deixe também de acompanhar o artigo a seguir, no qual venho agrupando todos os conteúdos que tenho produzido sobre .NET Core 2.0 e ASP.NET Core 2.0:




