summaryrefslogtreecommitdiff
path: root/src/token
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-08-26 00:44:29 -0400
committerOwen Jacobson <owen@grimoire.ca>2025-08-26 01:38:21 -0400
commitd0d5fa20200a7ad70173ba87ae47c33b60f44a3b (patch)
tree39d5dbd0cae7df293a87bc9fcfc78f57e72d5204 /src/token
parent0bbc83f09cc7517dddf16770a15f9e90815f48ba (diff)
Split `user` into a chat-facing entity and an authentication-facing entity.
The taxonomy is now as follows: * A _login_ is someone's identity for the purposes of authenticating to the service. Logins are not synchronized, and in fact are not published anywhere in the current API. They have a login ID, a name and a password. * A _user_ is someone's identity for the purpose of participating in conversations. Users _are_ synchronized, as before. They have a user ID, a name, and a creation instant for the purposes of synchronization. In practice, a user exists for every login - in fact, users' names are stored in the login table and are joined in, rather than being stored redundantly in the user table. A login ID and its corresponding user ID are always equal, and the user and login ID types support conversion and comparison to facilitate their use in this context. Tokens are now associated with logins, not users. The currently-acting identity is passed down into app types as a login, not a user, and then resolved to a user where appropriate within the app methods. As a side effect, the `GET /api/boot` method now returns a `login` key instead of a `user` key. The structure of the nested value is unchanged.
Diffstat (limited to 'src/token')
-rw-r--r--src/token/app.rs118
-rw-r--r--src/token/extract/identity.rs4
-rw-r--r--src/token/mod.rs12
-rw-r--r--src/token/repo/auth.rs105
-rw-r--r--src/token/repo/mod.rs1
-rw-r--r--src/token/repo/token.rs43
6 files changed, 26 insertions, 257 deletions
diff --git a/src/token/app.rs b/src/token/app.rs
index a7a843d..fb5d712 100644
--- a/src/token/app.rs
+++ b/src/token/app.rs
@@ -8,15 +8,9 @@ use sqlx::sqlite::SqlitePool;
use super::{
Broadcaster, Event as TokenEvent, Secret, Token,
extract::Identity,
- repo::{self, Provider as _, auth::Provider as _},
-};
-use crate::{
- clock::DateTime,
- db::NotFound as _,
- name::{self, Name},
- password::Password,
- user::{User, repo::Provider as _},
+ repo::{self, Provider as _},
};
+use crate::{clock::DateTime, db::NotFound as _, name};
pub struct Tokens<'a> {
db: &'a SqlitePool,
@@ -28,100 +22,20 @@ 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?;
-
- let user = user.as_snapshot().ok_or(LoginError::Rejected)?;
-
- if stored_hash.verify(password)? {
- let mut tx = self.db.begin().await?;
- let (token, secret) = Token::generate(&user, login_at);
- tx.tokens().create(&token, &secret).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);
- }
-
- let user_snapshot = user.as_snapshot().ok_or(LoginError::Rejected)?;
- let to_hash = to.hash()?;
-
- let mut tx = self.db.begin().await?;
- tx.users().set_password(&user, &to_hash).await?;
-
- let tokens = tx.tokens().revoke_all(&user).await?;
- let (token, secret) = Token::generate(&user_snapshot, changed_at);
- tx.tokens().create(&token, &secret).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<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(Identity { token, user })
+ Ok(Identity { token, login })
}
pub async fn limit_stream<S, E>(
@@ -208,33 +122,9 @@ impl<'a> Tokens<'a> {
}
#[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 d01ab53..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},
+ login::Login,
token::{Token, app::ValidateError},
- user::User,
};
#[derive(Clone, Debug)]
pub struct Identity {
pub token: Token,
- pub user: User,
+ pub login: Login,
}
impl FromRequestParts<App> for Identity {
diff --git a/src/token/mod.rs b/src/token/mod.rs
index 58ff08b..b2dd6f1 100644
--- a/src/token/mod.rs
+++ b/src/token/mod.rs
@@ -8,30 +8,26 @@ mod secret;
use uuid::Uuid;
-use crate::{
- clock::DateTime,
- user::{self, User},
-};
-
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 user: user::Id,
+ pub login: login::Id,
pub issued_at: DateTime,
pub last_used_at: DateTime,
}
impl Token {
- pub fn generate(user: &User, issued_at: &DateTime) -> (Self, Secret) {
+ pub fn generate(login: &Login, issued_at: &DateTime) -> (Self, Secret) {
let id = Id::generate();
let secret = Uuid::new_v4().to_string().into();
(
Self {
id,
- user: user.id.clone(),
+ login: login.id.clone(),
issued_at: *issued_at,
last_used_at: *issued_at,
},
diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs
deleted file mode 100644
index a42fa1a..0000000
--- a/src/token/repo/auth.rs
+++ /dev/null
@@ -1,105 +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",
- login.display_name as "display_name: String",
- login.canonical_name as "canonical_name: String",
- user.created_sequence as "created_sequence: Sequence",
- user.created_at as "created_at: DateTime",
- login.password as "password: StoredHash"
- from user
- join login using (id)
- where login.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))
- }
-
- pub async fn for_user(&mut self, user: &User) -> Result<(History, StoredHash), LoadError> {
- let row = sqlx::query!(
- r#"
- select
- id as "id: user::Id",
- login.display_name as "display_name: String",
- login.canonical_name as "canonical_name: String",
- user.created_sequence as "created_sequence: Sequence",
- user.created_at as "created_at: DateTime",
- login.password as "password: StoredHash"
- from user
- join login using (id)
- 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))
- }
-}
-
-#[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 afcde53..52a3987 100644
--- a/src/token/repo/token.rs
+++ b/src/token/repo/token.rs
@@ -3,10 +3,9 @@ use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite};
use crate::{
clock::DateTime,
db::NotFound,
- event::{Instant, Sequence},
+ login::{self, Login},
name::{self, Name},
token::{Id, Secret, Token},
- user::{self, History, User},
};
pub trait Provider {
@@ -31,11 +30,11 @@ impl Tokens<'_> {
"#,
token.id,
secret,
- token.user,
+ token.login,
token.issued_at,
token.last_used_at,
)
- .fetch_one(&mut *self.0)
+ .execute(&mut *self.0)
.await?;
Ok(())
@@ -71,8 +70,7 @@ impl Tokens<'_> {
}
// 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
@@ -80,7 +78,7 @@ impl Tokens<'_> {
where login = $1
returning id as "id: Id"
"#,
- user,
+ login.id,
)
.fetch_all(&mut *self.0)
.await?;
@@ -106,14 +104,11 @@ 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<(Token, 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
@@ -125,7 +120,7 @@ impl Tokens<'_> {
where secret = $2
returning
id as "id: Id",
- login as "login: user::Id",
+ login as "login: login::Id",
issued_at as "issued_at: DateTime",
last_used_at as "last_used_at: DateTime"
"#,
@@ -134,7 +129,7 @@ impl Tokens<'_> {
)
.map(|row| Token {
id: row.id,
- user: row.login,
+ login: row.login,
issued_at: row.issued_at,
last_used_at: row.last_used_at,
})
@@ -144,24 +139,18 @@ impl Tokens<'_> {
let user = sqlx::query!(
r#"
select
- id as "id: user::Id",
- login.display_name as "display_name: String",
- login.canonical_name as "canonical_name: String",
- user.created_sequence as "created_sequence: Sequence",
- user.created_at as "created_at: DateTime"
- from user
- join login using (id)
+ id as "id: login::Id",
+ display_name,
+ canonical_name
+ from login
where id = $1
"#,
- token.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)