Custom Authorizers

Create custom authorizers for complex authorization logic, external policy engines, or domain-specific rules.

Implementing the Authorizer Trait

use actix_security::http::security::config::{Authorizer, AuthorizationResult};
use actix_security::http::security::User;
use actix_web::dev::ServiceRequest;

#[derive(Clone)]
pub struct CustomAuthorizer {
    // Your configuration
}

impl Authorizer for CustomAuthorizer {
    fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
        // Your authorization logic
        match user {
            Some(u) if self.check_access(u, req) => AuthorizationResult::Granted,
            Some(_) => AuthorizationResult::Denied,
            None => AuthorizationResult::LoginRequired,
        }
    }
}

Authorization Results

Return one of three results:

pub enum AuthorizationResult {
    Granted,       // Allow access
    Denied,        // 403 Forbidden
    LoginRequired, // 401 Unauthorized or redirect to login
}

Example: Time-Based Access

use chrono::{Local, Timelike};

#[derive(Clone)]
pub struct BusinessHoursAuthorizer {
    inner: RequestMatcherAuthorizer,
}

impl Authorizer for BusinessHoursAuthorizer {
    fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
        // First, check standard authorization
        let result = self.inner.authorize(user, req);
        if result != AuthorizationResult::Granted {
            return result;
        }

        // Then, check business hours for certain paths
        if req.path().starts_with("/business/") {
            let hour = Local::now().hour();
            if hour < 9 || hour >= 17 {
                log::warn!("Access denied outside business hours");
                return AuthorizationResult::Denied;
            }
        }

        AuthorizationResult::Granted
    }
}

Example: IP-Based Access

use std::net::IpAddr;

#[derive(Clone)]
pub struct IpWhitelistAuthorizer {
    inner: RequestMatcherAuthorizer,
    admin_ips: Vec<IpAddr>,
}

impl Authorizer for IpWhitelistAuthorizer {
    fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
        // Check if admin path
        if req.path().starts_with("/admin/") {
            // Get client IP
            let client_ip = req.peer_addr()
                .map(|addr| addr.ip());

            // Check whitelist
            if let Some(ip) = client_ip {
                if !self.admin_ips.contains(&ip) {
                    log::warn!("Admin access denied from IP: {}", ip);
                    return AuthorizationResult::Denied;
                }
            } else {
                return AuthorizationResult::Denied;
            }
        }

        self.inner.authorize(user, req)
    }
}

Example: Resource Owner Check

#[derive(Clone)]
pub struct ResourceOwnerAuthorizer {
    inner: RequestMatcherAuthorizer,
}

impl Authorizer for ResourceOwnerAuthorizer {
    fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
        // Check standard authorization first
        let result = self.inner.authorize(user, req);
        if result != AuthorizationResult::Granted {
            return result;
        }

        // For user-specific paths, check ownership
        // Path: /users/{user_id}/...
        if let Some(user) = user {
            if let Some(captures) = regex::Regex::new(r"/users/(\w+)/")
                .unwrap()
                .captures(req.path())
            {
                let path_user_id = &captures[1];

                // Allow if admin OR owner
                if !user.has_role("ADMIN") && user.username != path_user_id {
                    return AuthorizationResult::Denied;
                }
            }
        }

        AuthorizationResult::Granted
    }
}

Example: External Policy Engine (OPA)

use reqwest::blocking::Client;

#[derive(Clone)]
pub struct OpaAuthorizer {
    opa_url: String,
    client: Client,
}

impl OpaAuthorizer {
    pub fn new(opa_url: &str) -> Self {
        Self {
            opa_url: opa_url.to_string(),
            client: Client::new(),
        }
    }
}

impl Authorizer for OpaAuthorizer {
    fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
        let input = serde_json::json!({
            "input": {
                "user": user.map(|u| &u.username),
                "roles": user.map(|u| &u.roles).unwrap_or(&HashSet::new()),
                "path": req.path(),
                "method": req.method().as_str(),
            }
        });

        match self.client
            .post(&format!("{}/v1/data/authz/allow", self.opa_url))
            .json(&input)
            .send()
        {
            Ok(resp) => {
                let result: serde_json::Value = resp.json().unwrap_or_default();
                if result["result"].as_bool().unwrap_or(false) {
                    AuthorizationResult::Granted
                } else if user.is_some() {
                    AuthorizationResult::Denied
                } else {
                    AuthorizationResult::LoginRequired
                }
            }
            Err(e) => {
                log::error!("OPA request failed: {}", e);
                AuthorizationResult::Denied // Fail closed
            }
        }
    }
}

Composing Authorizers

#[derive(Clone)]
pub struct CompositeAuthorizer {
    authorizers: Vec<Box<dyn Authorizer>>,
}

impl Authorizer for CompositeAuthorizer {
    fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
        // All authorizers must grant access
        for authorizer in &self.authorizers {
            match authorizer.authorize(user, req) {
                AuthorizationResult::Granted => continue,
                result => return result,
            }
        }
        AuthorizationResult::Granted
    }
}

// Usage
let authorizer = CompositeAuthorizer {
    authorizers: vec![
        Box::new(RequestMatcherAuthorizer::new()),
        Box::new(IpWhitelistAuthorizer::new()),
        Box::new(BusinessHoursAuthorizer::new()),
    ],
};

Using with SecurityTransform

let custom_authorizer = CustomAuthorizer::new();

App::new()
    .wrap(
        SecurityTransform::new()
            .config_authenticator(|| /* ... */)
            .config_authorizer(move || custom_authorizer.clone())
    )

Best Practices

1. Fail Closed

When in doubt, deny access:

fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
    // If anything goes wrong, deny
    match self.do_authorize(user, req) {
        Ok(result) => result,
        Err(e) => {
            log::error!("Authorization error: {}", e);
            AuthorizationResult::Denied
        }
    }
}

2. Log Security Decisions

fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
    let result = self.check_access(user, req);

    match &result {
        AuthorizationResult::Denied => {
            log::warn!(
                "Access denied: user={:?}, path={}, method={}",
                user.map(|u| &u.username),
                req.path(),
                req.method()
            );
        }
        _ => {}
    }

    result
}

3. Keep It Simple

Complex authorization logic should live in your business layer, not the authorizer:

// Good - simple authorizer, complex logic elsewhere
fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
    if self.policy_service.is_allowed(user, req.path(), req.method()) {
        AuthorizationResult::Granted
    } else {
        AuthorizationResult::Denied
    }
}

4. Test Thoroughly

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_admin_ip_whitelist() {
        let authorizer = IpWhitelistAuthorizer::new(vec!["10.0.0.1".parse().unwrap()]);

        // Test allowed IP
        // Test denied IP
        // Test missing IP
    }
}

Spring Security Comparison

Spring Security:

@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
    @Override
    public AuthorizationDecision check(
        Supplier<Authentication> authentication,
        RequestAuthorizationContext context
    ) {
        // Your logic
        return new AuthorizationDecision(allowed);
    }
}

Actix Security:

impl Authorizer for CustomAuthorizer {
    fn authorize(&self, user: Option<&User>, req: &ServiceRequest) -> AuthorizationResult {
        // Your logic
        if allowed {
            AuthorizationResult::Granted
        } else {
            AuthorizationResult::Denied
        }
    }
}