Security Context
Access the current authenticated user from anywhere in your application.
Overview
SecurityContext provides thread-safe access to the current user using Tokio's task-local storage. This is useful when you need to access user information outside of handlers.
Basic Usage
use actix_security::http::security::SecurityContext;
// Get the current user
if let Some(user) = SecurityContext::get_user() {
println!("Current user: {}", user.username);
}
// Check role
if SecurityContext::has_role("ADMIN") {
// Admin-specific logic
}
// Check authority
if SecurityContext::has_authority("posts:write") {
// Permission-specific logic
}
API Reference
get_user
Returns the current authenticated user, if any.
pub fn get_user() -> Option<User>
Example:
match SecurityContext::get_user() {
Some(user) => println!("Logged in as: {}", user.username),
None => println!("Not authenticated"),
}
has_role
Checks if the current user has a specific role.
pub fn has_role(role: &str) -> bool
Example:
if SecurityContext::has_role("ADMIN") {
// Show admin controls
}
has_authority
Checks if the current user has a specific authority.
pub fn has_authority(authority: &str) -> bool
Example:
if SecurityContext::has_authority("posts:delete") {
// Show delete button
}
is_authenticated
Checks if there is an authenticated user.
pub fn is_authenticated() -> bool
Example:
if SecurityContext::is_authenticated() {
// User is logged in
}
run_with
Executes code with a specific user context.
pub async fn run_with<F, R>(user: Option<User>, f: F) -> R
where
F: Future<Output = R>,
Example:
let user = User::new("test".to_string(), "".to_string())
.roles(&["USER".into()]);
let result = SecurityContext::run_with(Some(user), async {
// Code here has access to the user via SecurityContext
SecurityContext::get_user()
}).await;
Use Cases
Service Layer Authorization
pub struct PostService;
impl PostService {
pub async fn delete_post(&self, post_id: i64) -> Result<(), ServiceError> {
// Check authorization in service layer
let user = SecurityContext::get_user()
.ok_or(ServiceError::Unauthorized)?;
if !user.has_role("ADMIN") && !user.has_authority("posts:delete") {
return Err(ServiceError::Forbidden);
}
// Proceed with deletion
self.repository.delete(post_id).await
}
}
Audit Logging
pub fn log_action(action: &str, resource: &str) {
let username = SecurityContext::get_user()
.map(|u| u.username.clone())
.unwrap_or_else(|| "anonymous".to_string());
log::info!("AUDIT: {} performed {} on {}", username, action, resource);
}
// In handler
#[post("/posts")]
async fn create_post() -> impl Responder {
log_action("CREATE", "post");
// ...
}
Dynamic Query Filtering
pub async fn get_visible_posts(&self) -> Vec<Post> {
let user = SecurityContext::get_user();
match user {
Some(u) if u.has_role("ADMIN") => {
// Admins see all posts
self.repository.find_all().await
}
Some(u) => {
// Users see their own posts + published posts
self.repository.find_visible_for(&u.username).await
}
None => {
// Anonymous users see only published posts
self.repository.find_published().await
}
}
}
Conditional UI Elements (in templates)
pub struct TemplateContext {
pub can_edit: bool,
pub can_delete: bool,
pub is_admin: bool,
}
impl TemplateContext {
pub fn from_security_context() -> Self {
Self {
can_edit: SecurityContext::has_authority("posts:write"),
can_delete: SecurityContext::has_authority("posts:delete"),
is_admin: SecurityContext::has_role("ADMIN"),
}
}
}
How It Works
The security middleware sets up the context before handling each request:
// Simplified middleware flow
async fn call(&self, req: ServiceRequest) -> Result<ServiceResponse, Error> {
// 1. Authenticate user
let user = self.authenticator.authenticate(&req);
// 2. Run handler with security context
SecurityContext::run_with(user, async {
// 3. Your handler runs here with access to SecurityContext
self.service.call(req).await
}).await
}
Thread Safety
SecurityContext uses Tokio's task_local! macro, which provides:
- Task isolation - Each async task has its own context
- Thread safety - Safe to use across
.awaitpoints - No data races - Proper synchronization
// Safe to use across await points
async fn my_handler() {
let user = SecurityContext::get_user(); // Before await
some_async_operation().await;
let same_user = SecurityContext::get_user(); // After await
// Both return the same user
}
Testing with Security Context
#[tokio::test]
async fn test_with_security_context() {
let admin = User::new("admin".to_string(), "".to_string())
.roles(&["ADMIN".into()]);
SecurityContext::run_with(Some(admin), async {
assert!(SecurityContext::is_authenticated());
assert!(SecurityContext::has_role("ADMIN"));
assert_eq!(SecurityContext::get_user().unwrap().username, "admin");
}).await;
}
#[tokio::test]
async fn test_without_user() {
SecurityContext::run_with(None, async {
assert!(!SecurityContext::is_authenticated());
assert!(!SecurityContext::has_role("ADMIN"));
assert!(SecurityContext::get_user().is_none());
}).await;
}
Spring Security Comparison
Spring Security:
// Get current user
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// Check role
if (auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
// Admin logic
}
// Run with different context
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(newAuth);
SecurityContextHolder.setContext(context);
try {
// Code runs with new context
} finally {
SecurityContextHolder.clearContext();
}
Actix Security:
// Get current user
let user = SecurityContext::get_user();
let username = user.map(|u| u.username.clone());
// Check role
if SecurityContext::has_role("ADMIN") {
// Admin logic
}
// Run with different context
SecurityContext::run_with(Some(new_user), async {
// Code runs with new context
}).await;
Limitations
- Request scope only - Context is only available during request handling
- No cross-task sharing - Each spawned task needs its own context
- Async only - Uses Tokio's task-local storage
For spawned tasks, pass the user explicitly:
#[post("/process")]
async fn process(user: AuthenticatedUser) -> impl Responder {
let user_clone = user.clone();
tokio::spawn(async move {
// SecurityContext not available here
// Use user_clone directly
process_in_background(user_clone).await;
});
HttpResponse::Accepted().body("Processing")
}