Introduction to Torii

Torii is a powerful authentication framework for Rust applications that gives you complete control over your users' data. Unlike hosted solutions like Auth0, Clerk, or WorkOS that store user information in their cloud, Torii lets you own and manage your authentication stack while providing modern auth features through a flexible plugin system.

With Torii, you get the best of both worlds - 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 (using SeaORM)
  • JWT Support: Optional stateless JWT sessions
  • Extensible Plugin System: Add custom authentication methods or storage backends

Storage Support

PluginSQLitePostgreSQLMySQL (using SeaORM)
Password
OAuth2/OIDC
Passkey
Magic Link

Getting Started with Torii

This guide will walk you through the process of integrating Torii into your Rust application. By the end, you'll have a fully functional authentication system supporting multiple authentication methods.

Prerequisites

Before you begin, make sure you have:

  • A Rust project set up with Cargo
  • Basic understanding of async Rust (Torii uses async/await)
  • Database setup for your preferred storage backend (SQLite, PostgreSQL, or MySQL)

Installation

Add Torii to your Cargo.toml file:

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

The features you choose will depend on your authentication needs:

  • Authentication methods:
    • password: Email/password authentication
    • oauth: OAuth/social login support
    • passkey: WebAuthn/passkey authentication
    • magic-link: Email magic link authentication
  • Storage backends:
    • sqlite: SQLite storage
    • postgres: PostgreSQL storage
    • seaorm: SeaORM support with additional options (seaorm-sqlite, seaorm-postgres, or seaorm-mysql)

Basic Setup

Here's a minimal example to set up Torii with a SQLite database and password authentication:

use std::sync::Arc;
use torii::Torii;
use torii::SqliteStorage;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize the storage backend
    let storage = Arc::new(SqliteStorage::connect("sqlite://auth.db?mode=rwc").await?);
    
    // Migrate the storage schema
    storage.migrate().await?;

    // Create a Torii instance with password authentication
    let torii = Torii::new(storage)
        .with_password_plugin();

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

Different Initialization Options

Torii provides several ways to initialize the authentication system:

use std::sync::Arc;
use torii::Torii;
use torii::SqliteStorage;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let storage = Arc::new(SqliteStorage::connect("sqlite://auth.db?mode=rwc").await?);
    storage.migrate().await?;

    // 1. Simplest approach with a single storage backend
    let torii = Torii::new(storage.clone())
        .with_password_plugin();

    // 2. If you want separate storage for sessions (e.g., Redis):
    // let user_storage = storage.clone();
    // let session_storage = Arc::new(RedisStorage::connect("redis://localhost").await?);
    // let torii = Torii::with_storages(user_storage, session_storage)
    //     .with_password_plugin();

    // 3. If you need custom managers for additional behavior:
    // use torii_core::{DefaultUserManager, DefaultSessionManager, UserManager, SessionManager};
    // let user_manager: Arc<dyn UserManager + Send + Sync> = Arc::new(CustomUserManager::new(storage.clone()));
    // let session_manager: Arc<dyn SessionManager + Send + Sync> = Arc::new(DefaultSessionManager::new(storage.clone()));
    // let torii = Torii::with_managers(storage.clone(), storage.clone(), user_manager, session_manager);

    // 4. If your managers fully encapsulate their storage and you don't need plugins:
    // let user_manager: Arc<dyn UserManager + Send + Sync> = Arc::new(MyUserManager::new(my_db_conn.clone()));
    // let session_manager: Arc<dyn SessionManager + Send + Sync> = Arc::new(RedisSessionManager::new("redis://localhost"));
    // let torii = Torii::<()>::with_custom_managers(user_manager, session_manager);

    Ok(())
}

Adding Multiple Authentication Methods

Torii's plugin system makes it easy to add multiple authentication methods:

