Em qualquer aplicação não-trivial, duas perguntas precisam ser respondidas em quase toda chamada que cruza fronteira de segurança: "quem está chamando?" (autenticação, AuthN) e "essa identidade pode fazer essa operação?" (autorização, AuthZ). São duas perguntas distintas, com mecanismos distintos, e tratá-las como uma coisa só é a fonte mais comum de erros sutis em segurança de aplicação. Sistemas maduros separam as duas em aspects distintos: middleware faz AuthN globalmente, interceptor (ou policy) por operação faz AuthZ baseada no recurso.
A literatura, infelizmente, ajuda a confundir. Auth é prefixo ambíguo; bibliotecas chamadas "Auth" frequentemente misturam os dois mundos. Vale forçar o vocabulário no time: AuthN responde com identidade ("é o usuário Camila"); AuthZ responde com decisão sobre essa identidade ("Camila pode atualizar este pedido"). AuthN tipicamente acontece na borda do sistema (uma vez por request), com base em token JWT, sessão de cookie, ou certificado. AuthZ acontece em camadas mais internas, frequentemente por operação, e depende não só de quem é mas de o que está sendo acessado.
Há também a tentação histórica de implementar AuthZ como
"verificação inline": if user.role != "admin": raise
Forbidden() espalhado pelos handlers. Funciona em
sistema pequeno, vira tangling em sistema crescente, e
explode em sistema com regras complexas de autorização.
Aspect-driven é a saída — e o tipo de aspect (middleware vs
filter vs policy engine externo) varia conforme o domínio.
Este conceito articula a separação AuthN/AuthZ, mostra os modelos clássicos de autorização (RBAC, ABAC, ReBAC), e enuncia heurísticas de organização. Implementação detalhada de OAuth 2.1, OIDC, e threat modeling fica para o módulo 11 (Segurança); aqui o foco é como o cross-cutting concern se organiza em código de aplicação.
Autenticação — onde, e por quê na borda
AuthN responde "quem está chamando?". Em 2026, em aplicações
web modernas, a resposta tipicamente vem de um access
token assinado — geralmente JWT (JSON Web Token, RFC
7519, 2015) emitido por um provider OAuth 2.1/OIDC (Auth0,
Keycloak, Cognito, Azure AD, Google Identity, etc.). O cliente
envia o token via header Authorization: Bearer ...;
o servidor valida assinatura, expiração, e issuer; extrai
claims (subject, email, scopes, roles); e segue.
A regra arquitetural é fazer AuthN uma vez, na
borda. Middleware HTTP: o primeiro middleware após
tratamento de erro e correlation ID é o de AuthN. Ele extrai
o token, valida, popula HttpContext.User (em
.NET) ou request.state.user (em FastAPI) ou
contexto similar, e entrega para o resto do pipeline. Quem
vier depois lê a identidade do contexto, não revalida o
token. AuthN duplicada em camadas internas é desperdício e
vetor de inconsistência.
// .NET 10 — JWT bearer authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://auth.example.com/";
options.Audience = "catalog-api";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromSeconds(30),
};
});
builder.Services.AddAuthorization(); // ativa o pipeline de policies
var app = builder.Build();
app.UseAuthentication(); // popula User
app.UseAuthorization(); // executa policies por endpoint
app.MapControllers();
Note três detalhes. Primeiro: validação de
JWT é automática quando configurada — UseAuthentication
decodifica o token, valida assinatura via
JWKS (RFC 7517) buscado periodicamente do issuer, e
popula User. Segundo:
ClockSkew de 30s tolera relógios ligeiramente
diferentes entre servidores — sem isso, tokens emitidos há 1s
podem falhar se os relógios divergem milissegundos.
Terceiro:
UseAuthentication e UseAuthorization
são middlewares distintos, mesmo em quase todo
framework. A separação é estrutural.
Autorização — RBAC, ABAC, ReBAC
AuthZ é mais sutil porque depende do modelo de autorização escolhido. Há três modelos canônicos, e a escolha tem implicações arquiteturais profundas.
RBAC — Role-Based Access Control
O modelo mais antigo, formalizado nos anos 1990 por David Ferraiolo e Richard Kuhn (NIST). Permissões são associadas a papéis (admin, editor, viewer), e usuários recebem papéis. A pergunta de autorização é "esse usuário tem papel X?". Simples, escalável, suficiente para muitos sistemas.
RBAC perde tração quando autorização depende de contexto:
"editor pode editar artigo, mas só os artigos que ele
mesmo criou", ou "gerente pode aprovar despesa, mas só na
filial dele". Esses cenários puxam a equipe a criar dezenas
de papéis inflados (editor-da-filial-SP,
editor-da-filial-RJ...) ou a misturar lógica
inline ("se papel == editor && resource.author_id == user.id
→ ok"). Sintoma clássico de "RBAC esticando demais".
ABAC — Attribute-Based Access Control
Generalização: a decisão de autorização é uma função booleana
sobre atributos do usuário, do recurso, da
ação, e do ambiente. "Pode editar se
user.department == resource.department AND
user.security_clearance >= resource.classification
AND now está em horário comercial". É expressivo
o suficiente para qualquer cenário, mas a complexidade pode
crescer rápido.
Implementação típica de ABAC usa policy engine — OPA (Open Policy Agent), Cedar (AWS), Casbin. Você escreve regras em DSL (Rego para OPA, Cedar policy language), o engine avalia, e a aplicação só pergunta "permitido?". A separação engine ↔ aplicação é o que permite atualizar políticas sem deploy de código.
ReBAC — Relationship-Based Access Control
Modelo descrito no paper Zanzibar do Google (2019), Carl Lesniewski-Laas e equipe. Autorização baseada em grafo de relações: "Camila é proprietária do projeto P; projeto P contém o documento D; logo, Camila pode editar D". É o modelo por trás de Google Docs, GitHub, e qualquer sistema com permissões hierárquicas/colaborativas. Em 2026, bibliotecas como SpiceDB (Authzed), OpenFGA (originalmente Auth0/Okta), e Permify implementam Zanzibar para aplicações fora do Google.
A diferença prática: em ReBAC, autorização é uma query no grafo ("existe caminho de relação válido entre Camila e D que autorize action edit?"), não uma verificação local de atributos. A complexidade de modelagem é alta inicialmente, mas o sistema escala para hierarquias e compartilhamento de forma elegante.
A escolha entre os três é decisão arquitetural. Heurística pragmática: comece com RBAC; se virar dezenas de papéis inflados, considere ABAC com policy engine; se a essência do domínio é compartilhamento e hierarquia (multi-tenant SaaS, colaboração de documentos), avalie ReBAC desde o início.
Padrões de aplicação em código
Independente do modelo, a forma como AuthZ aparece no código tem três padrões frequentes. Reconhecer qual a equipe usa ajuda a articular consistência.
Atributo declarativo — [Authorize(Policy = "...")]
A regra fica anotada no método/endpoint, e o framework executa
antes do handler. ASP.NET Core
[Authorize(Policy = "EditarPedido")] com policy
definida no startup; FastAPI
Depends(require_permission("pedido:editar"));
Spring @PreAuthorize("hasRole('ADMIN')"). É a
forma mais limpa para regras simples (papel necessário,
escopo necessário). Menos limpa para regras dependentes de
recurso ("dono do pedido"), porque o atributo precisa ler o
ID do path/body.
Filter ou interceptor por recurso
Quando a regra depende do recurso (e portanto precisa carregar
o recurso para decidir), filter no nível de método (ou
Depends em FastAPI) é o lugar. Conceito 05
mostrou o exemplo de "dono do pedido pode ler o pedido". Esse
padrão consolida três operações em uma: validar que o recurso
existe, validar autorização, e disponibilizar o recurso ao
handler.
Policy engine externo
Para regras complexas ou que mudam sem deploy, o handler chama
um engine externo: opa.Evaluate(input),
spicedb.CheckPermission(...). O engine retorna
booleano (com motivo opcional), e o handler segue. A vantagem é
que regras vivem em DSL com versionamento próprio, podem ser
auditadas, testadas separadamente, e atualizadas sem alterar a
aplicação. Custos: latência adicional (rede até o engine),
complexidade operacional (mais um sistema para manter).
O mesmo cenário, três abordagens
Considere o cenário canônico: API de pedidos onde
GET /pedidos/{id} só pode ser acessado pelo
cliente que criou o pedido ou por um operador com
papel customer-support.
// startup
builder.Services.AddAuthorization(o =>
{
o.AddPolicy("LerPedido", p => p.Requirements.Add(new LerPedidoRequirement()));
});
builder.Services.AddScoped<IAuthorizationHandler, LerPedidoHandler>();
// requirement
public class LerPedidoRequirement : IAuthorizationRequirement { }
// handler
public class LerPedidoHandler : AuthorizationHandler<LerPedidoRequirement, Pedido>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext ctx,
LerPedidoRequirement req, Pedido pedido)
{
var userId = ctx.User.FindFirstValue("sub");
if (pedido.ClienteId == userId ||
ctx.User.IsInRole("customer-support"))
{
ctx.Succeed(req);
}
return Task.CompletedTask;
}
}
// endpoint
app.MapGet("/pedidos/{id}", async (Guid id, IAuthorizationService authz,
ClaimsPrincipal user, IPedidoRepository repo) =>
{
var pedido = await repo.ObterAsync(id);
if (pedido is null) return Results.NotFound();
var result = await authz.AuthorizeAsync(user, pedido, "LerPedido");
return result.Succeeded
? Results.Ok(pedido)
: Results.Forbid();
});
ASP.NET Core tem resource-based authorization
como cidadão de primeira classe. O
AuthorizationHandler<TRequirement, TResource>
recebe o recurso e a identidade — perfeito para regras
que precisam dos dois.
def require_pedido_access(
id: UUID,
user: User = Depends(current_user),
repo: PedidoRepo = Depends(get_repo),
) -> Pedido:
pedido = repo.obter(id)
if pedido is None:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if (pedido.cliente_id != user.id
and "customer-support" not in user.roles):
raise HTTPException(status.HTTP_403_FORBIDDEN)
return pedido
@app.get("/pedidos/{id}")
async def obter_pedido(pedido: Pedido = Depends(require_pedido_access)) -> PedidoOut:
return PedidoOut.from_domain(pedido)
Em FastAPI, dependency carrega o pedido e autoriza em uma operação. O endpoint recebe o pedido já validado — não repete a query. Ergonomia alta, regra explícita.
// middleware: valida AuthN e popula context
func WithAuth(jwks Verifier) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tok := r.Header.Get("Authorization")
user, err := jwks.Validate(tok)
if err != nil { http.Error(w, "unauthorized", 401); return }
ctx := context.WithValue(r.Context(), userKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// handler com authorization explícita
func (h *Handlers) ObterPedido(w http.ResponseWriter, r *http.Request) {
id, _ := uuid.Parse(chi.URLParam(r, "id"))
user := userFromCtx(r.Context())
pedido, err := h.repo.Obter(r.Context(), id)
if err != nil { http.NotFound(w, r); return }
if pedido.ClienteID != user.ID && !user.HasRole("customer-support") {
http.Error(w, "forbidden", 403)
return
}
respondJSON(w, 200, pedido)
}
// composição
r.Use(WithAuth(verifier))
r.Get("/pedidos/{id}", h.ObterPedido)
Go é o mais explícito. Authorization fica no handler porque depende do recurso carregado; refatorar para interceptor é possível com gRPC, mas em HTTP a comunidade prefere a versão direta.
Policy engine externo — quando vale
Aplicações com regras complexas e em mudança constante (e-commerce multi-tenant, plataformas SaaS, sistemas regulados) podem ganhar com policy engine externo. OPA (Open Policy Agent, CNCF, 2018) é o mais difundido. A regra é escrita em Rego:
# policy.rego — regra de leitura de pedido
package catalog.pedidos
default allow := false
# dono pode ler
allow if {
input.action == "read"
input.resource.cliente_id == input.user.id
}
# customer-support pode ler tudo
allow if {
input.action == "read"
"customer-support" in input.user.roles
}
# admin pode ler em qualquer ambiente
allow if {
input.action == "read"
input.user.is_admin
}
A aplicação chama OPA via HTTP ou via SDK embutido (Go OPA,
Python opa-python):
// Go — chamada ao OPA embutido
result, err := opa.Eval(ctx, "data.catalog.pedidos.allow",
rego.EvalInput(map[string]any{
"action": "read",
"resource": map[string]any{"cliente_id": pedido.ClienteID},
"user": user,
}))
if err != nil || !result.Allowed() {
http.Error(w, "forbidden", 403)
return
}
Vantagens: regras em texto, versionadas em git separado se útil; auditoria centralizada (OPA loga decisão); time de segurança consegue atualizar sem deploy de código. Custos: latência (mesmo embutido, é uma camada); curva de aprendizado do Rego; mais um sistema para operar. Heurística: vale para sistemas com 50+ regras complexas; é overkill para sistema com 5 regras simples.
IDOR (Insecure Direct Object Reference). O endpoint
autenticado GET /pedidos/{id} esquece a
verificação de autorização baseada em recurso, e qualquer
usuário autenticado consegue ler pedido de qualquer outro
passando o ID. É uma das vulnerabilidades top do OWASP
(categoria "Broken Access Control" no top 10 desde 2017,
sendo #1 em 2021). A defesa estrutural é exatamente o que
este conceito articula: todo endpoint que opera
sobre recurso identificável precisa de policy de
authorization que considere o usuário e o recurso. Atributo
declarativo de "tem que estar autenticado" não basta — vai
permitir o IDOR. Time maduro tem checklist de PR review
que inclui "esse endpoint tem authorization baseada em
recurso?".
Onde a separação fica clara
Em código maduro, fica visível na composição: middleware faz AuthN sem saber de operações específicas; filter/dependency faz AuthZ baseada na operação e no recurso; e handler de domínio nem menciona segurança. A leitura do handler conta o que ele faz; a leitura do filter conta as regras de quem pode fazê-lo.
Quando a separação não é clara, ficam pistas no código:
if user.role != ... espalhado em handlers;
decisões de autorização repetidas em múltiplos lugares com
lógica ligeiramente diferente; middleware de
autorização que conhece IDs de pedidos (sintoma claro de
mistura). Refatorar para a separação leva tempo, mas o
benefício composto é alto: menos bug, segurança auditável,
onboarding mais rápido.
Anti-padrões frequentes
Authorization no domínio.
Pedido.AdicionarItem(item, user) com
if !user.IsAdmin: throw Forbidden. Domínio
vira contaminado com plumbing. Solução: verificação fica
antes do domínio, idealmente em filter, que decide se a
operação prossegue.
JWT como armazenamento de permissões finas.
Token com permissions: ["pedido:read:123",
"pedido:write:123", ...] para cada recurso vira
gigante e impossível de invalidar. Permissões finas devem
ser consultadas dinamicamente; JWT carrega papéis e claims
grossas.
"Anyone authenticated" como AuthZ. Endpoint
protegido por [Authorize] sem policy específica
autoriza qualquer usuário logado. Ok para alguns endpoints
(perfil próprio, listagens públicas autenticadas), mas
raramente apropriado para operações sobre recurso. Sempre
pergunte: "qualquer usuário autenticado pode fazer isso?"
Se a resposta é "não", policy específica é obrigatória.
Fail open em vez de fail closed. Se o serviço de autorização (engine externo) está fora, o sistema permite ou bloqueia? Default arquitetural correto: bloqueia (fail closed). Permite só se a equipe articulou explicitamente o trade-off (degradação de UX vs risco de segurança).
Não logar decisões de authorization. Sem log de "user X tentou Y, resultado Z", investigação de incidente vira adivinhação. Logue cada decisão (especialmente negativa) em formato estruturado, com correlation_id. Volume é maior que logging normal — sampling é razoável em decisões positivas; negativas devem ir 100%.
Antes de adicionar endpoint novo, articule duas frases: "esse endpoint requer que o usuário esteja autenticado?" e "que regra decide se um usuário autenticado pode chamar esse endpoint específico, eventualmente sobre esse recurso específico?". Se a segunda frase contém "qualquer autenticado", confirme em revisão que isso é intencional. Se contém "depende do recurso", policy de authorization baseada em recurso é obrigatória — atributo declarativo sozinho não basta. Esse exercício, feito antes de cada PR que adiciona endpoint, captura a maior parte dos bugs de autorização antes da review.
Por que importa para a sua carreira
Auth é tópico onde sêniores se distinguem de plenos quase automaticamente. Em entrevista de design, "como você organizaria authorization em uma API com 50 endpoints e 10 papéis?" é convite direto para articular a separação AuthN/AuthZ, escolher modelo (RBAC vs ABAC vs ReBAC com justificativa), e enunciar onde as regras vivem. Em revisão de código, perceber endpoint vulnerável a IDOR e propor a correção é serviço de senior. Em pos-mortem de incidente de segurança, articular "tinha verificação de recurso? Como o framework expressa essa verificação? Estamos consistentes?" diferencia quem participa da operação de quem só observa. E em sistemas regulados (financeiro, saúde), a articulação de AuthZ frequentemente vira tema de auditoria — saber discutir em vocabulário maduro economiza meses de retrabalho.
Como praticar
- Authorization baseada em recurso nas três linguagens. Implemente em ASP.NET Core (resource-based authorization), FastAPI (Depends factory), e Go (middleware + handler) o mesmo cenário do lang-compare. Acrescente teste de segurança que tenta acessar pedido de outro usuário via IDOR — verifique que a resposta é 403, não 200. Esse teste é candidato direto a checklist de release.
- OPA com policy não-trivial. Suba OPA localmente. Escreva em Rego uma policy que permite leitura de pedido se: (a) usuário é dono, OU (b) usuário tem papel customer-support, OU (c) usuário é admin do tenant correspondente. Integre com uma das três linguagens. Verifique que mudar a policy não exige re-deploy do serviço — só recarregar OPA. Esse exercício faz você sentir o ganho operacional de policy externa.
- Auditoria de IDOR em projeto existente. Pegue um projeto seu (ou aberto) e liste todos os endpoints autenticados que recebem ID de recurso no path. Para cada um, verifique se há authorization baseada em recurso. Identifique pelo menos um endpoint vulnerável a IDOR e proponha em PR a correção — idealmente promovendo o padrão para todo o projeto via filter/dependency reusável. Esse é o tipo de trabalho que ganha respeito do time de segurança.
Referências para aprofundar
- paper Role-Based Access Control Models — David Ferraiolo, Richard Kuhn (NIST, 1992; ANSI/INCITS 359-2004).
- paper Zanzibar: Google's Consistent, Global Authorization System — Lea Pugh et al., Google (USENIX, 2019).
- paper Guide to Attribute Based Access Control (ABAC) — NIST SP 800-162 (Vincent Hu et al., 2014).
- livro OAuth 2 in Action — Justin Richer, Antonio Sanso (Manning, 2017).
- livro API Security in Action — Neil Madden (Manning, 2020).
- livro Building Microservices (2ª ed.) — Sam Newman (O'Reilly, 2021).
- docs OWASP Top 10 — Broken Access Control.
- docs ASP.NET Core Authorization.
- docs Open Policy Agent (OPA).
- docs OpenFGA e SpiceDB.
- artigo Authorization Academy — Oso Inc. (oso.dev).
- vídeo Authorization Best Practices — Sam Scott (oso.dev co-founder), CarolinaCon e outros eventos, 2022+.