ASP.NET Core - Autenticação e Autorização

Autenticação e autorização são termos recorrentes em APIs, sempre acompanhados de roles, claims, policies, JWT e OAuth. Embora pareça um assunto complexo, ele não é, e neste guia você vai desmistificar cada conceito e aplicá-los em uma API com ASP.NET Core 2.2.

Importante: já existe uma versão mais atual deste artigo, utilizando ASP.NET 5. Para acessá-la, clique aqui.

A diferença entre autenticação e autorização

Antes de escrever qualquer código, vale separar bem os dois conceitos. Afinal, autenticação e autorização aparecem juntas, mas respondem a perguntas diferentes.

O que é autenticação

A autenticação diz quem você é. Ela é responsável por identificar o usuário ou o sistema que está acessando a API.

Hoje em dia, é normal encontrar autenticação com terceiros, como Facebook, Google e até a Apple. Nesse caso, outra empresa confirma a identidade e a sua aplicação apenas confia nessa confirmação.

Esse mecanismo, interno ou externo, garante que quem acessa a sua API é realmente quem diz ser.

O que é autorização

A autorização, por sua vez, diz o que o usuário pode fazer. Ao contrário da autenticação, dificilmente você conseguirá terceirizar a autorização, porque ela faz parte do seu negócio.

Muitas vezes, as regras de acesso são específicas de cada aplicação. Por exemplo, um gerente pode aprovar pedidos em um sistema e não ter nenhum acesso em outro.

Em outras palavras, na dupla autenticação e autorização, a primeira identifica e a segunda decide. Sem identificar o usuário, não existe como decidir o que ele pode acessar.

Roles, claims e policies na autenticação e autorização

A autorização acontece por meio de roles, claims e policies, que restringem ou permitem o acesso do usuário a determinadas partes da aplicação. Por isso, entender os três termos é essencial para trabalhar com autenticação e autorização no ASP.NET Core.

Roles

Uma role, ou perfil, é um papel, uma função ou um cargo. Ela define o acesso de um usuário ou de um grupo de usuários a partes da aplicação.

Normalmente, usamos roles em cenários mais simples, que não exigem muita interação. Por exemplo, apenas sabendo que o usuário tem o papel de gerente, já podemos tomar uma decisão.

Claims

Uma claim, ou afirmação, é uma informação sobre o usuário, representada por um tipo e um valor. Na maioria dos casos, usamos claims para realizar alguma verificação antes de deixar o usuário prosseguir.

Imagine que você precisa validar se o usuário pertence a uma empresa específica ou se realizou determinada venda. Esse é um cenário interessante para claims, inclusive quando a validação depende do banco de dados.

No ASP.NET Core, até as roles são representadas internamente como claims. Assim, o modelo de claims acaba sendo a base de todo o sistema de autorização.

Policies

As policies, ou políticas, aplicam roles e claims para definir contratos de acesso a determinadas áreas da aplicação. Você pode ter várias roles e claims compondo uma única política.

Na prática, a política funciona como uma regra com nome. Então, os controladores só precisam informar o nome da regra, enquanto a lógica fica centralizada em um único lugar.

OAuth

O OAuth é um padrão aberto que especifica como uma aplicação obtém acesso delegado a recursos de outra. Com a chegada das APIs abertas, o mercado precisou de especificações mínimas de segurança, e o OAuth nasceu para preencher essa lacuna.

Hoje ele está consolidado no mercado e aparece em serviços de gigantes como Facebook, Twitter, Instagram, Spotify e Netflix. Na versão 2.0, o padrão foi publicado como a RFC 6749 da IETF.

Vale notar que o OAuth trata de autorização delegada. Para autenticação, o mercado costuma usar o OpenID Connect, uma camada construída sobre o OAuth 2.0.

Token e aplicações sem estado

Um token é uma cadeia de texto que carrega informações do usuário e uma assinatura gerada com uma chave privada que só o servidor conhece. Com isso, o servidor consegue confirmar que o token não foi alterado.

Pode parecer estranho guardar informações do usuário, não sensíveis, dentro de um token. Contudo, isso garante que não precisamos armazenar esses tokens no servidor.

Esse modelo torna possível o que chamamos de stateless applications, ou aplicações sem estado. De fato, isso é fundamental para escalar qualquer aplicação, já que a memória de uma máquina não é copiada quando uma nova instância sobe.

JWT

JWT é a sigla para JSON Web Token, e a pronúncia sugerida pela especificação é igual à da palavra inglesa "jot". O JWT nada mais é que o armazenamento das informações do token no formato JSON.

Um JWT tem três partes separadas por pontos: o cabeçalho, o conteúdo com as claims e a assinatura. As duas primeiras partes usam codificação Base64Url, por isso qualquer pessoa consegue ler o conteúdo de um token.

