rustango 0.48.0

Django-shaped batteries-included web framework for Rust: ORM + migrations + auto-admin + multi-tenancy + audit log + auth (sessions, JWT, OAuth2/OIDC, HMAC) + APIs (ViewSet, OpenAPI auto-derive, JSON:API) + jobs (in-mem + Postgres) + email + media (S3 / R2 / B2 / MinIO + presigned uploads + collections + tags) + production middleware (CSRF, CSP, rate-limiting, compression, idempotency, etc.).
Documentation
//! Passkey / WebAuthn support — issue #392.
//!
//! This is the **foundation slice**: credential storage. It defines the
//! `rustango_webauthn_credentials` table + a small store API that the
//! registration / authentication ceremonies (the next slice) build on.
//!
//! ## Design
//!
//! - **All-RustCrypto, no C dep.** The ceremony layer (CBOR attestation
//!   parsing + ES256 assertion verification) will use pure-Rust
//!   `ciborium` + `p256`, *not* `webauthn-rs` (which pulls `openssl`).
//!   This keeps rustango's dependency story C-free, matching `argon2` /
//!   `sha2` / `hmac`.
//! - **Operator-managed table** (`#[rustango(managed = false)]`),
//!   bootstrapped idempotently via [`ensure_table`] (the `audit` /
//!   `totp_store` pattern), so it never enters the migration graph.
//! - A user may register **several** passkeys (laptop, phone, security
//!   key), so the PK is a surrogate `id`; lookups are by `user_id` (list)
//!   or `credential_id` (the unique handle the authenticator returns at
//!   assertion time).
//!
//! Gated behind the `passkey` feature; builds without it are unchanged.

pub mod ceremony;
pub mod error;
pub mod session;
pub mod verify;

pub use ceremony::{
    authentication_options_json, generate_challenge, registration_options_json,
    verify_authentication, verify_registration, RegistrationOutcome,
};
pub use error::PasskeyError;
pub use session::{open_challenge, seal_challenge};

use crate::sql::{Auto, Pool};
use crate::Model;

/// One registered WebAuthn credential (passkey). `public_key` holds the
/// COSE-encoded public key bytes captured at registration; `sign_count`
/// is the authenticator's signature counter, bumped on each assertion to
/// detect cloned authenticators.
#[derive(Model, Debug, Clone)]
#[rustango(table = "rustango_webauthn_credentials", managed = false)]
#[allow(dead_code)]
pub struct WebauthnCredential {
    /// Surrogate primary key (a user may have many credentials).
    #[rustango(primary_key)]
    pub id: Auto<i64>,
    /// The owning user's id.
    #[rustango(index)]
    pub user_id: i64,
    /// Base64url (no padding) of the raw credential id the authenticator
    /// returns — the unique handle used to look the credential up at
    /// assertion time.
    #[rustango(max_length = 255, unique)]
    pub credential_id: String,
    /// COSE-encoded public key bytes captured at registration.
    pub public_key: Vec<u8>,
    /// Authenticator signature counter (replay / clone detection).
    #[rustango(default = "0")]
    pub sign_count: i64,
    /// Optional user-facing label ("MacBook Touch ID", "YubiKey 5").
    #[rustango(max_length = 64, default = "''")]
    pub label: String,
    /// When the credential was registered.
    #[rustango(default = "now()")]
    pub created_at: chrono::DateTime<chrono::Utc>,
}

/// Idempotently create the `rustango_webauthn_credentials` table for the
/// active backend.
///
/// Drift-free: the DDL (+ the `credential_id` UNIQUE and the `user_id`
/// index) is rendered from [`WebauthnCredential::SCHEMA`] through the
/// migration engine's dialect emitter — the same path
/// `makemigrations`/`migrate` use — instead of hand-written per-dialect
/// strings. The model is `managed = false` (deliberately ensure-based,
/// outside the migration set), so this helper is its only DDL path.
///
/// # Errors
/// Driver / SQL failures (other than the duplicate-object errors this
/// swallows to stay idempotent).
pub async fn ensure_table(pool: &Pool) -> Result<(), sqlx::Error> {
    use crate::core::Model as _;
    let snapshot =
        crate::migrate::SchemaSnapshot::from_models_forced(&[WebauthnCredential::SCHEMA]);
    let changes =
        crate::migrate::detect_changes(&crate::migrate::SchemaSnapshot::default(), &snapshot);
    let batch =
        crate::migrate::render_changes_split_with_dialect(&changes, &snapshot, pool.dialect())
            .map_err(sqlx::Error::Protocol)?;
    for stmt in batch.immediate.iter().chain(batch.deferred_fks.iter()) {
        if let Err(e) = crate::sql::raw_execute_pool(pool, stmt, Vec::new()).await {
            let msg = format!("{e}").to_lowercase();
            if msg.contains("already exists") || msg.contains("duplicate") {
                continue;
            }
            return Err(match e {
                crate::sql::ExecError::Driver(err) => err,
                other => sqlx::Error::Protocol(format!("{other}")),
            });
        }
    }
    Ok(())
}

/// Every credential registered to `user_id` — the allow-list the
/// authentication ceremony offers the authenticator.
///
/// # Errors
/// As the ORM fetch path ([`crate::sql::ExecError`]).
pub async fn for_user(
    pool: &Pool,
    user_id: i64,
) -> Result<Vec<WebauthnCredential>, crate::sql::ExecError> {
    use crate::sql::FetcherPool as _;
    WebauthnCredential::objects()
        .filter("user_id", user_id)
        .fetch(pool)
        .await
}

/// Look a credential up by its (unique) base64url credential id — the
/// handle the authenticator returns at assertion time.
///
/// # Errors
/// As the ORM fetch path ([`crate::sql::ExecError`]).
pub async fn by_credential_id(
    pool: &Pool,
    credential_id: &str,
) -> Result<Option<WebauthnCredential>, crate::sql::ExecError> {
    use crate::sql::FetcherPool as _;
    Ok(WebauthnCredential::objects()
        .filter("credential_id", credential_id.to_owned())
        .fetch(pool)
        .await?
        .into_iter()
        .next())
}

/// Persist a freshly-registered credential. `public_key` is the
/// COSE-encoded key bytes; `sign_count` is the authenticator's initial
/// counter (often 0).
///
/// # Errors
/// As the ORM insert path ([`crate::sql::ExecError`]).
pub async fn register(
    pool: &Pool,
    user_id: i64,
    credential_id: &str,
    public_key: Vec<u8>,
    sign_count: i64,
    label: &str,
) -> Result<(), crate::sql::ExecError> {
    let mut row = WebauthnCredential {
        id: Auto::Unset,
        user_id,
        credential_id: credential_id.to_owned(),
        public_key,
        sign_count,
        label: label.to_owned(),
        created_at: chrono::Utc::now(),
    };
    row.insert_pool(pool).await?;
    Ok(())
}

/// Advance the stored signature counter for a credential after a
/// successful assertion (clone detection: the new count must exceed the
/// stored one). No-op if the credential id is unknown.
///
/// # Errors
/// As the ORM update path ([`crate::sql::ExecError`]).
pub async fn update_sign_count(
    pool: &Pool,
    credential_id: &str,
    new_count: i64,
) -> Result<(), crate::sql::ExecError> {
    use crate::sql::UpdaterPool as _;
    WebauthnCredential::objects()
        .filter("credential_id", credential_id.to_owned())
        .update()
        .set("sign_count", new_count)
        .execute_pool(pool)
        .await?;
    Ok(())
}