Authentication

SwirlDB provides pluggable authentication through the AuthProvider trait. All auth providers work on all platforms (browser + server) and integrate with the policy engine for fine-grained access control.

Authentication Architecture

Actor-Based Identity

Authentication providers return an Actor representing the current user, app, or server:

trait AuthProvider {
    fn get_actor(&self) -> Actor;
    fn set_actor(&mut self, actor: Actor);
}

pub struct Actor {
    pub actor_type: ActorType,  // User | App | Server
    pub id: String,
    pub org_id: Option<String>,
    pub team_id: Option<String>,
    pub app_id: Option<String>,
    pub role: Option<String>,
    pub claims: HashMap<String, serde_json::Value>,
}

Built-in Providers

AnonymousAuth (Default)

Always returns an anonymous actor. Use for public-facing apps or when authentication is handled externally.

// Rust
use swirldb_core::auth::AnonymousAuth;

let auth = AnonymousAuth::new();
let actor = auth.get_actor();

assert_eq!(actor.actor_type, ActorType::User);
assert_eq!(actor.id, "anonymous");

// TypeScript
const auth = new AnonymousAuth();
const actor = auth.getActor();

Use case: Public apps, unauthenticated access, guest users

StaticAuth

Fixed user ID. Suitable for development, single-user apps, or when identity is determined at startup.

// Rust - Simple user ID
use swirldb_core::auth::StaticAuth;

let auth = StaticAuth::new("alice");

// Rust - With metadata
let auth = StaticAuth::with_metadata(
    "alice",
    Some("acme-corp".to_string()),  // org_id
    Some("engineering".to_string()), // team_id
    Some("admin".to_string()),       // role
);

// Rust - App actor
let auth = StaticAuth::app("my-app-id");

// Rust - Server actor
let auth = StaticAuth::server("server-01");

// TypeScript/Browser - Generate ID on first load
let userId = localStorage.getItem('userId');
if (!userId) {
    userId = crypto.randomUUID();
    localStorage.setItem('userId', userId);
}
const auth = StaticAuth.new(userId);

Use case: Development, single-user apps, CLI tools, embedded devices

JwtAuth

Decodes JWT claims to extract actor identity. Important: This only decodes the JWT, it does NOT validate signatures. Signature validation should be done server-side.

// Rust - From pre-validated token
use swirldb_core::auth::JwtAuth;

let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
let auth = JwtAuth::from_token(token)?;
let actor = auth.get_actor();

// Rust - From pre-parsed claims
let claims = HashMap::from([
    ("type".to_string(), json!("user")),
    ("sub".to_string(), json!("alice")),
    ("org_id".to_string(), json!("acme-corp")),
]);
let auth = JwtAuth::from_claims(claims, token)?;

// Rust - Refresh token
auth.refresh_token(new_token)?;

// TypeScript/Browser - Fetch JWT from server
const response = await fetch('/api/auth/login', {
    method: 'POST',
    body: JSON.stringify({ username, password })
});
const { token } = await response.json();
const auth = JwtAuth.fromToken(token);

// Store token for future sessions
localStorage.setItem('auth-token', token);

JWT Claims Mapping:

  • typeactor_type (user, app, server)
  • subid (subject/user ID)
  • org_idorg_id
  • team_idteam_id
  • app_idapp_id
  • rolerole
  • All other claims stored in claims map

Use case: Multi-user apps, SaaS platforms, API authentication

Browser Usage Patterns

Persistent Anonymous Identity

import { SwirlDB, StaticAuth } from '@swirldb/js';

// Generate stable anonymous ID
let userId = localStorage.getItem('anon-user-id');
if (!userId) {
    userId = crypto.randomUUID();
    localStorage.setItem('anon-user-id', userId);
}

const auth = StaticAuth.new(userId);
const db = await SwirlDB.withLocalStorage('my-app', { auth });

// User has stable identity across sessions
console.log('User ID:', auth.getActor().id);

JWT Authentication Flow

// 1. Check for stored token
let token = localStorage.getItem('auth-token');
let auth;

if (token) {
    // Try to use existing token
    try {
        auth = JwtAuth.fromToken(token);

        // TODO: Check if token is expired (client-side check)
        // Server should validate signature + expiration
    } catch (e) {
        console.error('Invalid token:', e);
        token = null;
    }
}

if (!token) {
    // 2. No token - redirect to login
    window.location.href = '/login';
}

// 3. Create DB with authenticated user
const db = await SwirlDB.withIndexedDB('my-app', { auth });

// 4. Sync with server using JWT
db.sync('wss://api.example.com/sync', {
    headers: {
        'Authorization': `Bearer ${auth.token()}`
    }
});

Token Refresh Strategy