Portanto, a assinatura garante a integridade, mas não esconde nada. Esse é o motivo pelo qual você nunca deve colocar informações sensíveis em um JWT.

Como funciona o fluxo de autenticação e autorização com JWT

O processo começa em uma aplicação, normalmente um frontend, que faz uma requisição para a sua API. Nessa requisição, ela envia o usuário e a senha.

Nem preciso dizer que suas aplicações precisam trafegar sobre HTTPS. Além de proteger as credenciais, o HTTPS também é um fator que o Google considera no ranqueamento de sites.

Com o usuário e a senha, a API faz a verificação normal do login. Se tudo der certo, ela usa uma chave privada para gerar um token no formato JWT com as informações que você desejar.

Nesse momento, você tem uma cadeia enorme, com todas as informações que colocou previamente e assinada com uma chave que só a API conhece. Veja um exemplo:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFuZHJlYmFsdGllcmkiLCJTdG9yZSI6InVzZXIiLCJuYmYiOjE1NjE2MDI2NjYsImV4cCI6MTU2MjIwNzQ2NiwiaWF0IjoxNTYxNjAyNjY2fQ.y2cvZ02PLJG-BFPUAWn1fChTvS4rPOv_iEEsLPd7eBg

Decodificada, essa chave tem o seguinte conteúdo:

{ "alg": "HS256", "typ": "JWT" }
{ "unique_name": "andrebaltieri", "Store": "user", "nbf": 1561602666, "exp": 1562207466, "iat": 1561602666 }

Além do nome de usuário e do perfil, o token informa a data de criação (iat), o início da validade (nbf) e a expiração (exp). Essas datas seguem o formato Unix, em segundos.

Conforme comentei, ninguém fica "logado" na API. Então, após o login, a API devolve o token, e o cliente precisa reenviá-lo a cada requisição no cabeçalho Authorization: Bearer {token}.

Esse é o ciclo completo de autenticação e autorização com JWT. A seguir, você vê como implementar cada etapa no ASP.NET Core 2.2.

A chave secreta da autenticação e autorização

A primeira coisa que precisamos fazer na API é definir uma chave, um segredo que só ela conhece. Mais adiante, essa chave assina e valida os tokens.

Neste cenário, o foco está em o que fazer, e não em como armazenar. Por isso, criei uma classe estática chamada Settings com a chave.

namespace Store
{
    public static class Settings
    {
        // Chave usada para assinar e validar os tokens
        public static string Secret = "43e4dbf0-52ed-4203-895d-42b586496bd4";
    }
}

Você poderia utilizar o appsettings.json ou outras fontes, mas este é o cenário mais simples. Como a chave não tende a mudar, a classe estática resolve o problema para um exemplo.

Importante: se você mudar a chave, todos os tokens ainda ativos perdem a validade, já que a API não consegue mais validar a assinatura deles.

Com a chave definida, precisamos convertê-la em bytes. Esse processo também pode usar RSA e outras abordagens, porém a forma abaixo é simples e funciona.

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

Adicionando autenticação no ASP.NET Core

O primeiro passo é adicionar autenticação à aplicação. Para isso, usamos o método AddAuthentication do ASP.NET Core, que traz uma implementação padrão ótima para o que precisamos.

services.AddAuthentication(x =>
{
    // Define o JWT Bearer como esquema padrão
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})

Note que definimos o DefaultAuthenticateScheme como JWT Bearer. Além disso, o DefaultChallengeScheme define como a API responde quando alguém sem token tenta acessar um recurso protegido.

Depois de definir o esquema, precisamos dizer como os tokens vão funcionar. Isso acontece no método AddJwtBearer.

.AddJwtBearer(x =>
{
    x.RequireHttpsMetadata = false;
    x.SaveToken = true;
    x.TokenValidationParameters = new TokenValidationParameters
    {
        // Valida a assinatura com a nossa chave
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = false,
        ValidateAudience = false
    };
});

O que mais importa aqui são os TokenValidationParameters. Neste caso, informamos a nossa chave por meio da classe SymmetricSecurityKey, que representa uma chave simétrica de assinatura.

Também precisamos informar ao ASP.NET Core que ele deve usar a autenticação. Isso acontece no método Configure, sempre antes do UseMvc.

app.UseAuthentication();

Por fim, é importante utilizar o SetCompatibilityVersion para garantir que nada quebre em versões futuras. A classe Startup fica assim, mas calma que ela ainda vai mudar.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors();
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

        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, IHostingEnvironment env)
    {
        app.UseCors(x => x
            .AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader());

        // A autenticação precisa vir antes do MVC
        app.UseAuthentication();

        app.UseMvc();
    }
}

