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

Introduction to Torii

Torii is an authentication framework for Rust applications that gives you complete control over your users' data. Unlike hosted solutions that store user information in their cloud, Torii lets you own and manage your authentication stack while providing modern auth features.

With Torii, you get powerful authentication capabilities combined with full data sovereignty and the ability to store user data wherever you choose.

Warning: This project is in early development and is not production-ready. The API is subject to change without notice. As this project has not undergone security audits, it should not be used in production environments.

Key Features

  • Data Sovereignty: Your user data stays in your own database
  • Multiple Authentication Methods:
    • Password-based authentication
    • Social OAuth/OpenID Connect
    • Passkey/WebAuthn support
    • Magic Link authentication
  • Flexible Storage: Store user data in SQLite, PostgreSQL, or MySQL
  • Session Management: Choose between database sessions or JWT tokens
  • Type Safety: Strongly typed APIs with compile-time guarantees

Storage Support

Authentication MethodSQLitePostgreSQLMySQL
Password
OAuth2/OIDC
Passkey
Magic Link

Getting Started with Torii

This guide will get you up and running with Torii authentication in your Rust application.

Prerequisites

  • A Rust project with Cargo
  • Basic understanding of async Rust
  • Database (SQLite, PostgreSQL, or MySQL)

Installation

Add Torii to your Cargo.toml:

[dependencies]
torii = { version = "0.5", features = ["password", "sqlite"] }
tokio = { version = "1", features = ["full"] }

Available Features

Authentication Methods:

  • password - Email/password authentication
  • oauth - OAuth/social login
  • passkey - WebAuthn/passkey authentication
  • magic-link - Email magic link authentication

Storage Backends:

  • sqlite - SQLite storage
  • postgres - PostgreSQL storage
  • seaorm - SeaORM support (SQLite, PostgreSQL, MySQL)

Basic Setup

Here's a complete example with SQLite and password authentication:

use torii::ToriiBuilder;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create Torii using the builder pattern
    // This connects to SQLite and applies migrations automatically
    let torii = ToriiBuilder::new()
        .with_seaorm("sqlite::memory:")
        .await?
        .apply_migrations(true)
        .build()
        .await?;

    // Now torii is ready to use for authentication
    Ok(())
}

User Registration and Login

Register a User

use torii::{Torii, ToriiError};
use torii_core::RepositoryProvider;

async fn register_user(
    torii: &Torii<impl RepositoryProvider>,
    email: &str,
    password: &str
) -> Result<(), ToriiError> {
    // Register a new user
    let user = torii.password().register(email, password).await?;

    println!("User registered: {}", user.id);
    Ok(())
}

Login a User

use torii::{Torii, ToriiError};
use torii_core::RepositoryProvider;

async fn login_user(
    torii: &Torii<impl RepositoryProvider>,
    email: &str,
    password: &str
) -> Result<(), ToriiError> {
    // Authenticate user - optional user_agent and ip_address for tracking
    let (user, session) = torii.password().authenticate(
        email,
        password,
        Some("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36".to_string()),
        Some("127.0.0.1".to_string())
    ).await?;

    // The session token can be stored in a cookie or returned to the client
    println!("User authenticated: {}", user.id);
    let token = session.token.as_ref().expect("freshly created session should have token");
    println!("Session token: {}", token);

    Ok(())
}

Verify a Session

use torii::{Torii, SessionToken, ToriiError};
use torii_core::RepositoryProvider;

async fn verify_session(
    torii: &Torii<impl RepositoryProvider>,
    session_token: &str
) -> Result<(), ToriiError> {
    // Parse the session token
    let token = SessionToken::new(session_token);

    // Verify and get session data (works for both JWT and opaque tokens)
    let session = torii.get_session(&token).await?;

    // Get the user associated with this session
    let user = torii.get_user(&session.user_id).await?
        .ok_or_else(|| ToriiError::AuthError("User not found".to_string()))?;

    println!("Session verified for user: {}", user.id);
    Ok(())
}

Session Types

Database Sessions (Default)

Sessions are stored in your database and can be revoked immediately:

use torii::ToriiBuilder;
use chrono::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Opaque sessions are the default - sessions are stored in the database
    let _torii = ToriiBuilder::new()
        .with_sqlite("sqlite::memory:")
        .await?
        .with_session_expiry(Duration::days(30))
        .apply_migrations(true)
        .build()
        .await?;

    Ok(())
}

JWT Sessions

Self-contained tokens that don't require database lookups:

use torii::{ToriiBuilder, JwtConfig};
use chrono::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create JWT configuration with HS256 algorithm
    // The secret must be at least 32 bytes for security
    let jwt_config = JwtConfig::new_hs256(
        b"your-secret-key-at-least-32-chars-long!".to_vec()
    )?
        .with_issuer("your-app-name")
        .with_metadata(true);

    let torii = ToriiBuilder::new()
        .with_sqlite("sqlite::memory:")
        .await?
        .with_jwt_sessions(jwt_config)
        .with_session_expiry(Duration::hours(2))
        .apply_migrations(true)
        .build()
        .await?;

    Ok(())
}

