summaryrefslogtreecommitdiff
path: root/src/login/app.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/login/app.rs')
-rw-r--r--src/login/app.rs107
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),
+ }
+ }
+}