---
title: "Autenticação JWT em Rust com Axum: Guia Prático | Rust Brasil"
url: "https://rustlang.com.br/blog/autenticacao-jwt-rust-axum-2026/"
markdown_url: "https://rustlang.com.br/blog/autenticacao-jwt-rust-axum-2026.MD"
description: "Como implementar autenticação JWT em Rust com Axum: hash de senha com Argon2, geração e validação de tokens, extractor de usuário, refresh, erros e checklist."
date: "2026-09-07"
author: "Equipe Rust Brasil"
---

# Autenticação JWT em Rust com Axum: Guia Prático | Rust Brasil

Como implementar autenticação JWT em Rust com Axum: hash de senha com Argon2, geração e validação de tokens, extractor de usuário, refresh, erros e checklist.


**Para autenticar uma API Rust com Axum, use a crate `jsonwebtoken` para emitir e validar tokens, `argon2` para o hash das senhas e um extractor `FromRequestParts` para transformar o header `Authorization: Bearer` em um tipo de usuário autenticado que os handlers recebem como argumento.** Esse é o desenho mais direto e mais usado em produção no ecossistema: o token carrega identidade e expiração, o extractor centraliza a validação e o compilador garante que nenhuma rota protegida esqueça a checagem.

Este guia mostra o caminho completo — cadastro, login, validação por requisição, autorização por papel, refresh token e erros — com as decisões de segurança explicadas em cada etapa.

## Resposta rápida: as escolhas que importam

| Decisão | Recomendação | Por quê |
|---|---|---|
| Crate de JWT | `jsonwebtoken` | padrão de fato, integra com `serde`, suporta HS256 e RS256 |
| Hash de senha | `argon2` (ou `bcrypt`) | resistente a GPU; nunca use SHA-256 puro |
| Algoritmo | HS256 para serviço único; RS256 para vários validadores | evita distribuir a chave de assinatura |
| Validade do access token | 5 a 15 minutos | limita a janela de um token vazado |
| Refresh token | opaco, persistido em banco, rotacionado | permite logout e revogação reais |
| Transporte no browser | cookie `HttpOnly` + `Secure` + `SameSite` | XSS não consegue ler o token |
| Ponto de validação | extractor `FromRequestParts` | uma implementação só, usada por tipo em todo handler |
| Segredo | variável de ambiente / secret manager | nunca no Git, nunca hardcoded |

Se você só quer o resumo: **access token curto no header ou cookie, refresh token com estado no banco, extractor tipado no Axum, Argon2 para senhas e algoritmo fixado na validação**. O resto deste artigo explica como montar isso sem armadilhas.

## O que um JWT é — e o que ele não é

Um JSON Web Token é composto de três partes separadas por ponto: header, payload e assinatura. As duas primeiras são apenas Base64URL — **qualquer pessoa com o token consegue ler o conteúdo**. A assinatura garante que o payload não foi alterado, não que ele seja secreto.

Isso tem consequências práticas:

- não coloque dados sensíveis nas claims (CPF, e-mail completo, permissões internas detalhadas);
- não confie em nada que o cliente envie fora do token assinado;
- lembre que um token válido continua válido até expirar, mesmo que o usuário seja bloqueado.

As claims registradas mais úteis são `sub` (identificador do usuário), `exp` (expiração), `iat` (emissão), `iss` (emissor) e `jti` (identificador único do token, útil para revogação).

## Dependências

```bash
cargo add axum
cargo add tokio --features macros,rt-multi-thread
cargo add serde --features derive
cargo add serde_json
cargo add jsonwebtoken
cargo add argon2
cargo add password-hash --features rand_core
cargo add sqlx --features runtime-tokio-rustls,postgres,uuid,chrono,macros
cargo add chrono --features serde
cargo add uuid --features v4,serde
cargo add thiserror
cargo add tower-http --features cors,trace
cargo add tracing tracing-subscriber
```

As assinaturas exatas de `jsonwebtoken` e `argon2` mudaram entre versões maiores. Depois de instalar, confirme com `cargo doc --open` ou com o README da versão fixada no seu `Cargo.lock` antes de copiar qualquer exemplo — inclusive os daqui.

## Modelando as claims

```rust
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,     // id do usuário
    pub role: String,    // papel simples: "user" | "admin"
    pub exp: i64,        // expiração (timestamp unix)
    pub iat: i64,        // emissão
    pub jti: String,     // id do token
}
```

