Method Security

Protect individual handlers with attribute macros for fine-grained access control.

Overview

Method security complements URL-based authorization by adding checks directly to handlers. This is useful when:

  • Different endpoints at similar URLs need different permissions
  • You want self-documenting security rules
  • Complex authorization logic is needed

Available Macros

MacroSpring EquivalentUse Case
#[secured("ROLE")]@SecuredSimple role check
#[pre_authorize(...)]@PreAuthorizeExpression-based access
#[permit_all]@PermitAllExplicitly public
#[deny_all]@DenyAllBlock all access
#[roles_allowed("ROLE")]@RolesAllowedJava EE style

@secured

Simple role-based access control:

use actix_security::secured;
use actix_security::http::security::AuthenticatedUser;

// Single role
#[secured("ADMIN")]
#[get("/admin/users")]
async fn list_users(user: AuthenticatedUser) -> impl Responder {
    HttpResponse::Ok().body("User list")
}

// Multiple roles (OR logic - any role grants access)
#[secured("ADMIN", "MANAGER")]
#[get("/reports")]
async fn view_reports(user: AuthenticatedUser) -> impl Responder {
    HttpResponse::Ok().body("Reports")
}

@pre_authorize

Expression-based access control with full expression language support:

use actix_security::pre_authorize;

// Role check
#[pre_authorize(role = "ADMIN")]
#[get("/admin")]
async fn admin_only(user: AuthenticatedUser) -> impl Responder { /* ... */ }

// Authority check
#[pre_authorize(authority = "users:write")]
#[post("/users")]
async fn create_user(user: AuthenticatedUser) -> impl Responder { /* ... */ }

// Multiple authorities (OR logic)
#[pre_authorize(authorities = ["users:read", "users:write"])]
#[get("/users")]
async fn list_users(user: AuthenticatedUser) -> impl Responder { /* ... */ }

// Authenticated only
#[pre_authorize(authenticated)]
#[get("/profile")]
async fn get_profile(user: AuthenticatedUser) -> impl Responder { /* ... */ }

// Full expression
#[pre_authorize("hasRole('USER') AND hasAuthority('posts:write')")]
#[post("/posts")]
async fn create_post(user: AuthenticatedUser) -> impl Responder { /* ... */ }

See Security Expressions for full expression syntax.

@permit_all

Mark endpoints as explicitly public:

use actix_security::permit_all;

#[permit_all]
#[get("/health")]
async fn health_check() -> impl Responder {
    HttpResponse::Ok().body("OK")
}

Note: The handler doesn't receive AuthenticatedUser since no auth is required.

@deny_all

Block all access (useful for deprecated endpoints):

use actix_security::deny_all;

#[deny_all]
#[get("/deprecated/endpoint")]
async fn deprecated_endpoint(_user: AuthenticatedUser) -> impl Responder {
    // This code is never reached
    HttpResponse::Ok().body("Never executed")
}

@roles_allowed

Java EE style role checking (alias for @secured):

use actix_security::roles_allowed;

#[roles_allowed("ADMIN")]
#[get("/admin")]
async fn admin_panel(user: AuthenticatedUser) -> impl Responder {
    HttpResponse::Ok().body("Admin Panel")
}

#[roles_allowed("ADMIN", "MANAGER")]
#[get("/management")]
async fn management(user: AuthenticatedUser) -> impl Responder {
    HttpResponse::Ok().body("Management")
}

Macro Placement

Important: Security macros must be placed before the route macro:

// ✓ Correct
#[secured("ADMIN")]
#[get("/admin")]
async fn admin() -> impl Responder { /* ... */ }

// ✗ Wrong - won't work
#[get("/admin")]
#[secured("ADMIN")]
async fn admin() -> impl Responder { /* ... */ }

Combining with URL Authorization

Method security and URL authorization work together:

// URL authorization
let authorizer = AuthorizationManager::request_matcher()
    .add_matcher("/api/.*", Access::new().authenticated());

// Method security adds additional checks
#[pre_authorize(authority = "posts:write")]
#[post("/api/posts")]  // URL requires authentication, method requires authority
async fn create_post(user: AuthenticatedUser) -> impl Responder { /* ... */ }

Error Handling

When access is denied, the macro returns 403 Forbidden:

#[secured("ADMIN")]
#[get("/admin")]
async fn admin(user: AuthenticatedUser) -> impl Responder {
    // If user doesn't have ADMIN role, this code never runs
    // A 403 Forbidden response is returned instead
    HttpResponse::Ok().body("Admin")
}

The actual implementation wraps your handler:

// Your code:
#[secured("ADMIN")]
async fn admin(user: AuthenticatedUser) -> impl Responder { /* ... */ }

// Expands to (simplified):
async fn admin(user: AuthenticatedUser) -> Result<impl Responder, AuthError> {
    if !user.has_role("ADMIN") {
        return Err(AuthError::Forbidden);
    }
    Ok(/* your original code */)
}

Complete Example

use actix_web::{get, post, delete, App, HttpServer, HttpResponse, Responder};
use actix_security::{secured, pre_authorize, permit_all, deny_all};
use actix_security::http::security::AuthenticatedUser;

// Public endpoints
#[permit_all]
#[get("/")]
async fn index() -> impl Responder {
    HttpResponse::Ok().body("Welcome!")
}

// Authenticated users
#[pre_authorize(authenticated)]
#[get("/dashboard")]
async fn dashboard(user: AuthenticatedUser) -> impl Responder {
    HttpResponse::Ok().body(format!("Hello, {}!", user.get_username()))
}

// Role-based
#[secured("USER")]
#[get("/profile")]
async fn profile(user: AuthenticatedUser) -> impl Responder {
    HttpResponse::Ok().body("Profile")
}

// Authority-based
#[pre_authorize(authority = "posts:write")]
#[post("/posts")]
async fn create_post(user: AuthenticatedUser) -> impl Responder {
    HttpResponse::Created().body("Post created")
}

// Complex expression
#[pre_authorize("hasRole('ADMIN') OR (hasRole('USER') AND hasAuthority('posts:delete'))")]
#[delete("/posts/{id}")]
async fn delete_post(user: AuthenticatedUser) -> impl Responder {
    HttpResponse::Ok().body("Post deleted")
}

// Deprecated
#[deny_all]
#[get("/old-api")]
async fn old_api(_user: AuthenticatedUser) -> impl Responder {
    HttpResponse::Ok().body("Never reached")
}

Spring Security Comparison

Spring Security:

@Secured("ROLE_ADMIN")
@GetMapping("/admin")
public String admin() { return "admin"; }

@PreAuthorize("hasRole('USER') and hasAuthority('posts:write')")
@PostMapping("/posts")
public String createPost() { return "created"; }

@PermitAll
@GetMapping("/public")
public String publicEndpoint() { return "public"; }

@DenyAll
@GetMapping("/deprecated")
public String deprecated() { return "never"; }

@RolesAllowed({"ADMIN", "MANAGER"})
@GetMapping("/management")
public String management() { return "management"; }

Actix Security:

#[secured("ADMIN")]
#[get("/admin")]
async fn admin() -> impl Responder { /* ... */ }

#[pre_authorize("hasRole('USER') AND hasAuthority('posts:write')")]
#[post("/posts")]
async fn create_post() -> impl Responder { /* ... */ }

#[permit_all]
#[get("/public")]
async fn public_endpoint() -> impl Responder { /* ... */ }

#[deny_all]
#[get("/deprecated")]
async fn deprecated() -> impl Responder { /* ... */ }

#[roles_allowed("ADMIN", "MANAGER")]
#[get("/management")]
async fn management() -> impl Responder { /* ... */ }