Web Framework Integration

Axum Integration

For quick web integration, use the torii-axum crate:

[dependencies]
torii-axum = { version = "0.5.0", features = ["password", "magic-link"] }
use std::sync::Arc;
use axum::{response::Json, routing::get, Router};
use torii::ToriiBuilder;
use torii_axum::{AuthUser, CookieConfig, LinkConfig};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Set up Torii using the builder pattern
    let torii = Arc::new(
        ToriiBuilder::new()
            .with_seaorm("sqlite::memory:")
            .await?
            .apply_migrations(true)
            .build()
            .await?
    );

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

    // Build your application with auth routes
    let app = Router::new()
        .nest("/auth", auth_routes)
        .route("/protected", get(protected_handler))
        .with_state(torii);

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

// Protected route handler
async fn protected_handler(user: AuthUser) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "user_id": user.id,
        "email": user.email
    }))
}

This provides automatic endpoints:

  • POST /auth/register - User registration
  • POST /auth/login - User login
  • POST /auth/magic-link - Request magic link email
  • POST /auth/magic-link/verify - Verify magic link
  • POST /auth/password/reset/request - Request password reset
  • GET /auth/user - Get current user
  • POST /auth/logout - User logout

For complete documentation on configuration options, middleware, and all available routes, see the Axum Integration guide.

Other Authentication Methods

Torii provides organized namespaces for different authentication methods:

  • torii.password(): Traditional email/password authentication
  • torii.oauth(): Social login (Google, GitHub, etc.)
  • torii.passkey(): Modern biometric authentication
  • torii.magic_link(): Email-based passwordless login

Each namespace contains focused methods for that authentication type.

OAuth Authentication

use torii::{Torii, ToriiError};
use torii_core::RepositoryProvider;

async fn start_oauth_flow<R: RepositoryProvider>(
    torii: &Torii<R>,
    provider: &str
) -> Result<String, ToriiError> {
    // Get the authorization URL for the provider
    let auth_url = torii.get_oauth_authorization_url(provider).await?;

    // Store the CSRF state in your session/cookies
    let csrf_state = auth_url.csrf_state;

    // Return the URL to redirect the user to
    Ok(auth_url.url)
}
// Send magic link email (requires mailer to be configured)
let token = torii.magic_link().send_link(
    "user@example.com",
    "https://example.com/auth/magic-link/verify"
).await?;

// Verify magic token (called when user clicks the link)
let (user, session) = torii.magic_link().authenticate(
    &token_from_url,
    Some("Browser".to_string()),
    Some("127.0.0.1".to_string())
).await?;

Examples

Check out the complete examples in the repository:

Next Steps

  • Learn about Core Concepts for deeper understanding
  • Explore different authentication methods
  • Configure production storage backends
  • Add email verification and password reset functionality

Remember: Torii gives you complete control over your user data while providing modern authentication features.

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

Core Concepts

Torii is an authentication framework that gives you control over your users' data while providing modern authentication features. Here are the essential concepts you need to understand.

Key Components

Torii consists of four main parts:

  • Torii Instance: The main coordinator that handles all authentication
  • Storage: Where user and session data is stored (SQLite, PostgreSQL, MySQL)
  • Authentication Methods: Password, OAuth, Passkeys, Magic Links
  • Sessions: How users stay authenticated after login

Users

Users are people who can authenticate with your application. Each user has:

  • Unique ID: A stable identifier that never changes
  • Email: Their email address (required)
  • Name: Optional display name
  • Verification Status: Whether their email is verified
  • Timestamps: When they were created and last updated

Sessions

Sessions keep users authenticated after they log in. Each session has:

  • Token: A secret string that identifies the session
  • User ID: Which user the session belongs to
  • Expiration: When the session expires
  • Client Info: Optional user agent and IP address

Session Types

Torii supports two session types:

  1. Database Sessions (default): Stored in your database, can be revoked immediately
  2. JWT Sessions: Self-contained tokens, fast but cannot be revoked

Authentication Methods

Torii supports multiple ways for users to authenticate:

  • Password: Traditional email/password login
  • OAuth: Social login (Google, GitHub, etc.)
  • Passkeys: Modern biometric authentication
  • Magic Links: Email-based passwordless login

Storage

Torii can store data in multiple databases:

  • SQLite: Great for development and small applications
  • PostgreSQL: Production-ready relational database
  • MySQL: Via SeaORM integration

All storage backends support all authentication methods.

Basic Usage

Here's the typical flow:

  1. Set up storage and create a Torii instance
  2. Register users with your chosen authentication method
  3. Users log in to create sessions
  4. Validate sessions to authenticate requests
  5. Users log out to end sessions

This simple foundation supports all of Torii's authentication features.