Mantenha o payload pequeno. Cada requisição carrega o token inteiro, e claims infladas viram custo de banda e superfície de erro. Papéis grosseiros (`user`, `admin`, `billing`) funcionam bem; permissões finas normalmente pertencem ao banco, consultadas no momento da ação.

## Emitindo e validando o token

```rust
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use uuid::Uuid;

pub struct JwtKeys {
    encoding: EncodingKey,
    decoding: DecodingKey,
}

impl JwtKeys {
    pub fn from_secret(secret: &[u8]) -> Self {
        Self {
            encoding: EncodingKey::from_secret(secret),
            decoding: DecodingKey::from_secret(secret),
        }
    }

    pub fn emitir(&self, user_id: &str, role: &str) -> Result<String, jsonwebtoken::errors::Error> {
        let agora = Utc::now();
        let claims = Claims {
            sub: user_id.to_string(),
            role: role.to_string(),
            iat: agora.timestamp(),
            exp: (agora + Duration::minutes(15)).timestamp(),
            jti: Uuid::new_v4().to_string(),
        };

        encode(&Header::new(Algorithm::HS256), &claims, &self.encoding)
    }

    pub fn validar(&self, token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
        let mut validation = Validation::new(Algorithm::HS256);
        validation.leeway = 5; // tolerância de relógio, em segundos

        decode::<Claims>(token, &self.decoding, &validation).map(|dados| dados.claims)
    }
}
```

Três detalhes salvam projetos aqui:

1. **Fixe o algoritmo.** `Validation::new(Algorithm::HS256)` impede que um token forjado peça outro algoritmo. Aceitar o algoritmo declarado pelo cliente é uma classe clássica de vulnerabilidade.
2. **Nunca aceite `alg: none`.** A crate já rejeita por padrão; não reescreva essa validação.
3. **Carregue o segredo do ambiente.** Em desenvolvimento, `dotenvy`; em produção, o secret manager da sua plataforma. Um segredo HS256 deve ter pelo menos 32 bytes aleatórios (`openssl rand -base64 48`).

## Hash de senha com Argon2

Senha nunca é guardada, nem cifrada de forma reversível: guarda-se um hash lento e salgado.

```rust
use argon2::{
    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
    Argon2,
};

pub fn hash_senha(senha: &str) -> Result<String, argon2::password_hash::Error> {
    let salt = SaltString::generate(&mut OsRng);
    Ok(Argon2::default()
        .hash_password(senha.as_bytes(), &salt)?
        .to_string())
}

pub fn verificar_senha(senha: &str, hash_armazenado: &str) -> bool {
    match PasswordHash::new(hash_armazenado) {
        Ok(parsed) => Argon2::default()
            .verify_password(senha.as_bytes(), &parsed)
            .is_ok(),
        Err(_) => false,
    }
}
```

Argon2 é propositalmente custoso em CPU e memória. Em um runtime assíncrono, isso significa **bloquear o executor**: uma verificação pode levar dezenas ou centenas de milissegundos. Rode o hash em `tokio::task::spawn_blocking` para não travar as outras tarefas do runtime.

```rust
let hash = hash_armazenado.clone();
let senha = payload.senha.clone();
let ok = tokio::task::spawn_blocking(move || verificar_senha(&senha, &hash))
    .await
    .map_err(|_| ApiError::Interno)?;
```

Esse é um dos erros mais comuns em APIs Rust: código correto do ponto de vista criptográfico que derruba a latência de toda a aplicação sob carga. Se o assunto de concorrência ainda não está confortável, revise o [tutorial de concorrência em Rust](/tutoriais/concorrencia/).

## O extractor de usuário autenticado

Este é o coração da integração com o Axum. Em vez de chamar uma função de verificação no início de cada handler — algo que se esquece —, você cria um **tipo** que só pode existir se o token for válido.

```rust
use axum::{
    extract::FromRequestParts,
    http::{header::AUTHORIZATION, request::Parts},
};

pub struct UsuarioAutenticado {
    pub id: String,
    pub role: String,
}

impl<S> FromRequestParts<S> for UsuarioAutenticado
where
    AppState: axum::extract::FromRef<S>,
    S: Send + Sync,
{
    type Rejection = ApiError;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let state = AppState::from_ref(state);

        let token = parts
            .headers
            .get(AUTHORIZATION)
            .and_then(|valor| valor.to_str().ok())
            .and_then(|valor| valor.strip_prefix("Bearer "))
            .ok_or(ApiError::NaoAutenticado)?;

        let claims = state
            .keys
            .validar(token.trim())
            .map_err(|_| ApiError::NaoAutenticado)?;

        Ok(UsuarioAutenticado {
            id: claims.sub,
            role: claims.role,
        })
    }
}
```

