ASP.NET - Autenticação com ApiKey

A autenticação com ApiKey é uma forma simplificada de proteger APIs, em que o cliente envia uma chave secreta junto com cada requisição. Neste artigo, vamos entender o conceito de API Keys e implementar essa proteção no ASP.NET Core com um atributo customizado.

O resultado é um mecanismo leve, sem pacotes adicionais e fácil de aplicar em controllers ou actions específicas. Ao final, você também conhece os cuidados de segurança que esse modelo exige.

Autenticação e autorização

Antes de continuar, é importante ter bem definidos os conceitos de autenticação e autorização. Embora andem juntos, eles respondem a perguntas diferentes.

A autenticação responde à pergunta "quem é você?". Já a autorização responde à pergunta "o que você pode fazer?".

Em uma API tradicional, o usuário envia login e senha, recebe um token e depois envia esse token no cabeçalho de cada requisição. Esse modelo envolve conceitos como JWT, Token, Roles e Claims, que funcionam muito bem para usuários finais.

Por outro lado, nem toda integração envolve uma pessoa digitando uma senha. Quando um sistema conversa com outro, a autenticação com ApiKey costuma ser uma alternativa mais direta.

O que é uma ApiKey

O conceito de chave de API, ou ApiKey, trata do envio de uma chave secreta na URL ou no cabeçalho da requisição. A API recebe essa chave, compara com as chaves conhecidas e decide se libera o acesso.

Em resumo, em vez de fazer um processo de autenticação convencional, enviamos diretamente a chave que dá acesso à API. Desse modo, a ApiKey substitui a autenticação via usuário e senha que geraria um token.

Onde usar autenticação com ApiKey

A autenticação com ApiKey não deve ser utilizada do lado do cliente, como em aplicações JavaScript que rodam no navegador. Afinal, o usuário teria acesso a essa chave pelas ferramentas de desenvolvedor do browser.

Essa chave deve servir para autenticar clientes que rodam do lado do servidor, como outra API, um worker ou um job agendado. Assim, a chave fica sempre no servidor, ou em um banco de dados, em segredo.

O Azure Functions é um ótimo exemplo desse modelo. Sempre que chamamos uma função HTTP protegida, precisamos informar o código de acesso, e a plataforma cuida da autenticação e da autorização.

# Chamada a uma Azure Function com a chave na query string
https://your-function.azurewebsites.net/api/endpoint?code=YOUR_API_KEY

Segundo a documentação de chaves de acesso do Azure Functions, a chave pode seguir no parâmetro code da URL ou no cabeçalho x-functions-key. Esse é exatamente o comportamento que vamos reproduzir na nossa API.

Segurança da autenticação com ApiKey

Como você deve imaginar, essa informação é bastante sensível. Qualquer pessoa com a sua ApiKey poderá fazer requisições para a sua API, então é importante guardá-la com muito carinho.

Além disso, uma chave compartilhada não identifica uma pessoa específica. Por isso, a própria Microsoft recomenda Microsoft Entra ID ou OAuth 2.0 para APIs voltadas a usuários finais.

Criando a aplicação

Para este processo, não vamos precisar instalar nenhum pacote adicional. Basta gerar um novo projeto ASP.NET do tipo webapi.

dotnet new webapi -o ApiKeyDemo
cd ApiKeyDemo
dotnet watch run

O comando dotnet watch run executa a aplicação e reinicia o processo sempre que um arquivo muda. Com isso, cada ajuste no código aparece imediatamente nos testes.

Este exemplo segue o modelo de projeto com a classe Startup, que separa o registro de serviços no ConfigureServices e o pipeline no Configure. Portanto, os trechos de configuração a seguir usam essa estrutura.

Também não precisamos de nenhum banco de dados nesta primeira versão. Toda a lógica da autenticação com ApiKey fica concentrada em uma única classe, o que facilita a leitura e os testes.

Criando o atributo de autenticação com ApiKey

Não existe muito mistério aqui. Sabendo o conceito de ApiKey, queremos interromper uma requisição e verificar se a chave existe na Query ou nos Headers.

Feito isso, precisamos validar a chave para garantir que ela é legítima. Esse é o processo básico de autenticação e autorização.

