ASP.NET Core 3 – Autenticação e Autorização com Bearer e JWT

A autenticação JWT no ASP.NET Core 3.0 permite proteger uma API com tokens assinados e liberar cada endpoint de acordo com o perfil (role) do usuário. Neste guia, você monta esse esquema da forma mais simples e funcional possível, sem banco de dados e sem bibliotecas extras.

Importante: já existe uma versão mais atual deste conteúdo, escrita para o ASP.NET 5. Você pode consultá-la em autenticação e autorização com Bearer e JWT no ASP.NET 5.

Por que revisitar a autenticação JWT no ASP.NET Core 3

Há algum tempo publiquei um artigo sobre autenticação, autorização, tokens, claims e policies no ASP.NET Core. Ele cobria praticamente tudo o que você precisa saber sobre essa parte da segurança de APIs.

No entanto, o ASP.NET Core 3 trouxe mudanças importantes no pipeline. Além disso, aquele material ficou um pouco complexo para quem só quer trabalhar com roles, que é o cenário mais comum.

Por isso, este artigo foca no essencial. Você vai criar um esquema de autenticação JWT e autorização baseada em perfis no ASP.NET Core 3.0, com o mínimo de código.

O que mudou no pipeline do ASP.NET Core 3

A principal novidade da versão 3.0 é o endpoint routing como modelo padrão. Com isso, o UseMvc deu lugar à dupla UseRouting e UseEndpoints.

Além disso, a autorização ganhou um middleware próprio, o UseAuthorization. Nas versões anteriores, o atributo [Authorize] funcionava apenas como filtro do MVC.

Segundo o guia oficial de migração do ASP.NET Core 2.2 para o 3.0, as chamadas de UseAuthentication e UseAuthorization precisam ficar entre UseRouting e UseEndpoints. Fora dessa posição, elas simplesmente não têm efeito.

Em outras palavras, a ordem dos middlewares passou a importar ainda mais. Vamos voltar a esse ponto quando montarmos o Startup.cs.

Como funciona um token JWT

Antes do código, vale entender o que é o token que vamos gerar. O JWT (JSON Web Token) é um padrão aberto, descrito na RFC 7519, para transportar informações entre duas partes de forma compacta.

Na prática, um JWT tem três partes separadas por ponto: header, payload e assinatura. O header indica o algoritmo, o payload carrega as claims e a assinatura garante que ninguém alterou o conteúdo.

Um detalhe importante é que o payload não é criptografado, apenas codificado em Base64Url. Portanto, qualquer pessoa com o token consegue ler as claims, mas só quem tem a chave consegue gerar uma assinatura válida.

Dessa forma, nunca coloque senhas ou dados sensíveis dentro do token. Guarde nele apenas o que a API precisa para identificar e autorizar o usuário, como nome e perfil.

O formato Bearer, por sua vez, define como o cliente envia o token. Ele vai no cabeçalho Authorization, precedido da palavra Bearer e de um espaço.

Preparando o projeto para a autenticação JWT

Para executar a autenticação, precisamos de um usuário, uma senha e um perfil. Como o objetivo aqui não é cobrir acesso a dados, os usuários ficam fixos (hard coded) em um repositório.

Além disso, a geração do token fica em um serviço separado, que você pode reutilizar depois. Basta informar um usuário com um perfil e ele devolve o token, então ficamos totalmente independentes de banco de dados.

Para testar, vamos criar dois itens: Models/User.cs e Repositories/UserRepository.cs. O primeiro modela o usuário, enquanto o segundo "recupera" esse usuário da fonte de dados.

Criando a aplicação

Primeiro, crie a aplicação executando os comandos abaixo no terminal. Para acompanhar este artigo, você precisa do SDK do .NET Core 3.0 instalado.

dotnet new webapi -o Shop
cd Shop
dotnet restore
dotnet add package Microsoft.AspNetCore.Authentication
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 3.0.0

O pacote Microsoft.AspNetCore.Authentication.JwtBearer traz o handler que valida o token a cada requisição. Por isso, mantenha a versão do pacote alinhada com a versão do framework, que neste caso é a 3.0.

Sem esse pacote, a autenticação JWT não funciona, porque o ASP.NET Core não sabe ler o cabeçalho Bearer sozinho.

