serverust_core/auth.rs
1//! Contrato de autenticação do framework — sem dependência de criptografia.
2//!
3//! `serverust-core` **não** implementa autenticação. Ele define apenas onde a
4//! decisão "esta requisição pode seguir?" é tomada, e deixa outro crate dizer
5//! quem é o requisitante. Isso mantém o core livre de cripto (ADR 0009) e faz
6//! com que qualquer implementação de autenticação — inclusive uma escrita pelo
7//! usuário — herde o default deny apenas inserindo os marcadores daqui.
8//!
9//! # Como as peças se encaixam
10//!
11//! ```text
12//! Layer de autenticação (App::layer) ← roda por FORA, em todas as rotas
13//! insere AuthEnabled sempre
14//! insere Authenticated se a credencial for válida
15//! ↓
16//! AuthGate (App::route) ← roda por DENTRO, só em rota não-pública
17//! AuthEnabled presente e Authenticated ausente → 401, handler não executa
18//! caso contrário → segue
19//! ```
20//!
21//! A ordem sai correta por construção: `App::layer` envolve o router inteiro,
22//! enquanto o [`AuthGate`] embrulha o `MethodRouter` de cada rota. O layer de
23//! fora sempre popula as extensions antes de o portão de dentro lê-las.
24//!
25//! Sem nenhum layer de autenticação instalado, o `AuthEnabled` nunca aparece e
26//! o portão deixa tudo passar: quem não usa autenticação não muda de
27//! comportamento.
28
29use std::pin::Pin;
30use std::sync::Arc;
31use std::task::{Context, Poll};
32
33use axum::extract::Request;
34use axum::response::{IntoResponse, Response};
35use http::StatusCode;
36use http::header::WWW_AUTHENTICATE;
37use http::request::Parts;
38use pin_project_lite::pin_project;
39use tower::{Layer, Service};
40
41/// Visão de autorização sobre a identidade da requisição, apagada de tipo.
42///
43/// Vive no core, e não no crate de autenticação, porque é **contrato** e não
44/// implementação: não toca cripto, não sabe o que é um JWT, e é o que permite
45/// que a macro `#[authorize]` gere código sem depender de qual crate
46/// autenticou a requisição.
47///
48/// A implementação de autenticação publica um `Arc<dyn AuthzFacts>` nas
49/// extensions; os guards de autorização leem de lá.
50///
51/// `has_scope` e `has_role` têm default `false`: um tipo de claims que só
52/// carrega identidade nega toda autorização em vez de concedê-la por omissão.
53pub trait AuthzFacts: Send + Sync + 'static {
54 /// Identificador do principal — tipicamente a claim `sub`.
55 fn subject(&self) -> &str;
56
57 /// Se o principal possui o escopo informado.
58 fn has_scope(&self, _scope: &str) -> bool {
59 false
60 }
61
62 /// Se o principal possui o papel informado.
63 fn has_role(&self, _role: &str) -> bool {
64 false
65 }
66}
67
68/// Marcador de que **existe autenticação instalada** nesta aplicação.
69///
70/// Uma implementação de autenticação insere isto nas extensions de toda
71/// requisição que passa por ela, independentemente de a credencial ser válida.
72/// É o que liga o [`AuthGate`]: sem este marcador o portão é inerte.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct AuthEnabled;
75
76/// Marcador de que **esta requisição traz identidade válida**.
77///
78/// Inserido pela implementação de autenticação somente quando a credencial foi
79/// verificada com sucesso. O [`AuthGate`] não olha *quem* é o requisitante —
80/// só se há identidade. Quem precisa dos dados usa o extractor tipado do crate
81/// de autenticação.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct Authenticated;
84
85/// Motivo estável pelo qual a autenticação falhou, registrado pela
86/// implementação para que o [`AuthGate`] o repasse ao cliente.
87///
88/// Sem isto o portão só saberia dizer "faltou identidade", e códigos úteis
89/// como `token_expired` — que dizem ao cliente para renovar em vez de
90/// reautenticar — se perderiam, porque o portão rejeita **antes** de qualquer
91/// extractor rodar.
92///
93/// O conteúdo é `&'static str` de propósito: o core não interpreta credencial
94/// e não deve alocar no caminho de rejeição.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct AuthFailure(pub &'static str);
97
98/// Marcador que **desliga** o [`AuthGate`] em toda a aplicação — o escape
99/// hatch `.allow_unannotated()` da decisão 5 da ADR 0009.
100///
101/// Existe para migração de serviço existente: instalar autenticação fecha
102/// todas as rotas de uma vez, e marcar dezenas de `#[public]` num único PR é
103/// onde o erro entra. Uma linha visível no builder é preferível à omissão
104/// distribuída por N rotas — e é o nome dela que desencoraja permanecer.
105///
106/// Não afeta `#[authorize]`: aquilo é autorização explícita, e afrouxar o
107/// default não é o mesmo que abrir mão do que foi pedido de propósito.
108///
109/// É marcador de request, e não flag de montagem, para que a ordem do builder
110/// continue irrelevante: `.allow_unannotated()` antes ou depois de `.route()`
111/// dá o mesmo resultado.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct AllowUnannotated;
114
115/// Portão de rota que implementa o **default deny**: com autenticação
116/// instalada, a rota só executa se a requisição estiver autenticada.
117///
118/// Aplicado automaticamente por [`crate::App::route`] a toda rota que não seja
119/// marcada como pública via [`crate::Route::public`]. Raramente é usado
120/// diretamente.
121///
122/// Rejeitar aqui, e não no layer de autenticação, é o que permite que rota
123/// pública e rota esquecida sejam tratadas de forma diferente: o layer roda em
124/// todas as rotas e não sabe qual delas casou, enquanto o portão só existe no
125/// caminho das rotas que exigem identidade.
126#[derive(Debug, Clone, Copy, Default)]
127pub struct AuthGate;
128
129impl<S> Layer<S> for AuthGate {
130 type Service = AuthGateService<S>;
131
132 fn layer(&self, inner: S) -> Self::Service {
133 AuthGateService { inner }
134 }
135}
136
137/// Service produzido por [`AuthGate`].
138#[derive(Debug, Copy)]
139pub struct AuthGateService<S> {
140 inner: S,
141}
142
143// Clone manual: o derivado adicionaria bound desnecessário em cenários com
144// `S` não-Clone, e queremos o mesmo requisito que o axum já impõe.
145impl<S: Clone> Clone for AuthGateService<S> {
146 fn clone(&self) -> Self {
147 Self {
148 inner: self.inner.clone(),
149 }
150 }
151}
152
153impl<S> Service<Request> for AuthGateService<S>
154where
155 S: Service<Request, Response = Response>,
156{
157 type Response = S::Response;
158 type Error = S::Error;
159 type Future = AuthGateFuture<S::Future>;
160
161 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
162 self.inner.poll_ready(cx)
163 }
164
165 fn call(&mut self, req: Request) -> Self::Future {
166 let extensions = req.extensions();
167 let auth_installed = extensions.get::<AuthEnabled>().is_some();
168 let authenticated = extensions.get::<Authenticated>().is_some();
169 let afrouxado = extensions.get::<AllowUnannotated>().is_some();
170
171 if auth_installed && !authenticated && !afrouxado {
172 // Repassa o motivo preciso quando a implementação registrou um;
173 // sem ele, a rejeição é genérica por política de rota.
174 let reason = extensions
175 .get::<AuthFailure>()
176 .map_or("authentication_required", |f| f.0);
177
178 AuthGateFuture::Denied {
179 response: Some(unauthorized(reason)),
180 }
181 } else {
182 AuthGateFuture::Allowed {
183 inner: self.inner.call(req),
184 }
185 }
186 }
187}
188
189pin_project! {
190 /// Future de [`AuthGateService`]: encaminha para o handler ou devolve o 401
191 /// já pronto.
192 ///
193 /// É um enum projetado com `pin-project-lite` em vez de um future boxado —
194 /// o caminho permitido não paga alocação nem dispatch dinâmico.
195 #[project = AuthGateFutureProj]
196 pub enum AuthGateFuture<F> {
197 Allowed { #[pin] inner: F },
198 Denied { response: Option<Response> },
199 }
200}
201
202impl<F, E> Future for AuthGateFuture<F>
203where
204 F: Future<Output = Result<Response, E>>,
205{
206 type Output = Result<Response, E>;
207
208 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
209 match self.project() {
210 AuthGateFutureProj::Allowed { inner } => inner.poll(cx),
211 AuthGateFutureProj::Denied { response } => Poll::Ready(Ok(response
212 .take()
213 .expect("AuthGateFuture::Denied pollado depois de concluído"))),
214 }
215 }
216}
217
218/// Resposta 401 do portão, com o motivo estável informado.
219///
220/// `WWW-Authenticate: Bearer` segue a RFC 6750 §3 — sem esse header o cliente
221/// não sabe qual esquema usar.
222///
223/// O motivo vem do [`AuthFailure`] quando a implementação registrou um (e aí
224/// carrega a precisão dela: `token_expired`, `invalid_issuer`, ...); na
225/// ausência dele é `authentication_required`, que significa rejeição por
226/// política da rota, sem tentativa de interpretar credencial.
227fn unauthorized(reason: &'static str) -> Response {
228 // A ADR 0009 pede um 401 "que ensina" o desenvolvedor a anotar a rota. A
229 // dica vai só em build de debug: em release ela seria vazamento de detalhe
230 // interno para quem chama a API, que não pode fazer nada com ela.
231 #[cfg(debug_assertions)]
232 let body = serde_json::json!({
233 "error": "unauthorized",
234 "reason": reason,
235 "hint": "rota não anotada: marque com #[public] se ela deve ser aberta",
236 });
237
238 #[cfg(not(debug_assertions))]
239 let body = serde_json::json!({
240 "error": "unauthorized",
241 "reason": reason,
242 });
243
244 (
245 StatusCode::UNAUTHORIZED,
246 [(WWW_AUTHENTICATE, "Bearer")],
247 axum::Json(body),
248 )
249 .into_response()
250}
251
252/// Verificação de autorização gerada pela macro `#[authorize]`.
253///
254/// Fica no core, e não na macro, por dois motivos: o código emitido no crate
255/// do usuário encolhe para uma chamada, e a política de rejeição passa a ter
256/// um lugar só — corrigi-la não exige recompilar quem já gerou o guard com
257/// uma versão antiga da macro.
258///
259/// Semântica: **todos** os escopos e **todos** os papéis listados são
260/// exigidos (AND). Empilhar `#[authorize]` também conjunta. Exigência
261/// alternativa (OR) não é expressável hoje, de propósito: um `any_of`
262/// ambíguo entre "qualquer um destes" e "qualquer um de tudo" é exatamente o
263/// tipo de default que a ADR 0009 pede para não existir.
264///
265/// Falha fechado em duas frentes:
266///
267/// - **Sem fatos nas extensions** — seja porque não há autenticação
268/// instalada, seja porque a rota é pública — a resposta é 401. Uma rota que
269/// pede autorização e não tem de onde extrair identidade não pode executar.
270/// - **Fatos presentes sem o escopo/papel** — 403 `insufficient_scope`,
271/// o código da RFC 6750 §3.1.
272#[doc(hidden)]
273// O `Result<_, Response>` é a assinatura do `Guard::check`, que esta função
274// alimenta: boxar aqui só adicionaria uma alocação no caminho de rejeição
275// para desboxar em seguida.
276#[allow(clippy::result_large_err)]
277pub fn check_authz(parts: &Parts, scopes: &[&str], roles: &[&str]) -> Result<(), Response> {
278 let Some(facts) = parts.extensions.get::<Arc<dyn AuthzFacts>>() else {
279 return Err(unauthorized("authentication_required"));
280 };
281
282 if scopes.iter().all(|s| facts.has_scope(s)) && roles.iter().all(|r| facts.has_role(r)) {
283 return Ok(());
284 }
285
286 Err(forbidden("insufficient_scope"))
287}
288
289/// Resposta 403 de autorização negada.
290///
291/// O corpo espelha o do 401 (`error` + `reason` estável) para que o cliente
292/// ramifique da mesma forma nos dois casos. Sem `hint` mesmo em debug: aqui a
293/// rota **está** anotada, e o que falta é permissão do principal — não há
294/// anotação a sugerir ao desenvolvedor.
295fn forbidden(reason: &'static str) -> Response {
296 (
297 StatusCode::FORBIDDEN,
298 [(WWW_AUTHENTICATE, "Bearer")],
299 axum::Json(serde_json::json!({ "error": "forbidden", "reason": reason })),
300 )
301 .into_response()
302}