Agora um handler protegido é simplesmente:

```rust
async fn perfil(usuario: UsuarioAutenticado) -> Json<serde_json::Value> {
    Json(serde_json::json!({ "id": usuario.id, "role": usuario.role }))
}
```

E um handler público não declara o argumento. **A proteção passa a ser visível na assinatura da função** — revisão de código e o próprio compilador ajudam a não esquecer nenhuma rota. Se você ainda não montou uma API com Axum, comece pelo [tutorial de API REST com Axum](/tutoriais/api-rest-axum/) e pelo [guia do Axum no ecossistema](/ecossistema/axum/).

A assinatura de `FromRequestParts` mudou entre versões do Axum (com e sem `#[async_trait]`). Confira a versão que você instalou antes de copiar.

## Autorização por papel

Autenticação responde “quem é você”; autorização responde “você pode fazer isso”. Separe as duas.

```rust
pub struct Admin(pub UsuarioAutenticado);

impl<S> FromRequestParts<S> for Admin
where
    AppState: axum::extract::FromRef<S>,
    S: Send + Sync,
{
    type Rejection = ApiError;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let usuario = UsuarioAutenticado::from_request_parts(parts, state).await?;

        if usuario.role != "admin" {
            return Err(ApiError::SemPermissao);
        }

        Ok(Admin(usuario))
    }
}
```

Um handler que exige `Admin` não compila sem a checagem. Para regras que dependem de dados (“o dono deste recurso”), a verificação precisa ir ao banco dentro do handler — o token não sabe quem é dono do quê.

## Rotas de cadastro e login

```rust
use axum::{routing::post, Json, Router};

#[derive(Deserialize)]
struct LoginPayload {
    email: String,
    senha: String,
}

async fn login(
    State(state): State<AppState>,
    Json(payload): Json<LoginPayload>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let usuario = sqlx::query!(
        "SELECT id, role, senha_hash FROM usuarios WHERE email = $1",
        payload.email
    )
    .fetch_optional(&state.db)
    .await
    .map_err(|_| ApiError::Interno)?;

    // Mesma resposta para e-mail inexistente e senha errada
    let usuario = usuario.ok_or(ApiError::CredenciaisInvalidas)?;

    let hash = usuario.senha_hash.clone();
    let senha = payload.senha.clone();
    let ok = tokio::task::spawn_blocking(move || verificar_senha(&senha, &hash))
        .await
        .map_err(|_| ApiError::Interno)?;

    if !ok {
        return Err(ApiError::CredenciaisInvalidas);
    }

    let token = state
        .keys
        .emitir(&usuario.id.to_string(), &usuario.role)
        .map_err(|_| ApiError::Interno)?;

    Ok(Json(serde_json::json!({ "access_token": token, "token_type": "Bearer" })))
}

pub fn rotas() -> Router<AppState> {
    Router::new()
        .route("/auth/registrar", post(registrar))
        .route("/auth/login", post(login))
}
```

Note a resposta idêntica para “usuário não existe” e “senha incorreta”. Mensagens diferentes permitem **enumeração de contas**: um atacante descobre quais e-mails estão cadastrados. Pelo mesmo motivo, considere executar o hash mesmo quando o usuário não existe, para não vazar a diferença pelo tempo de resposta.

Valide o payload de entrada antes de tocar no banco — tamanho mínimo de senha, formato de e-mail, limites de string. O artigo sobre [validação de dados com validator e garde](/blog/rust-validacao-dados-validator-garde-serde-2026/) cobre as opções do ecossistema. Para as queries e o schema, veja o [guia de SQLx](/ecossistema/sqlx/) e o [tutorial de Rust com PostgreSQL](/tutoriais/rust-postgresql/).

## Erros que não vazam informação

```rust
use axum::{http::StatusCode, response::{IntoResponse, Response}};

#[derive(Debug, thiserror::Error)]
pub enum ApiError {
    #[error("não autenticado")]
    NaoAutenticado,
    #[error("sem permissão")]
    SemPermissao,
    #[error("credenciais inválidas")]
    CredenciaisInvalidas,
    #[error("erro interno")]
    Interno,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, mensagem) = match self {
            ApiError::NaoAutenticado => (StatusCode::UNAUTHORIZED, "token ausente ou inválido"),
            ApiError::SemPermissao => (StatusCode::FORBIDDEN, "acesso negado"),
            ApiError::CredenciaisInvalidas => (StatusCode::UNAUTHORIZED, "credenciais inválidas"),
            ApiError::Interno => (StatusCode::INTERNAL_SERVER_ERROR, "erro interno"),
        };

        (status, Json(serde_json::json!({ "erro": mensagem }))).into_response()
    }
}
```

