Pular para o conteúdo principal

Proteja sua API Actix Web com RBAC (controle de acesso baseado em papel) e validação de JWT

Este guia irá ajudá-lo a implementar autorização para proteger suas APIs Actix Web usando controle de acesso baseado em papel (RBAC) e JSON Web Tokens (JWTs) emitidos pelo Logto.

Antes de começar​

Seus aplicativos cliente precisam obter tokens de acesso (Access tokens) do Logto. Se você ainda não configurou a integração do cliente, confira nossos Guias rápidos para React, Vue, Angular ou outros frameworks de cliente, ou veja nosso Guia máquina para máquina para acesso servidor a servidor.

Este guia foca na validação no lado do servidor desses tokens em seu aplicativo Actix Web.

Uma figura mostrando o foco deste guia

O que você vai aprender​

  • Validação de JWT: Aprenda a validar tokens de acesso (Access tokens) e extrair informações de autenticação (Authentication)
  • Implementação de middleware: Crie middleware reutilizável para proteção de API
  • Modelos de permissão: Entenda e implemente diferentes padrões de autorização (Authorization):
    • Recursos de API globais para endpoints de aplicação
    • Permissões de organização para controle de funcionalidades específicas do locatário
    • Recursos de API em nível de organização para acesso a dados multi-inquilino
  • Integração com RBAC: Implemente permissões e escopos baseados em papel (Role-based access control (RBAC)) em seus endpoints de API

Pré-requisitos​

  • Última versão estável do Rust instalada
  • Compreensão básica de Actix Web e desenvolvimento de API web
  • Um aplicativo Logto configurado (veja Guias rápidos se necessário)

Visão geral dos modelos de permissão​

Antes de implementar a proteção, escolha o modelo de permissão que se encaixa na arquitetura do seu aplicativo. Isso está alinhado com os três principais cenários de autorização do Logto:

RBAC de recursos globais de API
  • Caso de uso: Proteger recursos de API compartilhados em todo o seu aplicativo (não específicos de organização)
  • Tipo de token: Token de acesso (Access token) com público global (global audience)
  • Exemplos: APIs públicas, serviços principais do produto, endpoints de administração
  • Melhor para: Produtos SaaS com APIs usadas por todos os clientes, microsserviços sem isolamento de locatário
  • Saiba mais: Proteger recursos globais de API

💡 Escolha seu modelo antes de prosseguir – a implementação fará referência à abordagem escolhida ao longo deste guia.

Passos rápidos de preparação​

Configure recursos e permissões do Logto​

  1. Criar recurso de API: Vá para Console → Recursos de API e registre sua API (ex: https://api.seuapp.com)
  2. Definir permissões: Adicione escopos como read:products, write:orders – veja Definir recursos de API com permissões
  3. Criar papéis globais: Vá para Console → Papéis e crie papéis que incluam as permissões da sua API – veja Configurar papéis globais
  4. Atribuir papéis: Atribua papéis a usuários ou aplicativos M2M que precisam de acesso à API
Novo no RBAC?:

Comece com nosso guia de controle de acesso baseado em papel para instruções passo a passo de configuração.

Atualize seu aplicativo cliente​

Solicite os escopos apropriados em seu cliente:

O processo geralmente envolve atualizar a configuração do seu cliente para incluir um ou mais dos seguintes:

  • Parâmetro scope nos fluxos OAuth
  • Parâmetro resource para acesso a recursos de API
  • organization_id para contexto de organização
Antes de codificar:

Certifique-se de que o usuário ou app M2M que você está testando recebeu os papéis ou papéis de organização adequados que incluam as permissões necessárias para sua API.

Inicialize seu projeto de API​

Para inicializar um novo projeto Actix Web, crie um diretório e configure a estrutura básica:

cargo new seu-nome-da-api
cd seu-nome-da-api

Adicione as dependências do Actix Web ao seu Cargo.toml:

Cargo.toml
[dependencies]
actix-web = "4.0"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Crie uma aplicação Actix Web básica:

src/main.rs
use actix_web::{web, App, HttpServer, Result};
use serde_json::{json, Value};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(hello_handler))
})
.bind("127.0.0.1:8080")?
.run()
.await
}

async fn hello_handler() -> Result<web::Json<Value>> {
Ok(web::Json(json!({ "message": "Hello from Actix Web" })))
}

Inicie o servidor de desenvolvimento:

cargo run
nota:

Consulte a documentação do Actix Web para mais detalhes sobre como configurar rotas, middlewares e outros recursos.

Inicialize constantes e utilitários​

