diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2024-10-29 23:29:22 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2024-10-29 23:29:22 -0400 |
| commit | 66d3fcf2e22f057bacce8d97d43a13c1c5a9ad09 (patch) | |
| tree | 60995943e14a6568cf2b37622ce97df121865a6d /src/token | |
| parent | e328d33fc7d6a0f2e3d260d8bddee3ef633318eb (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.rs | 43 | ||||
| -rw-r--r-- | src/token/repo/auth.rs | 29 | ||||
| -rw-r--r-- | src/token/repo/token.rs | 18 | ||||
| -rw-r--r-- | src/token/secret.rs | 2 |
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); |
