trictrac/server/relay-server/src/auth.rs

97 lines
3.4 KiB
Rust
Raw Normal View History

//! 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 {
/// Accepts either a username or an email address.
pub login: 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<Option<Self::User>, Self::Error> {
let Some(user) = db::get_user_by_username_or_email(&self.pool, &creds.login).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<Self>) -> Result<Option<Self::User>, 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<String, AuthError> {
let salt = SaltString::generate(&mut OsRng);
Argon2::default()
.hash_password(password.as_bytes(), &salt)
.map(|h| h.to_string())
.map_err(|_| AuthError::PasswordHash)
}