Your First Secured App
This guide walks you through building a complete secured application step by step.
What We'll Build
A simple REST API with:
- Public endpoints (no auth required)
- User-only endpoints (requires USER role)
- Admin-only endpoints (requires ADMIN role)
- Authority-based endpoints (requires specific permissions)
Project Setup
cargo new my-secured-app
cd my-secured-app
Update Cargo.toml:
[package]
name = "my-secured-app"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4"
actix-security = { version = "0.2", features = ["argon2", "http-basic"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Step 1: Define Your Users
First, create a function that configures your user store:
use actix_security::http::security::{
AuthenticationManager, Argon2PasswordEncoder, PasswordEncoder, User,
};
use actix_security::http::security::web::MemoryAuthenticator;
fn create_authenticator(encoder: Argon2PasswordEncoder) -> MemoryAuthenticator {
AuthenticationManager::in_memory_authentication()
.password_encoder(encoder.clone())
// Admin user with full access
.with_user(
User::with_encoded_password("admin", encoder.encode("admin123"))
.roles(&["ADMIN".into(), "USER".into()])
.authorities(&[
"users:read".into(),
"users:write".into(),
"posts:read".into(),
"posts:write".into(),
])
)
// Regular user
.with_user(
User::with_encoded_password("user", encoder.encode("user123"))
.roles(&["USER".into()])
.authorities(&["posts:read".into()])
)
// Guest with limited access
.with_user(
User::with_encoded_password("guest", encoder.encode("guest123"))
.roles(&["GUEST".into()])
)
}
Step 2: Configure URL-Based Authorization
Create rules for URL patterns:
use actix_security::http::security::{AuthorizationManager, Access};
use actix_security::http::security::web::RequestMatcherAuthorizer;
fn create_authorizer() -> RequestMatcherAuthorizer {
AuthorizationManager::request_matcher()
.login_url("/login")
.http_basic()
// Admin section requires ADMIN role
.add_matcher("/admin/.*", Access::new().roles(vec!["ADMIN"]))
// API requires authentication
.add_matcher("/api/.*", Access::new().authenticated())
// Everything else is public
}
Step 3: Create Your Handlers
use actix_web::{get, post, web, HttpResponse, Responder};
use actix_security::{secured, pre_authorize, permit_all};
use actix_security::http::security::AuthenticatedUser;
// ============= Public Endpoints =============
#[permit_all]
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().body("Welcome to My App!")
}
#[permit_all]
#[get("/health")]
async fn health() -> impl Responder {
HttpResponse::Ok().body("OK")
}
// ============= User Endpoints =============
#[secured("USER")]
#[get("/profile")]
async fn get_profile(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Ok().body(format!(
"Profile for: {}\nRoles: {:?}\nAuthorities: {:?}",
user.get_username(),
user.get_roles(),
user.get_authorities()
))
}
#[pre_authorize("hasRole('USER') AND hasAuthority('posts:read')")]
#[get("/posts")]
async fn list_posts(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Ok().body(format!("Posts for {}", user.get_username()))
}
#[pre_authorize(authority = "posts:write")]
#[post("/posts")]
async fn create_post(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Created().body(format!("Post created by {}", user.get_username()))
}
// ============= Admin Endpoints =============
#[secured("ADMIN")]
#[get("/admin/dashboard")]
async fn admin_dashboard(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Ok().body(format!("Admin Dashboard - Welcome {}!", user.get_username()))
}
#[pre_authorize("hasRole('ADMIN') AND hasAuthority('users:write')")]
#[post("/admin/users")]
async fn create_user(user: AuthenticatedUser) -> impl Responder {
HttpResponse::Created().body("User created")
}
Step 4: Wire It All Together
use actix_web::{App, HttpServer};
use actix_security::http::security::middleware::SecurityTransform;
use actix_security::http::security::SecurityHeaders;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("🚀 Starting secured server at http://127.0.0.1:8080");
let encoder = Argon2PasswordEncoder::new();
HttpServer::new(move || {
let enc = encoder.clone();
App::new()
// Add security headers
.wrap(SecurityHeaders::default())
// Add authentication & authorization
.wrap(
SecurityTransform::new()
.config_authenticator(move || create_authenticator(enc.clone()))
.config_authorizer(create_authorizer)
)
// Register routes
.service(index)
.service(health)
.service(get_profile)
.service(list_posts)
.service(create_post)
.service(admin_dashboard)
.service(create_user)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Step 5: Test Your Application
# Start the server
cargo run
Test Public Endpoints
curl http://127.0.0.1:8080/
# Output: Welcome to My App!
curl http://127.0.0.1:8080/health
# Output: OK
Test User Endpoints
# Guest can't access profile
curl -u guest:guest123 http://127.0.0.1:8080/profile
# Output: 403 Forbidden
# User can access profile
curl -u user:user123 http://127.0.0.1:8080/profile
# Output: Profile for: user...
# User can read posts
curl -u user:user123 http://127.0.0.1:8080/posts
# Output: Posts for user
# User can't create posts (no posts:write authority)
curl -X POST -u user:user123 http://127.0.0.1:8080/posts
# Output: 403 Forbidden
Test Admin Endpoints
# Admin can access everything
curl -u admin:admin123 http://127.0.0.1:8080/admin/dashboard
# Output: Admin Dashboard - Welcome admin!
curl -X POST -u admin:admin123 http://127.0.0.1:8080/posts
# Output: Post created by admin
curl -X POST -u admin:admin123 http://127.0.0.1:8080/admin/users
# Output: User created
# Regular user can't access admin
curl -u user:user123 http://127.0.0.1:8080/admin/dashboard
# Output: 403 Forbidden
Complete Source Code
See the full working example in the test crate.
Next Steps
- Learn about different Authentication methods
- Explore Authorization patterns
- Master Security Expressions
- Add Security Headers