Custom Authenticators
Create custom authenticators for database-backed user stores, OAuth, JWT, and more.
Implementing the Authenticator Trait
use actix_security::http::security::config::Authenticator;
use actix_security::http::security::User;
use actix_web::dev::ServiceRequest;
#[derive(Clone)]
pub struct DatabaseAuthenticator {
pool: sqlx::PgPool, // Your database connection pool
}
impl Authenticator for DatabaseAuthenticator {
fn authenticate(&self, req: &ServiceRequest) -> Option<User> {
// 1. Extract credentials from request
let auth_header = req.headers().get("Authorization")?;
let (username, password) = parse_basic_auth(auth_header)?;
// 2. Look up user in database
// Note: This is sync, consider using block_on or async authenticator
let user_record = self.find_user(&username)?;
// 3. Verify password
if !self.verify_password(&password, &user_record.password_hash) {
return None;
}
// 4. Build and return User
Some(User {
username: user_record.username,
password: user_record.password_hash,
roles: user_record.roles.into_iter().collect(),
authorities: user_record.authorities.into_iter().collect(),
})
}
}
Example: JWT Authentication
use actix_security::http::security::config::Authenticator;
use actix_security::http::security::User;
use actix_web::dev::ServiceRequest;
use jsonwebtoken::{decode, DecodingKey, Validation};
#[derive(Clone)]
pub struct JwtAuthenticator {
secret: String,
}
#[derive(Debug, Deserialize)]
struct Claims {
sub: String, // username
roles: Vec<String>,
authorities: Vec<String>,
exp: usize,
}
impl Authenticator for JwtAuthenticator {
fn authenticate(&self, req: &ServiceRequest) -> Option<User> {
// Extract Bearer token
let auth_header = req.headers().get("Authorization")?.to_str().ok()?;
let token = auth_header.strip_prefix("Bearer ")?;
// Decode and validate JWT
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(self.secret.as_bytes()),
&Validation::default(),
).ok()?;
let claims = token_data.claims;
// Build User from claims
Some(User {
username: claims.sub,
password: String::new(), // Not needed for JWT
roles: claims.roles.into_iter().collect(),
authorities: claims.authorities.into_iter().collect(),
})
}
}
Example: API Key Authentication
use actix_security::http::security::config::Authenticator;
use actix_security::http::security::User;
use actix_web::dev::ServiceRequest;
use std::collections::HashMap;
#[derive(Clone)]
pub struct ApiKeyAuthenticator {
api_keys: HashMap<String, User>, // API key -> User
}
impl ApiKeyAuthenticator {
pub fn new() -> Self {
let mut api_keys = HashMap::new();
// Register API keys
api_keys.insert(
"sk_live_abc123".to_string(),
User::new("service_a".to_string(), String::new())
.roles(&["SERVICE".into()])
.authorities(&["api:read".into(), "api:write".into()]),
);
Self { api_keys }
}
}
impl Authenticator for ApiKeyAuthenticator {
fn authenticate(&self, req: &ServiceRequest) -> Option<User> {
// Check X-API-Key header
let api_key = req.headers()
.get("X-API-Key")?
.to_str()
.ok()?;
self.api_keys.get(api_key).cloned()
}
}
Example: Session-Based Authentication
use actix_security::http::security::config::Authenticator;
use actix_security::http::security::User;
use actix_session::SessionExt;
use actix_web::dev::ServiceRequest;
#[derive(Clone)]
pub struct SessionAuthenticator {
user_service: UserService, // Your user service
}
impl Authenticator for SessionAuthenticator {
fn authenticate(&self, req: &ServiceRequest) -> Option<User> {
// Get session
let session = req.get_session();
// Get user ID from session
let user_id: i64 = session.get("user_id").ok()??;
// Load user from database
self.user_service.find_by_id(user_id)
}
}
Combining Multiple Authenticators
#[derive(Clone)]
pub struct CompositeAuthenticator {
authenticators: Vec<Box<dyn Authenticator>>,
}
impl Authenticator for CompositeAuthenticator {
fn authenticate(&self, req: &ServiceRequest) -> Option<User> {
// Try each authenticator in order
for auth in &self.authenticators {
if let Some(user) = auth.authenticate(req) {
return Some(user);
}
}
None
}
}
// Usage
let authenticator = CompositeAuthenticator {
authenticators: vec![
Box::new(JwtAuthenticator::new()),
Box::new(ApiKeyAuthenticator::new()),
Box::new(BasicAuthenticator::new()),
],
};
Using with SecurityTransform
use actix_security::http::security::middleware::SecurityTransform;
let jwt_auth = JwtAuthenticator {
secret: "your-secret-key".to_string(),
};
App::new()
.wrap(
SecurityTransform::new()
.config_authenticator(move || jwt_auth.clone())
.config_authorizer(|| {
AuthorizationManager::request_matcher()
// No http_basic() needed for JWT
})
)
Best Practices
1. Handle Errors Gracefully
fn authenticate(&self, req: &ServiceRequest) -> Option<User> {
// Return None on any error - don't panic
let header = req.headers().get("Authorization")?;
let header_str = header.to_str().ok()?; // Use ok()? for Result
// ...
}
2. Use Constant-Time Comparison
use subtle::ConstantTimeEq;
fn verify_api_key(provided: &str, expected: &str) -> bool {
provided.as_bytes().ct_eq(expected.as_bytes()).into()
}
3. Log Security Events
fn authenticate(&self, req: &ServiceRequest) -> Option<User> {
let result = self.do_authenticate(req);
match &result {
Some(user) => log::info!("User {} authenticated", user.username),
None => log::warn!("Authentication failed for request to {}", req.path()),
}
result
}
4. Rate Limit Authentication
Consider rate limiting authentication attempts to prevent brute force attacks.
Spring Security Comparison
Spring Security:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// Your authentication logic
return new UsernamePasswordAuthenticationToken(
username, password, authorities);
}
}
Actix Security:
impl Authenticator for CustomAuthenticator {
fn authenticate(&self, req: &ServiceRequest) -> Option<User> {
// Your authentication logic
Some(User { /* ... */ })
}
}