diff options
Diffstat (limited to 'src/token')
| -rw-r--r-- | src/token/app.rs | 126 | ||||
| -rw-r--r-- | src/token/extract/identity.rs | 20 | ||||
| -rw-r--r-- | src/token/mod.rs | 28 | ||||
| -rw-r--r-- | src/token/repo/auth.rs | 103 | ||||
| -rw-r--r-- | src/token/repo/mod.rs | 1 | ||||
| -rw-r--r-- | src/token/repo/token.rs | 97 |
6 files changed, 91 insertions, 284 deletions
diff --git a/src/token/app.rs b/src/token/app.rs index 56c0e21..fb5d712 100644 --- a/src/token/app.rs +++ b/src/token/app.rs @@ -6,16 +6,11 @@ use futures::{ use sqlx::sqlite::SqlitePool; use super::{ - Broadcaster, Event as TokenEvent, Id, Secret, - repo::{self, Provider as _, auth::Provider as _}, -}; -use crate::{ - clock::DateTime, - db::NotFound as _, - name::{self, Name}, - password::Password, - user::{User, repo::Provider as _}, + Broadcaster, Event as TokenEvent, Secret, Token, + extract::Identity, + repo::{self, Provider as _}, }; +use crate::{clock::DateTime, db::NotFound as _, name}; pub struct Tokens<'a> { db: &'a SqlitePool, @@ -27,106 +22,33 @@ impl<'a> Tokens<'a> { Self { db, token_events } } - pub async fn login( - &self, - name: &Name, - password: &Password, - login_at: &DateTime, - ) -> Result<Secret, LoginError> { - let mut tx = self.db.begin().await?; - let (user, stored_hash) = tx - .auth() - .for_name(name) - .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?; - - user.as_snapshot().ok_or(LoginError::Rejected)?; - - if stored_hash.verify(password)? { - let mut tx = self.db.begin().await?; - let secret = tx.tokens().issue(&user, login_at).await?; - tx.commit().await?; - Ok(secret) - } else { - Err(LoginError::Rejected) - } - } - - pub async fn change_password( - &self, - user: &User, - password: &Password, - to: &Password, - changed_at: &DateTime, - ) -> Result<Secret, LoginError> { - let mut tx = self.db.begin().await?; - let (user, stored_hash) = tx - .auth() - .for_user(user) - .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); - } - - user.as_snapshot().ok_or(LoginError::Rejected)?; - let to_hash = to.hash()?; - - let mut tx = self.db.begin().await?; - let tokens = tx.tokens().revoke_all(&user).await?; - tx.users().set_password(&user, &to_hash).await?; - let secret = tx.tokens().issue(&user, changed_at).await?; - tx.commit().await?; - - for event in tokens.into_iter().map(TokenEvent::Revoked) { - self.token_events.broadcast(event); - } - - Ok(secret) - } - pub async fn validate( &self, secret: &Secret, used_at: &DateTime, - ) -> Result<(Id, User), ValidateError> { + ) -> Result<Identity, ValidateError> { let mut tx = self.db.begin().await?; - let (token, user) = tx + let (token, login) = tx .tokens() .validate(secret, used_at) .await .not_found(|| ValidateError::InvalidToken)?; tx.commit().await?; - let user = user.as_snapshot().ok_or(ValidateError::LoginDeleted)?; - - Ok((token, user)) + Ok(Identity { token, login }) } pub async fn limit_stream<S, E>( &self, - token: Id, + token: &Token, events: S, ) -> Result<impl Stream<Item = E> + std::fmt::Debug + use<S, E>, ValidateError> where S: Stream<Item = E> + std::fmt::Debug, E: std::fmt::Debug, { + let token = token.id.clone(); + // Subscribe, first. let token_events = self.token_events.subscribe(); @@ -187,46 +109,22 @@ impl<'a> Tokens<'a> { Ok(()) } - pub async fn logout(&self, token: &Id) -> Result<(), ValidateError> { + pub async fn logout(&self, token: &Token) -> Result<(), ValidateError> { let mut tx = self.db.begin().await?; tx.tokens().revoke(token).await?; tx.commit().await?; self.token_events - .broadcast(TokenEvent::Revoked(token.clone())); + .broadcast(TokenEvent::Revoked(token.id.clone())); Ok(()) } } #[derive(Debug, thiserror::Error)] -pub enum LoginError { - #[error("invalid login")] - Rejected, - #[error(transparent)] - Database(#[from] sqlx::Error), - #[error(transparent)] - Name(#[from] name::Error), - #[error(transparent)] - PasswordHash(#[from] password_hash::Error), -} - -impl From<repo::auth::LoadError> for LoginError { - fn from(error: repo::auth::LoadError) -> Self { - use repo::auth::LoadError; - match error { - LoadError::Database(error) => error.into(), - LoadError::Name(error) => error.into(), - } - } -} - -#[derive(Debug, thiserror::Error)] pub enum ValidateError { #[error("invalid token")] InvalidToken, - #[error("user deleted")] - LoginDeleted, #[error(transparent)] Database(#[from] sqlx::Error), #[error(transparent)] diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs index d1c0334..960fe60 100644 --- a/src/token/extract/identity.rs +++ b/src/token/extract/identity.rs @@ -10,14 +10,14 @@ use crate::{ app::App, clock::RequestedAt, error::{Internal, Unauthorized}, - token::{self, app::ValidateError}, - user::User, + login::Login, + token::{Token, app::ValidateError}, }; #[derive(Clone, Debug)] pub struct Identity { - pub token: token::Id, - pub user: User, + pub token: Token, + pub login: Login, } impl FromRequestParts<App> for Identity { @@ -30,11 +30,13 @@ impl FromRequestParts<App> for Identity { let secret = cookie.secret().ok_or(LoginError::Unauthorized)?; let app = State::<App>::from_request_parts(parts, state).await?; - match app.tokens().validate(&secret, &used_at).await { - Ok((token, user)) => Ok(Identity { token, user }), - Err(ValidateError::InvalidToken) => Err(LoginError::Unauthorized), - Err(other) => Err(other.into()), - } + app.tokens() + .validate(&secret, &used_at) + .await + .map_err(|err| match err { + ValidateError::InvalidToken => LoginError::Unauthorized, + other => other.into(), + }) } } diff --git a/src/token/mod.rs b/src/token/mod.rs index 33403ef..b2dd6f1 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -6,4 +6,32 @@ mod id; pub mod repo; mod secret; +use uuid::Uuid; + pub use self::{broadcaster::Broadcaster, event::Event, id::Id, secret::Secret}; +use crate::{clock::DateTime, login, login::Login}; + +#[derive(Clone, Debug)] +pub struct Token { + pub id: Id, + pub login: login::Id, + pub issued_at: DateTime, + pub last_used_at: DateTime, +} + +impl Token { + pub fn generate(login: &Login, issued_at: &DateTime) -> (Self, Secret) { + let id = Id::generate(); + let secret = Uuid::new_v4().to_string().into(); + + ( + Self { + id, + login: login.id.clone(), + issued_at: *issued_at, + last_used_at: *issued_at, + }, + secret, + ) + } +} diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs deleted file mode 100644 index 600855d..0000000 --- a/src/token/repo/auth.rs +++ /dev/null @@ -1,103 +0,0 @@ -use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; - -use crate::{ - clock::DateTime, - db::NotFound, - event::{Instant, Sequence}, - name::{self, Name}, - password::StoredHash, - user::{self, History, User}, -}; - -pub trait Provider { - fn auth(&mut self) -> Auth<'_>; -} - -impl Provider for Transaction<'_, Sqlite> { - fn auth(&mut self) -> Auth<'_> { - Auth(self) - } -} - -pub struct Auth<'t>(&'t mut SqliteConnection); - -impl Auth<'_> { - pub async fn for_name(&mut self, name: &Name) -> Result<(History, StoredHash), LoadError> { - let name = name.canonical(); - let row = sqlx::query!( - r#" - select - id as "id: user::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 user - where canonical_name = $1 - "#, - name, - ) - .fetch_one(&mut *self.0) - .await?; - - let login = History { - user: User { - 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)) - } - - pub async fn for_user(&mut self, user: &User) -> Result<(History, StoredHash), LoadError> { - let row = sqlx::query!( - r#" - select - id as "id: user::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 user - where id = $1 - "#, - user.id, - ) - .fetch_one(&mut *self.0) - .await?; - - let user = History { - user: User { - id: row.id, - name: Name::new(row.display_name, row.canonical_name)?, - }, - created: Instant::new(row.created_at, row.created_sequence), - }; - - Ok((user, row.password_hash)) - } -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub enum LoadError { - Database(#[from] sqlx::Error), - Name(#[from] name::Error), -} - -impl<T> NotFound for Result<T, LoadError> { - type Ok = T; - type Error = LoadError; - - fn optional(self) -> Result<Option<T>, LoadError> { - match self { - Ok(value) => Ok(Some(value)), - Err(LoadError::Database(sqlx::Error::RowNotFound)) => Ok(None), - Err(other) => Err(other), - } - } -} diff --git a/src/token/repo/mod.rs b/src/token/repo/mod.rs index d8463eb..9df5bbb 100644 --- a/src/token/repo/mod.rs +++ b/src/token/repo/mod.rs @@ -1,4 +1,3 @@ -pub mod auth; mod token; pub use self::token::{LoadError, Provider}; diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs index 7ac4ac5..52a3987 100644 --- a/src/token/repo/token.rs +++ b/src/token/repo/token.rs @@ -1,13 +1,11 @@ use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; -use uuid::Uuid; use crate::{ clock::DateTime, db::NotFound, - event::{Instant, Sequence}, + login::{self, Login}, name::{self, Name}, - token::{Id, Secret}, - user::{self, History, User}, + token::{Id, Secret, Token}, }; pub trait Provider { @@ -23,33 +21,23 @@ impl Provider for Transaction<'_, Sqlite> { pub struct Tokens<'t>(&'t mut SqliteConnection); impl Tokens<'_> { - // Issue a new token for an existing user. The issued_at timestamp will - // determine the token's initial expiry deadline. - pub async fn issue( - &mut self, - user: &History, - issued_at: &DateTime, - ) -> Result<Secret, sqlx::Error> { - let id = Id::generate(); - let secret = Uuid::new_v4().to_string(); - let user = user.id(); - - let secret = sqlx::query_scalar!( + pub async fn create(&mut self, token: &Token, secret: &Secret) -> Result<(), sqlx::Error> { + sqlx::query!( r#" insert - into token (id, secret, user, issued_at, last_used_at) - values ($1, $2, $3, $4, $4) - returning secret as "secret!: Secret" + into token (id, secret, login, issued_at, last_used_at) + values ($1, $2, $3, $4, $5) "#, - id, + token.id, secret, - user, - issued_at, + token.login, + token.issued_at, + token.last_used_at, ) - .fetch_one(&mut *self.0) + .execute(&mut *self.0) .await?; - Ok(secret) + Ok(()) } pub async fn require(&mut self, token: &Id) -> Result<(), sqlx::Error> { @@ -67,34 +55,30 @@ impl Tokens<'_> { Ok(()) } - // Revoke a token by its secret. - pub async fn revoke(&mut self, token: &Id) -> Result<(), sqlx::Error> { - sqlx::query_scalar!( + pub async fn revoke(&mut self, token: &Token) -> Result<(), sqlx::Error> { + sqlx::query!( r#" - delete - from token + delete from token where id = $1 - returning id as "id: Id" "#, - token, + token.id, ) - .fetch_one(&mut *self.0) + .execute(&mut *self.0) .await?; Ok(()) } // Revoke tokens for a login - pub async fn revoke_all(&mut self, user: &user::History) -> Result<Vec<Id>, sqlx::Error> { - let user = user.id(); + pub async fn revoke_all(&mut self, login: &Login) -> Result<Vec<Id>, sqlx::Error> { let tokens = sqlx::query_scalar!( r#" delete from token - where user = $1 + where login = $1 returning id as "id: Id" "#, - user, + login.id, ) .fetch_all(&mut *self.0) .await?; @@ -120,54 +104,53 @@ impl Tokens<'_> { Ok(tokens) } - // Validate a token by its secret, retrieving the associated Login record. - // Will return an error if the token is not valid. If successful, the - // retrieved token's last-used timestamp will be set to `used_at`. pub async fn validate( &mut self, secret: &Secret, used_at: &DateTime, - ) -> Result<(Id, History), LoadError> { + ) -> Result<(Token, Login), LoadError> { // I would use `update … returning` to do this in one query, but // sqlite3, as of this writing, does not allow an update's `returning` // clause to reference columns from tables joined into the update. Two // queries is fine, but it feels untidy. - let (token, user) = sqlx::query!( + let token = sqlx::query!( r#" update token set last_used_at = $1 where secret = $2 returning - id as "token: Id", - user as "user: user::Id" + id as "id: Id", + login as "login: login::Id", + issued_at as "issued_at: DateTime", + last_used_at as "last_used_at: DateTime" "#, used_at, secret, ) - .map(|row| (row.token, row.user)) + .map(|row| Token { + id: row.id, + login: row.login, + issued_at: row.issued_at, + last_used_at: row.last_used_at, + }) .fetch_one(&mut *self.0) .await?; let user = sqlx::query!( r#" select - id as "id: user::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" - from user + id as "id: login::Id", + display_name, + canonical_name + from login where id = $1 "#, - user, + token.login, ) .map(|row| { - Ok::<_, name::Error>(History { - user: User { - id: row.id, - name: Name::new(row.display_name, row.canonical_name)?, - }, - created: Instant::new(row.created_at, row.created_sequence), + Ok::<_, name::Error>(Login { + id: row.id, + name: Name::new(row.display_name, row.canonical_name)?, }) }) .fetch_one(&mut *self.0) |
