Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Axum Integration

The torii-axum crate provides ready-to-use authentication routes and middleware for Axum web applications. It handles all the HTTP concerns while delegating authentication logic to the core torii crate.

Installation

Add the required dependencies to your Cargo.toml:

[dependencies]
torii = { version = "0.5", features = ["password", "magic-link", "mailer"] }
torii-axum = { version = "0.5", features = ["password", "magic-link"] }
torii-storage-seaorm = { version = "0.5" }
axum = "0.8"
tokio = { version = "1", features = ["full"] }

Available Features

The torii-axum crate supports these feature flags:

  • password - Email/password authentication routes
  • magic-link - Magic link (passwordless) authentication routes
  • oauth - OAuth authentication routes (coming soon)
  • passkey - Passkey/WebAuthn routes (coming soon)

Basic Setup

Here's a minimal example to get authentication routes running:

use std::sync::Arc;
use axum::Router;
use torii::Torii;
use torii_axum::{routes, CookieConfig, LinkConfig};
use torii_storage_seaorm::SeaORMStorage;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Set up database
    let storage = SeaORMStorage::connect("sqlite::memory:").await?;
    storage.migrate().await?;
    
    // Create Torii instance
    let repositories = Arc::new(storage.into_repository_provider());
    let torii = Arc::new(Torii::new(repositories));

    // Create authentication routes
    let auth_routes = routes(torii.clone())
        .with_cookie_config(CookieConfig::development())
        .with_link_config(LinkConfig::new("http://localhost:3000"))
        .build();

    // Build application
    let app = Router::new()
        .nest("/auth", auth_routes)
        .with_state(torii);

    // Start server
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;
    Ok(())
}

Configuration

The CookieConfig controls how session cookies are set:

#![allow(unused)]
fn main() {
use torii_axum::{CookieConfig, CookieSameSite};

// Development settings (insecure, for local testing)
let config = CookieConfig::development();

// Production settings (secure defaults)
let config = CookieConfig::default();

// Custom configuration
let config = CookieConfig::new("session_id")
    .http_only(true)
    .secure(true)
    .same_site(CookieSameSite::Strict)
    .path("/");
}
OptionDefaultDescription
name"session_id"Cookie name
http_onlytruePrevents JavaScript access
securetrueOnly sent over HTTPS
same_siteLaxCSRF protection level
path"/"Cookie path

The LinkConfig is required when using password or magic-link features. It configures the URLs used in verification emails:

#![allow(unused)]
fn main() {
use torii_axum::LinkConfig;

// Basic setup - uses default path prefix "/auth"
let config = LinkConfig::new("https://example.com");

// Custom path prefix (if you mount auth routes elsewhere)
let config = LinkConfig::new("https://example.com")
    .with_path_prefix("/api/v1/auth");
}

This generates URLs like:

  • Magic link: https://example.com/auth/magic-link/verify?token=...
  • Password reset: https://example.com/auth/password/reset?token=...

Important: The path_prefix must match where you mount the auth routes in your application. If you use .nest("/api/v1/auth", auth_routes), set .with_path_prefix("/api/v1/auth").

Email Configuration

To send verification emails, configure a mailer on your Torii instance:

#![allow(unused)]
fn main() {
use torii::Torii;
use torii_mailer::MailerConfig;

// Configure mailer from environment variables
let torii = Torii::new(repositories)
    .with_mailer_from_env()?;

// Or configure manually
let mailer_config = MailerConfig {
    transport: TransportConfig::Smtp {
        host: "smtp.example.com".to_string(),
        port: Some(587),
        username: Some("user".to_string()),
        password: Some("pass".to_string()),
        tls: Some(TlsType::StartTls),
    },
    from_address: "noreply@example.com".to_string(),
    from_name: Some("My App".to_string()),
    app_name: "My App".to_string(),
    app_url: "https://example.com".to_string(),
};

let torii = Torii::new(repositories)
    .with_mailer(mailer_config)?;
}

For local development, emails are saved to ./emails/ by default when no SMTP is configured.

Available Routes

Core Routes (always available)

MethodPathDescription
GET/healthHealth check
GET/sessionGet current session
GET/userGet current user
POST/logoutLogout (also DELETE /session)

Password Routes (feature = "password")

MethodPathDescription
POST/registerRegister new user
POST/loginLogin with email/password
POST/passwordChange password (requires auth)
POST/password/reset/requestRequest password reset email
POST/password/reset/verifyVerify reset token is valid
POST/password/reset/confirmComplete password reset
MethodPathDescription
POST/magic-linkRequest magic link email
POST/magic-link/verifyVerify magic link token

Request/Response Examples

Register User

curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "securepassword123"}'

Response:

{
  "user": {
    "id": "usr_abc123",
    "email": "user@example.com",
    "name": null,
    "email_verified": false
  },
  "session": {
    "token": "ses_xyz789",
    "user_id": "usr_abc123",
    "expires_at": "2024-01-15T12:00:00Z"
  }
}

Login

curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "securepassword123"}'
curl -X POST http://localhost:3000/auth/magic-link \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com"}'

Response:

{
  "message": "Magic link sent to your email"
}

The user receives an email with a link like: https://example.com/auth/magic-link/verify?token=abc123

Your frontend should extract the token from the URL and POST it:

curl -X POST http://localhost:3000/auth/magic-link/verify \
  -H "Content-Type: application/json" \
  -d '{"token": "abc123"}'

Request Password Reset

curl -X POST http://localhost:3000/auth/password/reset/request \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com"}'

Response (always succeeds to prevent email enumeration):

{
  "message": "If an account with that email exists, a password reset link has been sent."
}

Complete Password Reset

