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:
type→actor_type(user, app, server)sub→id(subject/user ID)org_id→org_idteam_id→team_idapp_id→app_idrole→role- All other claims stored in
claimsmap
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
expclaim before accepting token - Validate issuer: Check
issclaim matches your server - Use short lifetimes: Issue tokens valid for 15-60 minutes, refresh frequently
Token Storage
- Browser: Store in
localStoragefor persistence, orsessionStoragefor 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)