summaryrefslogtreecommitdiff
path: root/src/token
diff options
context:
space:
mode:
Diffstat (limited to 'src/token')
-rw-r--r--src/token/app.rs126
-rw-r--r--src/token/extract/identity.rs20
-rw-r--r--src/token/mod.rs28
-rw-r--r--src/token/repo/auth.rs103
-rw-r--r--src/token/repo/mod.rs1
-rw-r--r--src/token/repo/token.rs97
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)