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 Method | SQLite | PostgreSQL | MySQL |
|---|---|---|---|
| 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 authenticationoauth- OAuth/social loginpasskey- WebAuthn/passkey authenticationmagic-link- Email magic link authentication
Storage Backends:
sqlite- SQLite storagepostgres- PostgreSQL storageseaorm- 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 registrationPOST /auth/login- User loginPOST /auth/magic-link- Request magic link emailPOST /auth/magic-link/verify- Verify magic linkPOST /auth/password/reset/request- Request password resetGET /auth/user- Get current userPOST /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 authenticationtorii.oauth(): Social login (Google, GitHub, etc.)torii.passkey(): Modern biometric authenticationtorii.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)
}
Magic Link Authentication
// 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:
- examples/axum-example - Complete web server with authentication and email support
- examples/todos - Complete todo application
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 routesmagic-link- Magic link (passwordless) authentication routesoauth- 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
Cookie 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("/"); }
| Option | Default | Description |
|---|---|---|
name | "session_id" | Cookie name |
http_only | true | Prevents JavaScript access |
secure | true | Only sent over HTTPS |
same_site | Lax | CSRF protection level |
path | "/" | Cookie path |
Link Configuration
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_prefixmust 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)
| Method | Path | Description |
|---|---|---|
GET | /health | Health check |
GET | /session | Get current session |
GET | /user | Get current user |
POST | /logout | Logout (also DELETE /session) |
Password Routes (feature = "password")
| Method | Path | Description |
|---|---|---|
POST | /register | Register new user |
POST | /login | Login with email/password |
POST | /password | Change password (requires auth) |
POST | /password/reset/request | Request password reset email |
POST | /password/reset/verify | Verify reset token is valid |
POST | /password/reset/confirm | Complete password reset |
Magic Link Routes (feature = "magic-link")
| Method | Path | Description |
|---|---|---|
POST | /magic-link | Request magic link email |
POST | /magic-link/verify | Verify 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"}'
Request Magic Link
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
Verify Magic Link
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:
| Variable | Description | Default |
|---|---|---|
MAILER_SMTP_HOST | SMTP server hostname | (file transport) |
MAILER_SMTP_PORT | SMTP server port | 587 |
MAILER_SMTP_USERNAME | SMTP username | - |
MAILER_SMTP_PASSWORD | SMTP password | - |
MAILER_SMTP_TLS | TLS mode: none, starttls, tls | starttls |
MAILER_FROM_ADDRESS | Sender email address | noreply@example.com |
MAILER_FROM_NAME | Sender display name | - |
MAILER_APP_NAME | Application name (in emails) | Your App |
MAILER_APP_URL | Application URL (in emails) | https://example.com |
MAILER_FILE_OUTPUT_DIR | Directory for file transport | ./emails |
Error Handling
All routes return structured JSON errors:
{
"error": "Invalid credentials",
"code": 401
}
Common error codes:
| Code | Meaning |
|---|---|
| 400 | Bad request (validation error) |
| 401 | Unauthorized (not authenticated or invalid credentials) |
| 404 | Not found (user or session) |
| 409 | Conflict (email already registered) |
| 500 | Internal 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:
- Database Sessions (default): Stored in your database, can be revoked immediately
- 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:
- Set up storage and create a Torii instance
- Register users with your chosen authentication method
- Users log in to create sessions
- Validate sessions to authenticate requests
- Users log out to end sessions
This simple foundation supports all of Torii's authentication features.