Axum integration

Add strongly typed authorization to an Axum service — the check itself is a single line in your handler.

Praetor ships an optional, tiny Axum integration behind the axum feature. It does one thing: hand your handler the Ability your middleware built, so you can call authorize and let the compiler check your rules. Praetor still never validates tokens — that stays in your application, exactly where it belongs.

middleware: validate + build → extract → authorize

This guide assumes you have read the main guide and know how to model actions, subjects, and claims. Here we focus purely on wiring an ability through an Axum request.

Enable the feature

The integration is off by default, so the core library stays dependency-free. Turn it on with the axum feature:

cargo add praetor --features axum

Or in your Cargo.toml directly:

[dependencies]
praetor = { version = "0.1", features = ["axum"] }
Note. Praetor targets Axum 0.8+. Enabling the feature adds the extractor under praetor::axum; the rest of the library is unchanged.

How it fits together

The division of labor is the whole idea. Your application does the authentication; Praetor does the authorization; the feature is just the thin seam between them.

Your middleware

Validate the token, decode your Claims, call build_ability, and insert the ability into the request as an Arc.

Praetor's extractor

Praetor<T> pulls that shared ability back out in your handler so you can authorize against the resource you loaded.

Concretely, a request flows through four steps — the first three are yours, the fourth is one line:

  1. Middleware validates the request and builds an ability from the claims.
  2. Middleware inserts the ability into the request extensions.
  3. The handler extracts it with Praetor<AppAbility>.
  4. The handler authorizes: ability.authorize(action, &resource)?.

1 · Define your rules

This is where Praetor earns its keep. Your entire authorization model is one function that turns claims into an ability — ordinary Rust, checked by the compiler, with no policy files or DSL. Anyone may read messages; only the author may update their own:

use praetor::{Ability, AbilitySubject};

#[derive(Clone, Copy, PartialEq, Eq)]
enum Action { Read, Update }

#[derive(Clone, Copy, PartialEq, Eq)]
enum Subject { Message }

#[derive(Clone)]
struct Claims { user_id: u64 }

#[derive(Clone)]
struct Message { id: u64, author_id: u64, body: String }

impl AbilitySubject<Subject> for Message {
    fn subject_type(&self) -> Subject { Subject::Message }
}

// A type alias keeps handler signatures short.
type AppAbility = Ability<Action, Subject, Claims>;

// Your whole authorization model: claims in, ability out.
fn build_ability(claims: Claims) -> AppAbility {
    Ability::builder(claims)
        .can(Action::Read, Subject::Message)
        .can_if(Action::Update, Subject::Message, |claims, m: &Message| {
            claims.user_id == m.author_id
        })
        .build()
}
That predicate is just a closure. |claims, m: &Message| claims.user_id == m.author_id is normal, type-checked Rust — no query language, no reflection. Grow it however your model needs.

2 · Insert the ability

Your middleware validates the request the way it always has, then builds the ability and stores it in the request extensions as an Arc. Praetor never inspects the token itself:

use std::sync::Arc;
use axum::{extract::Request, middleware::Next, response::Response};

async fn auth(mut request: Request, next: Next) -> Response {
    // Pretend we validated a JWT and decoded these claims.
    let claims = Claims { user_id: 1 };
    request.extensions_mut().insert(Arc::new(build_ability(claims)));
    next.run(request).await
}
Why Arc? An ability holds your rule closures, so it is shared rather than cloned. Wrapping it in Arc lets every handler extract it cheaply — the extractor only clones the pointer.

3 · Extract & authorize

Add the Praetor<AppAbility> extractor to your handler. It hands you the shared ability, which derefs to the underlying Ability — so the authorization check is a single ? line after you load the resource:

use axum::extract::{Path, State};
use praetor::axum::Praetor;

async fn update_message(
    Praetor(ability): Praetor<AppAbility>,
    Path(id): Path<u64>,
    State(state): State<AppState>,
) -> Result<String, ApiError> {
    let message = state.messages.get(&id).cloned().ok_or(ApiError::NotFound)?;

    ability.authorize(Action::Update, &message)?; // <- the whole check

    Ok(format!("updated message {}", message.id))
}

Map Praetor's Forbidden onto your own error type so ? turns a denied request into an HTTP 403:

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};

enum ApiError { NotFound, Forbidden }

impl From<praetor::Forbidden> for ApiError {
    fn from(_: praetor::Forbidden) -> Self { ApiError::Forbidden }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        match self {
            ApiError::NotFound  => (StatusCode::NOT_FOUND,  "not found").into_response(),
            ApiError::Forbidden => (StatusCode::FORBIDDEN, "forbidden").into_response(),
        }
    }
}
Destructuring Praetor(ability) gives you an Arc<AppAbility>. Because it derefs to the ability, you call ability.authorize(..), ability.can(..), and friends directly — no unwrapping.

4 · Wire up the router

Attach the middleware as a layer so every routed request gets an ability before it reaches a handler:

use axum::{Router, middleware, routing::get};

let app = Router::new()
    .route("/messages/{id}", get(update_message))
    .layer(middleware::from_fn(auth))
    .with_state(state);

That is the entire integration: define rules once, insert the ability in middleware, and authorize in one line per handler.

Missing abilities

If a handler using Praetor<T> runs without the middleware ever inserting an ability, extraction fails with MissingAbility, which converts into a 500 Internal Server Error. That is deliberate: a missing ability means the layer was not wired up, which is a server misconfiguration rather than a client problem.

Want a different status? Insert the ability unconditionally for every request, or wrap the extractor in your own type that maps absence to, say, 401 Unauthorized. Praetor stays out of that policy decision.

Run the example

The repository ships a complete, runnable version of everything above in examples/axum.rs. It builds the router, drives two requests in-process, and prints the results — 200 for the owner and 403 for someone else:

cargo run --example axum --features axum
update own message:    200 OK updated message 1: mine
update others message: 403 Forbidden forbidden

What's next

You now have authorization wired through an Axum request end to end. From here:

  • Revisit the main guide for the full rule model — conditional denies, precedence, and heterogeneous resources.
  • Browse the praetor::axum API on docs.rs.
  • Read the source and open issues on GitHub.

← Back to the guide