Guards and Interceptors
Armature provides a powerful middleware system through Guards and Interceptors, inspired by Angular and NestJS.
Table of Contents
Overview
Guards
Guards are used to protect routes by determining whether a request should be allowed to proceed. They run before the route handler and can:
- Authenticate users
- Check permissions/roles
- Validate API keys
- Implement custom authorization logic
Guards return Result<bool, Error>:
Ok(true)- Allow request to proceedOk(false)- Block request (should return error instead)Err(e)- Block request with specific error
Interceptors
Interceptors are used to transform requests and responses. They can:
- Log requests and responses
- Modify response data
- Cache responses
- Add custom headers
- Measure performance
- Handle errors
Interceptors wrap the handler execution and can modify both input and output.
Guards
Built-in Guards
AuthenticationGuard
Checks for a valid Bearer token in the Authorization header.
use armature_framework::{Guard, AuthenticationGuard, GuardContext};
let guard = AuthenticationGuard;
let context = GuardContext::new(request);
match guard.can_activate(&context).await {
Ok(true) => println!("Authenticated!"),
Ok(false) => println!("Not authenticated"),
Err(e) => println!("Auth error: {}", e),
}
Usage:
# Valid request
curl http://localhost:3000/api/protected \
-H "Authorization: Bearer your-token-here"
# Invalid request (will fail)
curl http://localhost:3000/api/protected
RolesGuard
Checks if the authenticated user has specific roles.
use armature_framework::{Guard, RolesGuard, GuardContext};
let guard = RolesGuard::new(vec!["admin".to_string(), "moderator".to_string()]);
let context = GuardContext::new(request);
match guard.can_activate(&context).await {
Ok(true) => println!("Has required role!"),
Ok(false) | Err(_) => println!("Insufficient permissions"),
}
Guards fail closed.
RolesGuard(andarmature-auth'sRoleGuard/PermissionGuard) require verified role/permission information on the request โ a bearer token alone is not sufficient. That information is supplied by an authentication layer that has already validated the token. Shiparmature_auth::JwtAuthMiddleware(below) ahead of the guarded route: it verifies the JWT signature and populates theRequestRoles/UserContextextension the guard reads. Without such a layer, role-protected routes reject every request.Guards declared by a module apply only to that module's controllers' routes, not globally.
JwtAuthMiddleware (armature-auth)
Verifies a JWT and attaches the caller's identity so the role/permission guards can enforce.
use armature_auth::JwtAuthMiddleware;
// Required mode: a missing/invalid/expired token is rejected with 401 before the handler.
let auth = JwtAuthMiddleware::new(jwt_manager);
// Optional mode: attaches identity when a valid token is present, otherwise passes through
// so a downstream guard makes the authorization decision.
let auth = JwtAuthMiddleware::optional(jwt_manager);
On a valid token it inserts a UserContext (from the sub/roles/permissions claims,
configurable) and an armature_core::RequestRoles extension, which RolesGuard,
RoleGuard, and PermissionGuard consume.
ApiKeyGuard
Validates API keys from the x-api-key header.
use armature_framework::{Guard, ApiKeyGuard, GuardContext};
let valid_keys = vec![
"key-123".to_string(),
"key-456".to_string(),
];
let guard = ApiKeyGuard::new(valid_keys);
let context = GuardContext::new(request);
match guard.can_activate(&context).await {
Ok(true) => println!("Valid API key"),
Ok(false) | Err(_) => println!("Invalid API key"),
}
Usage:
curl http://localhost:3000/api/data \
-H "x-api-key: key-123"
Custom Guards
Create custom guards by implementing the Guard trait:
use armature_framework::{Guard, GuardContext, Error};
use async_trait::async_trait;
pub struct IpWhitelistGuard {
allowed_ips: Vec<String>,
}
impl IpWhitelistGuard {
pub fn new(ips: Vec<String>) -> Self {
Self { allowed_ips: ips }
}
}
#[async_trait]
impl Guard for IpWhitelistGuard {
async fn can_activate(&self, context: &GuardContext) -> Result<bool, Error> {
// Get client IP from headers or connection
let client_ip = context
.get_header("x-forwarded-for")
.or_else(|| context.get_header("x-real-ip"))
.ok_or_else(|| Error::Forbidden("Cannot determine client IP".to_string()))?;
if self.allowed_ips.contains(client_ip) {
Ok(true)
} else {
Err(Error::Forbidden(format!("IP {} not whitelisted", client_ip)))
}
}
}
Function-based Custom Guard
For simpler cases, use CustomGuard:
use armature_framework::{CustomGuard, GuardContext, Error};
let guard = CustomGuard::new(|context: &GuardContext| {
// Only allow requests on weekdays
let weekday = chrono::Local::now().weekday();
if weekday == chrono::Weekday::Sat || weekday == chrono::Weekday::Sun {
Err(Error::Forbidden("Service only available on weekdays".to_string()))
} else {
Ok(true)
}
});
Guard Usage Examples
Protecting a Single Route
use armature_framework::prelude::*;
use armature_framework::{Guard, AuthenticationGuard, GuardContext};
router.add_route(Route {
method: HttpMethod::GET,
path: "/api/protected".to_string(),
handler: Arc::new(move |req| {
Box::pin(async move {
// Apply guard
let guard = AuthenticationGuard;
let context = GuardContext::new(req.clone());
match guard.can_activate(&context).await {
Ok(true) => {},
Ok(false) | Err(e) => return Err(e),
}
// Handler logic
Ok(HttpResponse::ok().json(json!({ "data": "protected data" }))?)
})
}),
});
Chaining Multiple Guards
use armature_framework::{Guard, AuthenticationGuard, RolesGuard, GuardContext};
router.add_route(Route {
method: HttpMethod::DELETE,
path: "/api/admin/users/:id".to_string(),
handler: Arc::new(move |req| {
Box::pin(async move {
let context = GuardContext::new(req.clone());
// Check authentication
let auth_guard = AuthenticationGuard;
match auth_guard.can_activate(&context).await {
Ok(true) => {},
Ok(false) | Err(e) => return Err(e),
}
// Check role
let role_guard = RolesGuard::new(vec!["admin".to_string()]);
match role_guard.can_activate(&context).await {
Ok(true) => {},
Ok(false) | Err(e) => return Err(e),
}
// Handler logic - only reached if both guards pass
let user_id = req.path_params.get("id").unwrap();
Ok(HttpResponse::ok().json(json!({ "deleted": user_id }))?)
})
}),
});
Interceptors
Built-in Interceptors
LoggingInterceptor
Logs all incoming requests and outgoing responses with timing.
use armature_framework::{Interceptor, LoggingInterceptor, ExecutionContext};
let interceptor = LoggingInterceptor;
// Output:
// โ GET /api/users
// โ GET /api/users - 200 (12.3ms)
TransformInterceptor
Transforms responses using a custom function.
use armature_framework::{Interceptor, TransformInterceptor, HttpResponse};
let interceptor = TransformInterceptor::new(|mut response| {
// Add custom header to all responses
response.headers.insert(
"X-Powered-By".to_string(),
"Armature".to_string(),
);
response
});
CacheInterceptor
Caches responses for a specified TTL (Time To Live).
use armature_framework::{Interceptor, CacheInterceptor};
let interceptor = CacheInterceptor::new(60); // Cache for 60 seconds
Custom Interceptors
Create custom interceptors by implementing the Interceptor trait:
use armature_framework::{Interceptor, ExecutionContext, HttpResponse, Error};
use async_trait::async_trait;
use std::future::Future;
use std::pin::Pin;
pub struct CompressionInterceptor {
min_size: usize,
}
impl CompressionInterceptor {
pub fn new(min_size: usize) -> Self {
Self { min_size }
}
}
#[async_trait]
impl Interceptor for CompressionInterceptor {
async fn intercept(
&self,
context: ExecutionContext,
next: Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>>,
) -> Result<HttpResponse, Error> {
let mut response = next.await?;
// Compress if response is large enough
if response.body.len() > self.min_size {
// Compress the body (pseudo-code)
// response.body = compress(response.body);
response.headers.insert(
"Content-Encoding".to_string(),
"gzip".to_string(),
);
}
Ok(response)
}
}
Metrics Interceptor Example
use armature_framework::{Interceptor, ExecutionContext, HttpResponse, Error};
use async_trait::async_trait;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
pub struct MetricsInterceptor {
request_count: Arc<AtomicU64>,
total_duration_ms: Arc<AtomicU64>,
}
impl MetricsInterceptor {
pub fn new() -> Self {
Self {
request_count: Arc::new(AtomicU64::new(0)),
total_duration_ms: Arc::new(AtomicU64::new(0)),
}
}
pub fn get_metrics(&self) -> (u64, u64) {
(
self.request_count.load(Ordering::Relaxed),
self.total_duration_ms.load(Ordering::Relaxed),
)
}
}
#[async_trait]
impl Interceptor for MetricsInterceptor {
async fn intercept(
&self,
context: ExecutionContext,
next: Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>>,
) -> Result<HttpResponse, Error> {
let start = std::time::Instant::now();
let result = next.await;
let duration = start.elapsed().as_millis() as u64;
self.request_count.fetch_add(1, Ordering::Relaxed);
self.total_duration_ms.fetch_add(duration, Ordering::Relaxed);
result
}
}
Interceptor Usage Examples
Applying Interceptors to Routes
Currently, interceptors need to be manually applied within route handlers. A more integrated approach (similar to NestJS's @UseInterceptors()) can be added to the macro system in the future.
use armature_framework::prelude::*;
router.add_route(Route {
method: HttpMethod::GET,
path: "/api/data".to_string(),
handler: Arc::new(move |req| {
Box::pin(async move {
// Simulate interceptor logging
let start = std::time::Instant::now();
println!("โ {} {}", req.method, req.path);
// Handler logic
let response = HttpResponse::ok().json(json!({ "data": "value" }))?;
// Log completion
let duration = start.elapsed();
println!("โ {} {} - {} ({:?})", req.method, req.path, response.status, duration);
Ok(response)
})
}),
});
Complete Example
Here's a complete example demonstrating guards and interceptors:
use armature_framework::prelude::*;
use armature_framework::{AuthenticationGuard, Guard, GuardContext, RolesGuard};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Serialize, Deserialize)]
struct ApiResponse {
message: String,
}
#[tokio::main]
async fn main() {
let mut router = Router::new();
// Public endpoint - no guards
router.add_route(Route {
method: HttpMethod::GET,
path: "/public".to_string(),
handler: Arc::new(move |req| {
Box::pin(async move {
println!("โ {} {}", req.method, req.path);
let response = HttpResponse::ok().json(ApiResponse {
message: "Public data".to_string(),
})?;
println!("โ {} {} - 200", req.method, req.path);
Ok(response)
})
}),
});
// Protected endpoint - authentication required
router.add_route(Route {
method: HttpMethod::GET,
path: "/protected".to_string(),
handler: Arc::new(move |req| {
Box::pin(async move {
let guard = AuthenticationGuard;
let context = GuardContext::new(req.clone());
match guard.can_activate(&context).await {
Ok(true) => {},
Ok(false) | Err(e) => return Err(e),
}
Ok(HttpResponse::ok().json(ApiResponse {
message: "Protected data".to_string(),
})?)
})
}),
});
// Admin endpoint - authentication + role required
router.add_route(Route {
method: HttpMethod::DELETE,
path: "/admin/delete".to_string(),
handler: Arc::new(move |req| {
Box::pin(async move {
let context = GuardContext::new(req.clone());
// Check authentication
let auth_guard = AuthenticationGuard;
match auth_guard.can_activate(&context).await {
Ok(true) => {},
Ok(false) | Err(e) => return Err(e),
}
// Check role
let role_guard = RolesGuard::new(vec!["admin".to_string()]);
match role_guard.can_activate(&context).await {
Ok(true) => {},
Ok(false) | Err(e) => return Err(e),
}
Ok(HttpResponse::ok().json(ApiResponse {
message: "Admin action completed".to_string(),
})?)
})
}),
});
let app = Application::new(Container::new(), router);
println!("Server running on http://localhost:3000");
app.listen(3000).await.unwrap();
}
Testing the Example
# 1. Public endpoint (no auth required)
curl http://localhost:3000/public
# โ 200 OK
# 2. Protected endpoint (no token - will fail)
curl http://localhost:3000/protected
# โ 403 Forbidden
# 3. Protected endpoint (with token)
curl http://localhost:3000/protected \
-H "Authorization: Bearer my-token"
# โ 200 OK
# 4. Admin endpoint (token but no role - will fail in real implementation)
curl -X DELETE http://localhost:3000/admin/delete \
-H "Authorization: Bearer user-token"
# โ 403 Forbidden
# 5. Admin endpoint (admin token)
curl -X DELETE http://localhost:3000/admin/delete \
-H "Authorization: Bearer admin-token"
# โ 200 OK
Best Practices
Guard Order: Apply guards in order of cost - fast checks first (API key) before expensive ones (database lookups)
Error Messages: Be specific but don't leak security information:
// Good Err(Error::Forbidden("Invalid credentials".to_string())) // Bad - leaks information Err(Error::Forbidden("User john@example.com not found".to_string()))Guard Composition: Create reusable guard combinations:
async fn apply_admin_guards(context: &GuardContext) -> Result<(), Error> { let auth = AuthenticationGuard; match auth.can_activate(context).await { Ok(true) => {}, _ => return Err(Error::Forbidden("Not authenticated".to_string())), } let roles = RolesGuard::new(vec!["admin".to_string()]); match roles.can_activate(context).await { Ok(true) => Ok(()), _ => Err(Error::Forbidden("Admin role required".to_string())), } }Interceptor Performance: Keep interceptors lightweight - avoid heavy computation or I/O
Logging: Use structured logging in interceptors:
println!( "request_id={} method={} path={} duration_ms={} status={}", request_id, method, path, duration, status );
Future Enhancements
The following features are planned for future releases:
Decorator Syntax: Apply guards via macros
#[controller("/api")] #[use_guards(AuthenticationGuard)] struct ApiController { #[get("/admin")] #[use_guards(RolesGuard::new(vec!["admin"]))] async fn admin_only(&self) -> Result<Json<Data>, Error> { // ... } }Global Guards: Apply guards to all routes
let app = Application::new(container, router) .use_global_guard(LoggingInterceptor) .use_global_guard(AuthenticationGuard);Interceptor Chaining: Multiple interceptors with defined order
#[use_interceptors(LoggingInterceptor, CacheInterceptor)]Exception Filters: Dedicated error handling interceptors
#[use_filters(HttpExceptionFilter)]
See Also
- Authentication Guide - JWT and OAuth2 integration
- Dependency Injection - Injecting guards and interceptors
- Middleware - Alternative middleware patterns