Adicionando autorização com policies

Se usamos o AddAuthentication para autenticação, o AddAuthorization cuida da autorização. Bem intuitivo, não é?

Neste cenário, teremos duas políticas, uma para usuário e outra para administrador. Ambas ficam no método ConfigureServices da classe Startup.

services.AddAuthorization(options =>
{
    // Cada política exige a claim Store com um valor específico
    options.AddPolicy("user", policy => policy.RequireClaim("Store", "user"));
    options.AddPolicy("admin", policy => policy.RequireClaim("Store", "admin"));
});

No código acima, criei duas policies baseadas na mesma claim. Como tipo da claim, utilizei o nome da aplicação, Store, e cada política exige um valor diferente.

Dessa forma, definimos as políticas e as afirmações, mas ainda não as usamos. Veremos isso nos controladores logo mais.

Políticas globais de autenticação e autorização

Em diversos cenários, acho mais fácil restringir toda a API e liberar só o que for necessário, conforme a demanda. Essa abordagem reduz o risco de esquecer um endpoint desprotegido.

Para isso, definimos uma política global que exige autenticação em todos os endpoints. Isso também acontece no ConfigureServices, dentro do método AddMvc.

services.AddMvc(config =>
{
    // Exige usuário autenticado em todos os endpoints
    var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Com isso, a autenticação e autorização passam a valer para toda a API por padrão. O resultado final da classe Startup fica assim.

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

namespace Store
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors();
            services.AddMvc(config =>
            {
                var policy = new AuthorizationPolicyBuilder()
                                .RequireAuthenticatedUser()
                                .Build();
                config.Filters.Add(new AuthorizeFilter(policy));
            }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddAuthorization(options =>
            {
                options.AddPolicy("user", policy => policy.RequireClaim("Store", "user"));
                options.AddPolicy("admin", policy => policy.RequireClaim("Store", "admin"));
            });

            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, IHostingEnvironment env)
        {
            app.UseCors(x => x
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader());

            app.UseAuthentication();

            app.UseMvc();
        }
    }
}

Gerando o token JWT

Para este exemplo, criei um serviço para gerar o token, assim nem tudo fica no controlador. Mesmo assim, sinta-se à vontade para organizar de outra forma.

Na verdade, tudo o que precisamos fazer aqui é buscar o usuário no banco, gerar um token e aplicar as claims a ele. Como buscar o usuário no banco não é o foco deste artigo, essa parte fica simplificada.

O ASP.NET Core conta com a classe JwtSecurityTokenHandler, que faz todo o trabalho pesado. Basta passar para ela um SecurityTokenDescriptor, que define as claims, a data de expiração e as credenciais de assinatura do token.

Para não criar muitas classes, adicionei uma propriedade Token ao User apenas para devolver o usuário direto na resposta. Em um projeto real, prefira um ViewModel próprio para a resposta.

var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(Settings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
    // Claims que irão dentro do token
    Subject = new ClaimsIdentity(new Claim[]
    {
        new Claim(ClaimTypes.Name, user.Username),
        new Claim("Store", user.Role)
    }),
    Expires = DateTime.UtcNow.AddDays(7),
    SigningCredentials = new SigningCredentials(
        new SymmetricSecurityKey(key),
        SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);

No SecurityTokenDescriptor, o Subject reúne a identidade e as claims do usuário. Neste caso, deixei apenas um perfil por usuário para facilitar, mas com vários perfis basta percorrer a lista e adicionar uma claim para cada um.

Em seguida, o Expires define o tempo de vida do token. Como esta é uma API de exemplo e não quero renovar o token o tempo todo, usei sete dias, embora normalmente eu use duas horas.

Por fim, as credenciais de assinatura usam novamente a nossa chave com o algoritmo HMAC SHA256. O UserService completo fica assim.

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Store.Database;
using Store.Models;

namespace Store.Services
{
    public class UserService
    {
        public User Authenticate(string username, string password)
        {
            // Busca simplificada apenas para o exemplo
            var user = Context.Users
                .Where(x => x.Username == username && x.Password == password)
                .FirstOrDefault();

            if (user == null)
                return null;

            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(Settings.Secret);
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(new Claim[]
                {
                    new Claim(ClaimTypes.Name, user.Username),
                    new Claim("Store", user.Role)
                }),
                Expires = DateTime.UtcNow.AddDays(7),
                SigningCredentials = new SigningCredentials(
                    new SymmetricSecurityKey(key),
                    SecurityAlgorithms.HmacSha256Signature)
            };
            var token = tokenHandler.CreateToken(tokenDescriptor);
            user.Token = tokenHandler.WriteToken(token);

            // Nunca devolva a senha na resposta
            user.Password = null;

            return user;
        }
    }
}

