Skip to main content

Rust (Axum)

The most direct integration: depend on the crate, mount the router, share your DatabaseConnection. The auth backend lives in your process.

Setup

# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
identsphere-core = "0.1"
identsphere-axum = "0.1"
sea-orm = { version = "1", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }

Wire it up

use std::sync::Arc;
use axum::{Router, routing::get, http::StatusCode};
use sea_orm::Database;
use identsphere_axum::{routes, AppConfig, AppState};
use identsphere_core::{
services::{AuthService, AuthServiceConfig, AuditService, ApiKeyResolver, Authorizer},
rbac::RbacConfig,
providers::{
cache::PostgresOnlyCache,
email::LogOnlySender,
storage::LocalFsStorage,
},
AuthService as _,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();

let db = Arc::new(
Database::connect(std::env::var("DATABASE_URL")?).await?
);

let cfg = Arc::new(AppConfig {
app_name: "Acme".into(),
public_base_url: "https://auth.example.com".into(),
from_email: "no-reply@example.com".into(),
cookies_secure: true,
rp_id: "auth.example.com".into(),
rp_origin: "https://auth.example.com".into(),
rp_name: "Acme".into(),
..Default::default()
});

let auth_cfg = AuthServiceConfig {
jwt_secret: std::env::var("IDENTSPHERE_JWT_SECRET")?,
issuer: "acme".into(),
access_expiry_secs: 900,
refresh_expiry_secs: 30 * 24 * 60 * 60,
..Default::default()
};
let auth_service = Arc::new(AuthService::new(auth_cfg).await?);

let cache = Arc::new(PostgresOnlyCache::new(db.clone()));
let rbac = Arc::new(RbacConfig::default_matrix());
let authorizer = Arc::new(Authorizer::new(db.clone(), rbac));
let api_key_resolver = Arc::new(ApiKeyResolver::new(db.clone(), cache.clone()));
let audit_service = Arc::new(AuditService::new(db.clone()));

let state = AppState {
config: cfg,
db,
auth_service,
api_key_resolver,
authorizer,
session_cache: cache,
email_sender: Arc::new(LogOnlySender),
object_storage: Arc::new(LocalFsStorage::new("./uploads", "http://localhost:4000/files")),
auth_user_extender: Arc::new(identsphere_axum::NoopExtender),
audit_service,
};

let app = Router::new()
// Mount the SDK routes
.nest("/v1/auth", routes::auth::router())
.nest("/v1/auth/mfa", routes::mfa::router())
.nest("/v1/auth/email-otp", routes::email_otp::router())
.nest("/v1/auth/email", routes::email_verification::router())
.nest("/v1/auth/password", routes::password_reset::router())
.nest("/v1/auth/oauth", routes::oauth::router())
.nest("/v1/auth/sessions", routes::sessions::router())
.nest("/v1/auth/trusted-browsers", routes::trusted_browsers::router())
.nest("/v1/auth/invitations", routes::invitations::router_public())
.nest("/v1/auth/passkey", routes::passkey::public_routes())
// Authenticated user routes
.nest("/v1/users/me", routes::users::router())
.nest("/v1/users/me/avatar", routes::avatar::router())
.nest("/v1/users/me/passkeys", routes::passkey::authenticated_routes())
// Your business routes
.route("/api/me", get(my_handler))
.with_state(state);

axum::serve(
tokio::net::TcpListener::bind("0.0.0.0:4000").await?,
app,
)
.await?;

Ok(())
}

async fn my_handler(
axum::Extension(auth): axum::Extension<identsphere_core::auth_user::AuthUser>,
) -> Result<axum::Json<serde_json::Value>, StatusCode> {
Ok(axum::Json(serde_json::json!({
"user_id": auth.user_id,
"org_id": auth.organization_id,
})))
}

Auth middleware

Mount the auth middleware on routes that should require a logged-in user:

use identsphere_axum::middleware::auth_middleware;

let protected = Router::new()
.route("/api/projects", get(list_projects))
.route_layer(axum::middleware::from_fn_with_state(state.clone(), auth_middleware));

The middleware:

  1. Extracts the access token from cookie or Authorization header.
  2. Validates the JWT signature + expiry.
  3. Checks the session-cache for revocation.
  4. Attaches AuthUser to the request extensions.

Your handler then takes Extension(auth): Extension<AuthUser>.

Authorization

use identsphere_core::rbac::Permission;

async fn list_members(
State(state): State<AppState>,
Extension(auth): Extension<AuthUser>,
Path(org_id): Path<Uuid>,
) -> RouteResult<Json<Vec<MemberView>>> {
identsphere_axum::routes::guards::require_org_match(&auth, org_id)?;
state.authorizer
.authorize(&auth, &Permission::MembersList)
.await?;
// ...
}

Customizing AuthUser

Implement AuthUserExtender to inject host-specific claims:

use async_trait::async_trait;
use identsphere_axum::AuthUserExtender;
use identsphere_core::auth_user::AuthUser;

struct MyExtender { db: Arc<DatabaseConnection> }

#[async_trait]
impl AuthUserExtender for MyExtender {
async fn extend(&self, auth: &mut AuthUser) {
// Populate auth.extensions["billing_tier"] from your own table.
let tier: String = sqlx::query_scalar("SELECT tier FROM my_app.users WHERE id = $1")
.bind(auth.user_id)
.fetch_one(&*self.db)
.await
.unwrap_or_default();
auth.extensions.insert("billing_tier".into(), serde_json::json!(tier));
}
}

Wire it in AppState.auth_user_extender.

CORS

If your frontend is on a different origin:

use tower_http::cors::{CorsLayer, AllowOrigin};
use axum::http::{Method, HeaderName, HeaderValue};

let cors = CorsLayer::new()
.allow_origin(AllowOrigin::exact(HeaderValue::from_static("https://app.example.com")))
.allow_credentials(true)
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
.allow_headers([HeaderName::from_static("content-type"), HeaderName::from_static("x-csrf-token")]);

let app = app.layer(cors);

allow_credentials(true) is required for cookie auth across origins.

Rate limiting

Wrap login + reset + OTP-request with tower-governor:

use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer};

let conf = GovernorConfigBuilder::default()
.per_second(2)
.burst_size(5)
.finish()
.unwrap();

let rate_limited = routes::auth::router()
.layer(GovernorLayer { config: Box::leak(Box::new(conf)) });

let app = Router::new().nest("/v1/auth", rate_limited);