A maneira mais fácil de realizar esse procedimento é criar um atributo customizado. No caso, trata-se apenas de uma classe que herda de Attribute.

Para organizar melhor, crie uma pasta chamada Attributes na raiz da aplicação e adicione o arquivo ApiKeyAttribute.cs dentro dela.

Nossa classe começa com as heranças e com o AttributeUsage, que limita o uso do atributo a classes e métodos.

[AttributeUsage(validOn: AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAttribute : Attribute, IAsyncActionFilter
{
    // ...
}

Dessa forma, poderemos utilizar a notação [ApiKey] tanto na classe, ou seja, no controller, quanto em um método, ou seja, na action.

Definições da autenticação com ApiKey

Para melhorar nosso código, vamos criar duas constantes. A primeira guarda o nome do parâmetro que vamos buscar, e a segunda guarda a chave que vamos comparar.

private const string ApiKeyName = "api_key";
private const string ApiKey = "balta_demo_IlTevUM/z0ey3NwCV/unWg==";

Aqui está o ponto central da autenticação com ApiKey: o valor de ApiKey é a chave de acesso a toda a API. Quem utilizar essa chave "está no comando".

Você pode armazenar essa chave no appsettings.json, por exemplo. Assim, consegue versionar o código tranquilamente e substituir o valor durante o deploy.

De qualquer forma, a ideia é ter uma chave, ou várias, para saber se a requisição é legítima. Posteriormente, você pode evoluir essa lógica e ler as chaves de um banco de dados.

Interceptando a requisição

Ao herdar de Attribute e implementar IAsyncActionFilter, somos obrigados a implementar o método OnActionExecutionAsync. É nele que inspecionamos a requisição atual.

Neste caso, vamos inspecionar o context.HttpContext.Request, que possui as propriedades Headers e Query. Elas representam, respectivamente, os cabeçalhos e a URL da requisição.

Logo, para obter um valor da URL, utilizamos context.HttpContext.Request.Query. Já para obter um valor dos cabeçalhos, utilizamos context.HttpContext.Request.Headers.

A única coisa que exige atenção é que podemos ter mais de um valor com o mesmo nome, ou mesmo nenhum valor. Por isso, recomendamos o método TryGetValue, que evita exceções na execução do código.

[AttributeUsage(validOn: AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAttribute : Attribute, IAsyncActionFilter
{
    private const string ApiKeyName = "api_key";
    private const string ApiKey = "balta_demo_IlTevUM/z0ey3NwCV/unWg==";

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        if (!context.HttpContext.Request.Query.TryGetValue(ApiKeyName, out var extractedApiKey))
        {
            // Não encontrou a chave
        }

        await next();
    }
}

Por fim, chamamos o delegate next() para dar continuidade à requisição quando desejarmos. Se não chamarmos next(), a action nunca executa e a autenticação com ApiKey bloqueia o acesso.

ApiKey não encontrada

Caso a ApiKey não esteja presente na Query, ou nos Headers, dependendo da sua escolha, vamos atualizar o resultado do contexto atual e interromper a requisição.

if (!context.HttpContext.Request.Query.TryGetValue(ApiKeyName, out var extractedApiKey))
{
    context.Result = new ContentResult()
    {
        StatusCode = 401,
        Content = "ApiKey not found"
    };
    return;
}

Note que, neste caso, a autenticação com ApiKey responde com o código 401 Unauthorized. Afinal, a ausência de uma chave indica que a requisição não está autenticada.

A documentação oficial de filtros explica que atribuir um valor a context.Result faz um curto-circuito na execução. Ou seja, a action e os filtros seguintes não rodam.

ApiKey inválida

O próximo passo é verificar se a chave enviada é válida. Para isso, utilizamos o método Equals da string, que compara o valor com outro do mesmo tipo.

if (!ApiKey.Equals(extractedApiKey))
{
    context.Result = new ContentResult()
    {
        StatusCode = 403,
        Content = "Unauthorized access"
    };
    return;
}

Note que, aqui, a autenticação com ApiKey utiliza o código 403 Forbidden, pois a chave existe, mas não confere. Você poderia utilizar o 401 também, já que essa decisão envolve mais visão e preferência do que regra.

O atributo ApiKey completo

A versão final do nosso atributo [ApiKey], definido em Attributes/ApiKeyAttribute.cs, fica assim.

[AttributeUsage(validOn: AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAttribute : Attribute, IAsyncActionFilter
{
    private const string ApiKeyName = "api_key";
    private const string ApiKey = "balta_demo_IlTevUM/z0ey3NwCV/unWg==";

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // Chave ausente: requisição não autenticada
        if (!context.HttpContext.Request.Query.TryGetValue(ApiKeyName, out var extractedApiKey))
        {
            context.Result = new ContentResult()
            {
                StatusCode = 401,
                Content = "ApiKey not found"
            };
            return;
        }

        // Chave diferente da esperada: acesso negado
        if (!ApiKey.Equals(extractedApiKey))
        {
            context.Result = new ContentResult()
            {
                StatusCode = 403,
                Content = "Unauthorized access"
            };
            return;
        }

        await next();
    }
}

Com poucas linhas, temos a autenticação com ApiKey pronta para uso. Em seguida, vamos registrar o pipeline e aplicar o atributo.

Adicionando autenticação e autorização

Nosso próximo passo é informar a aplicação que estamos utilizando autenticação e autorização. Normalmente, a linha app.UseAuthorization já vem no template, mas, caso o UseAuthentication não esteja presente, o método fica assim.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...
    app.UseRouting();

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

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

A ordem desses itens deve ser exatamente a mostrada acima. Se você inverter o UseAuthentication com o UseAuthorization, por exemplo, os mecanismos de identidade da aplicação não vão funcionar corretamente.

Vale notar que o nosso atributo roda como um filtro do MVC, depois que o roteamento escolhe a action. Para entender melhor essa sequência, leia o artigo sobre middleware no ASP.NET aqui do balta.io.

Utilizando o atributo ApiKey

Com tudo pronto, vamos utilizar nosso atributo no WeatherForecastController.cs, adicionando-o a qualquer método. Neste exemplo, criamos um método novo, do tipo GET, para facilitar os testes.

[ApiController]
[Route("weather")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet("")]
    [ApiKey]
    public IActionResult Get()
    {
        return Ok(new { message = "You have access!" });
    }
}

Se preferir proteger todas as actions de uma vez, coloque o [ApiKey] diretamente sobre a classe do controller. Isso funciona porque o AttributeUsage permite os dois alvos.

Testando a autenticação com ApiKey

Agora vamos testar a API. Se você utilizou um método GET, basta chamar estes três endereços no navegador.

https://localhost:5001/weather
https://localhost:5001/weather?api_key=12345
https://localhost:5001/weather?api_key=balta_demo_IlTevUM/z0ey3NwCV/unWg==

Na primeira requisição, você deve receber um erro 401, pois não informamos a ApiKey. Na segunda, o retorno deve ser 403, porque a ApiKey não confere.

Por fim, na terceira requisição, devemos visualizar a mensagem "You have access!". Esse resultado confirma que a autenticação com ApiKey está funcionando.

Autenticação com ApiKey pelo cabeçalho

Enviar a chave na URL é prático para testes, porém traz um risco. URLs costumam aparecer em logs de servidores, proxies e históricos de navegação.

Por isso, em produção, prefira enviar a chave em um cabeçalho HTTP. A troca no atributo é pequena, já que basta consultar Headers em vez de Query.

private const string ApiKeyHeaderName = "X-API-KEY";

// Busca a chave no cabeçalho da requisição
if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey))
{
    context.Result = new ContentResult()
    {
        StatusCode = 401,
        Content = "ApiKey not found"
    };
    return;
}

Para testar, você pode usar o curl informando o cabeçalho manualmente. Dessa forma, a chave não aparece na URL.

curl -H "X-API-KEY: balta_demo_IlTevUM/z0ey3NwCV/unWg==" https://localhost:5001/weather

Lendo a chave das configurações

Manter a chave como constante no código funciona para demonstrações. Contudo, em um projeto real, ela deve ficar fora do repositório.

Como atributos não recebem injeção de dependência pelo construtor, podemos obter o IConfiguration a partir do RequestServices do contexto. Em seguida, lemos a chave pela configuração.

// Obtém a configuração da aplicação a partir do contêiner de serviços
var configuration = context.HttpContext.RequestServices
    .GetRequiredService<IConfiguration>();

var apiKey = configuration.GetValue<string>("ApiKey");

Com isso, cada ambiente pode ter a sua própria chave em variáveis de ambiente, no appsettings ou em um cofre de segredos. Assim, o pipeline de deploy substitui o valor sem mexer no código.

Autenticação com ApiKey ou JWT

Uma dúvida comum é quando escolher a autenticação com ApiKey e quando partir para um token JWT. Os dois modelos resolvem problemas diferentes, então a escolha depende de quem consome a API.

O JWT nasce de um login e carrega informações sobre o usuário, como claims e roles. Além disso, ele expira depois de um tempo definido, o que limita o estrago caso alguém intercepte o token.

Já a autenticação com ApiKey identifica um sistema, e não uma pessoa. Normalmente, a chave vale até alguém revogá-la, por isso a rotação precisa fazer parte do processo.

Na prática, a autenticação com ApiKey funciona bem em integrações entre servidores, webhooks e serviços internos. Por outro lado, aplicações com login de usuários se beneficiam mais de um fluxo baseado em tokens.

Nada impede que uma mesma API use os dois modelos. Por exemplo, os endpoints públicos podem exigir JWT, enquanto um endpoint de integração aceita apenas uma chave de parceiro.

Nesse cenário, o atributo [ApiKey] fica apenas nas actions de integração. Assim, cada grupo de endpoints mantém o mecanismo de proteção mais adequado.

Outro ponto importante envolve auditoria. Como uma chave pode ser compartilhada por vários processos, vale emitir uma chave diferente para cada cliente e registrar qual chave fez cada chamada.

Com essa separação, revogar o acesso de um parceiro não afeta os demais. Além disso, os logs mostram com clareza a origem de cada requisição.

Boas práticas para autenticação com ApiKey

A autenticação com ApiKey é simples, mas alguns cuidados aumentam bastante a segurança. A lista abaixo reúne os principais.

  • Use sempre HTTPS, pois a chave trafega em todas as requisições.
  • Prefira cabeçalhos à query string para evitar a chave em logs.
  • Gere chaves longas e aleatórias, nunca palavras fáceis de adivinhar.
  • Permita mais de uma chave por cliente para facilitar a rotação.
  • Revogue imediatamente qualquer chave que vazar.

Além disso, considere comparar as chaves com CryptographicOperations.FixedTimeEquals. Esse método compara bytes em tempo constante e dificulta ataques que medem o tempo de resposta.

Para uma visão mais ampla de proteção, o artigo sobre segurança em APIs ASP.NET Core reúne outras práticas essenciais. Elas complementam bem a autenticação com ApiKey.

Conclusão

A autenticação com ApiKey resolve de forma simples a comunicação entre servidores. Com um atributo customizado e um filtro de action, protegemos qualquer controller ou action do ASP.NET Core.

Vimos também como diferenciar uma chave ausente, com 401, de uma chave inválida, com 403. Além disso, evoluímos o exemplo para ler a chave do cabeçalho e das configurações.

Por outro lado, lembre que a ApiKey não substitui um sistema de identidade completo para usuários finais. Use cada modelo no cenário certo e guarde suas chaves com muito cuidado.

FAQ

Posso usar autenticação com ApiKey em aplicações JavaScript no navegador?

Não é recomendado. Qualquer usuário consegue ver a chave pelas ferramentas de desenvolvedor do navegador.

Devo retornar 401 ou 403 quando a ApiKey é inválida?

O 401 indica falta de autenticação e o 403 indica acesso negado. Para chave inválida, os dois são aceitáveis, então escolha um padrão e mantenha a consistência.

Onde devo guardar a ApiKey da minha API?

Guarde fora do código, em configurações por ambiente, variáveis de ambiente ou em um cofre de segredos como o Azure Key Vault.

É melhor enviar a ApiKey na URL ou no cabeçalho?

No cabeçalho. Assim, a chave não aparece em logs de servidores, proxies e históricos de navegação.

A ApiKey substitui o JWT?

Não em todos os casos. A ApiKey atende bem integrações entre servidores, enquanto o JWT identifica usuários e carrega claims e roles.