use torii::{Torii, Provider, SeaORMStorage};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let storage = Arc::new(SeaORMStorage::connect("sqlite://auth.db?mode=rwc").await?);
    storage.migrate().await?;

    let torii = Torii::new(storage)
        // Password authentication
        .with_password_plugin()

        // OAuth providers
        .with_oauth_provider(Provider::google(
            "YOUR_GOOGLE_CLIENT_ID",
            "YOUR_GOOGLE_CLIENT_SECRET",
            "https://your-app.com/auth/callback/google"
        ))
        .with_oauth_provider(Provider::github(
            "YOUR_GITHUB_CLIENT_ID",
            "YOUR_GITHUB_CLIENT_SECRET",
            "https://your-app.com/auth/callback/github"
        ))

        // Passkey/WebAuthn
        .with_passkey_plugin(
            "your-app.com",  // Relying Party ID
            "https://your-app.com"  // Relying Party Origin
        )

        // Magic Link authentication
        .with_magic_link_plugin();

    // Now torii is ready to use with multiple authentication methods
    Ok(())
}

Optional JWT Sessions

Torii supports JWT-based sessions for stateless authentication:

use torii::{Torii, SqliteStorage, JwtConfig};
use chrono::Duration;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let storage = Arc::new(SqliteStorage::connect("sqlite://auth.db?mode=rwc").await?);
    storage.migrate().await?;

    // Configure with JWT sessions using HS256 signing
    let jwt_config = JwtConfig::hs256("your-secret-key");
    
    let torii = Torii::new(storage)
        .with_password_plugin()
        .with_jwt_sessions(jwt_config);
    
    // Alternatively, you can configure session expiration
    // let torii = Torii::new(storage)
    //     .with_password_plugin()
    //     .with_session_config(SessionConfig::default().expires_in(Duration::days(7)));

    Ok(())
}

User Registration

To register a new user with password authentication:

