diff options
Diffstat (limited to 'src/token')
| -rw-r--r-- | src/token/app.rs | 49 | ||||
| -rw-r--r-- | src/token/extract/cookie.rs (renamed from src/token/extract/identity_token.rs) | 22 | ||||
| -rw-r--r-- | src/token/extract/identity.rs | 15 | ||||
| -rw-r--r-- | src/token/extract/mod.rs | 4 | ||||
| -rw-r--r-- | src/token/repo/auth.rs | 69 | ||||
| -rw-r--r-- | src/token/repo/mod.rs | 2 | ||||
| -rw-r--r-- | src/token/repo/token.rs | 66 |
7 files changed, 147 insertions, 80 deletions
diff --git a/src/token/app.rs b/src/token/app.rs index 15fd858..c19d6a0 100644 --- a/src/token/app.rs +++ b/src/token/app.rs @@ -7,12 +7,14 @@ use futures::{ use sqlx::sqlite::SqlitePool; use super::{ - repo::auth::Provider as _, repo::Provider as _, Broadcaster, Event as TokenEvent, Id, Secret, + repo::{self, auth::Provider as _, Provider as _}, + Broadcaster, Event as TokenEvent, Id, Secret, }; use crate::{ clock::DateTime, db::NotFound as _, login::{Login, Password}, + name::{self, Name}, }; pub struct Tokens<'a> { @@ -27,10 +29,10 @@ impl<'a> Tokens<'a> { pub async fn login( &self, - name: &str, + name: &Name, password: &Password, login_at: &DateTime, - ) -> Result<Secret, LoginError> { + ) -> Result<(Login, Secret), LoginError> { let mut tx = self.db.begin().await?; let (login, stored_hash) = tx .auth() @@ -45,6 +47,8 @@ impl<'a> Tokens<'a> { // if the account is deleted during that time. tx.commit().await?; + let snapshot = login.as_snapshot().ok_or(LoginError::Rejected)?; + let token = if stored_hash.verify(password)? { let mut tx = self.db.begin().await?; let token = tx.tokens().issue(&login, login_at).await?; @@ -54,7 +58,7 @@ impl<'a> Tokens<'a> { Err(LoginError::Rejected)? }; - Ok(token) + Ok((snapshot, token)) } pub async fn validate( @@ -63,14 +67,16 @@ impl<'a> Tokens<'a> { used_at: &DateTime, ) -> Result<(Id, Login), ValidateError> { let mut tx = self.db.begin().await?; - let login = tx + let (token, login) = tx .tokens() .validate(secret, used_at) .await .not_found(|| ValidateError::InvalidToken)?; tx.commit().await?; - Ok(login) + let login = login.as_snapshot().ok_or(ValidateError::LoginDeleted)?; + + Ok((token, login)) } pub async fn limit_stream<E>( @@ -158,17 +164,42 @@ pub enum LoginError { #[error("invalid login")] Rejected, #[error(transparent)] - DatabaseError(#[from] sqlx::Error), + Database(#[from] sqlx::Error), + #[error(transparent)] + Name(#[from] name::Error), #[error(transparent)] - PasswordHashError(#[from] password_hash::Error), + 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("login deleted")] + LoginDeleted, + #[error(transparent)] + Database(#[from] sqlx::Error), #[error(transparent)] - DatabaseError(#[from] sqlx::Error), + Name(#[from] name::Error), +} + +impl From<repo::LoadError> for ValidateError { + fn from(error: repo::LoadError) -> Self { + match error { + repo::LoadError::Database(error) => error.into(), + repo::LoadError::Name(error) => error.into(), + } + } } #[derive(Debug)] diff --git a/src/token/extract/identity_token.rs b/src/token/extract/cookie.rs index a1e900e..af5787d 100644 --- a/src/token/extract/identity_token.rs +++ b/src/token/extract/cookie.rs @@ -12,19 +12,21 @@ use crate::token::Secret; // The usage pattern here - receive the extractor as an argument, return it in // the response - is heavily modelled after CookieJar's own intended usage. #[derive(Clone)] -pub struct IdentityToken { +pub struct Identity { cookies: CookieJar, } -impl fmt::Debug for IdentityToken { +impl fmt::Debug for Identity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("IdentityToken") + f.debug_struct("IdentityCookie") .field("identity", &self.secret()) .finish() } } -impl IdentityToken { +impl Identity { + const COOKIE_NAME: &str = "identity"; + // Creates a new, unpopulated identity token store. #[cfg(test)] pub fn new() -> Self { @@ -40,7 +42,7 @@ impl IdentityToken { // included. pub fn secret(&self) -> Option<Secret> { self.cookies - .get(IDENTITY_COOKIE) + .get(Self::COOKIE_NAME) .map(Cookie::value) .map(Secret::from) } @@ -49,7 +51,7 @@ impl IdentityToken { // back to the client when this extractor is included in a response. pub fn set(self, secret: impl Into<Secret>) -> Self { let secret = secret.into().reveal(); - let identity_cookie = Cookie::build((IDENTITY_COOKIE, secret)) + let identity_cookie = Cookie::build((Self::COOKIE_NAME, secret)) .http_only(true) .path("/") .permanent() @@ -64,15 +66,13 @@ impl IdentityToken { // extractor is included in a response. pub fn clear(self) -> Self { Self { - cookies: self.cookies.remove(IDENTITY_COOKIE), + cookies: self.cookies.remove(Self::COOKIE_NAME), } } } -const IDENTITY_COOKIE: &str = "identity"; - #[async_trait::async_trait] -impl<S> FromRequestParts<S> for IdentityToken +impl<S> FromRequestParts<S> for Identity where S: Send + Sync, { @@ -84,7 +84,7 @@ where } } -impl IntoResponseParts for IdentityToken { +impl IntoResponseParts for Identity { type Error = <CookieJar as IntoResponseParts>::Error; fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> { diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs index 60ad220..a69f509 100644 --- a/src/token/extract/identity.rs +++ b/src/token/extract/identity.rs @@ -4,7 +4,7 @@ use axum::{ response::{IntoResponse, Response}, }; -use super::IdentityToken; +use super::IdentityCookie; use crate::{ app::App, @@ -25,19 +25,10 @@ impl FromRequestParts<App> for Identity { type Rejection = LoginError<Internal>; async fn from_request_parts(parts: &mut Parts, state: &App) -> Result<Self, Self::Rejection> { - // After Rust 1.82 (and #[feature(min_exhaustive_patterns)] lands on - // stable), the following can be replaced: - // - // ``` - // let Ok(identity_token) = IdentityToken::from_request_parts( - // parts, - // state, - // ).await; - // ``` - let identity_token = IdentityToken::from_request_parts(parts, state).await?; + let Ok(cookie) = IdentityCookie::from_request_parts(parts, state).await; let RequestedAt(used_at) = RequestedAt::from_request_parts(parts, state).await?; - let secret = identity_token.secret().ok_or(LoginError::Unauthorized)?; + 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 { diff --git a/src/token/extract/mod.rs b/src/token/extract/mod.rs index b4800ae..fc0f52b 100644 --- a/src/token/extract/mod.rs +++ b/src/token/extract/mod.rs @@ -1,4 +1,4 @@ +mod cookie; mod identity; -mod identity_token; -pub use self::{identity::Identity, identity_token::IdentityToken}; +pub use self::{cookie::Identity as IdentityCookie, identity::Identity}; diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs index 9aee81f..bdc4c33 100644 --- a/src/token/repo/auth.rs +++ b/src/token/repo/auth.rs @@ -2,8 +2,10 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use crate::{ clock::DateTime, + db::NotFound, event::{Instant, Sequence}, login::{self, password::StoredHash, History, Login}, + name::{self, Name}, }; pub trait Provider { @@ -19,38 +21,53 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Auth<'t>(&'t mut SqliteConnection); impl<'t> Auth<'t> { - pub async fn for_name(&mut self, name: &str) -> Result<(History, StoredHash), sqlx::Error> { - let found = sqlx::query!( + 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: login::Id", - name, - password_hash as "password_hash: StoredHash", + 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" - from login - where name = $1 - "#, + created_at as "created_at: DateTime", + password_hash as "password_hash: StoredHash" + from login + where canonical_name = $1 + "#, name, ) - .map(|row| { - ( - History { - login: Login { - id: row.id, - name: row.name, - }, - created: Instant { - at: row.created_at, - sequence: row.created_sequence, - }, - }, - row.password_hash, - ) - }) .fetch_one(&mut *self.0) .await?; - Ok(found) + 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)] +#[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 9169743..d8463eb 100644 --- a/src/token/repo/mod.rs +++ b/src/token/repo/mod.rs @@ -1,4 +1,4 @@ pub mod auth; mod token; -pub use self::token::Provider; +pub use self::token::{LoadError, Provider}; diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs index c592dcd..35ea385 100644 --- a/src/token/repo/token.rs +++ b/src/token/repo/token.rs @@ -3,7 +3,10 @@ use uuid::Uuid; use crate::{ clock::DateTime, + db::NotFound, + event::{Instant, Sequence}, login::{self, History, Login}, + name::{self, Name}, token::{Id, Secret}, }; @@ -100,53 +103,78 @@ impl<'c> Tokens<'c> { } // Validate a token by its secret, retrieving the associated Login record. - // Will return [None] if the token is not valid. The token's last-used - // timestamp will be set to `used_at`. + // 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, Login), sqlx::Error> { + ) -> Result<(Id, History), 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. - sqlx::query!( + let (token, login) = sqlx::query!( r#" update token set last_used_at = $1 where secret = $2 + returning + id as "token: Id", + login as "login: login::Id" "#, used_at, secret, ) - .execute(&mut *self.0) + .map(|row| (row.token, row.login)) + .fetch_one(&mut *self.0) .await?; let login = sqlx::query!( r#" select - token.id as "token_id: Id", - login.id as "login_id: login::Id", - login.name as "login_name" + 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" from login - join token on login.id = token.login - where token.secret = $1 + where id = $1 "#, - secret, + login, ) .map(|row| { - ( - row.token_id, - Login { - id: row.login_id, - name: row.login_name, + Ok::<_, name::Error>(History { + login: Login { + id: row.id, + name: Name::new(row.display_name, row.canonical_name)?, }, - ) + created: Instant::new(row.created_at, row.created_sequence), + }) }) .fetch_one(&mut *self.0) - .await?; + .await??; + + Ok((token, login)) + } +} + +#[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; - Ok(login) + 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), + } } } |
