axum-security 0.0.2

A security toolbox for the Axum library
Documentation

axum-security

A security toolkit for Axum.

Features

axum-security is modular -- enable only what you need.

Feature Description
cookie Server-side session management with pluggable stores
jwt JWT authentication (header or cookie)
basic-auth HTTP Basic Authentication
oauth2 OAuth 2.0 authorization code flow
oidc OpenID Connect (auto-discovery, ID token verification)
rbac Role-based access control
pbac Policy-based access control
headers Security response headers (CSP, HSTS, etc.)

Additional features: macros, tracing.

Time crate integration: jiff, chrono, time.

Installation

cargo add axum-security --features cookie,jwt

Pick the features you need. No features are enabled by default.

Cookie sessions

The cookie feature provides server-side session management. Sessions are stored in a pluggable store (an in-memory store is included for development).

use axum_security::cookie::{CookieContext, CookieSession, MemStore, SameSite};

// Build the cookie service
let cookie_service = CookieContext::builder()
    .cookie(|c| {
        c.name("session")
            .max_age(Duration::from_hours(24))
            .secure()
            .http_only()
            .same_site(SameSite::Strict)
    })
    .dev_cookie(|c| c.name("dev-session"))
    .use_dev_cookie(cfg!(debug_assertions))
    .store(MemStore::new())
    .expires_max_age()
    .build::<User>();

let router = Router::new()
    .route("/me", get(authorized))
    .route("/login", get(login))
    .layer(cookie_service.clone())
    .with_state(cookie_service);

Create sessions via CookieContext and extract them with CookieSession:

async fn login(session: CookieContext<User>, /* ... */) -> impl IntoResponse {
    let cookie = session.create_session(user).await.unwrap();
    (Some(cookie), "Logged in")
}

async fn authorized(user: CookieSession<User>) -> Json<User> {
    Json(user.state)
}

// Use Option<CookieSession<User>> for optional authentication.

JWT

The jwt feature adds JWT-based authentication. By default, tokens are read from the Authorization header.

use axum_security::jwt::{Jwt, JwtContext};

let jwt_service = JwtContext::builder()
    .jwt_secret("my-secret")
    .build::<AccessToken>();

let router = Router::new()
    .route("/me", get(authorized))
    .route("/login", get(login))
    .layer(jwt_service.clone())
    .with_state(jwt_service);

Encode tokens via JwtContext and extract them with Jwt:

async fn login(context: JwtContext<AccessToken>, /* ... */) -> Result<String, StatusCode> {
    context
        .encode_token(&token)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}

async fn authorized(Jwt(token): Jwt<AccessToken>) -> Json<AccessToken> {
    Json(token)
}

Basic auth

The basic-auth feature provides HTTP Basic Authentication via a BasicAuthenticator trait.

use axum_security::basic_auth::{BasicAuth, BasicAuthLayer, BasicAuthenticator};

struct MyAuth;

impl BasicAuthenticator for MyAuth {
    type User = User;
    type Error = StatusCode;

    async fn authenticate(
        &self,
        username: &str,
        password: &str,
    ) -> Result<Option<User>, StatusCode> {
        if username == "admin" && password == "secret" {
            Ok(Some(User { username: username.to_owned() }))
        } else {
            Ok(None)
        }
    }
}

let router = Router::new()
    .route("/hello", get(hello))
    .layer(BasicAuthLayer::new(MyAuth));

Extract with BasicAuth<User> or Option<BasicAuth<User>>:

async fn hello(BasicAuth(user): BasicAuth<User>) -> String {
    format!("Hello, {}!", user.username)
}

OAuth 2.0

The oauth2 feature handles the authorization code flow. Implement OAuth2Handler to process the token response after a successful login.

use axum_security::oauth2::{
    AfterLoginCookies, OAuth2Context, OAuth2Ext, OAuth2Handler, TokenResponse,
};