Registre o detalhe técnico com `tracing` e devolva a mensagem genérica ao cliente. Diferenciar “token expirado” de “assinatura inválida” na resposta HTTP entrega informação útil a quem está atacando. Para a modelagem de erros em geral, consulte o [guia de thiserror e anyhow](/ecossistema/anyhow-thiserror/).

## Refresh token: o que torna o logout possível

Access tokens curtos resolvem a exposição, mas obrigariam o usuário a fazer login a cada 15 minutos. A solução é um segundo token, com natureza diferente:

| | Access token | Refresh token |
|---|---|---|
| Formato | JWT assinado | valor opaco aleatório |
| Validade | 5–15 minutos | dias ou semanas |
| Armazenamento no servidor | nenhum | **hash persistido no banco** |
| Uso | toda requisição | só no endpoint `/auth/refresh` |
| Revogável | não (até expirar) | sim, apagando a linha |

O fluxo é: `/auth/refresh` recebe o refresh token, procura o hash no banco, confere validade e revogação, **emite um novo par e invalida o antigo** (rotação). Se um refresh token já usado reaparecer, é sinal de roubo — revogue toda a família de tokens daquele usuário.

Guarde o refresh token em cookie `HttpOnly`, `Secure`, `SameSite=Strict` e com `Path` restrito ao endpoint de refresh. Guarde apenas o hash no banco, nunca o valor em claro: um dump de banco não deve virar acesso às contas.

## Middleware, CORS e rate limiting

O extractor cobre a validação por rota. Algumas proteções, porém, são transversais e vivem como camadas `tower`:

```rust
use tower_http::{cors::CorsLayer, trace::TraceLayer};

let app = Router::new()
    .merge(auth::rotas())
    .route("/perfil", get(perfil))
    .layer(TraceLayer::new_for_http())
    .layer(cors_restrito())
    .with_state(state);
```

Pontos de atenção:

- **CORS com origens explícitas.** `Any` combinado com credenciais é uma configuração perigosa e frequentemente rejeitada pelo navegador.
- **Rate limiting no `/auth/login`.** Sem limite por IP e por conta, a rota vira alvo de força bruta. `tower` tem camadas para isso; veja o artigo sobre [serviços resilientes com Tower e Axum](/blog/rust-servicos-resilientes-tower-axum-2026/).
- **Limite de tamanho de corpo**, para não aceitar payloads absurdos na rota pública.
- **TLS obrigatório.** Token em HTTP puro é token exposto; veja [rustls em produção](/blog/rustls-tls-openssl-producao-2026/).

Para documentar as rotas protegidas com o esquema `bearerAuth`, o [guia de utoipa e Swagger UI](/blog/utoipa-axum-openapi-swagger-ui-rust-2026/) mostra a anotação necessária.

## Testando a autenticação

Testes de autenticação são baratos e pegam regressões graves:

```rust
#[tokio::test]
async fn rejeita_requisicao_sem_token() {
    let app = app_de_teste().await;

    let resposta = app
        .oneshot(Request::builder().uri("/perfil").body(Body::empty()).unwrap())
        .await
        .unwrap();

    assert_eq!(resposta.status(), StatusCode::UNAUTHORIZED);
}
```

Cubra pelo menos: sem header; header sem `Bearer`; token com assinatura de outra chave; token expirado; papel insuficiente em rota de admin; login com senha errada; refresh token já usado. Se o `Router` for construído por uma função reutilizável, esses testes rodam sem subir servidor nem infraestrutura — o padrão descrito no [guia de testes em Rust](/blog/testes-rust-estrategias-boas-praticas-2026/).

## Quando não usar JWT

JWT não é obrigatório. Para um monólito com sessões de navegador, uma **sessão em cookie com estado no servidor** é mais simples, revogável por natureza e evita todo o aparato de refresh. JWT brilha quando há vários serviços validando tokens sem consultar um banco central, ou clientes que não são navegadores.

