auth
Find a file
2026-04-14 12:21:51 +02:00
src updates 2026-04-14 12:21:51 +02:00
tests updates 2026-04-14 12:21:51 +02:00
.gitignore init 2025-05-05 12:00:49 +02:00
Cargo.lock updates 2026-04-14 12:21:51 +02:00
Cargo.toml feat!: Add deterministic session IDs, secure token hashing, and session indexing 2025-12-14 17:00:34 +01:00
README.md updates 2026-04-14 12:21:51 +02:00

♞ Authur

authur is a lightweight, framework-agnostic Rust authentication library providing user management, session handling, CSRF protection, and role-based access control (RBAC). It integrates with Rocket and Axum via optional feature flags.

Note: This crate requires a nightly Rust toolchain due to use of #![feature(unsized_const_params)] for the UserWithRole<ROLE> extractor.

Features

  • User management — create users, find users, change passwords (bcrypt hashed)
  • Session tokens — login, session creation, session listing and termination
  • API keys — named bearer-token sessions for programmatic access
  • CSRF protection — per-user CSRF tokens with HTMX-friendly JS injection
  • Role-based access control — arbitrary roles, built-in AdminUser alias
  • Profile pictures — store and retrieve binary image data per user
  • Multiple auth methods — cookie sessions, Authorization: Bearer, HTTP Basic Auth
  • VFS-backed storage — physical filesystem for production, in-memory for testing
  • Framework extractors — request guards / extractors for Rocket and Axum

Installation

[dependencies]
authur = { path = "...", features = ["axum"] }   # or "rocket"

Available features: rocket, axum (default: none).

Quick Start

Initialize the database

use authur::{UserDB, Roles};

// Production: persists to disk
let db = UserDB::new("./data/users").await;

// Testing: in-memory only
let db = UserDB::new_memory().await;

// Ensure a default admin user exists (username: "admin", password: "admin")
db.ensure_admin().await;

User management

use authur::{UserDB, Roles};

// Create a user
let user = db.create("alice".to_string(), "hunter2", Roles::default()).await;

// Create a user with roles
let roles = Roles::default().with("admin").with("moderator");
let admin = db.create("bob".to_string(), "s3cur3", roles).await;

// Look up a user
let user = db.find("alice").await;

// List all usernames
let names: Vec<String> = db.find_all().await;

// Change password (requires old password)
db.passwd("alice", "hunter2", "newpass").await?;

// Delete a user and all their data (sessions, profile picture, CSRF token)
// Returns true if the user existed, false if not found
db.delete("alice").await;

Sessions & login

use authur::session::Sessions;

// Login — returns (Session, Roles) on success
let (session, roles) = db.login("alice", "hunter2").await.unwrap();

// The plain token is only available immediately after creation
println!("token: {}", session.token);

// Validate a token and retrieve the associated user
let user = db.from_session(token).await;

// List all active sessions for a user
let sessions = db.list_sessions("alice").await;

// Terminate a session by its ID
db.end_session(&session.id).await;

API keys

use authur::session::Sessions;

// Create a named API key
let api_key = db.api_key("my-service", "alice").await;
println!("key: {}", api_key.token); // store this — it won't be shown again

// Authenticate the same way as a regular session token
let user = db.from_session(api_key.token).await;

CSRF protection

use authur::csrf::CSRF;

// Get or create the current CSRF token for a user
let token = db.get_csrf("alice").await;

// Verify and rotate (returns false if invalid)
let valid = db.verify_csrf(&submitted_token, "alice").await;

// Reset to a new token
db.reset_csrf("alice").await;

// Generate a <script> snippet for HTMX — updates all elements with class "csrf"
let snippet = db.update_csrf("alice").await;

Profile pictures

use authur::profile_pic::ProfilePic;

let bytes: Vec<u8> = std::fs::read("avatar.png").unwrap();
db.set_profile_pic(bytes, "alice").await;

let pic: Option<Vec<u8>> = db.profile_pic("alice").await;

Role-based access control

use authur::Roles;

let roles = Roles::default().with("editor").with("admin");

roles.has_role("editor"); // true
roles.has_role("admin");  // true
roles.has_role("viewer"); // false

Framework Extractors

Extractors pull authenticated users from incoming requests. They require UserDB<PhysicalFS> to be registered as application state.

Type Auth method Failure
UserAuth session cookie 401
APIUser Authorization: Bearer <token> 401
BasicAuthUser Authorization: Basic <b64> 401
MaybeUser session cookie (optional) never — returns Anonymous
UserWithRole<"role"> session cookie + role check 401/403
AdminUser alias for UserWithRole<"admin"> 401/403

Axum example

use authur::{UserDB, UserAuth, AdminUser};
use axum::{Router, routing::get, extract::State};
use vfs::PhysicalFS;

#[derive(Clone)]
struct AppState {
    db: UserDB<PhysicalFS>,
}

impl axum::extract::FromRef<AppState> for UserDB<PhysicalFS> {
    fn from_ref(state: &AppState) -> Self { state.db.clone() }
}

async fn profile(UserAuth(user): UserAuth) -> String {
    format!("Hello, {}!", user.username)
}

async fn admin_panel(AdminUser(user): AdminUser) -> String {
    format!("Admin: {}", user.username)
}

let db = UserDB::new("./data").await;
let app = Router::new()
    .route("/profile", get(profile))
    .route("/admin", get(admin_panel))
    .with_state(AppState { db });

Rocket example

use authur::{UserDB, UserAuth, AdminUser};
use vfs::PhysicalFS;

#[rocket::get("/profile")]
async fn profile(user: UserAuth) -> String {
    format!("Hello, {}!", user.0.username)
}

#[rocket::get("/admin")]
async fn admin_panel(user: AdminUser) -> String {
    format!("Admin: {}", user.0.username)
}

#[rocket::launch]
async fn rocket() -> _ {
    let db = UserDB::<PhysicalFS>::new("./data").await;
    rocket::build()
        .manage(db)
        .mount("/", rocket::routes![profile, admin_panel])
}

Session Security

  • Session tokens are generated with 64 bytes of CSPRNG entropy
  • The token is stored on disk as a bcrypt hash; the plaintext is never persisted
  • Sessions are looked up by a blake3 hash of the token (deterministic ID)
  • Verification requires bcrypt comparison against the stored hash

Limitations / Not Yet Implemented

  • 2FA / TOTP — not implemented
  • Rate limiting — no built-in brute-force protection on login
  • Profile picture validation — no size or MIME-type checks
  • Atomicity — session index writes are not atomic (fine for single-process use)