impl OAuth2Handler for LoginHandler {
    async fn after_login(
        &self,
        token_res: TokenResponse,
        cookies: &mut AfterLoginCookies<'_>,
    ) -> impl IntoResponse {
        // Fetch user info, create a session, add cookies
        cookies.add(session_cookie);
        Redirect::to("/")
    }
}

let oauth2_service = OAuth2Context::github()
    .client_id_env("CLIENT_ID")
    .client_secret_env("CLIENT_SECRET")
    .redirect_url("http://localhost:3000/redirect")
    .login_path("/login")
    .build(handler);

let router = Router::new()
    .route("/me", get(authorized))
    .layer(cookie_service)
    .with_oauth2(oauth2_service);

OIDC

The oidc feature adds OpenID Connect with auto-discovery and ID token verification. PKCE and nonce replay protection are always enabled.

use axum_security::oidc::{
    AfterLoginCookies, OidcContext, OidcExt, OidcHandler, OidcTokenResponse,
};

impl OidcHandler for LoginHandler {
    async fn after_login(
        &self,
        token_res: OidcTokenResponse,
        cookies: &mut AfterLoginCookies<'_>,
    ) -> impl IntoResponse {
        let user = User {
            subject: token_res.claims.subject,
            email: token_res.claims.email,
            name: token_res.claims.name,
        };
        cookies.add(self.cookie_service.create_session(user).await.unwrap());
        Redirect::to("/")
    }
}

let oidc_context = OidcContext::google()
    .await?
    .client_id_env("GOOGLE_CLIENT_ID")
    .client_secret_env("GOOGLE_CLIENT_SECRET")
    .redirect_url("http://localhost:3000/auth/oidc/callback")
    .login_path("/login")
    .logout_path("/logout")
    .scopes(&["openid", "email", "profile"])
    .build(handler);

let router = Router::new()
    .route("/me", get(me))
    .layer(cookie_service)
    .with_oidc(oidc_context);

RBAC

The rbac feature provides role-based access control. Implement the RBAC trait to tell the library how to extract roles from your user type.

use axum_security::rbac::RBAC;

impl RBAC for Role {
    type Resource = User;

    fn extract_roles(resource: &Self::Resource) -> impl IntoIterator<Item = &Self> {
        Some(&resource.role)
    }
}

Protect routes with the #[requires] macro (needs the macros feature):

#[axum_security::rbac::requires(Role::Admin)]
async fn admin_only(cookie: CookieSession<User>) -> String {
    format!("hi admin: {}", cookie.state.name)
}

Or use the RBACExt methods on routes: .requires(), .requires_all(), .allows().

PBAC

The pbac feature adds policy-based access control. Policies are composable -- combine them with .and(), .or(), and .not().

use axum_security::pbac::{HasRole, PolicyRouterExt};

fn is_not_banned(u: &User) -> bool {
    !u.banned
}

let admin_policy = HasRole::new(Role::Admin).and(is_not_banned);

let mod_policy = HasRole::new(Role::Admin)
    .or(HasRole::new(Role::Mod))
    .and(is_not_banned);

let router = Router::new()
    .route("/admin", get(handler).with_policy(admin_policy))
    .route("/mod-area", get(handler).with_policy(mod_policy))
    .layer(cookie_service);

Any Fn(&User) -> bool works as a policy. Use AsyncPolicy or AsyncFalliblePolicy for async checks.

Security headers

The headers feature adds a Tower layer that sets security response headers. Use SecurityHeaders::recommended() for sensible defaults, or apply individual headers.

use axum_security::headers::{CrossOriginOpenerPolicy, SecurityHeaders, XssProtection};

let security_layer = SecurityHeaders::recommended()
    .use_dev_headers(cfg!(debug_assertions))
    .add(XssProtection::ZERO);

let coop_layer = CrossOriginOpenerPolicy::SAME_ORIGIN;

let router = Router::new()
    .route("/", get(index))
    .layer(security_layer)
    .layer(coop_layer);

Examples

See the examples/ directory for complete, runnable examples of each feature.

Roadmap

  • CSP nonce support
  • OIDC logout support
  • Eager/lazy session loading

License

This project is licensed under the MIT license.