curl -X POST http://localhost:3000/auth/password/reset/confirm \
  -H "Content-Type: application/json" \
  -d '{"token": "reset_token_here", "new_password": "newsecurepassword"}'

Authentication Extractors

Use these extractors in your route handlers to access authentication state:

AuthUser

Requires authentication - returns 401 if not authenticated:

#![allow(unused)]
fn main() {
use torii_axum::AuthUser;

async fn protected_handler(AuthUser(user): AuthUser) -> String {
    format!("Hello, {}!", user.email)
}
}

OptionalAuthUser

Authentication is optional:

#![allow(unused)]
fn main() {
use torii_axum::OptionalAuthUser;

async fn maybe_protected(OptionalAuthUser(user): OptionalAuthUser) -> String {
    match user {
        Some(u) => format!("Hello, {}!", u.email),
        None => "Hello, guest!".to_string(),
    }
}
}

Session Token Extractors

For custom authentication logic:

#![allow(unused)]
fn main() {
use torii_axum::{SessionTokenFromCookie, SessionTokenFromBearer, SessionTokenFromRequest};

// From cookie only
async fn from_cookie(SessionTokenFromCookie(token): SessionTokenFromCookie) { }

// From Authorization: Bearer header only
async fn from_bearer(SessionTokenFromBearer(token): SessionTokenFromBearer) { }

// From either cookie or bearer (cookie preferred)
async fn from_either(SessionTokenFromRequest(token): SessionTokenFromRequest) { }
}

Middleware

Auth Middleware

Add authentication state to all requests:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use axum::{Router, middleware};
use torii::Torii;
use torii_axum::{auth_middleware, HasTorii};
use torii_storage_seaorm::SeaORMRepositoryProvider;

#[derive(Clone)]
struct AppState {
    torii: Arc<Torii<SeaORMRepositoryProvider>>,
}

impl HasTorii<SeaORMRepositoryProvider> for AppState {
    fn torii(&self) -> &Arc<Torii<SeaORMRepositoryProvider>> {
        &self.torii
    }
}

let state = AppState { torii };

let app = Router::new()
    .route("/protected", get(protected_handler))
    .layer(middleware::from_fn_with_state(
        state.clone(),
        auth_middleware::<AppState, SeaORMRepositoryProvider>
    ))
    .with_state(state);
}

Require Auth Middleware

Protect entire route groups:

#![allow(unused)]
fn main() {
use torii_axum::require_auth;

let protected_routes = Router::new()
    .route("/dashboard", get(dashboard))
    .route("/settings", get(settings))
    .layer(middleware::from_fn(require_auth));
}

Complete Example

Here's a complete example with all features:

use std::sync::Arc;
use axum::{Router, routing::get, response::Json, middleware};
use torii::Torii;
use torii_axum::{
    routes, AuthUser, OptionalAuthUser, CookieConfig, LinkConfig,
    auth_middleware, HasTorii,
};
use torii_storage_seaorm::SeaORMStorage;

#[derive(Clone)]
struct AppState {
    torii: Arc<Torii<torii_storage_seaorm::SeaORMRepositoryProvider>>,
}

impl HasTorii<torii_storage_seaorm::SeaORMRepositoryProvider> for AppState {
    fn torii(&self) -> &Arc<Torii<torii_storage_seaorm::SeaORMRepositoryProvider>> {
        &self.torii
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Database setup
    let storage = SeaORMStorage::connect("sqlite:./app.db?mode=rwc").await?;
    storage.migrate().await?;
    let repositories = Arc::new(storage.into_repository_provider());
    
    // Torii with email support
    let torii = Arc::new(
        Torii::new(repositories)
            .with_mailer_from_env()
            .unwrap_or_else(|_| Torii::new(repositories.clone()))
    );
    
    let state = AppState { torii: torii.clone() };

    // Auth routes
    let auth_routes = routes(torii)
        .with_cookie_config(CookieConfig::default())
        .with_link_config(LinkConfig::new("https://example.com"))
        .build();

    // Application routes
    let app = Router::new()
        .nest("/auth", auth_routes)
        .route("/", get(home))
        .route("/dashboard", get(dashboard))
        .layer(middleware::from_fn_with_state(
            state.clone(),
            auth_middleware::<AppState, _>
        ))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    println!("Server running on http://localhost:3000");
    axum::serve(listener, app).await?;
    Ok(())
}

async fn home(OptionalAuthUser(user): OptionalAuthUser) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "message": "Welcome!",
        "authenticated": user.is_some()
    }))
}

async fn dashboard(AuthUser(user): AuthUser) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "user_id": user.id,
        "email": user.email
    }))
}

Environment Variables

When using with_mailer_from_env(), these environment variables are supported:

VariableDescriptionDefault
MAILER_SMTP_HOSTSMTP server hostname(file transport)
MAILER_SMTP_PORTSMTP server port587
MAILER_SMTP_USERNAMESMTP username-
MAILER_SMTP_PASSWORDSMTP password-
MAILER_SMTP_TLSTLS mode: none, starttls, tlsstarttls
MAILER_FROM_ADDRESSSender email addressnoreply@example.com
MAILER_FROM_NAMESender display name-
MAILER_APP_NAMEApplication name (in emails)Your App
MAILER_APP_URLApplication URL (in emails)https://example.com
MAILER_FILE_OUTPUT_DIRDirectory for file transport./emails

Error Handling

All routes return structured JSON errors:

{
  "error": "Invalid credentials",
  "code": 401
}

Common error codes:

CodeMeaning
400Bad request (validation error)
401Unauthorized (not authenticated or invalid credentials)
404Not found (user or session)
409Conflict (email already registered)
500Internal server error

Next Steps

  • Learn about Core Concepts for deeper understanding
  • Explore the examples directory
  • Configure production storage backends
  • Set up proper email delivery for production