Em seguida, abra o projeto no Visual Studio Code e remova os arquivos WeatherForecast gerados pelo template. Existe um na raiz do projeto e outro na pasta Controllers.

Modelando o usuário

Agora, crie uma pasta chamada Models na raiz e, dentro dela, o arquivo User.cs com este código:

namespace Shop.Models
{
    public class User
    {
        public int Id { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public string Role { get; set; }
    }
}

A propriedade Role é a peça central do exemplo. Afinal, na autenticação JWT é ela que vai parar dentro do token e definir o que cada usuário pode acessar.

Criando o repositório de usuários

O próximo item é o repositório. Para isso, crie a pasta Repositories na raiz e, dentro dela, o arquivo UserRepository.cs:

using System.Collections.Generic;
using System.Linq;
using Shop.Models;

namespace Shop.Repositories
{
    public static class UserRepository
    {
        public static User Get(string username, string password)
        {
            // Usuários fixos apenas para fins de demonstração
            var users = new List<User>
            {
                new User { Id = 1, Username = "batman", Password = "batman", Role = "manager" },
                new User { Id = 2, Username = "robin", Password = "robin", Role = "employee" }
            };

            return users.FirstOrDefault(x =>
                x.Username.ToLower() == username.ToLower()
                && x.Password == password);
        }
    }
}

Repare que a comparação usa o parâmetro password recebido. Assim, a busca só devolve o usuário quando o nome e a senha conferem.

Em um sistema real, obviamente, as senhas ficam no banco e sempre com hash. Aqui, contudo, o foco é a autenticação JWT, então simplificamos essa parte de propósito.

Definindo a chave secreta

Para finalizar a preparação, precisamos de uma chave para assinar o token. Essa chave é uma string que só o servidor conhece, por isso a chamamos de chave privada.

Para este cenário, crie o arquivo Settings.cs na raiz da aplicação:

namespace Shop
{
    public static class Settings
    {
        // Em produção, leia este valor da configuração ou de um cofre de segredos
        public static string Secret = "fedaf7d8863b48e197b9287d492b708e";
    }
}

Esse arquivo serve apenas para expor a chave usada pela autenticação JWT, nada além disso. Se quiser deixá-la nas configurações, fica ainda melhor, mas aqui usamos uma classe estática para facilitar.

Vale reforçar que o algoritmo HMAC SHA256 exige uma chave de pelo menos 256 bits. Ou seja, uma string ASCII de 32 caracteres, como a do exemplo, atende o mínimo.

Gerando o token da autenticação JWT

O pacote System.IdentityModel.Tokens.Jwt, que chega como dependência do pacote de JwtBearer, contém a classe JwtSecurityTokenHandler. Ela gera um token a partir das informações que fornecemos.

Essas informações ficam em um SecurityTokenDescriptor, que define, entre outras opções, o usuário, o perfil e a validade. Além disso, as claims definidas ali ficam disponíveis na ClaimsIdentity do ASP.NET Core quando o token volta nas requisições.

Em outras palavras, o handler gera o token e ainda deixa o usuário acessível por User.Identity.Name dentro dos controllers.

Você poderia chamar JwtSecurityTokenHandler.CreateToken direto em um controller. No entanto, prefiro externalizar a geração para um serviço, que pode ser chamado de qualquer lugar sem duplicar código.

Criando o TokenService

Crie uma pasta Services na raiz e, dentro dela, o arquivo TokenService.cs:

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Shop.Models;

namespace Shop.Services
{
    public static class TokenService
    {
        public static string GenerateToken(User user)
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(Settings.Secret);

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                // Claims que vão dentro do payload do token
                Subject = new ClaimsIdentity(new Claim[]
                {
                    new Claim(ClaimTypes.Name, user.Username),
                    new Claim(ClaimTypes.Role, user.Role)
                }),
                // Validade de duas horas
                Expires = DateTime.UtcNow.AddHours(2),
                // Assinatura com a chave privada
                SigningCredentials = new SigningCredentials(
                    new SymmetricSecurityKey(key),
                    SecurityAlgorithms.HmacSha256Signature)
            };

            var token = tokenHandler.CreateToken(tokenDescriptor);
            return tokenHandler.WriteToken(token);
        }
    }
}

