> ## Content Index
> Fetch the complete content index at: https://www.azurebrasil.cloud/llms.txt
> Use this file to discover other available public pages before exploring further.

# JWT ou Cookies no ASP.NET Core? Depende
- URL: https://www.azurebrasil.cloud/blog/jwt-ou-cookies-no-asp-net-core-depende/
- Published: 2026-05-19T16:17:17.000Z
- Updated: 2026-05-19T16:17:17.000Z
- Description: Sua API RESTful tá linda. Endpoints no plural, verbos HTTP corretos, `ProblemDetails` pra erro, `GET` que não altera estado, `DELETE` idempotente. Parabéns, agora vamos falar de autenticação
- Author: Bruno Brito
- Tags: aspnet, c#, developer

Me responde: como você autentica?

Jogou um `[Authorize]` na controller e colou um trecho de JWT do ChatGPT? Colou o token no `localStorage` porque "funcionou"? Se sim, respira fundo.

"Usar JWT" virou o default do universo da programação sem que ninguém parasse pra perguntar por quê. Nem todo projeto precisa de JWT. Alguns precisam e mesmo assim deveriam considerar cookies. Essa escolha não é preferência pessoal, é arquitetura. E arquitetura errada dói em produção.

## O que estamos comparando

Antes de meter a mão no código, vamos alinhar o vocabulário.

**JWT Bearer Authentication**: o servidor gera um token JWT, devolve pro client, e o client envia esse token no header `Authorization: Bearer <token>` a cada request. O servidor valida sem precisar de estado. Stateless por design.

**Cookie Authentication**: o servidor cria um cookie e é o browser quem vai gerenciar, enviando automaticamente a cada request pro mesmo domínio.

No ASP.NET Core, ambos são `AuthenticationScheme`. Ambos usam o mesmo middleware. Ambos decoram endpoints com `[Authorize]`. A diferença tá no transporte, no storage e na revogação. É aí que a coisa fica interessante.

