Security

Escopo: C# (setup). Princípios transversais em shared/platform/security.md.

Esta página trata do que é específico do .NET: em que arquivo cada valor mora, que ferramenta guarda o segredo e que recurso do ASP.NET Core resolve autenticação e autorização. Os princípios que valem para qualquer linguagem (segredo fora do repositório, validação no servidor, cookie com HttpOnly, Secure e SameSite) estão em shared/platform/security.md.

Conceitos fundamentais

ConceitoO que é
Config (configuração)Valor não sensível que muda entre ambientes, commitado no repositório
Secret (segredo)Credencial, chave ou token; nunca vai para o repositório
JSON (JavaScript Object Notation · Notação de Objetos JavaScript)Formato de serialização de dados usado em appsettings
JWT (JSON Web Token · token assinado que identifica o usuário)Token que o servidor assina e o cliente devolve a cada requisição; carrega quem é o usuário, então o servidor não precisa guardar sessão
Middleware (função que roda antes do handler)Componente que intercepta a requisição antes ou depois de chegar ao handler
Payload (corpo da mensagem)Dados que acompanham a requisição ou o token

Onde cada segredo mora

CamadaArquivo / mecanismoValor
Config base não sensível (commitado)appsettings.jsonURLs, timeouts, limites, feature flags
Override por ambiente (commitado)appsettings.{Environment}.jsonValores que mudam sem ser sensíveis
Secrets em desenvolvimentodotnet user-secretsConnection string local, chave de teste
Secrets em staging/produçãoVariáveis de ambiente do hostConnection strings reais, signing keys
Secrets gerenciadosAzure Key Vault, AWS Secrets Manager, etc.Rotação automática, auditoria

O ConfigurationBuilder do .NET lê as camadas nessa ordem, e cada uma sobrescreve a anterior. A variável de ambiente vem por último e vence o appsettings.json, que é o comportamento desejado: o valor real da produção fica no host, e o arquivo commitado guarda só o padrão de desenvolvimento.


dotnet user-secrets em desenvolvimento

O user-secrets guarda os valores num arquivo fora da pasta do projeto, então não há como commitá-los por engano. Ele faz o papel das variáveis de ambiente enquanto você desenvolve na sua máquina.

dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost;Database=App;Trusted_Connection=True"
dotnet user-secrets set "Auth:Secret" "dev-only-never-use-in-prod"
dotnet user-secrets list

O .NET só lê esses valores quando ASPNETCORE_ENVIRONMENT=Development. Em staging e produção a configuração vem das variáveis de ambiente ou de um gerenciador de segredos.


Variáveis de ambiente: convenção de nome

O : que separa as seções no JSON não é aceito no nome de variável de ambiente em todos os sistemas. O .NET usa dois underscores no lugar: Auth:Secret no JSON vira Auth__Secret no ambiente.

ConnectionStrings__Default="Server=prod-db;Database=App;User=sa;Password=..."
Auth__Secret="prod-signing-secret"
Auth__Authority="https://login.microsoftonline.com/tenant-id"

Options pattern: configuração tipada

Ler builder.Configuration["Auth:Secret"] direto no código espalha o nome da chave como texto solto. Um erro de digitação devolve null em silêncio, e o null só quebra quando alguém tenta assinar um token. O Options pattern amarra a seção inteira a um record: a leitura acontece uma vez, no startup, e a chave errada aparece ali, antes de a aplicação atender qualquer requisição.

❌ Ruim: chaves soltas no código
var authority = builder.Configuration["Auth:Authority"];
var secret    = builder.Configuration["Auth:Secret"]; // string?: null passa despercebido
✅ Bom: record tipado resolvido uma vez no startup
public record AuthOptions(string Authority, string Audience, string Secret)
{
    public const string Section = "Auth";
}

public static WebApplicationBuilder AddAuth(this WebApplicationBuilder builder)
{
    var authOptions = builder.Configuration
        .GetSection(AuthOptions.Section)
        .Get<AuthOptions>()!;

    builder.Services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = authOptions.Authority;
            options.Audience = authOptions.Audience;
            options.TokenValidationParameters.IssuerSigningKey =
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authOptions.Secret));
        });

    return builder;
}

Deixe o middleware validar o token

ReadJwtToken() abre o token e lê o conteúdo sem conferir a assinatura. Qualquer pessoa consegue montar um token com o role que quiser, e esse método aceita. O AddJwtBearer combinado com RequireAuthorization() confere assinatura, emissor, público e validade antes de o handler rodar. O handler recebe a requisição já autenticada e volta a tratar só do domínio.

❌ Ruim: ReadJwtToken dentro do handler
app.MapGet("/orders", async (HttpContext ctx, IOrderRepository repo, CancellationToken ct) =>
{
    var bearerToken = ctx.Request.Headers.Authorization.ToString().Replace("Bearer ", "");
    var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(bearerToken); // não valida
    if (jwtToken.ValidTo < DateTime.UtcNow) return Results.Unauthorized();

    var orders = await repo.FindAllAsync(ct);
    return Results.Ok(orders);
});
✅ Bom: middleware valida antes de o handler rodar
app.MapGet("/orders", async (IOrderRepository orderRepository, CancellationToken ct) =>
{
    var orders = await orderRepository.FindAllAsync(ct);
    return TypedResults.Ok(orders);
})
.RequireAuthorization();

Autorização declarada na rota

Conferir o papel do usuário com um if dentro do handler espalha a mesma regra por vários arquivos, e a rota nova que esqueceu o if fica aberta sem ninguém notar. A policy declara a regra uma vez, no startup, e a rota diz de qual precisa. Quem lê a definição da rota vê a exigência ali, na mesma linha.

// Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("OrderManager", policy =>
        policy.RequireRole("admin", "manager"));
});

// endpoint sem lógica de autorização no corpo
app.MapDelete("/orders/{id}", async (Guid id, IOrderRepository orderRepository, CancellationToken ct) =>
{
    await orderRepository.DeleteAsync(id, ct);
    return TypedResults.NoContent();
})
.RequireAuthorization("OrderManager");

AddSession aceita as três flags obrigatórias direto na configuração: HttpOnly impede que JavaScript leia o cookie, Secure impede que ele trafegue fora de HTTPS e SameSite impede que outro site o envie junto de uma requisição. O motivo de cada uma está em shared/platform/security.md.

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromHours(8);
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict;
    options.Cookie.IsEssential = true;
});

O que o Git nunca deve ver

appsettings.local.json
appsettings.*.local.json
*.pfx
*.key
.env
.env.*
secrets.json

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.