// Refresh token before expiration
async function refreshAuthToken(auth) {
    const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${auth.token()}`
        }
    });

    const { token: newToken } = await response.json();

    // Update auth provider
    auth.refreshToken(newToken);

    // Update storage
    localStorage.setItem('auth-token', newToken);
}

// Refresh every 50 minutes (assuming 1hr token lifetime)
setInterval(() => refreshAuthToken(auth), 50 * 60 * 1000);

Server Usage Patterns

Config-Based Authentication

use swirldb_core::{SwirlDB, auth::StaticAuth};
use swirldb_server::storage::RedbAdapter;

// Load from environment or config file
let server_id = std::env::var("SERVER_ID")
    .unwrap_or_else(|_| "server-01".to_string());

let auth = StaticAuth::server(&server_id);
let storage = Arc::new(RedbAdapter::new("./data")?);
let db = SwirlDB::with_storage_and_auth(storage, "db", auth).await;

// Server actor for internal operations
println!("Running as: {:?}", db.get_actor());

JWT Validation (Server-Side)

use jsonwebtoken::{decode, Validation, DecodingKey};
use swirldb_core::auth::JwtAuth;

// Extract JWT from HTTP header
let token = extract_bearer_token(&req)?;

// STEP 1: Validate signature (critical!)
let secret = std::env::var("JWT_SECRET")?;
let key = DecodingKey::from_secret(secret.as_bytes());
let validation = Validation::default();

let token_data = decode::<HashMap<String, serde_json::Value>>(
    &token,
    &key,
    &validation,
)?;

// STEP 2: Create auth provider from validated claims
let auth = JwtAuth::from_claims(token_data.claims, &token)?;
let actor = auth.get_actor();

// STEP 3: Apply actor to DB operations
let db = SwirlDB::with_auth(auth);
db.set_path("user.profile", profile_data)?;

Per-Request Authentication

use axum::{
    extract::State,
    headers::{Authorization, authorization::Bearer},
    TypedHeader,
};

async fn handle_sync(
    State(db): State<Arc<SwirlDB>>,
    TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<Json<SyncResponse>> {
    // Validate token and get actor
    let actor = validate_jwt_token(auth.token())?;

    // Clone DB with this actor's identity
    let user_db = db.with_actor(actor);

    // All operations now scoped to this user
    let data = user_db.get_path("user.data")?;

    Ok(Json(SyncResponse { data }))
}

Integration with Policy Engine

Auth providers work seamlessly with SwirlDB's policy engine for fine-grained access control.

Actor-Based Policies

use swirldb_core::{SwirlDB, auth::JwtAuth, policy::Policy};

// Create DB with JWT auth
let auth = JwtAuth::from_token(token)?;
let db = SwirlDB::with_auth(auth);

// Define policies based on actor attributes
db.add_policy(Policy::new()
    .path("user.*.private")
    .allow_read(|actor, path| {
        // Users can only read their own private data
        path.starts_with(&format!("user.{}.private", actor.id))
    })
);

db.add_policy(Policy::new()
    .path("org.*")
    .allow_write(|actor, _| {
        // Only org admins can write
        actor.org_id.is_some() && actor.role == Some("admin".to_string())
    })
);

// Attempts to access unauthorized data will be denied
db.get_path("user.alice.private")?;  // OK if actor.id == "alice"
db.get_path("user.bob.private")?;    // Denied if actor.id == "alice"

Multi-Tenancy

// Enforce org-level data isolation
db.add_policy(Policy::new()
    .path("data.*")
    .allow_all(|actor, path| {
        // All operations scoped to actor's org
        if let Some(org_id) = &actor.org_id {
            path.starts_with(&format!("data.{}", org_id))
        } else {
            false
        }
    })
);

// Alice (org: acme-corp) can access:
db.get_path("data.acme-corp.projects")?;  // ✅ Allowed
db.get_path("data.competitor-corp.projects")?;  // ❌ Denied

Security Best Practices

JWT Signature Validation

  • Always validate server-side: JwtAuth only decodes, does not validate signatures
  • Use strong secrets: At least 256 bits (32 bytes) for HS256
  • Check expiration: Verify exp claim before accepting token
  • Validate issuer: Check iss claim matches your server
  • Use short lifetimes: Issue tokens valid for 15-60 minutes, refresh frequently

Token Storage

  • Browser: Store in localStorage for persistence, or sessionStorage for session-only
  • Avoid cookies: Unless using httpOnly cookies with CSRF protection
  • Clear on logout: Remove tokens from storage when user logs out
  • Refresh strategy: Implement token refresh before expiration

Anonymous Users

  • Generate stable IDs: Use crypto.randomUUID() and store in localStorage
  • Rate limiting: Apply stricter limits to anonymous users
  • Upgrade path: Allow anonymous users to register and keep their data
  • Privacy: Don't assume anonymous means no PII - still apply data protection

Server Configuration

  • Environment variables: Store secrets in env vars, never in code
  • Key rotation: Plan for periodic secret rotation
  • Audit logging: Log authentication failures and unusual patterns
  • Rate limiting: Protect auth endpoints from brute force attacks

Future Enhancements

  • OAuth 2.0 provider (Google, GitHub, etc.)
  • OpenID Connect (OIDC) integration
  • Multi-factor authentication (MFA) support
  • WebAuthn / FIDO2 for passwordless auth
  • Session management with automatic timeout
  • Device fingerprinting for anomaly detection
  • Custom claim validation rules
  • Actor delegation and impersonation (admin tools)