Acredite ou não, isso é tudo o que precisamos para gerar um token no formato JWT com o ASP.NET Core 3.0.

Entendendo cada parte do token

A claim ClaimTypes.Name guarda o nome do usuário. Por sua vez, a claim ClaimTypes.Role guarda o perfil, que o ASP.NET Core usa para avaliar o [Authorize(Roles = "...")].

A propriedade Expires define até quando o token vale. Depois desse prazo, o middleware recusa o token e a API responde com 401.

Já o SigningCredentials usa a chave do Settings.cs para assinar o token da autenticação JWT. Assim, só o nosso servidor consegue produzir uma assinatura válida, e qualquer alteração no payload invalida o token.

Configurando a autenticação JWT no Startup

Até aqui, temos o usuário, o repositório e um serviço que gera o token com as claims do usuário. Agora precisamos informar à aplicação que vamos trabalhar com autenticação, autorização e tokens no formato JWT.

Dessa forma, a autenticação JWT vai validar o token a cada requisição. Com isso, conseguiremos definir quais perfis têm acesso a cada ação dos controllers.

Registrando o esquema Bearer da autenticação JWT

O primeiro passo é avisar ao ASP.NET Core que usamos autenticação. Para isso, chamamos AddAuthentication no método ConfigureServices do Startup.cs:

services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
});

Ao registrar a autenticação, também informamos o esquema padrão, neste caso o JwtBearer. Além disso, definimos o mesmo esquema para o challenge, que é a resposta enviada quando o usuário não está autenticado.

Configurando a validação do token

Depois de escolher o JwtBearer, precisamos configurá-lo com AddJwtBearer, ainda no ConfigureServices. O método final fica assim:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors();
    services.AddControllers();

    var key = Encoding.ASCII.GetBytes(Settings.Secret);

    services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(x =>
    {
        // Apenas para desenvolvimento, em produção mantenha true
        x.RequireHttpsMetadata = false;
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false
        };
    });
}

O ValidateIssuerSigningKey garante que o middleware da autenticação JWT confira a assinatura com a nossa chave. Por outro lado, desligamos a validação de issuer e audience porque o token não define esses valores neste exemplo.

Em cenários com mais de uma API ou com um provedor de identidade, vale ligar essas validações. Nesse caso, preencha ValidIssuer e ValidAudience com os mesmos valores usados na geração do token.

Ativando os middlewares da autenticação JWT

Para fechar, precisamos dizer ao ASP.NET Core que de fato usamos autenticação e autorização. Isso acontece no método Configure, com as linhas abaixo:

app.UseAuthentication();
app.UseAuthorization();

Como resultado, o Startup.cs completo fica assim:

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;

