auth
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
♞ 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 theUserWithRole<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
AdminUseralias - 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)