use argon2::Argon2; use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use rand_core::OsRng; use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use crate::error::BoxedError; use crate::id::Id as BaseId; pub trait Provider { fn logins(&mut self) -> Logins; } impl<'c> Provider for Transaction<'c, Sqlite> { fn logins(&mut self) -> Logins { Logins(self) } } pub struct Logins<'t>(&'t mut SqliteConnection); // This also implements FromRequestParts (see `src/login/extract/login.rs`). As // a result, it can be used as an extractor. #[derive(Debug)] pub struct Login { pub id: Id, pub name: String, // The omission of the hashed password is deliberate, to minimize the // chance that it ends up tangled up in debug output or in some other chunk // of logic elsewhere. } impl<'c> Logins<'c> { /// Create a new login, if the name is not already taken. Returns a [Login] /// if a new login has actually been created, or `None` if an existing login /// was found. pub async fn create( &mut self, name: &str, password: &str, ) -> Result, BoxedError> { let id = Id::generate(); let password_hash = StoredHash::new(password)?; let insert_res = sqlx::query_as!( Login, r#" insert or fail into login (id, name, password_hash) values ($1, $2, $3) returning id as "id: Id", name "#, id, name, password_hash, ) .fetch_one(&mut *self.0) .await; let result = match insert_res { Ok(id) => Ok(Some(id)), Err(err) => { if let Some(true) = err .as_database_error() .map(|db_err| db_err.is_unique_violation()) { // Login with the same username (or, very rarely, same ID) already // exists. Ok(None) } else { Err(err) } } }?; Ok(result) } /// Authenticates `name` and `password` against an existing [Login]. Returns /// that [Login] if one was found and the password was correct, or `None` if /// either condition does not hold. pub async fn authenticate( &mut self, name: &str, password: &str, ) -> Result, BoxedError> { let found = self.for_name(name).await?; let login = if let Some((login, stored_hash)) = found { if stored_hash.verify(password)? { // User found and password validation succeeded. Some(login) } else { // Password validation failed. None } } else { // User not found. None }; Ok(login) } async fn for_name(&mut self, name: &str) -> Result, BoxedError> { let found = sqlx::query!( r#" select id as "id: Id", name, password_hash as "password_hash: StoredHash" from login where name = $1 "#, name, ) .map(|rec| { ( Login { id: rec.id, name: rec.name, }, rec.password_hash, ) }) .fetch_optional(&mut *self.0) .await?; Ok(found) } } /// Stable identifier for a [Login]. Prefixed with `L`. #[derive(Debug, sqlx::Type)] #[sqlx(transparent)] pub struct Id(BaseId); impl From for Id { fn from(id: BaseId) -> Self { Self(id) } } impl Id { pub fn generate() -> Self { BaseId::generate("L") } } #[derive(Debug, sqlx::Type)] #[sqlx(transparent)] 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), } } }