namespace Shop
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors();
            services.AddControllers();

            var key = Encoding.ASCII.GetBytes(Settings.Secret);

            services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(x =>
            {
                x.RequireHttpsMetadata = false;
                x.SaveToken = true;
                x.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false
                };
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseCors(x => x
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader());

            // A ordem importa: autenticação antes da autorização
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Observe a sequência: UseRouting, UseCors, UseAuthentication, UseAuthorization e, por fim, UseEndpoints. Essa é exatamente a ordem que a documentação do ASP.NET Core 3.0 recomenda.

Se você inverter autenticação e autorização, o middleware de autorização vai avaliar um usuário ainda anônimo. Consequentemente, todos os endpoints protegidos vão responder com 401, mesmo com um token válido.

Isso é tudo que precisamos informar ao ASP.NET Core para a autenticação JWT e a autorização funcionarem.

Autenticando o usuário

Para este exemplo, criei o arquivo HomeController.cs na pasta Controllers. Toda a autenticação e a autorização ficam concentradas nele.

Se você ainda não conhece o novo modelo de controllers do ASP.NET Core 3, veja este vídeo que publiquei no YouTube. Ele serve de base para o código abaixo.

Seguindo esse modelo, o método de login fica assim:

[HttpPost]
[Route("login")]
[AllowAnonymous]
public ActionResult<dynamic> Authenticate([FromBody] User model)
{
    // Recupera o usuário
    var user = UserRepository.Get(model.Username, model.Password);

    // Verifica se o usuário existe
    if (user == null)
        return NotFound(new { message = "Invalid username or password" });

    // Gera o token
    var token = TokenService.GenerateToken(user);

    // Oculta a senha antes de devolver os dados
    user.Password = "";

    // Retorna o usuário e o token
    return new
    {
        user = user,
        token = token
    };
}

Como o método não executa nenhuma operação assíncrona, ele devolve ActionResult<dynamic> diretamente. Assim, evitamos o aviso do compilador sobre um método async sem await.

Agora, rode a API com dotnet watch run para testar a autenticação JWT. Em seguida, faça uma requisição POST no Postman para https://localhost:5001/v1/account/login com o seguinte corpo:

{ "username": "robin", "password": "robin" }

Como resultado, você recebe os dados do usuário Robin e um token com o nome e o perfil dele. A partir de então, esse token libera as próximas requisições à API.

Autorizando com roles na autenticação JWT

Para deixar a brincadeira mais interessante, vamos criar mais quatro métodos no HomeController. Um deles aceita qualquer usuário, outro exige apenas autenticação e os dois últimos ficam restritos a funcionários e gerentes.

Essas regras ficam a cargo de três atributos:

  • [AllowAnonymous] permite o acesso sem autenticação alguma.
  • [Authorize] exige que o usuário esteja autenticado, mas não verifica o perfil.
  • [Authorize(Roles = "Role1,Role2")] exige autenticação e que o usuário pertença a um dos perfis listados, separados por vírgula.

Dessa forma, os métodos ficam decorados assim:

[HttpGet]
[Route("anonymous")]
[AllowAnonymous]
public string Anonymous() => "Anonymous";

[HttpGet]
[Route("authenticated")]
[Authorize]
public string Authenticated() => $"Authenticated - {User.Identity.Name}";

[HttpGet]
[Route("employee")]
[Authorize(Roles = "employee,manager")]
public string Employee() => "Employee";

[HttpGet]
[Route("manager")]
[Authorize(Roles = "manager")]
public string Manager() => "Manager";

Quando você lista vários perfis no mesmo atributo, basta que o usuário pertença a um deles. Por outro lado, se aplicar dois atributos [Authorize] no mesmo método, o usuário precisa satisfazer os dois.

O HomeController completo

Como resultado final, o HomeController fica com este código:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Shop.Models;
using Shop.Repositories;
using Shop.Services;

namespace Shop.Controllers
{
    [Route("v1/account")]
    public class HomeController : Controller
    {
        [HttpPost]
        [Route("login")]
        [AllowAnonymous]
        public ActionResult<dynamic> Authenticate([FromBody] User model)
        {
            var user = UserRepository.Get(model.Username, model.Password);

            if (user == null)
                return NotFound(new { message = "Invalid username or password" });

            var token = TokenService.GenerateToken(user);
            user.Password = "";

            return new
            {
                user = user,
                token = token
            };
        }

        [HttpGet]
        [Route("anonymous")]
        [AllowAnonymous]
        public string Anonymous() => "Anonymous";

        [HttpGet]
        [Route("authenticated")]
        [Authorize]
        public string Authenticated() => $"Authenticated - {User.Identity.Name}";

        [HttpGet]
        [Route("employee")]
        [Authorize(Roles = "employee,manager")]
        public string Employee() => "Employee";

        [HttpGet]
        [Route("manager")]
        [Authorize(Roles = "manager")]
        public string Manager() => "Manager";
    }
}

Testando a autenticação JWT nos endpoints protegidos

Agora salve o arquivo e faça quatro requisições GET no Postman:

https://localhost:5001/v1/account/anonymous
https://localhost:5001/v1/account/authenticated
https://localhost:5001/v1/account/employee
https://localhost:5001/v1/account/manager

Não esqueça de enviar o token na aba Headers das requisições que exigem autenticação. O cabeçalho segue este formato:

Authorization: Bearer YOUR_TOKEN

Com o token do Robin, as três primeiras rotas respondem normalmente. Já a rota manager devolve 403, porque o Robin está autenticado, mas não tem o perfil exigido.

Sem token, por sua vez, apenas a rota anonymous responde. As demais retornam 401, indicando que a requisição não trouxe uma credencial válida.

Na autenticação JWT, essa diferença entre 401 e 403 ajuda bastante no diagnóstico. Afinal, o 401 aponta problema no token, enquanto o 403 aponta falta de permissão.

Recuperando o usuário logado

Você deve ter percebido que o método Authenticated lê o nome do usuário logado. Isso acontece pela propriedade User.Identity.Name, que o middleware preenche sempre que um token válido chega no cabeçalho.

Além disso, o ClaimsPrincipal do ASP.NET Core oferece métodos como User.IsInRole("manager") e User.FindFirst(ClaimTypes.Role). Todos esses valores vêm das claims que a autenticação JWT recebeu do TokenService.

Portanto, se precisar de mais dados do usuário, como o id ou o e-mail, basta adicionar novas claims na geração do token. Se quiser se aprofundar nesse assunto, veja como customizar claims no ASP.NET.

Cuidados ao levar a autenticação JWT para produção

O exemplo deste artigo foi pensado para ensinar o fluxo, não para ir direto à produção. Por isso, alguns pontos merecem atenção antes de publicar a sua API.

Primeiro, tire a chave secreta da autenticação JWT do código e leve para a configuração, para variáveis de ambiente ou para um cofre de segredos. Assim, ela não vai parar no controle de versão.

Em seguida, mantenha RequireHttpsMetadata como true e sirva a API apenas por HTTPS. Afinal, o token trafega no cabeçalho e precisa de um canal protegido.

Além disso, nunca guarde senhas em texto puro. Use hash com algoritmos próprios para senha e compare sempre o hash, nunca o valor original.

Também vale restringir o CORS. O AllowAnyOrigin facilita os testes, porém em produção o ideal é liberar apenas as origens que de fato consomem a API.

Por fim, defina uma validade curta para o token e avalie o uso de refresh tokens. Para uma lista mais completa, confira as boas práticas de segurança em APIs ASP.NET Core.

Confira também

Conclusão

Neste artigo, você montou a autenticação JWT no ASP.NET Core 3.0 do zero, com um repositório simples, um serviço de token e um controller com rotas protegidas por roles. Tudo isso sem banco de dados e com poucas linhas de configuração.

O ponto mais sensível da versão 3.0 é a ordem dos middlewares. Portanto, mantenha UseAuthentication e UseAuthorization entre UseRouting e UseEndpoints, nessa ordem.

Com essa base, a autenticação JWT fica pronta para evoluir com policies, claims customizadas e um provedor de identidade. Para projetos novos, contudo, prefira a versão atualizada deste conteúdo, que acompanha as versões mais recentes do framework.

FAQ

Preciso do pacote JwtBearer para usar autenticação JWT no ASP.NET Core 3?

Sim, o pacote Microsoft.AspNetCore.Authentication.JwtBearer traz o handler que valida o token. Use a versão compatível com o framework, como a 3.0.0.

O token JWT é criptografado?

Não, um JWT assinado tem o payload apenas codificado em Base64Url. Por isso, não coloque senhas ou dados sensíveis dentro dele.

Por que minha API retorna 401 mesmo com o token correto?

Na maioria dos casos, a ordem dos middlewares está errada. Confira se UseAuthentication vem antes de UseAuthorization e se ambos ficam entre UseRouting e UseEndpoints.

Qual a diferença entre 401 e 403?

O 401 indica que o token está ausente, expirado ou inválido. Já o 403 indica que o usuário está autenticado, mas não tem o perfil exigido.

Como exigir mais de um perfil ao mesmo tempo?

Aplique vários atributos [Authorize] no mesmo método. Dentro de um único atributo, os perfis separados por vírgula funcionam como "ou".

Glossário

Termo Significado
JWT JSON Web Token, padrão aberto para transportar claims de forma compacta e assinada.
Bearer Esquema de envio do token no cabeçalho Authorization, precedido da palavra Bearer.
Claim Informação sobre o usuário guardada no token, como nome ou perfil.
Role Perfil do usuário, usado para liberar ou bloquear endpoints.
Endpoint routing Modelo de roteamento padrão do ASP.NET Core 3, baseado em UseRouting e UseEndpoints.
Middleware Componente do pipeline que processa a requisição e a resposta em sequência.
HMAC SHA256 Algoritmo de assinatura simétrica usado para assinar o token com a chave privada.
ClaimsPrincipal Objeto que representa o usuário autenticado e suas claims dentro da aplicação.

Fontes