summaryrefslogtreecommitdiff
path: root/src/token
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-10-29 23:29:22 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-10-29 23:29:22 -0400
commit66d3fcf2e22f057bacce8d97d43a13c1c5a9ad09 (patch)
tree60995943e14a6568cf2b37622ce97df121865a6d /src/token
parente328d33fc7d6a0f2e3d260d8bddee3ef633318eb (diff)
Add `change password` UI + API.
The protocol here re-checks the caller's password, as a "I left myself logged in" anti-pranking check.
Diffstat (limited to 'src/token')
-rw-r--r--src/token/app.rs43
-rw-r--r--src/token/repo/auth.rs29
-rw-r--r--src/token/repo/token.rs18
-rw-r--r--src/token/secret.rs2
4 files changed, 90 insertions, 2 deletions
diff --git a/src/token/app.rs b/src/token/app.rs
index c19d6a0..5c0aeb0 100644
--- a/src/token/app.rs
+++ b/src/token/app.rs
@@ -13,7 +13,7 @@ use super::{
use crate::{
clock::DateTime,
db::NotFound as _,
- login::{Login, Password},
+ login::{repo::Provider as _, Login, Password},
name::{self, Name},
};
@@ -61,6 +61,47 @@ impl<'a> Tokens<'a> {
Ok((snapshot, token))
}
+ pub async fn change_password(
+ &self,
+ login: &Login,
+ password: &Password,
+ to: &Password,
+ changed_at: &DateTime,
+ ) -> Result<(Login, Secret), LoginError> {
+ let mut tx = self.db.begin().await?;
+ let (login, stored_hash) = tx
+ .auth()
+ .for_login(login)
+ .await
+ .optional()?
+ .ok_or(LoginError::Rejected)?;
+ // Split the transaction here to avoid holding the tx open (potentially blocking
+ // other writes) while we do the fairly expensive task of verifying the
+ // password. It's okay if the token issuance transaction happens some notional
+ // amount of time after retrieving the login, as inserting the token will fail
+ // if the account is deleted during that time.
+ tx.commit().await?;
+
+ if !stored_hash.verify(password)? {
+ return Err(LoginError::Rejected);
+ }
+
+ let snapshot = login.as_snapshot().ok_or(LoginError::Rejected)?;
+ let to_hash = to.hash()?;
+
+ let mut tx = self.db.begin().await?;
+ let tokens = tx.tokens().revoke_all(&login).await?;
+ tx.logins().set_password(&login, &to_hash).await?;
+ let secret = tx.tokens().issue(&login, changed_at).await?;
+ tx.commit().await?;
+
+ for event in tokens.into_iter().map(TokenEvent::Revoked) {
+ self.token_events.broadcast(event);
+ }
+
+ Ok((snapshot, secret))
+ }
+
pub async fn validate(
&self,
secret: &Secret,
diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs
index bdc4c33..b51db8c 100644
--- a/src/token/repo/auth.rs
+++ b/src/token/repo/auth.rs
@@ -50,6 +50,35 @@ impl<'t> Auth<'t> {
Ok((login, row.password_hash))
}
+
+ pub async fn for_login(&mut self, login: &Login) -> Result<(History, StoredHash), LoadError> {
+ let row = sqlx::query!(
+ r#"
+ select
+ id as "id: login::Id",
+ display_name as "display_name: String",
+ canonical_name as "canonical_name: String",
+ created_sequence as "created_sequence: Sequence",
+ created_at as "created_at: DateTime",
+ password_hash as "password_hash: StoredHash"
+ from login
+ where id = $1
+ "#,
+ login.id,
+ )
+ .fetch_one(&mut *self.0)
+ .await?;
+
+ let login = History {
+ login: Login {
+ id: row.id,
+ name: Name::new(row.display_name, row.canonical_name)?,
+ },
+ created: Instant::new(row.created_at, row.created_sequence),
+ };
+
+ Ok((login, row.password_hash))
+ }
}
#[derive(Debug, thiserror::Error)]
diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs
index 35ea385..33b89d5 100644
--- a/src/token/repo/token.rs
+++ b/src/token/repo/token.rs
@@ -84,6 +84,24 @@ impl<'c> Tokens<'c> {
Ok(())
}
+ // Revoke tokens for a login
+ pub async fn revoke_all(&mut self, login: &login::History) -> Result<Vec<Id>, sqlx::Error> {
+ let login = login.id();
+ let tokens = sqlx::query_scalar!(
+ r#"
+ delete
+ from token
+ where login = $1
+ returning id as "id: Id"
+ "#,
+ login,
+ )
+ .fetch_all(&mut *self.0)
+ .await?;
+
+ Ok(tokens)
+ }
+
// Expire and delete all tokens that haven't been used more recently than
// `expire_at`.
pub async fn expire(&mut self, expire_at: &DateTime) -> Result<Vec<Id>, sqlx::Error> {
diff --git a/src/token/secret.rs b/src/token/secret.rs
index 28c93bb..8f70646 100644
--- a/src/token/secret.rs
+++ b/src/token/secret.rs
@@ -1,6 +1,6 @@
use std::fmt;
-#[derive(sqlx::Type)]
+#[derive(PartialEq, Eq, sqlx::Type)]
#[sqlx(transparent)]
pub struct Secret(String);