OAuth2 e OpenID Connect ficam de fora. Já têm [cobertura dedicada](https://avera.com.br/blog/jwt-cookies-oauth-bearer?ref=azurebrasil.cloud). Aqui é mão na massa.

## JWT Bearer Authentication

Vamos lá. JWT Bearer é o que 90% dos tutoriais ensinam. Pra ser justo, tem motivo: funciona bem em cenários distribuídos.

O fluxo é direto:

1. Client envia credenciais (login + senha) pra um endpoint de autenticação
2. Servidor valida, gera um JWT assinado com claims do usuário
3. Client recebe o token e envia no header `Authorization` a cada request
4. Servidor valida a assinatura e as claims, sem consultar banco, sem session store

### Configuração no ASP.NET Core

```csharp
// Program.cs — .NET 8+
var builder = WebApplication.CreateBuilder(args);

var jwtKey = new SymmetricSecurityKey(
    RandomNumberGenerator.GetBytes(32)); // 256 bits — RNG criptográfico

builder.Services.AddSingleton(jwtKey);

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = jwtKey,
            ClockSkew = TimeSpan.Zero
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

```

E o endpoint de login que gera o token:

```csharp
app.MapPost("/auth/login", (LoginRequest request, IConfiguration config,
    SymmetricSecurityKey jwtKey) =>
{
    // Validação real aqui — banco, Identity, o que for
    if (request.Username != "bruno" || request.Password != "senha-segura")
        return Results.Unauthorized();

    var claims = new[]
    {
        new Claim(ClaimTypes.Name, request.Username),
        new Claim(ClaimTypes.Role, "Admin"),
        new Claim("department", "Engineering")
    };

    var credentials = new SigningCredentials(jwtKey, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: config["Jwt:Issuer"],
        audience: config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(30),
        signingCredentials: credentials);

    return Results.Ok(new
    {
        token = new JwtSecurityTokenHandler().WriteToken(token)
    });
});

```

Esse código roda. Cola no `Program.cs` e funciona.

Um detalhe que parece bobo mas não é: a chave de assinatura. Metade dos tutoriais por aí pega uma string do `appsettings.json` e converte com `Encoding.UTF8.GetBytes()`. Isso é derivação de chave amadora. A entropia da sua chave fica limitada ao que alguém digitou — e `"minha-chave-super-secreta-123"` não tem 256 bits de entropia, tem entropia de senha humana. `RandomNumberGenerator.GetBytes(32)` gera 256 bits uniformes direto do CSPRNG do sistema operacional. É o que o .NET usa internamente no Data Protection, no ASP.NET Identity, em tudo que é sério. Se sua chave de assinatura não veio de um RNG criptográfico, ela não é uma chave, é uma senha disfarçada.

Em produção, você gera a chave uma vez com `RandomNumberGenerator`, persiste num secret store (Azure Key Vault, AWS Secrets Manager, até User Secrets em dev) e carrega no startup. Se a chave morre com o processo, todo token emitido morre junto. Geração via RNG e persistência segura são coisas separadas, resolva as duas.

### O que funciona bem

O token carrega tudo. O servidor não precisa de session store, então escala horizontal fica trivial, qualquer instância valida o token. Funciona em SPA, mobile, microservices, API pública. O token viaja no header, não depende de browser, não tem problema de domínio.

### O que machuca

Eu já implementei JWT numa aplicação que era monolítica e server-rendered. Razor Pages, tudo no mesmo domínio, um servidor só. Resultado? Complexidade de refresh token que cookie resolvia de graça. Na prática, eu resolvi um problema que não existia.

O token é auto-contido. Uma vez emitido, é válido até expirar. Usuário trocou a senha? Token antigo ainda funciona. Precisa de blocklist no servidor? Parabéns, você acabou de reintroduzir estado no seu sistema "stateless".

Storage no client é outra dor de cabeça. `localStorage`? XSS leva o token. Cookie `HttpOnly`? Aí você tá usando cookie pra transportar JWT, e perde metade da vantagem. E o payload é legível. Base64 não é criptografia. Qualquer pessoa decodifica e lê as claims. Não coloque dados sensíveis ali. Nunca.

Você vai dizer que JWT escala sem estado no servidor. Sim. Mas e quando o usuário troca a senha e os tokens antigos ainda são válidos por 30 minutos? E quando você precisa de refresh token, que precisa de um endpoint, que precisa de storage seguro no client, que precisa de rotação? O "stateless" virou um ecossistema inteiro de complexidade.

## Cookie Authentication

Agora o outro lado. Cookie Authentication é o veterano. Existe desde que a web existe. E no ASP.NET Core, funciona com uma simplicidade que muita gente ignora porque "JWT é mais moderno". Moderno não quer dizer melhor (e eu aprendi isso da forma cara).

O fluxo:

1. Client envia credenciais pro endpoint de login
2. Servidor valida, cria um `ClaimsPrincipal`, chama `HttpContext.SignInAsync()`
3. O middleware serializa as claims num cookie `HttpOnly`, `Secure`, `SameSite=Strict`
4. O browser envia o cookie automaticamente a cada request pro mesmo domínio
5. Logout? `HttpContext.SignOutAsync()`. Morreu.

### Configuração no ASP.NET Core

```csharp
// Program.cs — .NET 8+
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication("Cookies")
    .AddCookie("Cookies", options =>
    {
        options.LoginPath = "/auth/login";
        options.LogoutPath = "/auth/logout";
        options.ExpireTimeSpan = TimeSpan.FromHours(8);
        options.SlidingExpiration = true;
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Strict;
    });

builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

```

Login e logout:

```csharp
app.MapPost("/auth/login", async (LoginRequest request, HttpContext context) =>
{
    // Validação real aqui — banco, Identity, o que for
    if (request.Username != "bruno" || request.Password != "senha-segura")
        return Results.Unauthorized();

    var claims = new[]
    {
        new Claim(ClaimTypes.Name, request.Username),
        new Claim(ClaimTypes.Role, "Admin"),
        new Claim("department", "Engineering")
    };

    var identity = new ClaimsIdentity(claims, "Cookies");
    var principal = new ClaimsPrincipal(identity);

    await context.SignInAsync("Cookies", principal);

    return Results.Ok();
});

app.MapPost("/auth/logout", async (HttpContext context) =>
{
    await context.SignOutAsync("Cookies");
    return Results.Ok();
});

```

Perceba a diferença: não tem `JwtSecurityTokenHandler`, não tem `SigningCredentials`, não tem token pra devolver. O middleware cuida de tudo. O browser cuida do resto.

### O que funciona bem

Revogação é trivial. `SignOutAsync()` e acabou. Quer invalidar server-side? Muda o `DataProtection` key ou implemente validação customizada no `OnValidatePrincipal`. Sem blocklist, sem dança.

`HttpOnly` impede JavaScript de ler o cookie. XSS não leva a credencial. `SameSite=Strict` impede que o cookie viaje em requests cross-origin, o que já mitiga CSRF. Se precisar de mais, `AddAntiforgery()` resolve. E o sliding expiration renova o cookie automaticamente enquanto o usuário tá ativo. Sem refresh token, sem rotação, sem endpoint extra.

### O que machuca

Cookie funciona lindamente quando tudo roda no mesmo domínio. Aí você adiciona um app mobile e o castelo de cartas cai.

SPA em `app.exemplo.com` consumindo API em `api.exemplo.com`? Precisa de `SameSite=None`, `Secure`, configuração CORS cirúrgica. Funciona, mas a complexidade cresce. Apps nativos (iOS, Android) não gerenciam cookies como um browser. Você acaba montando uma camada extra pra simular o comportamento, e nesse ponto JWT é mais simples. E se o cookie depende de estado no servidor (session, data protection keys compartilhadas), escala horizontal exige Redis ou SQL Server distribuído.

## Comparação direta

Na prática, a escolha resume esses trade-offs:

| Critério          | JWT Bearer                                            | Cookie Auth                                        |
| ----------------- | ----------------------------------------------------- | -------------------------------------------------- |
| Stateless         | Sim, token auto-contido                               | Depende, pode ser stateful com session store       |
| Cross-origin      | Funciona nativamente via header                       | Requer CORS + SameSite=None \+ Secure              |
| Mobile nativo     | Ideal, header Authorization                           | Complexo, sem gerenciamento nativo de cookies      |
| Revogação         | Difícil, blocklist + refresh token                    | Trivial, SignOutAsync() \+ invalidação server-side |
| XSS risk          | Alto se localStorage                                  | Baixo com HttpOnly                                 |
| CSRF risk         | Baixo, token no header, não é enviado automaticamente | Médio, mitigado com SameSite \+ antiforgery        |
| Complexidade      | Média, refresh token + rotação + storage              | Baixa, browser + middleware cuidam de quase tudo   |
| Escala horizontal | Fácil, qualquer instância valida                      | Requer data protection keys distribuídas           |

Nenhum ganha em tudo. Depende do contexto. Sempre dependeu.

## Quando usar qual

Direto ao ponto.

Se você tem uma SPA consumindo uma API separada (Angular, React, Vue + API .NET), vai de JWT Bearer. O token viaja no header `Authorization`, cross-origin funciona, o frontend gerencia o ciclo de vida. E guarde o token num cookie `HttpOnly`. Sim, cookie transportando JWT. Parece irônico, mas é a combinação mais segura: você ganha o stateless do JWT com o `HttpOnly` do cookie.

Se sua aplicação é monolítica e server-rendered (Razor Pages, MVC, Blazor Server), vai de Cookie Auth. Tudo roda no mesmo domínio, o browser gerencia o cookie, revogação é trivial, sliding expiration funciona de graça. Não complique o que é simples.

Mobile nativo consumindo API .NET? JWT Bearer. Sem browser, sem cookie management. O token vai no header, qualquer `HttpClient` manda.

Microservices com API Gateway? JWT Bearer. O token é portátil, cada serviço valida independentemente, sem acoplamento a session store.

E quando o projeto é híbrido (app server-rendered com SPA parcial, dashboard admin + API pública)? Usa os dois. Configura os dois `AuthenticationScheme` no mesmo projeto:

```csharp
builder.Services.AddAuthentication()
    .AddCookie("Cookies", options => { /* ... */ })
    .AddJwtBearer("Bearer", options => { /* ... */ });

// Nos endpoints:
app.MapGet("/dashboard", () => "admin")
    .RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = "Cookies" });

app.MapGet("/api/data", () => "json")
    .RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = "Bearer" });

```

Na teoria, escolher é simples. Na prática, 80% dos projetos começam monolíticos e viram distribuídos. Eu já vi time começar com cookie, crescer pra microservices, e ter que migrar pra JWT no meio do caminho, com usuários logados. Não é bonito. Pense no futuro, mas não over-engineer o presente. Comece com o que resolve hoje.

## Conclusão

Sua API já tem endpoints corretos e verbos no lugar certo. Agora sabe como escolher a autenticação também.

JWT Bearer pra cenários distribuídos. Cookie Auth pra cenários server-rendered no mesmo domínio. Projeto híbrido? O ASP.NET Core te deixa usar os dois.