Defina as constantes e utilitários necessários em seu código para lidar com a extração e validação do token. Uma solicitação válida deve incluir um cabeçalho Authorization no formato Bearer <token de acesso (access token)>.

lib.rs
use serde::{Deserialize, Serialize};
use std::fmt;

pub const JWKS_URI: &str = "https://your-tenant.logto.app/oidc/jwks";
pub const ISSUER: &str = "https://your-tenant.logto.app/oidc";

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthInfo {
pub sub: String,
pub client_id: Option<String>,
pub organization_id: Option<String>,
pub scopes: Vec<String>,
pub audience: Vec<String>,
}

impl AuthInfo {
pub fn new(
sub: String,
client_id: Option<String>,
organization_id: Option<String>,
scopes: Vec<String>,
audience: Vec<String>,
) -> Self {
Self {
sub,
client_id,
organization_id,
scopes,
audience,
}
}
}

#[derive(Debug)]
pub struct AuthorizationError {
pub message: String,
pub status_code: u16,
}

impl AuthorizationError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
status_code: 403,
}
}

pub fn with_status(message: impl Into<String>, status_code: u16) -> Self {
Self {
message: message.into(),
status_code,
}
}
}

impl fmt::Display for AuthorizationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}

impl std::error::Error for AuthorizationError {}

pub fn extract_bearer_token(authorization: Option<&str>) -> Result<&str, AuthorizationError> {
let auth_header = authorization.ok_or_else(|| {
AuthorizationError::with_status("Cabeçalho de autorização está ausente (Authorization header is missing)", 401)
})?;

if !auth_header.starts_with("Bearer ") {
return Err(AuthorizationError::with_status(
"O cabeçalho de autorização deve começar com \"Bearer \" (Authorization header must start with \"Bearer \")",
401,
));
}

Ok(&auth_header[7..]) // Remove o prefixo 'Bearer ' (Remove 'Bearer ' prefix)
}

Recupere informações sobre seu tenant Logto​

Você precisará dos seguintes valores para validar tokens emitidos pelo Logto:

  • URI do JSON Web Key Set (JWKS): A URL para as chaves públicas do Logto, usada para verificar assinaturas de JWT.
  • Emissor (Issuer): O valor esperado do emissor (URL OIDC do Logto).

Primeiro, encontre o endpoint do seu tenant Logto. Você pode encontrá-lo em vários lugares:

  • No Logto Console, em Configurações → Domínios.
  • Em qualquer configuração de aplicativo onde você configurou no Logto, Configurações → Endpoints & Credenciais.

Buscar no endpoint de descoberta do OpenID Connect​

Esses valores podem ser obtidos no endpoint de descoberta do OpenID Connect do Logto:

https://<seu-endpoint-logto>/oidc/.well-known/openid-configuration

Aqui está um exemplo de resposta (outros campos omitidos para brevidade):

{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}

Como o Logto não permite personalizar o URI do JWKS ou o emissor, você pode definir esses valores manualmente no seu código. No entanto, isso não é recomendado para aplicações em produção, pois pode aumentar a sobrecarga de manutenção caso alguma configuração mude no futuro.

  • URI do JWKS: https://<seu-endpoint-logto>/oidc/jwks
  • Emissor: https://<seu-endpoint-logto>/oidc

Valide o token e as permissões​

Após extrair o token e buscar a configuração OIDC, valide o seguinte:

  • Assinatura: O JWT deve ser válido e assinado pelo Logto (via JWKS).
  • Emissor (Issuer): Deve corresponder ao emissor do seu tenant Logto.
  • Público (Audience): Deve corresponder ao indicador de recurso da API registrado no Logto, ou ao contexto da organização se aplicável.
  • Expiração: O token não pode estar expirado.
  • Permissões (escopos) (Permissions (scopes)): O token deve incluir os escopos necessários para sua API / ação. Os escopos são strings separadas por espaço na reivindicação scope.
  • Contexto da organização: Se estiver protegendo recursos de API em nível de organização, valide a reivindicação organization_id.

Veja JSON Web Token para saber mais sobre a estrutura e reivindicações do JWT.

O que verificar para cada modelo de permissão​

As reivindicações e regras de validação diferem conforme o modelo de permissão:

  • Reivindicação de público (aud): Indicador de recurso de API
  • Reivindicação de organização (organization_id): Não presente
  • Escopos (permissões) a verificar (scope): Permissões do recurso de API

Para permissões de organização que não são de API, o contexto da organização é representado pela reivindicação aud (por exemplo, urn:logto:organization:abc123). A reivindicação organization_id só está presente para tokens de recursos de API em nível de organização.

dica:

Sempre valide tanto as permissões (escopos) quanto o contexto (público, organização) para APIs multi-tenant seguras.

Adicione a lógica de validação​

Usamos jsonwebtoken para validar JWTs. Adicione as dependências necessárias ao seu Cargo.toml:

Cargo.toml
[dependencies]
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }

Primeiro, adicione estas utilidades compartilhadas para lidar com a validação de JWT:

jwt_validator.rs
use crate::{AuthInfo, AuthorizationError, ISSUER, JWKS_URI};
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use serde_json::Value;
use std::collections::HashMap;

pub struct JwtValidator {
jwks: HashMap<String, DecodingKey>,
}

impl JwtValidator {
pub async fn new() -> Result<Self, AuthorizationError> {
let jwks = Self::fetch_jwks().await?;
Ok(Self { jwks })
}

async fn fetch_jwks() -> Result<HashMap<String, DecodingKey>, AuthorizationError> {
let response = reqwest::get(JWKS_URI).await.map_err(|e| {
AuthorizationError::with_status(format!("Falha ao buscar JWKS: {}", e), 401)
})?;

let jwks: Value = response.json().await.map_err(|e| {
AuthorizationError::with_status(format!("Falha ao analisar JWKS: {}", e), 401)
})?;

let mut keys = HashMap::new();

if let Some(keys_array) = jwks["keys"].as_array() {
for key in keys_array {
if let (Some(kid), Some(kty), Some(n), Some(e)) = (
key["kid"].as_str(),
key["kty"].as_str(),
key["n"].as_str(),
key["e"].as_str(),
) {
if kty == "RSA" {
if let Ok(decoding_key) = DecodingKey::from_rsa_components(n, e) {
keys.insert(kid.to_string(), decoding_key);
}
}
}
}
}

if keys.is_empty() {
return Err(AuthorizationError::with_status("Nenhuma chave válida encontrada no JWKS", 401));
}

Ok(keys)
}

pub fn validate_jwt(&self, token: &str) -> Result<AuthInfo, AuthorizationError> {
let header = decode_header(token).map_err(|e| {
AuthorizationError::with_status(format!("Cabeçalho do token inválido: {}", e), 401)
})?;

let kid = header.kid.ok_or_else(|| {
AuthorizationError::with_status("Token sem reivindicação kid", 401)
})?;

let key = self.jwks.get(&kid).ok_or_else(|| {
AuthorizationError::with_status("ID de chave desconhecido", 401)
})?;

let mut validation = Validation::new(Algorithm::RS256);
validation.set_issuer(&[ISSUER]);
validation.validate_aud = false; // Vamos verificar o público manualmente

let token_data = decode::<Value>(token, key, &validation).map_err(|e| {
AuthorizationError::with_status(format!("Token inválido: {}", e), 401)
})?;

let claims = token_data.claims;
self.verify_payload(&claims)?;

Ok(self.create_auth_info(claims))
}

fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Implemente sua lógica de verificação aqui com base no modelo de permissão
// Isso será mostrado na seção de modelos de permissão abaixo
Ok(())
}

fn create_auth_info(&self, claims: Value) -> AuthInfo {
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').map(|s| s.to_string()).collect())
.unwrap_or_default();

let audience = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect(),
Value::String(s) => vec![s.clone()],
_ => vec![],
};

AuthInfo::new(
claims["sub"].as_str().unwrap_or_default().to_string(),
claims["client_id"].as_str().map(|s| s.to_string()),
claims["organization_id"].as_str().map(|s| s.to_string()),
scopes,
audience,
)
}
}

Em seguida, implemente o middleware para verificar o token de acesso:

middleware.rs
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
web, Error, HttpMessage, HttpResponse,
};
use futures::future::{ok, Ready};
use std::sync::Arc;

pub struct JwtMiddleware {
validator: Arc<JwtValidator>,
}

impl JwtMiddleware {
pub fn new(validator: Arc<JwtValidator>) -> Self {
Self { validator }
}
}

impl<S, B> Transform<S, ServiceRequest> for JwtMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = JwtMiddlewareService<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;

fn new_transform(&self, service: S) -> Self::Future {
ok(JwtMiddlewareService {
service,
validator: self.validator.clone(),
})
}
}

pub struct JwtMiddlewareService<S> {
service: S,
validator: Arc<JwtValidator>,
}

impl<S, B> Service<ServiceRequest> for JwtMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = futures::future::LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

forward_ready!(service);

