//! Authentication backend for axum-login. //! //! Implements [`AuthUser`] on [`db::User`] and provides [`AuthBackend`] which //! validates credentials against the database using Argon2 password hashing. use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use argon2::password_hash::rand_core::OsRng; use argon2::Argon2; use axum_login::{AuthUser, AuthnBackend, UserId}; use deadpool_postgres::Pool; use crate::db; // ── AuthUser ───────────────────────────────────────────────────────────────── impl AuthUser for db::User { type Id = i64; fn id(&self) -> Self::Id { self.id } /// Changing the password invalidates all existing sessions for this user. fn session_auth_hash(&self) -> &[u8] { self.password_hash.as_bytes() } } // ── Credentials ────────────────────────────────────────────────────────────── #[derive(Clone)] pub struct Credentials { pub username: String, pub password: String, } // ── Error ──────────────────────────────────────────────────────────────────── #[derive(Debug, thiserror::Error)] pub enum AuthError { #[error("database error: {0}")] Database(#[from] db::DbError), #[error("password hashing error")] PasswordHash, } // ── Backend ─────────────────────────────────────────────────────────────────── #[derive(Clone)] pub struct AuthBackend { pool: Pool, } impl AuthBackend { pub fn new(pool: Pool) -> Self { Self { pool } } } impl AuthnBackend for AuthBackend { type User = db::User; type Credentials = Credentials; type Error = AuthError; async fn authenticate( &self, creds: Self::Credentials, ) -> Result, Self::Error> { let Some(user) = db::get_user_by_username(&self.pool, &creds.username).await? else { return Ok(None); }; let parsed = PasswordHash::new(&user.password_hash).map_err(|_| AuthError::PasswordHash)?; let valid = Argon2::default() .verify_password(creds.password.as_bytes(), &parsed) .is_ok(); Ok(valid.then_some(user)) } async fn get_user(&self, user_id: &UserId) -> Result, Self::Error> { Ok(db::get_user_by_id(&self.pool, *user_id).await?) } } // ── Password hashing helper ─────────────────────────────────────────────────── /// Hashes a plaintext password with Argon2id. Used by the registration endpoint. pub fn hash_password(password: &str) -> Result { let salt = SaltString::generate(&mut OsRng); Argon2::default() .hash_password(password.as_bytes(), &salt) .map(|h| h.to_string()) .map_err(|_| AuthError::PasswordHash) }