diff options
Diffstat (limited to 'src/login/app.rs')
| -rw-r--r-- | src/login/app.rs | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/src/login/app.rs b/src/login/app.rs new file mode 100644 index 0000000..ced76d6 --- /dev/null +++ b/src/login/app.rs @@ -0,0 +1,107 @@ +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<chrono::Utc>; + +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<Option<String>, 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<Option<Login>, 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<Self, password_hash::Error> { + 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<bool, password_hash::Error> { + 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), + } + } +} |