#![allow(unused)]
fn main() {
use torii::{Torii, ToriiError};

async fn register_user(
    torii: &Torii<impl torii_core::storage::UserStorage + torii_core::storage::PasswordStorage>,
    email: &str,
    password: &str
) -> Result<(), ToriiError> {
    // Register a new user
    let user = torii.register_user_with_password(email, password).await?;

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

User Login

To authenticate a user with password:

#![allow(unused)]
fn main() {
use torii::{Torii, ToriiError};

async fn login_user(
    torii: &Torii<impl torii_core::storage::UserStorage + torii_core::storage::PasswordStorage>,
    email: &str,
    password: &str
) -> Result<(), ToriiError> {
    // Authenticate user - optional user_agent and ip_address for tracking
    let (user, session) = torii.login_user_with_password(
        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);
    println!("Session token: {}", session.token);

    Ok(())
}
}

Session Verification

To verify a user's session token:

#![allow(unused)]
fn main() {
use torii::{Torii, SessionToken, ToriiError};

async fn verify_session(
    torii: &Torii<impl torii_core::storage::UserStorage>,
    session_token: &str
) -> Result<(), ToriiError> {
    // Parse the session token
    let token = SessionToken::new(session_token.to_string());
    
    // Verify and get session data
    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(())
}
}

OAuth Authentication Flow

For OAuth authentication, you'll need to implement these steps:

  1. Generate an authorization URL:
#![allow(unused)]
fn main() {
use torii::{Torii, ToriiError};

async fn start_oauth_flow(
    torii: &Torii<impl torii_core::storage::UserStorage + torii_core::storage::OAuthStorage>,
    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)
}
}
  1. Handle the OAuth callback:
#![allow(unused)]
fn main() {
use torii::{Torii, ToriiError};

async fn handle_oauth_callback(
    torii: &Torii<impl torii_core::storage::UserStorage + torii_core::storage::OAuthStorage>,
    provider: &str,
    code: &str,
    state: &str
) -> Result<(), ToriiError> {
    // Exchange the code for tokens and authenticate the user
    let (user, session) = torii.exchange_oauth_code(
        provider, 
        code, 
        state,
        Some("Browser User Agent".to_string()),
        Some("127.0.0.1".to_string())
    ).await?;

    println!("OAuth user authenticated: {}", user.id);
    println!("Session token: {}", session.token);

    Ok(())
}
}

Passkey Authentication

Passkey (WebAuthn) authentication is performed in two steps:

  1. Start registration:
#![allow(unused)]
fn main() {
use torii::{Torii, ToriiError};

async fn start_passkey_registration(
    torii: &Torii<impl torii_core::storage::UserStorage + torii_core::storage::PasskeyStorage>,
    email: &str
) -> Result<(), ToriiError> {
    // Begin the passkey registration
    let options = torii.begin_passkey_registration(email).await?;
    
    // Return the challenge ID and WebAuthn options to the client for processing
    println!("Challenge ID: {}", options.challenge_id);
    println!("WebAuthn Options: {}", serde_json::to_string(&options.options).unwrap());
    
    Ok(())
}
}
  1. Complete registration:
#![allow(unused)]
fn main() {
use torii::{Torii, ChallengeId, PasskeyRegistrationCompletion, ToriiError};

async fn complete_passkey_registration(
    torii: &Torii<impl torii_core::storage::UserStorage + torii_core::storage::PasskeyStorage>,
    email: &str,
    challenge_id: &str,
    response: serde_json::Value
) -> Result<(), ToriiError> {
    // Complete the passkey registration
    let completion = PasskeyRegistrationCompletion {
        email: email.to_string(),
        challenge_id: ChallengeId::new(challenge_id.to_string()),
        response,
    };
    
    let user = torii.complete_passkey_registration(&completion).await?;
    println!("User registered with passkey: {}", user.id);
    
    Ok(())
}
}

Magic link authentication is useful for passwordless email-based login:

#![allow(unused)]
fn main() {
use torii::{Torii, ToriiError};

async fn send_magic_link(
    torii: &Torii<impl torii_core::storage::UserStorage + torii_core::storage::MagicLinkStorage>,
    email: &str
) -> Result<(), ToriiError> {
    // Generate a magic token
    let token = torii.generate_magic_token(email).await?;
    
    // Create a magic link URL (this would typically be sent via email)
    let magic_link = format!("https://your-app.com/auth/magic-link?token={}", token.token);
    println!("Magic Link: {}", magic_link);
    
    Ok(())
}

async fn verify_magic_link(
    torii: &Torii<impl torii_core::storage::UserStorage + torii_core::storage::MagicLinkStorage>,
    token: &str
) -> Result<(), ToriiError> {
    // Verify the magic token and create a session
    let (user, session) = torii.verify_magic_token(
        token,
        Some("Browser User Agent".to_string()),
        Some("127.0.0.1".to_string())
    ).await?;
    
    println!("User authenticated via magic link: {}", user.id);
    println!("Session token: {}", session.token);
    
    Ok(())
}
}

Example Application

You can find a complete example of a Todo application using Torii with Axum in the examples/todos directory. This example demonstrates:

  • Setting up Torii with SQLite
  • Adding password authentication
  • Creating a web server with Axum
  • Implementing user registration and login
  • Managing authenticated sessions with cookies

Next Steps

Now that you have a basic understanding of how to use Torii, you can:

  • Integrate Torii with your web framework (Axum, Actix, Rocket, etc.)
  • Learn about the core concepts of Users and Sessions
  • Explore each authentication method in more depth
  • Configure a storage backend for production use

Remember that Torii is designed to give you flexibility while maintaining control over your user data.

Core Concepts

Torii is built around several core concepts that form the foundation of the authentication system. Understanding these concepts is essential for effectively implementing and extending Torii in your applications.

Main Components

The Torii framework consists of several key components:

  1. The Torii Coordinator: The main Torii struct that coordinates all authentication activities
  2. Storage Backends: Implementations for persisting user and session data
  3. Authentication Plugins: Modules for different authentication methods
  4. User and Session Management: APIs for creating and verifying sessions

Users

Users are the central entity in the Torii authentication system. Each user represents an individual who can authenticate with your application.

User Structure

The core User struct contains the following fields:

FieldTypeDescription
idUserIdThe unique identifier for the user
nameOption<String>The user's name (optional)
emailStringThe user's email address
email_verified_atOption<DateTime<Utc>>Timestamp when the email was verified (if any)
created_atDateTime<Utc>Timestamp when the user was created
updated_atDateTime<Utc>Timestamp when the user was last updated

User IDs

Each user has a unique UserId that identifies them in the system. This ID is:

  • Stable and will not change during the user's lifetime
  • Treated as an opaque identifier rather than a specific format (though it uses UUIDs internally by default)
  • Used to link user accounts to authentication methods, sessions, and application data

Sessions

Sessions represent authenticated user sessions and are created when a user successfully logs in.

Session Structure

The Session struct contains the following fields:

FieldTypeDescription
tokenSessionTokenThe unique token identifying the session
user_idUserIdThe ID of the authenticated user
user_agentOption<String>The user agent of the client that created the session
ip_addressOption<String>The IP address of the client that created the session
created_atDateTime<Utc>Timestamp when the session was created
expires_atDateTime<Utc>Timestamp when the session will expire

Session Tokens

Each session is identified by a unique SessionToken that:

  • Functions as a bearer token or cookie for authentication
  • Should be kept secret and transmitted securely (e.g., via HTTPS)
  • Has an expiration time after which it will no longer be valid
  • Can be revoked to force a user to log out

Session Types

Torii supports two types of sessions:

  1. Database Sessions (default): Sessions are stored in your database and can be individually revoked
  2. JWT Sessions (optional): Stateless sessions using JWT tokens that don't require database lookups but cannot be individually revoked

Authentication Methods

Torii provides several authentication methods through its plugin system:

Password Authentication

Traditional email/password authentication with secure password hashing.

Key features:

  • Argon2id password hashing
  • Email verification capabilities
  • Password reset functionality

OAuth Authentication

Social login and OpenID Connect support for external identity providers.

Supported providers:

  • Google
  • GitHub
  • More providers can be added

Passkey Authentication (WebAuthn)

Passwordless authentication using the Web Authentication API (WebAuthn).

Key features:

  • FIDO2-compliant
  • Supports hardware security keys, platform authenticators (Windows Hello, Touch ID, etc.)
  • Challenge-response authentication flow

Email-based passwordless authentication using one-time tokens.

Key features:

  • Generates secure tokens
  • Time-limited validation
  • Simple user experience

Storage System

Torii abstracts the storage layer through traits, allowing different storage backend implementations:

Available Storage Backends

  1. SQLite: For development, testing, or small applications
  2. PostgreSQL: For production-ready applications requiring a robust database
  3. SeaORM: Supporting SQLite, PostgreSQL, and MySQL through the SeaORM ORM

Each storage backend implements the following core storage traits:

  • UserStorage: For user management
  • SessionStorage: For session management
  • PasswordStorage: For password authentication
  • OAuthStorage: For OAuth accounts
  • PasskeyStorage: For WebAuthn credentials
  • MagicLinkStorage: For magic link tokens

Initialization Patterns

Torii provides several ways to initialize the system based on your application's needs:

  1. Single Storage: Use the same storage for users and sessions

    #![allow(unused)]
    fn main() {
    Torii::new(storage)
    }
  2. Split Storage: Use different storage backends for users and sessions

    #![allow(unused)]
    fn main() {
    Torii::with_storages(user_storage, session_storage)
    }
  3. Custom Managers: Provide custom user and session managers

    #![allow(unused)]
    fn main() {
    Torii::with_managers(user_storage, session_storage, user_manager, session_manager)
    }
  4. Stateless Managers: Use custom managers without storage

    #![allow(unused)]
    fn main() {
    Torii::with_custom_managers(user_manager, session_manager)
    }

Error Handling

Torii uses a structured error system with the ToriiError enum that includes:

  • PluginNotFound: When an authentication plugin is not available
  • AuthError: When authentication fails
  • StorageError: When there's an issue with the storage backend

Understanding these core concepts provides the foundation for working with Torii's authentication flows in your applications.