use argon2::Argon2; use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use rand_core::OsRng; use sqlx::sqlite::SqlitePool; use super::repo::{ logins::{Login, Provider as _}, tokens::Provider as _, }; use crate::error::BoxedError; type DateTime = chrono::DateTime; pub struct Logins<'a> { db: &'a SqlitePool, } impl<'a> Logins<'a> { pub fn new(db: &'a SqlitePool) -> Self { Self { db } } pub async fn login( &self, name: &str, password: &str, login_at: DateTime, ) -> Result, BoxedError> { let mut tx = self.db.begin().await?; let login = if let Some((login, stored_hash)) = tx.logins().for_login(name).await? { if stored_hash.verify(password)? { // Password verified; use the login. Some(login) } else { // Password NOT verified. None } } else { let password_hash = StoredHash::new(password)?; Some(tx.logins().create(name, &password_hash).await?) }; // If `login` is Some, then we have an identity and can issue a token. // If `login` is None, then neither creating a new login nor // authenticating an existing one succeeded, and we must reject the // login attempt. let token = if let Some(login) = login { Some(tx.tokens().issue(&login.id, login_at).await?) } else { None }; tx.commit().await?; Ok(token) } pub async fn validate( &self, secret: &str, used_at: DateTime, ) -> Result, BoxedError> { let mut tx = self.db.begin().await?; tx.tokens().expire(used_at).await?; let login = tx.tokens().validate(secret, used_at).await?; tx.commit().await?; Ok(login) } pub async fn logout(&self, secret: &str) -> Result<(), BoxedError> { let mut tx = self.db.begin().await?; tx.tokens().revoke(secret).await?; tx.commit().await?; Ok(()) } } #[derive(Debug, sqlx::Type)] #[sqlx(transparent)] pub struct StoredHash(String); impl StoredHash { fn new(password: &str) -> Result { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); let hash = argon2 .hash_password(password.as_bytes(), &salt)? .to_string(); Ok(Self(hash)) } fn verify(&self, password: &str) -> Result { let hash = PasswordHash::new(&self.0)?; match Argon2::default().verify_password(password.as_bytes(), &hash) { // Successful authentication, not an error Ok(()) => Ok(true), // Unsuccessful authentication, also not an error Err(password_hash::errors::Error::Password) => Ok(false), // Password validation failed for some other reason, treat as an error Err(err) => Err(err), } } }