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 service architecture.
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 Service Architecture: Add custom authentication methods or storage backends
Storage Support
Authentication Method | SQLite | PostgreSQL | MySQL (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.4.0", features = ["password", "sqlite"] }
tokio = { version = "1", features = ["full"] }
The features you choose will depend on your authentication needs:
- Authentication methods:
password
: Email/password authenticationoauth
: OAuth/social login supportpasskey
: WebAuthn/passkey authenticationmagic-link
: Email magic link authentication
- Storage backends:
sqlite
: SQLite storagepostgres
: PostgreSQL storageseaorm
: SeaORM support with additional options (seaorm-sqlite
,seaorm-postgres
, orseaorm-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_storage_seaorm::SeaORMStorage; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Initialize the database connection let storage = SeaORMStorage::connect("sqlite://auth.db?mode=rwc").await?; // Run migrations to set up the database schema storage.migrate().await?; // Create repository provider and Torii instance let repositories = Arc::new(storage.into_repository_provider()); let torii = Arc::new(Torii::new(repositories)); // Now torii is ready to use for authentication Ok(()) }
Session Configuration
Torii supports two types of session management:
Opaque Sessions (Default)
Database-backed sessions with immediate revocation support:
#![allow(unused)] fn main() { use std::sync::Arc; use torii::{Torii, SessionConfig}; use chrono::Duration; let torii = Torii::new(repositories) .with_session_config( SessionConfig::default() .expires_in(Duration::days(30)) ); }
JWT Sessions
Stateless sessions with better performance for distributed systems:
#![allow(unused)] fn main() { use torii::{Torii, JwtConfig, SessionConfig}; use chrono::Duration; // Create JWT configuration 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 = Torii::new(repositories) .with_jwt_sessions(jwt_config); // Or with custom expiration let torii = Torii::new(repositories) .with_session_config( SessionConfig::default() .with_jwt(jwt_config) .expires_in(Duration::hours(2)) ); }
Complete Example
Here's a complete example with JWT sessions and password authentication:
use std::sync::Arc; use torii::Torii; use torii_storage_seaorm::SeaORMStorage; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Set up database let storage = SeaORMStorage::connect("sqlite://auth.db?mode=rwc").await?; storage.migrate().await?; // Create repository provider and Torii instance let repositories = Arc::new(storage.into_repository_provider()); let torii = Arc::new(Torii::new(repositories)); // Register a user let user = torii.register_user_with_password("user@example.com", "secure_password").await?; println!("User registered: {}", user.id); // Login and create session let (user, session) = torii.login_user_with_password( "user@example.com", "secure_password", Some("Mozilla/5.0 (compatible browser)".to_string()), Some("192.168.1.100".to_string()) ).await?; println!("Login successful!"); println!("User: {}", user.id); println!("Session token: {}", session.token); // Validate session let validated_session = torii.get_session(&session.token).await?; println!("Session valid for user: {}", validated_session.user_id); Ok(()) }
Authentication Methods
The example above shows password authentication. Torii supports multiple authentication methods:
- Password Authentication: Email/password with secure hashing
- OAuth/Social Login: Google, GitHub, and other OAuth providers
- Passkey/WebAuthn: Modern biometric authentication
- Magic Link: Passwordless email-based authentication
See the feature flags in your Cargo.toml
to enable additional authentication methods.
User Registration
To register a new user with password authentication:
#![allow(unused)] fn main() { 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.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}; 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.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}; 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(()) } }
OAuth Authentication Flow
For OAuth authentication, you'll need to implement these steps:
- 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) } }
- 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:
- 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(()) } }
- 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
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(()) } }
Web Framework Integration
Axum Integration
For Axum web applications, use the torii-axum
crate for plug-and-play authentication:
[dependencies]
torii-axum = { version = "0.4.0", features = ["password", "sqlite"] }
use std::sync::Arc; use axum::{response::Json, routing::get, Router}; use torii::Torii; use torii_axum::{AuthUser, CookieConfig}; use torii_storage_seaorm::SeaORMStorage; #[tokio::main] async fn main() -> anyhow::Result<()> { // Set up database and Torii let storage = SeaORMStorage::connect("sqlite::memory:").await?; storage.migrate().await?; let repositories = Arc::new(storage.into_repository_provider()); let torii = Arc::new(Torii::new(repositories)); // Create authentication routes with cookie configuration let auth_routes = torii_axum::routes(torii.clone()) .with_cookie_config(CookieConfig::development()) .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 for:
POST /auth/register
- User registrationPOST /auth/login
- User loginGET /auth/user
- Get current userPOST /auth/logout
- User logout- And more...
Example Applications
You can find complete examples in the repository:
- examples/axum-sqlite-password - Axum integration with SQLite and password authentication
- examples/todos - Complete todo application demonstrating Torii integration
These examples demonstrate:
- Setting up Torii with different storage backends
- Adding password authentication
- Creating web servers 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:
- Learn about Session Management - Choose between opaque and JWT sessions
- Configure JWT Sessions for stateless authentication
- Use Opaque Sessions for traditional session management
- 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.
Quick Reference
JWT Sessions (Stateless)
#![allow(unused)] fn main() { let jwt_config = JwtConfig::new_hs256(secret_key.to_vec()); let torii = Torii::new(repositories).with_jwt_sessions(jwt_config); }
Opaque Sessions (Stateful - Default)
#![allow(unused)] fn main() { let torii = Torii::new(repositories); // Default uses opaque sessions }
Environment Variables for Production
export JWT_SECRET="your-secret-key-at-least-32-characters-long"
export DATABASE_URL="sqlite://production.db"
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:
- The Torii Coordinator: The main
Torii
struct that coordinates all authentication activities - Storage Backends: Implementations for persisting user and session data
- Authentication Services: Modules for different authentication methods
- 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:
Field | Type | Description |
---|---|---|
id | UserId | The unique identifier for the user |
name | Option<String> | The user's name (optional) |
String | The user's email address | |
email_verified_at | Option<DateTime<Utc>> | Timestamp when the email was verified (if any) |
created_at | DateTime<Utc> | Timestamp when the user was created |
updated_at | DateTime<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 (uses base58-encoded IDs)
- 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:
Field | Type | Description |
---|---|---|
token | SessionToken | The unique token identifying the session |
user_id | UserId | The ID of the authenticated user |
user_agent | Option<String> | The user agent of the client that created the session |
ip_address | Option<String> | The IP address of the client that created the session |
created_at | DateTime<Utc> | Timestamp when the session was created |
expires_at | DateTime<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:
- Database Sessions (default): Sessions are stored in your database and can be individually revoked
- 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 service architecture:
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:
- 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
Magic Link Authentication
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
- SQLite: For development, testing, or small applications
- PostgreSQL: For production-ready applications requiring a robust database
- SeaORM: Supporting SQLite, PostgreSQL, and MySQL through the SeaORM ORM
Each storage backend implements the following core storage traits:
UserStorage
: For user managementSessionStorage
: For session managementPasswordStorage
: For password authenticationOAuthStorage
: For OAuth accountsPasskeyStorage
: For WebAuthn credentialsMagicLinkStorage
: For magic link tokens
Initialization Patterns
Torii provides several ways to initialize the system based on your application's needs:
-
Single Storage: Use the same storage for users and sessions
#![allow(unused)] fn main() { Torii::new(storage) }
-
Split Storage: Use different storage backends for users and sessions
#![allow(unused)] fn main() { Torii::with_storages(user_storage, session_storage) }
-
Custom Managers: Provide custom user and session managers
#![allow(unused)] fn main() { Torii::with_managers(user_storage, session_storage, user_manager, session_manager) }
-
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:
ServiceNotFound
: When an authentication service is not availableAuthError
: When authentication failsStorageError
: 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.
Session Management
Torii provides flexible session management through a session provider architecture that supports both stateful and stateless sessions. This allows you to choose the session strategy that best fits your application's requirements.
Session Provider Types
Torii supports two main types of session providers:
1. Opaque Sessions (Default)
- Database-backed: Session data is stored in your database
- Stateful: Requires database lookups for validation
- Revocable: Can be invalidated immediately by deleting from storage
- Best for: Traditional web applications, when you need immediate session revocation
2. JWT Sessions
- Self-contained: All session data is encoded in the token
- Stateless: No database lookup required for validation
- Performant: Fast validation with no storage overhead
- Best for: Microservices, APIs, distributed systems
Configuration
Default Configuration (Opaque Sessions)
By default, Torii uses opaque sessions backed by your database:
#![allow(unused)] fn main() { use torii::{Torii, SessionConfig}; use chrono::Duration; let torii = Torii::new(repositories) .with_session_config( SessionConfig::default() .expires_in(Duration::days(30)) ); }
JWT Sessions
To use JWT sessions, configure Torii with a JWT configuration:
#![allow(unused)] fn main() { use torii::{Torii, SessionConfig, JwtConfig}; use chrono::Duration; // Create JWT configuration 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); // Include IP and user agent in JWT let torii = Torii::new(repositories) .with_session_config( SessionConfig::default() .with_jwt(jwt_config) .expires_in(Duration::hours(24)) ); // Or use the convenience method: let torii = Torii::new(repositories) .with_jwt_sessions(jwt_config); }
Usage Examples
Creating Sessions
Session creation works the same regardless of provider type:
#![allow(unused)] fn main() { use torii::{Torii, UserId}; // Create a session for a user let session = torii.create_session( &user_id, Some("Mozilla/5.0 (compatible browser)".to_string()), // user agent Some("192.168.1.100".to_string()) // IP address ).await?; println!("Session token: {}", session.token); }
The session provider determines whether this creates:
- An opaque token (random string) + database record
- A JWT token with embedded session data
Validating Sessions
Session validation is also transparent:
#![allow(unused)] fn main() { use torii::{SessionToken, ToriiError}; async fn authenticate_request( torii: &Torii<impl RepositoryProvider>, session_token: &str ) -> Result<Session, ToriiError> { let token = SessionToken::new(session_token); // This works for both JWT and opaque tokens let session = torii.get_session(&token).await?; // Session is valid and not expired Ok(session) } }
For opaque tokens: Torii looks up the session in the database For JWT tokens: Torii validates the signature and expiration
Session Termination
#![allow(unused)] fn main() { // Delete a specific session torii.delete_session(&session_token).await?; // Delete all sessions for a user (useful for "log out everywhere") torii.delete_sessions_for_user(&user_id).await?; }
Note: For JWT sessions, delete_session
is a no-op since JWTs are stateless. The tokens remain valid until they expire naturally. To implement JWT revocation, you would need to maintain a blacklist.
JWT Algorithm Support
Torii supports both symmetric and asymmetric JWT algorithms:
HMAC with SHA-256 (HS256)
#![allow(unused)] fn main() { use torii::JwtConfig; let jwt_config = JwtConfig::new_hs256(b"your-secret-key-must-be-at-least-32-bytes-long!") .with_issuer("your-app") .with_metadata(true); }
RSA with SHA-256 (RS256)
#![allow(unused)] fn main() { use torii::JwtConfig; use std::fs; // Load RSA keys from PEM files let private_key = fs::read("private_key.pem")?; let public_key = fs::read("public_key.pem")?; let jwt_config = JwtConfig::new_rs256(private_key, public_key) .with_issuer("your-app") .with_metadata(true); // Or load from files directly let jwt_config = JwtConfig::from_rs256_pem_files( "private_key.pem", "public_key.pem" )?; }
Session Metadata
When using JWT sessions with metadata enabled, additional information is embedded in the token:
#![allow(unused)] fn main() { let jwt_config = JwtConfig::new_hs256(secret_key) .with_metadata(true); // Enable metadata // The resulting JWT will include: // - User ID (subject) // - Issued at time // - Expiration time // - Issuer (if specified) // - User agent (if provided during session creation) // - IP address (if provided during session creation) }
Performance Considerations
Aspect | Opaque Sessions | JWT Sessions |
---|---|---|
Creation | Database write required | CPU-only (signing) |
Validation | Database read required | CPU-only (verification) |
Revocation | Immediate (delete from DB) | Not supported* |
Token Size | Small (~32 chars) | Larger (~150-300 chars) |
Horizontal Scaling | Requires shared database | Fully stateless |
Security | Server-side secrets only | Signature verification |
*JWT revocation requires implementing a token blacklist or short expiration times.
Security Best Practices
For Opaque Sessions:
- Use HTTPS to protect tokens in transit
- Implement secure session storage (encrypted at rest)
- Set appropriate session timeouts
- Clear sessions on logout
For JWT Sessions:
- Use strong signing keys (≥32 bytes for HS256)
- Keep private keys secure and rotated
- Use short expiration times (hours, not days)
- Include
iss
(issuer) claims for validation - Validate all JWT claims on every request
- Consider token binding to prevent token theft
Migration Between Session Types
You can change session providers without breaking existing sessions by:
- Gradual migration: Accept both token types during transition
- Forced re-authentication: Require users to log in again
- Token conversion: Convert opaque tokens to JWTs during validation
#![allow(unused)] fn main() { // Example: Accept both token types during migration async fn validate_legacy_session( torii: &Torii<impl RepositoryProvider>, token_str: &str ) -> Result<Session, ToriiError> { let token = SessionToken::new(token_str); match torii.get_session(&token).await { Ok(session) => Ok(session), Err(_) => { // If modern validation fails, try legacy lookup // This allows graceful migration fallback_session_validation(token_str).await } } } }
Common Issues
"Expected Vec, found &[u8]" Error
When using HS256 JWT configuration, you may encounter this error:
#![allow(unused)] fn main() { // ❌ This will cause a compile error let jwt_config = JwtConfig::new_hs256(b"my-secret-key"); }
Solution: Add .to_vec()
to convert the byte slice:
#![allow(unused)] fn main() { // ✅ This works correctly let jwt_config = JwtConfig::new_hs256(b"my-secret-key-32-bytes-long!!!".to_vec()); }
Import Errors
If you can't import JwtConfig
, ensure you're using the correct path:
#![allow(unused)] fn main() { // ✅ Correct import from main crate use torii::JwtConfig; // ❌ Don't import from torii_core directly // use torii_core::JwtConfig; }
Next Steps
- Learn more about JWT Sessions
- Learn more about Opaque Sessions
- See Getting Started for complete examples
JWT Sessions
JWT (JSON Web Token) sessions provide a stateless authentication mechanism where all session information is encoded within the token itself. This eliminates the need for database lookups during session validation, making it ideal for high-performance applications and microservices.
When to Use JWT Sessions
JWT sessions are best suited for:
- Microservices architectures where session state sharing is complex
- High-traffic applications that need fast session validation
- Distributed systems without centralized session storage
- APIs that serve mobile or SPA clients
- Scenarios where session revocation is not critical
Configuration
Basic JWT Setup
#![allow(unused)] fn main() { use torii::{Torii, JwtConfig}; use chrono::Duration; // Simple HS256 configuration let jwt_config = JwtConfig::new_hs256(b"your-secret-key-at-least-32-bytes-long!".to_vec()); let torii = Torii::new(repositories) .with_jwt_sessions(jwt_config); }
Important: JWT secret keys must be at least 32 bytes long for HS256. Use .to_vec()
to convert byte slices to Vec<u8>
as required by the API.
Advanced JWT Configuration
#![allow(unused)] fn main() { use torii::{Torii, JwtConfig, SessionConfig}; use chrono::Duration; let jwt_config = JwtConfig::new_hs256(b"your-secret-key-at-least-32-bytes-long!".to_vec()) .with_issuer("your-application-name") // Add issuer claim .with_metadata(true); // Include IP and user agent let torii = Torii::new(repositories) .with_session_config( SessionConfig::default() .with_jwt(jwt_config) .expires_in(Duration::hours(2)) // Short-lived for security ); }
RSA Key Configuration
For production environments, RSA keys provide better security:
#![allow(unused)] fn main() { use torii::JwtConfig; use std::fs; // Load keys from files let jwt_config = JwtConfig::from_rs256_pem_files( "/path/to/private_key.pem", "/path/to/public_key.pem" )? .with_issuer("your-app") .with_metadata(true); // Or load keys manually let private_key = fs::read("/path/to/private_key.pem")?; let public_key = fs::read("/path/to/public_key.pem")?; let jwt_config = JwtConfig::new_rs256(private_key, public_key); }
JWT Token Structure
When using JWT sessions, Torii creates tokens with the following structure:
Standard Claims
{
"sub": "user_123456789", // Subject (User ID)
"iat": 1699123456, // Issued At (Unix timestamp)
"exp": 1699127056, // Expiration (Unix timestamp)
"iss": "your-application-name" // Issuer (optional)
}
With Metadata Enabled
{
"sub": "user_123456789",
"iat": 1699123456,
"exp": 1699127056,
"iss": "your-application-name",
"metadata": {
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"ip_address": "192.168.1.100"
}
}
Usage Examples
Creating JWT Sessions
#![allow(unused)] fn main() { use torii::{Torii, UserId, ToriiError}; async fn create_jwt_session( torii: &Torii<impl RepositoryProvider>, user_id: &UserId, user_agent: Option<String>, ip_address: Option<String> ) -> Result<String, ToriiError> { let session = torii.create_session(user_id, user_agent, ip_address).await?; // The token is a JWT string Ok(session.token.to_string()) } // Example usage let user_id = UserId::new("user_123"); let jwt_token = create_jwt_session( &torii, &user_id, Some("Mozilla/5.0 (compatible browser)".to_string()), Some("192.168.1.100".to_string()) ).await?; // JWT token looks like: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTY5OTEyMzQ1NiwiZXhwIjoxNjk5MTI3MDU2fQ.signature }
Validating JWT Sessions
#![allow(unused)] fn main() { use torii::{SessionToken, ToriiError, Session}; async fn validate_jwt_session( torii: &Torii<impl RepositoryProvider>, jwt_token: &str ) -> Result<Session, ToriiError> { let token = SessionToken::new(jwt_token); // Torii automatically detects this is a JWT and validates it let session = torii.get_session(&token).await?; // Session contains decoded information from the JWT println!("User ID: {}", session.user_id); println!("Expires at: {}", session.expires_at); println!("User agent: {:?}", session.user_agent); Ok(session) } }
Manual JWT Operations
For advanced use cases, you can work with JWTs directly:
#![allow(unused)] fn main() { use torii::{SessionToken, JwtConfig, Session, JwtClaims}; // Create a JWT manually let jwt_config = JwtConfig::new_hs256(b"your-secret-key-32-bytes-long!!!".to_vec()); let session = Session::builder() .user_id(user_id) .expires_at(Utc::now() + Duration::hours(2)) .build()?; let claims = session.to_jwt_claims(Some("your-app".to_string()), true); let jwt_token = SessionToken::new_jwt(&claims, &jwt_config)?; // Verify a JWT manually let verified_claims = jwt_token.verify_jwt(&jwt_config)?; println!("User: {}, Expires: {}", verified_claims.sub, verified_claims.exp); }
Authentication Middleware Example
Here's how to implement JWT authentication middleware for a web application:
#![allow(unused)] fn main() { use axum::{ extract::Request, http::{HeaderMap, StatusCode}, middleware::Next, response::Response, }; use torii::{Torii, SessionToken, RepositoryProvider}; pub async fn jwt_auth_middleware( headers: HeaderMap, mut request: Request, next: Next ) -> Result<Response, StatusCode> { // Extract JWT from Authorization header let auth_header = headers.get("authorization") .and_then(|header| header.to_str().ok()) .and_then(|header| header.strip_prefix("Bearer ")) .ok_or(StatusCode::UNAUTHORIZED)?; // Get Torii instance from app state let torii: &Torii<impl RepositoryProvider> = request .extensions() .get() .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; // Validate the JWT session let token = SessionToken::new(auth_header); let session = torii.get_session(&token).await .map_err(|_| StatusCode::UNAUTHORIZED)?; // Add session to request extensions for use in handlers request.extensions_mut().insert(session); Ok(next.run(request).await) } // Usage in Axum use axum::{Router, middleware}; let app = Router::new() .route("/protected", get(protected_handler)) .layer(middleware::from_fn(jwt_auth_middleware)) .with_state(torii); }
Security Considerations
Key Management
#![allow(unused)] fn main() { // ❌ Bad: Hardcoded secret let jwt_config = JwtConfig::new_hs256(b"hardcoded-secret".to_vec()); // ✅ Good: Environment variable let secret = std::env::var("JWT_SECRET") .expect("JWT_SECRET environment variable must be set"); let jwt_config = JwtConfig::new_hs256(secret.as_bytes().to_vec()); // ✅ Better: RSA keys for production let jwt_config = JwtConfig::from_rs256_pem_files( std::env::var("JWT_PRIVATE_KEY_PATH")?, std::env::var("JWT_PUBLIC_KEY_PATH")? )?; }
Token Expiration
#![allow(unused)] fn main() { use chrono::Duration; // ✅ Short-lived tokens for better security let torii = Torii::new(repositories) .with_session_config( SessionConfig::default() .with_jwt(jwt_config) .expires_in(Duration::hours(1)) // 1 hour max ); }
Validation Best Practices
#![allow(unused)] fn main() { async fn secure_jwt_validation( torii: &Torii<impl RepositoryProvider>, token_str: &str, expected_issuer: &str, max_age_hours: i64 ) -> Result<Session, ToriiError> { let token = SessionToken::new(token_str); let session = torii.get_session(&token).await?; // Additional validation if let SessionToken::Jwt(jwt_str) = &token { let config = JwtConfig::new_hs256(get_secret().to_vec()); let claims = token.verify_jwt(&config)?; // Validate issuer if claims.iss.as_deref() != Some(expected_issuer) { return Err(ToriiError::AuthError("Invalid issuer".to_string())); } // Validate token age let now = Utc::now().timestamp(); if now - claims.iat > max_age_hours * 3600 { return Err(ToriiError::AuthError("Token too old".to_string())); } } Ok(session) } }
Limitations and Considerations
No Session Revocation
#![allow(unused)] fn main() { // ❌ This doesn't actually invalidate JWT tokens torii.delete_session(&jwt_token).await?; // This is a no-op for JWTs // ✅ Workarounds: // 1. Use short expiration times // 2. Implement a token blacklist // 3. Force re-authentication by changing signing keys }
Token Size
JWTs are larger than opaque tokens:
#![allow(unused)] fn main() { // Opaque token: ~32 characters // "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" // JWT token: ~150-300 characters // "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTY5OTEyMzQ1NiwiZXhwIjoxNjk5MTI3MDU2fQ.signature" }
Consider this when:
- Storing tokens in cookies (size limits)
- Sending tokens in headers (HTTP limits)
- Mobile applications (bandwidth considerations)
Testing JWT Sessions
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use torii::{Torii, JwtConfig}; use chrono::Duration; #[tokio::test] async fn test_jwt_session_flow() { let repositories = setup_test_repositories().await; let jwt_config = JwtConfig::new_hs256(b"test-secret-key-32-bytes-long!!!".to_vec()) .with_issuer("test-app") .with_metadata(true); let torii = Torii::new(repositories) .with_jwt_sessions(jwt_config); // Create user and session let user = torii.register_user_with_password("test@example.com", "password123").await?; let session = torii.create_session( &user.id, Some("Test Agent".to_string()), Some("127.0.0.1".to_string()) ).await?; // Verify the token is a JWT assert!(session.token.as_str().contains('.')); // JWTs contain dots // Validate session let validated_session = torii.get_session(&session.token).await?; assert_eq!(validated_session.user_id, user.id); assert_eq!(validated_session.user_agent, Some("Test Agent".to_string())); } } }
RSA Key Generation
To generate RSA keys for production use:
# Generate private key
openssl genrsa -out private_key.pem 2048
# Generate public key
openssl rsa -in private_key.pem -pubout -out public_key.pem
# Verify the keys
openssl rsa -in private_key.pem -text -noout
Next Steps
- Learn about Opaque Sessions for comparison
- See Session Management for choosing between session types
- Review Getting Started for complete application examples
Opaque Sessions
Opaque sessions use random, non-meaningful tokens that reference session data stored in your database. This is the traditional session management approach and Torii's default behavior. The session token itself contains no information - it's just a secure random string used to look up session data.
When to Use Opaque Sessions
Opaque sessions are best suited for:
- Traditional web applications with server-side session management
- Applications requiring immediate session revocation (logout, security events)
- Scenarios with sensitive session metadata that shouldn't be in tokens
- Compliance requirements that mandate server-side session control
- Applications with long-lived sessions (weeks/months)
Configuration
Default Configuration
Opaque sessions are enabled by default when you create a Torii instance:
#![allow(unused)] fn main() { use torii::{Torii, SessionConfig}; use chrono::Duration; // Default configuration uses opaque sessions let torii = Torii::new(repositories); // Explicitly configure opaque sessions with custom expiration let torii = Torii::new(repositories) .with_session_config( SessionConfig::default() .expires_in(Duration::days(30)) ); }
Advanced Configuration
#![allow(unused)] fn main() { use torii::{Torii, SessionConfig, SessionProviderType}; use chrono::Duration; // Explicit opaque session configuration let session_config = SessionConfig { expires_in: Duration::days(7), provider_type: SessionProviderType::Opaque, }; let torii = Torii::new(repositories) .with_session_config(session_config); }
How Opaque Sessions Work
Session Creation Flow
- User authenticates successfully
- Torii generates a cryptographically secure random token
- Session data is stored in the database with the token as the key
- The opaque token is returned to the client
#![allow(unused)] fn main() { use torii::{Torii, UserId}; async fn create_opaque_session( torii: &Torii<impl RepositoryProvider>, user_id: &UserId ) -> Result<String, ToriiError> { let session = torii.create_session( user_id, Some("Mozilla/5.0 (compatible browser)".to_string()), Some("192.168.1.100".to_string()) ).await?; // The token is an opaque string like: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" println!("Opaque token: {}", session.token); Ok(session.token.to_string()) } }
Session Validation Flow
- Client sends the opaque token
- Torii looks up the session in the database using the token
- If found and not expired, the session is valid
- Session data is returned
#![allow(unused)] fn main() { use torii::{SessionToken, Session, ToriiError}; async fn validate_opaque_session( torii: &Torii<impl RepositoryProvider>, token_str: &str ) -> Result<Session, ToriiError> { let token = SessionToken::new(token_str); // Torii performs a database lookup let session = torii.get_session(&token).await?; // Session contains all stored information println!("User ID: {}", session.user_id); println!("Created: {}", session.created_at); println!("Expires: {}", session.expires_at); println!("User Agent: {:?}", session.user_agent); println!("IP Address: {:?}", session.ip_address); Ok(session) } }
Database Schema
Opaque sessions are stored in your database with the following structure:
-- SQLite example schema
CREATE TABLE sessions (
token TEXT PRIMARY KEY, -- The opaque token
user_id TEXT NOT NULL, -- Reference to users table
user_agent TEXT, -- Optional user agent string
ip_address TEXT, -- Optional IP address
created_at DATETIME NOT NULL, -- Session creation time
updated_at DATETIME NOT NULL, -- Last activity time
expires_at DATETIME NOT NULL, -- Expiration time
FOREIGN KEY (user_id) REFERENCES users (id)
);
-- Index for efficient user lookups
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
Usage Examples
Web Application Authentication
#![allow(unused)] fn main() { use axum::{ extract::{Request, State}, http::{HeaderMap, StatusCode}, middleware::Next, response::Response, }; use torii::{Torii, SessionToken, RepositoryProvider}; // Middleware for session-based authentication pub async fn session_auth_middleware( headers: HeaderMap, State(torii): State<Torii<impl RepositoryProvider>>, mut request: Request, next: Next ) -> Result<Response, StatusCode> { // Extract session token from cookie let session_token = headers .get("cookie") .and_then(|cookie| cookie.to_str().ok()) .and_then(|cookie_str| { cookie_str .split(';') .find(|cookie| cookie.trim().starts_with("session_token=")) .and_then(|cookie| cookie.split('=').nth(1)) }) .ok_or(StatusCode::UNAUTHORIZED)?; // Validate the opaque session token let token = SessionToken::new(session_token); let session = torii.get_session(&token).await .map_err(|_| StatusCode::UNAUTHORIZED)?; // Add session to request for use in handlers request.extensions_mut().insert(session); Ok(next.run(request).await) } }
Session Management Operations
#![allow(unused)] fn main() { use torii::{Torii, UserId, SessionToken, ToriiError}; // Create a new session async fn login_user( torii: &Torii<impl RepositoryProvider>, email: &str, password: &str ) -> Result<String, ToriiError> { // Authenticate user let (user, session) = torii.login_user_with_password( email, password, Some("Browser/1.0".to_string()), Some("192.168.1.1".to_string()) ).await?; Ok(session.token.to_string()) } // Logout - immediately invalidate session async fn logout_user( torii: &Torii<impl RepositoryProvider>, session_token: &str ) -> Result<(), ToriiError> { let token = SessionToken::new(session_token); // Immediately removes session from database torii.delete_session(&token).await?; Ok(()) } // Logout from all devices async fn logout_all_devices( torii: &Torii<impl RepositoryProvider>, user_id: &UserId ) -> Result<(), ToriiError> { // Removes all sessions for this user torii.delete_sessions_for_user(user_id).await?; Ok(()) } // Clean up expired sessions (run periodically) async fn cleanup_expired_sessions( torii: &Torii<impl RepositoryProvider> ) -> Result<(), ToriiError> { torii.session_service.cleanup_expired_sessions().await?; Ok(()) } }
Session Activity Tracking
#![allow(unused)] fn main() { use torii::{Session, SessionToken, ToriiError}; use chrono::Utc; async fn track_session_activity( torii: &Torii<impl RepositoryProvider>, session_token: &str, new_ip: Option<String> ) -> Result<(), ToriiError> { let token = SessionToken::new(session_token); let mut session = torii.get_session(&token).await?; // Update session activity session.updated_at = Utc::now(); if let Some(ip) = new_ip { session.ip_address = Some(ip); } // Save updated session back to database // Note: This requires direct repository access as Torii doesn't // expose session updates through the main API Ok(()) } }
Security Considerations
Token Generation
Torii generates cryptographically secure random tokens:
#![allow(unused)] fn main() { // Torii's token generation (internal implementation) use rand::{TryRngCore, rngs::OsRng}; use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD}; fn generate_secure_token() -> String { let mut bytes = vec![0u8; 32]; // 256 bits of entropy OsRng.try_fill_bytes(&mut bytes).unwrap(); BASE64_URL_SAFE_NO_PAD.encode(bytes) } }
Session Storage Security
#![allow(unused)] fn main() { // Example: Encrypt session data at rest use aes_gcm::{Aes256Gcm, Key, Nonce}; use aes_gcm::aead::{Aead, NewAead}; async fn store_encrypted_session( session: &Session, encryption_key: &[u8; 32] ) -> Result<(), ToriiError> { let cipher = Aes256Gcm::new(Key::from_slice(encryption_key)); let nonce = Nonce::from_slice(b"unique nonce"); // Use unique nonce let session_data = serde_json::to_vec(session)?; let encrypted_data = cipher.encrypt(nonce, session_data.as_ref()) .map_err(|e| ToriiError::StorageError(e.to_string()))?; // Store encrypted_data in database Ok(()) } }
Session Hijacking Prevention
#![allow(unused)] fn main() { async fn validate_session_security( torii: &Torii<impl RepositoryProvider>, token: &SessionToken, current_ip: &str, current_user_agent: &str ) -> Result<Session, ToriiError> { let session = torii.get_session(token).await?; // Check IP address consistency (optional - can be too strict) if let Some(session_ip) = &session.ip_address { if session_ip != current_ip { // Log suspicious activity log::warn!("IP address changed for session: {} -> {}", session_ip, current_ip); // Optionally invalidate session // torii.delete_session(token).await?; // return Err(ToriiError::AuthError("Session IP mismatch".to_string())); } } // Check user agent consistency if let Some(session_ua) = &session.user_agent { if session_ua != current_user_agent { log::warn!("User agent changed for session"); } } Ok(session) } }
Performance Optimization
Database Indexing
-- Essential indexes for opaque sessions
CREATE INDEX idx_sessions_token ON sessions(token); -- Primary lookup
CREATE INDEX idx_sessions_user_id ON sessions(user_id); -- User sessions
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); -- Cleanup queries
-- Composite index for user session management
CREATE INDEX idx_sessions_user_expires ON sessions(user_id, expires_at);
Connection Pooling
#![allow(unused)] fn main() { use sqlx::SqlitePool; use torii::SqliteRepositoryProvider; // Use connection pooling for better performance let pool = SqlitePool::connect_with( sqlx::sqlite::SqliteConnectOptions::new() .filename("sessions.db") .create_if_missing(true) ).await?; // Configure pool settings pool.set_max_connections(20); pool.set_min_connections(5); let repositories = SqliteRepositoryProvider::new(pool); }
Caching Strategy
#![allow(unused)] fn main() { use std::collections::HashMap; use std::sync::{Arc, RwLock}; use chrono::{DateTime, Utc}; // Simple in-memory session cache #[derive(Clone)] pub struct SessionCache { cache: Arc<RwLock<HashMap<String, (Session, DateTime<Utc>)>>>, ttl_seconds: u64, } impl SessionCache { pub fn new(ttl_seconds: u64) -> Self { Self { cache: Arc::new(RwLock::new(HashMap::new())), ttl_seconds, } } pub fn get(&self, token: &str) -> Option<Session> { let cache = self.cache.read().unwrap(); cache.get(token).and_then(|(session, cached_at)| { if Utc::now().signed_duration_since(*cached_at).num_seconds() < self.ttl_seconds as i64 { Some(session.clone()) } else { None } }) } pub fn set(&self, token: String, session: Session) { let mut cache = self.cache.write().unwrap(); cache.insert(token, (session, Utc::now())); } } }
Monitoring and Analytics
Session Metrics
#![allow(unused)] fn main() { use prometheus::{Counter, Histogram, Gauge}; lazy_static! { static ref SESSION_CREATIONS: Counter = Counter::new( "torii_sessions_created_total", "Total number of sessions created" ).expect("metric can be created"); static ref SESSION_VALIDATIONS: Counter = Counter::new( "torii_sessions_validated_total", "Total number of session validations" ).expect("metric can be created"); static ref SESSION_VALIDATION_DURATION: Histogram = Histogram::new( "torii_session_validation_duration_seconds", "Time spent validating sessions" ).expect("metric can be created"); static ref ACTIVE_SESSIONS: Gauge = Gauge::new( "torii_active_sessions", "Number of currently active sessions" ).expect("metric can be created"); } async fn monitored_session_validation( torii: &Torii<impl RepositoryProvider>, token: &SessionToken ) -> Result<Session, ToriiError> { let timer = SESSION_VALIDATION_DURATION.start_timer(); let result = torii.get_session(token).await; timer.observe_duration(); match &result { Ok(_) => SESSION_VALIDATIONS.inc(), Err(_) => { // Track validation failures log::warn!("Session validation failed for token"); } } result } }
Testing Opaque Sessions
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use torii::{Torii, SessionConfig}; use chrono::Duration; #[tokio::test] async fn test_opaque_session_lifecycle() { let repositories = setup_test_repositories().await; let torii = Torii::new(repositories) .with_session_config( SessionConfig::default() .expires_in(Duration::minutes(30)) ); // Create user and session let user = torii.register_user_with_password("test@example.com", "password123").await?; let session = torii.create_session( &user.id, Some("Test Agent".to_string()), Some("127.0.0.1".to_string()) ).await?; // Verify token is opaque (not a JWT) assert!(!session.token.as_str().contains('.')); assert_eq!(session.token.as_str().len(), 43); // Base64 encoded 32 bytes // Validate session let validated_session = torii.get_session(&session.token).await?; assert_eq!(validated_session.user_id, user.id); assert_eq!(validated_session.user_agent, Some("Test Agent".to_string())); // Delete session torii.delete_session(&session.token).await?; // Verify session is gone let result = torii.get_session(&session.token).await; assert!(result.is_err()); } #[tokio::test] async fn test_session_expiration() { let repositories = setup_test_repositories().await; let torii = Torii::new(repositories) .with_session_config( SessionConfig::default() .expires_in(Duration::seconds(1)) ); let user = torii.register_user_with_password("test@example.com", "password123").await?; let session = torii.create_session(&user.id, None, None).await?; // Session should be valid immediately assert!(torii.get_session(&session.token).await.is_ok()); // Wait for expiration tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; // Session should now be expired let result = torii.get_session(&session.token).await; assert!(result.is_err()); } } }
Next Steps
- Learn about JWT Sessions for comparison
- See Session Management for choosing between session types
- Review Getting Started for complete application examples
- Explore performance optimization techniques for high-traffic applications