Permitindo acesso anônimo

Quando adicionamos a política global, restringimos todo o acesso à API. Porém, pelo menos o login precisa ficar liberado, e para isso usamos o atributo AllowAnonymous nas ações dos controladores.

[HttpPost("login")]
[AllowAnonymous]
public IActionResult Authenticate([FromBody] User model)

Então, sempre que quiser liberar o acesso a uma ação específica, basta aplicar esse atributo. O AccountController completo fica desta forma.

using System;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Store.Models;
using Store.Services;

namespace Store.Controllers
{
    [Route("v1/account")]
    public class AccountController : Controller
    {
        [HttpPost("login")]
        [AllowAnonymous]
        public IActionResult Authenticate([FromBody] User model)
        {
            try
            {
                var service = new UserService();
                var user = service.Authenticate(model.Username, model.Password);

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

                return Ok(user);
            }
            catch (Exception ex)
            {
                return BadRequest(ex);
            }
        }
    }
}

Autenticação e autorização por perfil

Toda a API está restrita, mas apenas para usuários anônimos. Ou seja, qualquer usuário autenticado já tem acesso a todos os endpoints.

Nós criamos duas políticas de acesso, uma para usuário (user) e outra para administrador (admin), lá na classe Startup. Para usá-las, basta aplicar o atributo Authorize informando o nome da política.

// Qualquer usuário com a claim Store = user
[HttpGet]
[Route("v1/categories")]
[Authorize("user")]
public IList<Category> Get() => Database.Context.Categories();

// Apenas administradores
[HttpPost]
[Route("v1/categories")]
[Authorize("admin")]
public IActionResult Post([FromBody] Category model) => Ok(model);

Aqui vale um cuidado com maiúsculas e minúsculas. Segundo a documentação de autorização baseada em claims, os valores das claims são comparados de forma ordinal, então Admin e admin são valores diferentes.

Por outro lado, a busca pelo nome da política normalmente ignora a diferença de caixa. Ainda assim, manter o mesmo padrão em todo o código evita surpresas.

Pronto! Temos uma API com autenticação e autorização usando claims e policies de forma simples.

Boas práticas de autenticação e autorização

O exemplo acima foi simplificado de propósito para focar nos conceitos. Contudo, alguns cuidados fazem diferença quando a autenticação e autorização vão para produção:

  • Guarde a chave fora do código: use variáveis de ambiente ou um cofre de segredos em vez de uma classe estática versionada.
  • Use tokens curtos: prefira expirações de poucas horas e um fluxo de renovação, em vez de tokens válidos por dias.
  • Valide emissor e audiência: em produção, ative ValidateIssuer e ValidateAudience para recusar tokens emitidos para outras aplicações.
  • Exija HTTPS: mantenha o RequireHttpsMetadata ativo fora do ambiente de desenvolvimento.
  • Nunca guarde senhas em texto puro: armazene apenas o hash das senhas no banco.

Se quiser ir além do JWT manual, o ASP.NET Core Identity cuida de usuários, senhas e perfis para você. O curso Introdução ao ASP.NET Core Identity mostra esse caminho em detalhes.

Vale lembrar também que o ASP.NET Core 2.2 já não recebe suporte da Microsoft. Por isso, para projetos novos, use uma versão atual e consulte o artigo mais recente indicado no início.

Conclusão

Autenticação e autorização deixam de parecer complicadas quando você separa os conceitos: a autenticação identifica, e a autorização decide o que cada um pode fazer. Com roles, claims e policies, essas decisões ficam centralizadas e fáceis de manter.

No ASP.NET Core 2.2, o AddAuthentication, o AddJwtBearer e o AddAuthorization resolvem quase todo o trabalho. Assim, você protege a API inteira com uma política global e libera apenas o que for necessário com o AllowAnonymous.

FAQ

Qual a diferença entre autenticação e autorização?

A autenticação identifica quem está acessando a aplicação. Já a autorização define o que essa identidade pode fazer.

O JWT é criptografado?

Não, o JWT comum é apenas assinado, e o conteúdo pode ser lido por qualquer pessoa. Por isso, nunca coloque dados sensíveis dentro dele.

O que acontece se eu trocar a chave secreta?

Todos os tokens emitidos com a chave antiga deixam de ser válidos. Os usuários precisam fazer login novamente para receber um novo token.

Quando usar roles e quando usar claims?

Roles atendem cenários simples, baseados em cargos ou perfis. Claims são melhores quando a decisão depende de informações específicas do usuário.

Como liberar um endpoint quando existe uma política global?

Aplique o atributo AllowAnonymous na ação ou no controlador. Assim, o endpoint passa a aceitar requisições sem token.