Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction to Torii

Torii is 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 MethodSQLitePostgreSQLMySQL (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 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_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:

  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(())
}
}

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 registration
  • POST /auth/login - User login
  • GET /auth/user - Get current user
  • POST /auth/logout - User logout
  • And more...

Example Applications

You can find complete examples in the repository:

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:

  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 Services: 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 (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:

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 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:

  • 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:

  • ServiceNotFound: When an authentication service 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.

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

AspectOpaque SessionsJWT Sessions
CreationDatabase write requiredCPU-only (signing)
ValidationDatabase read requiredCPU-only (verification)
RevocationImmediate (delete from DB)Not supported*
Token SizeSmall (~32 chars)Larger (~150-300 chars)
Horizontal ScalingRequires shared databaseFully stateless
SecurityServer-side secrets onlySignature 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:

  1. Gradual migration: Accept both token types during transition
  2. Forced re-authentication: Require users to log in again
  3. 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

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

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

  1. User authenticates successfully
  2. Torii generates a cryptographically secure random token
  3. Session data is stored in the database with the token as the key
  4. 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

  1. Client sends the opaque token
  2. Torii looks up the session in the database using the token
  3. If found and not expired, the session is valid
  4. 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