arcature 0.1.1

Arcature: an opinionated full-stack Rust web framework. One package, batteries included.
Documentation
//! Authentication, sessions, authorization policies, password hashing, and
//! CSRF protection.
//!
//! This module owns the **integration seams** an Arcature application needs to
//! authenticate users safely: Argon2id password hashing, tower-sessions Axum
//! session middleware, double-submit CSRF protection, the `Auth<U>` /
//! `OptionalAuth<U>` / `AuthManager<U>` extractors, the `Session` and `Flash`
//! ergonomics, and the `Policy` authorization seam.
//!
//! # What this module owns
//!
//! * **Argon2id password hashing** with audited salt generation, PHC-formatted
//!   stored hashes, parameter configuration, verification, and rehash-on-
//!   parameter-change detection ([`PasswordHasher`], [`verify_password`]).
//! * **Secure sessions** over tower-sessions: cookie attributes (name,
//!   `SameSite`, `Secure`, `HttpOnly`, path, domain, `Max-Age`, expiry) and a
//!   signed cookie jar, built into a [`tower_sessions::SessionManagerLayer`]
//!   from a resolved [`SessionConfig`].
//! * **CSRF protection** for cookie-authenticated browser requests via a
//!   double-submit token ([`CsrfLayer`], [`CsrfToken`]). Bearer-token APIs and
//!   safe-method requests are exempt by design.
//! * **Auth extractors** ([`Auth`], [`OptionalAuth`], [`AuthManager`]) that
//!   load the authenticated user from the session + application state.
//! * **Session/Flash ergonomics** ([`Session`], [`Flash`]).
//! * **Authorization** via the [`Policy`] trait and [`Auth::authorize`].
//!
//! # Where each of those lives
//!
//! Every name above is re-exported from `arcature::auth`, so `use
//! arcature::auth::Auth` is the path to write and the submodule is an
//! implementation detail. When you do need the submodule: the extractors are
//! in [`extract`], the handler-facing session API in [`session_api`], the
//! one-time messages in [`flash`], the authorization seam in [`policy`], the
//! cookie/middleware configuration in [`session`], and password hashing in
//! [`password`]. The [`dx`] module is the pre-`0.1.1` spelling of the first
//! four and is deprecated.
//!
//! # What this module does not own
//!
//! It does not own the User model, role/permission/account tables, or any
//! application-specific identity schema -- applications own domain identity.
//! It does not reimplement cryptography (Argon2id, HMAC, SHA-2, and TLS come
//! from RustCrypto, `cookie`, and the certified rustls + aws-lc-rs path). It
//! does not persist sessions to a specific store by default; the application
//! wires any [`tower_sessions::SessionStore`].
//!
//! # Security note -- secrets are never logged
//!
//! Passwords, session signing keys, and tokens are wrapped in
//! [`secrecy`]-backed types whose `Debug`/`Display` never expose the secret
//! and which zeroize on drop. No plaintext password, signing key, or token
//! appears in `Debug`, `Display`, error output, or logs.

pub mod csrf;
/// Deprecated compatibility re-exports; see the module docs for the new homes.
#[deprecated(
    since = "0.1.1",
    note = "split into arcature::auth::{extract, session_api, flash, policy}"
)]
pub mod dx;
pub mod error;
pub mod extract;
pub mod flash;
// The security-critical pieces of a sign-in screen -- the ones where the
// obvious implementation leaks something. Behind `auth-flows`, off by
// default. These keep their module rather than being flattened into
// `arcature::auth`: `flows::CredentialChecker` says which layer it belongs
// to, and the layer is the thing a reviewer needs to see.
#[cfg(feature = "auth-flows")]
pub mod flows;
pub mod password;
pub mod password_config;
pub mod policy;
pub mod session;
pub mod session_api;
// Sessions in the application's own database, rather than in a process-local
// `HashMap` that a deploy empties. Behind `session-store-db`, off by default,
// because it brings a table and a migration with it.
#[cfg(feature = "session-store-db")]
pub mod session_store;

// Re-export the certified tower-sessions crate so downstream code targets the
// Arcature-pinned version and reaches the certified `cookie` crate through
// `tower_sessions::cookie`.
pub use tower_sessions;

// Re-export the certified argon2 crate.
pub use argon2;