fn call(&self, req: ServiceRequest) -> Self::Future {
let validator = self.validator.clone();

Box::pin(async move {
let authorization = req
.headers()
.get("authorization")
.and_then(|h| h.to_str().ok());

match extract_bearer_token(authorization)
.and_then(|token| validator.validate_jwt(token))
{
Ok(auth_info) => {
// Armazena as informações de autenticação nas extensões da requisição para uso genérico
req.extensions_mut().insert(auth_info);
let fut = self.service.call(req);
fut.await
}
Err(e) => {
let response = HttpResponse::build(
actix_web::http::StatusCode::from_u16(e.status_code)
.unwrap_or(actix_web::http::StatusCode::FORBIDDEN),
)
.json(serde_json::json!({ "error": e.message }));

Ok(req.into_response(response))
}
}
})
}
}

De acordo com seu modelo de permissão, implemente a lógica de verificação apropriada em JwtValidator:

jwt_validator.rs
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Verifique se a reivindicação de público corresponde ao seu indicador de recurso de API
let audiences = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
Value::String(s) => vec![s.as_str()],
_ => vec![],
};

if !audiences.contains(&"https://your-api-resource-indicator") {
return Err(AuthorizationError::new("Público inválido"));
}

// Verifique os escopos necessários para recursos globais de API
let required_scopes = vec!["api:read", "api:write"]; // Substitua pelos escopos necessários
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').collect::<Vec<_>>())
.unwrap_or_default();

for required_scope in &required_scopes {
if !scopes.contains(required_scope) {
return Err(AuthorizationError::new("Escopo insuficiente"));
}
}

Ok(())
}

Aplique o middleware à sua API​

Agora, aplique o middleware às suas rotas de API protegidas.

main.rs
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Result};
use serde_json::{json, Value};
use std::sync::Arc;

mod lib;
mod jwt_validator;
mod middleware as jwt_middleware;

use lib::AuthInfo;
use jwt_validator::JwtValidator;
use jwt_middleware::JwtMiddleware;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
let validator = Arc::new(JwtValidator::new().await.expect("Falha ao inicializar o validador de JWT"));

HttpServer::new(move || {
App::new()
.app_data(web::Data::new(validator.clone()))
.wrap(Logger::default())
.service(
web::scope("/api/protected")
.wrap(JwtMiddleware::new(validator.clone()))
.route("", web::get().to(protected_handler))
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}

async fn protected_handler(req: HttpRequest) -> Result<web::Json<Value>> {
// Acessa as informações de autenticação das extensões da requisição
let auth = req.extensions().get::<AuthInfo>().unwrap();
Ok(web::Json(json!({ "auth": auth })))
}

Teste sua API protegida​

Obter tokens de acesso (Access tokens)​

Do seu aplicativo cliente: Se você configurou uma integração de cliente, seu aplicativo pode obter tokens automaticamente. Extraia o token de acesso e use-o nas requisições de API.

Para testes com curl / Postman:

  1. Tokens de usuário: Use as ferramentas de desenvolvedor do seu aplicativo cliente para copiar o token de acesso do localStorage ou da aba de rede.

  2. Tokens máquina para máquina: Use o fluxo de credenciais do cliente. Aqui está um exemplo não normativo usando curl:

    curl -X POST https://your-tenant.logto.app/oidc/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=client_credentials" \
    -d "client_id=your-m2m-client-id" \
    -d "client_secret=your-m2m-client-secret" \
    -d "resource=https://your-api-resource-indicator" \
    -d "scope=api:read api:write"

    Pode ser necessário ajustar os parâmetros resource e scope de acordo com seu recurso de API e permissões; um parâmetro organization_id também pode ser exigido se sua API for voltada para organização.

dica:

Precisa inspecionar o conteúdo do token? Use nosso decodificador de JWT para decodificar e verificar seus JWTs.

Testar endpoints protegidos​

Requisição com token válido
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected

Resposta esperada:

{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
Token ausente
curl http://localhost:3000/api/protected

Resposta esperada (401):

{
"error": "Authorization header is missing"
}
Token inválido
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected

Resposta esperada (401):

{
"error": "Invalid token"
}

Testes específicos do modelo de permissão​

Cenários de teste para APIs protegidas com escopos globais:

  • Escopos válidos: Teste com tokens que incluam os escopos de API necessários (por exemplo, api:read, api:write)
  • Escopos ausentes: Espere 403 Proibido quando o token não tiver os escopos necessários
  • Público errado: Espere 403 Proibido quando o público não corresponder ao recurso de API
# Token sem escopos necessários - espera-se 403
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected

Leitura adicional​

RBAC na prática: Implementando autorização segura para seu aplicativo

Construa um aplicativo SaaS multi-inquilino: Um guia completo do design à implementação