Se a sua aplicação tem um único backend e um único frontend, avalie honestamente se a complexidade extra do par access/refresh está pagando por si mesma. E se o requisito envolve login social, SSO corporativo ou MFA, considere um provedor de identidade dedicado em vez de reimplementar OAuth2 e OIDC.

## Checklist de autenticação antes de produção

- [ ] segredo JWT com 32+ bytes aleatórios, vindo do ambiente e fora do Git;
- [ ] algoritmo fixado na `Validation`;
- [ ] `exp` sempre presente e curto no access token;
- [ ] senhas com Argon2 ou bcrypt, jamais SHA puro;
- [ ] hash de senha em `spawn_blocking`;
- [ ] respostas idênticas para e-mail inexistente e senha errada;
- [ ] rate limiting em `/auth/login` e `/auth/registrar`;
- [ ] refresh token opaco, com hash no banco e rotação;
- [ ] logout revoga o refresh token;
- [ ] cookies `HttpOnly`, `Secure`, `SameSite` quando o cliente é navegador;
- [ ] CORS com origens explícitas;
- [ ] TLS em todo ambiente que não seja `localhost`;
- [ ] tokens e senhas nunca aparecem em logs;
- [ ] rotas protegidas declaram o extractor na assinatura;
- [ ] testes para token ausente, inválido, expirado e papel insuficiente;
- [ ] plano de rotação da chave de assinatura.

## Perguntas frequentes sobre JWT em Rust

### Qual crate usar para JWT em Rust?

`jsonwebtoken` é a escolha padrão: cobre HS256 e RS256, integra com `serde` e expõe a validação de forma explícita. Para hash de senha, use `argon2` ou `bcrypt` — são problemas distintos e não devem compartilhar a mesma biblioteca nem a mesma chave.

### Como validar o token em cada requisição no Axum?

Com um extractor `FromRequestParts` que lê `Authorization`, remove o prefixo `Bearer `, decodifica com a chave e as validações configuradas e devolve as claims. Handlers protegidos recebem esse tipo como argumento, e o Axum rejeita a requisição antes do corpo do handler executar.

### JWT no localStorage ou em cookie?

Prefira cookie `HttpOnly`, `Secure` e `SameSite` em aplicações web: um XSS não consegue ler o valor. `localStorage` é acessível a qualquer script da página. Para clientes nativos e chamadas servidor a servidor, o header `Authorization` continua sendo a opção adequada.

### Como fazer logout com JWT?

Um JWT assinado permanece válido até `exp`, então logout exige estado: mantenha access tokens curtos e revogue o refresh token persistido no banco. Para invalidar um access token antes do vencimento, mantenha uma lista de `jti` revogados em Redis, consultada no extractor.

### HS256 ou RS256?

HS256 usa segredo compartilhado e basta quando o mesmo serviço emite e valida. RS256 usa par de chaves e é preferível quando vários serviços validam tokens de um emissor único, pois só ele guarda a chave privada. Em qualquer caso, fixe o algoritmo esperado na validação.

### Preciso de banco de dados para usar JWT?

Para validar o access token, não — a assinatura basta. Mas você precisará de banco para as credenciais, para os refresh tokens e para qualquer revogação. Na prática, uma API real com JWT tem estado; o que o JWT evita é uma consulta ao banco a cada requisição autenticada.

## Conclusão

Autenticação JWT em Rust com Axum é menos sobre a crate e mais sobre o desenho. As decisões que sustentam o sistema são: Argon2 fora do runtime assíncrono, algoritmo fixado na validação, access token curto, refresh token com estado e rotação, erros genéricos na resposta e — o ponto onde o Rust realmente ajuda — **a proteção expressa como tipo na assinatura do handler**.

Esse último detalhe é o que diferencia a abordagem. Em muitas linguagens, esquecer o middleware em uma rota é um bug silencioso descoberto em produção. Com um extractor tipado, a rota protegida declara sua exigência e o compilador cobra. Aproveite isso: modele `UsuarioAutenticado`, `Admin` e outros papéis como tipos distintos em vez de checar strings dentro dos handlers.

Para continuar, monte a API completa seguindo o [tutorial de API REST com Axum](/tutoriais/api-rest-axum/), publique o resultado com o [guia de deploy no Shuttle](/blog/shuttle-deploy-rust-axum-postgresql-2026/) e use o projeto no portfólio — autenticação bem feita é um dos itens mais avaliados nas [vagas Rust](/vagas/) e na [trilha de backend web](/carreira/nicho-web-backend/).