pub use csrf::{CsrfConfig, CsrfLayer, CsrfMiddleware, CsrfToken};
pub use error::{
    CsrfConfigError, CsrfError, PasswordHashError, PasswordVerifyError, SessionBuildError,
    SessionConfigError, SigningKeyReason,
};
pub use extract::{
    Auth, AuthError, AuthManager, Current, LoginBuilder, OptionalAuth, OptionalCurrent, UserLoader,
};
pub use flash::{Flash, FlashError, FlashLevel, FlashMessage};
pub use password::{
    PasswordHashString, PasswordHasher, PasswordSecret, RehashOutcome, verify_password,
};
pub use password_config::PasswordConfig;
pub use policy::{AuthzError, Policy};
pub use session::{SameSite, SessionConfig, SessionKey, SessionLayer};
pub use session_api::{Session, SessionError};
#[cfg(feature = "session-store-db")]
pub use session_store::{DbSessionStore, SessionStoreError};

// The redirect mapper writes the same session key the `Flash` extractor
// reads, and one spelling of it has to be authoritative.
pub(crate) use flash::FLASH_DATA_KEY;

// Re-export the redacting secret wrapper for credential/token holders.
pub use secrecy;

use serde::Serialize;
use serde::de::DeserializeOwned;

/// The application identity contract.
///
/// Implemented by the application's user type. The framework uses this to
/// store/retrieve the user ID in the session and to type the auth extractors
/// ([`Auth<U>`], [`OptionalAuth<U>`], [`AuthManager<U>`]).
///
/// The application owns identity schema -- this trait does NOT mandate a fixed
/// `User` table, role model, or permission system.
///
/// # Example
///
/// ```
/// use arcature::AuthUser;
///
/// # #[allow(dead_code)]
/// pub struct User {
///     pub id: uuid::Uuid,
///     pub email: String,
/// }
///
/// impl AuthUser for User {
///     type Id = uuid::Uuid;
///     const SESSION_KEY: &'static str = "user_id";
///
///     fn id(&self) -> &uuid::Uuid {
///         &self.id
///     }
/// }
/// # fn main() {}
/// ```
pub trait AuthUser: Send + Sync + 'static {
    /// The type stored in the session to identify the user. Must be
    /// serializable/deserializable (e.g. `Uuid`, `i64`, `String`).
    type Id: Serialize + DeserializeOwned + Clone + Send + Sync + 'static;

    /// The session key under which the user ID is stored. Defaults to
    /// `"user_id"`.
    const SESSION_KEY: &'static str = "user_id";

    /// Get the ID to store in the session on login.
    fn id(&self) -> &Self::Id;

    /// The stored verifier this user authenticates against -- the password
    /// hash, in almost every case.
    ///
    /// Return it and a password change signs every session bound to this user
    /// out, including the ones on devices nobody has in front of them. That is
    /// the point of changing a password after a laptop is stolen, and without
    /// this the reset link an application just built hands the account back
    /// while the thief's session keeps working.
    ///
    /// # How the invalidation works
    ///
    /// Login stamps a digest of this value into the session, and every
    /// authenticated request compares that stamp against the value the user
    /// row holds *now*. A mismatch means the credential moved under the
    /// session, so the session is flushed and the request is unauthenticated.
    /// Nothing is stored anywhere but the session the browser already carries:
    /// there is no per-user session index to consult, and consequently the
    /// mechanism works the same on
    /// [`MemoryStore`](tower_sessions::MemoryStore) as on a database store.
    ///
    /// The session holds a SHA-256 of what is returned, never the value
    /// itself, so a leaked session row does not become an offline attack on
    /// the password hash. Returning something *other* than the password hash
    /// is fine as long as it changes when the credential does -- a
    /// `password_changed_at` timestamp works -- but note that a low-entropy
    /// value is guessable from its digest by anyone who reads the session
    /// store, which for a timestamp means learning when the password changed.
    ///
    /// # The default
    ///
    /// `None`, which enforces nothing. Existing implementations keep their
    /// current behaviour, and an application that has no password to speak of
    /// -- one that authenticates entirely through an identity provider -- is
    /// not being asked to invent a value.
    ///
    /// ```
    /// use arcature::AuthUser;
    ///
    /// # #[allow(dead_code)]
    /// struct User {
    ///     id: i64,
    ///     password_hash: String,
    /// }
    ///
    /// impl AuthUser for User {
    ///     type Id = i64;
    ///
    ///     fn id(&self) -> &i64 {
    ///         &self.id
    ///     }
    ///
    ///     fn stored_credential(&self) -> Option<&[u8]> {
    ///         Some(self.password_hash.as_bytes())
    ///     }
    /// }
    /// # fn main() {}
    /// ```
    fn stored_credential(&self) -> Option<&[u8]> {
        None
    }
}