summaryrefslogtreecommitdiff
path: root/src/login
diff options
context:
space:
mode:
Diffstat (limited to 'src/login')
-rw-r--r--src/login/app.rs107
-rw-r--r--src/login/extract/login.rs12
-rw-r--r--src/login/mod.rs1
-rw-r--r--src/login/repo/logins.rs95
-rw-r--r--src/login/routes.rs38
5 files changed, 128 insertions, 125 deletions
diff --git a/src/login/app.rs b/src/login/app.rs
new file mode 100644
index 0000000..ced76d6
--- /dev/null
+++ b/src/login/app.rs
@@ -0,0 +1,107 @@
+use argon2::Argon2;
+use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
+use rand_core::OsRng;
+use sqlx::sqlite::SqlitePool;
+
+use super::repo::{
+ logins::{Login, Provider as _},
+ tokens::Provider as _,
+};
+use crate::error::BoxedError;
+
+type DateTime = chrono::DateTime<chrono::Utc>;
+
+pub struct Logins<'a> {
+ db: &'a SqlitePool,
+}
+
+impl<'a> Logins<'a> {
+ pub fn new(db: &'a SqlitePool) -> Self {
+ Self { db }
+ }
+
+ pub async fn login(
+ &self,
+ name: &str,
+ password: &str,
+ login_at: DateTime,
+ ) -> Result<Option<String>, BoxedError> {
+ let mut tx = self.db.begin().await?;
+
+ let login = if let Some((login, stored_hash)) = tx.logins().for_login(name).await? {
+ if stored_hash.verify(password)? {
+ // Password verified; use the login.
+ Some(login)
+ } else {
+ // Password NOT verified.
+ None
+ }
+ } else {
+ let password_hash = StoredHash::new(password)?;
+ Some(tx.logins().create(name, &password_hash).await?)
+ };
+
+ // If `login` is Some, then we have an identity and can issue a token.
+ // If `login` is None, then neither creating a new login nor
+ // authenticating an existing one succeeded, and we must reject the
+ // login attempt.
+ let token = if let Some(login) = login {
+ Some(tx.tokens().issue(&login.id, login_at).await?)
+ } else {
+ None
+ };
+
+ tx.commit().await?;
+
+ Ok(token)
+ }
+
+ pub async fn validate(
+ &self,
+ secret: &str,
+ used_at: DateTime,
+ ) -> Result<Option<Login>, BoxedError> {
+ let mut tx = self.db.begin().await?;
+ tx.tokens().expire(used_at).await?;
+ let login = tx.tokens().validate(secret, used_at).await?;
+ tx.commit().await?;
+
+ Ok(login)
+ }
+
+ pub async fn logout(&self, secret: &str) -> Result<(), BoxedError> {
+ let mut tx = self.db.begin().await?;
+ tx.tokens().revoke(secret).await?;
+ tx.commit().await?;
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, sqlx::Type)]
+#[sqlx(transparent)]
+pub struct StoredHash(String);
+
+impl StoredHash {
+ fn new(password: &str) -> Result<Self, password_hash::Error> {
+ let salt = SaltString::generate(&mut OsRng);
+ let argon2 = Argon2::default();
+ let hash = argon2
+ .hash_password(password.as_bytes(), &salt)?
+ .to_string();
+ Ok(Self(hash))
+ }
+
+ fn verify(&self, password: &str) -> Result<bool, password_hash::Error> {
+ let hash = PasswordHash::new(&self.0)?;
+
+ match Argon2::default().verify_password(password.as_bytes(), &hash) {
+ // Successful authentication, not an error
+ Ok(()) => Ok(true),
+ // Unsuccessful authentication, also not an error
+ Err(password_hash::errors::Error::Password) => Ok(false),
+ // Password validation failed for some other reason, treat as an error
+ Err(err) => Err(err),
+ }
+ }
+}
diff --git a/src/login/extract/login.rs b/src/login/extract/login.rs
index 4155ec2..a5f648b 100644
--- a/src/login/extract/login.rs
+++ b/src/login/extract/login.rs
@@ -8,10 +8,7 @@ use crate::{
app::App,
clock::RequestedAt,
error::InternalError,
- login::{
- extract::IdentityToken,
- repo::{logins::Login, tokens::Provider as _},
- },
+ login::{extract::IdentityToken, repo::logins::Login},
};
#[async_trait::async_trait]
@@ -24,15 +21,12 @@ impl FromRequestParts<App> for Login {
//
// let Ok(identity_token) = IdentityToken::from_request_parts(parts, state).await;
let identity_token = IdentityToken::from_request_parts(parts, state).await?;
- let RequestedAt(requested_at) = RequestedAt::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::Forbidden)?;
let app = State::<App>::from_request_parts(parts, state).await?;
- let mut tx = app.db.begin().await?;
- tx.tokens().expire(requested_at).await?;
- let login = tx.tokens().validate(secret, requested_at).await?;
- tx.commit().await?;
+ let login = app.logins().validate(secret, used_at).await?;
login.ok_or(LoginError::Forbidden)
}
diff --git a/src/login/mod.rs b/src/login/mod.rs
index c2b2924..5070301 100644
--- a/src/login/mod.rs
+++ b/src/login/mod.rs
@@ -1,5 +1,6 @@
pub use self::routes::router;
+pub mod app;
mod extract;
pub mod repo;
mod routes;
diff --git a/src/login/repo/logins.rs b/src/login/repo/logins.rs
index 5f042fd..26a5b09 100644
--- a/src/login/repo/logins.rs
+++ b/src/login/repo/logins.rs
@@ -1,10 +1,8 @@
-use argon2::Argon2;
-use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
-use rand_core::OsRng;
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
use crate::error::BoxedError;
use crate::id::Id as BaseId;
+use crate::login::app::StoredHash;
pub trait Provider {
fn logins(&mut self) -> Logins;
@@ -30,77 +28,40 @@ pub struct Login {
}
impl<'c> Logins<'c> {
- /// Create a new login, if the name is not already taken. Returns a [Login]
- /// if a new login has actually been created, or `None` if an existing login
- /// was found.
pub async fn create(
&mut self,
name: &str,
- password: &str,
- ) -> Result<Option<Login>, BoxedError> {
+ password_hash: &StoredHash,
+ ) -> Result<Login, BoxedError> {
let id = Id::generate();
- let password_hash = StoredHash::new(password)?;
- let insert_res = sqlx::query_as!(
+ let login = sqlx::query_as!(
Login,
r#"
insert or fail
into login (id, name, password_hash)
values ($1, $2, $3)
- returning id as "id: Id", name
+ returning
+ id as "id: Id",
+ name
"#,
id,
name,
password_hash,
)
.fetch_one(&mut *self.0)
- .await;
+ .await?;
- let result = match insert_res {
- Ok(id) => Ok(Some(id)),
- Err(err) => {
- if let Some(true) = err
- .as_database_error()
- .map(|db_err| db_err.is_unique_violation())
- {
- // Login with the same username (or, very rarely, same ID) already
- // exists.
- Ok(None)
- } else {
- Err(err)
- }
- }
- }?;
- Ok(result)
+ Ok(login)
}
- /// Authenticates `name` and `password` against an existing [Login]. Returns
- /// that [Login] if one was found and the password was correct, or `None` if
- /// either condition does not hold.
- pub async fn authenticate(
+ /// Retrieves a login by name, plus its stored password hash for
+ /// verification. If there's no login with the requested name, this will
+ /// return [None].
+ pub async fn for_login(
&mut self,
name: &str,
- password: &str,
- ) -> Result<Option<Login>, BoxedError> {
- let found = self.for_name(name).await?;
-
- let login = if let Some((login, stored_hash)) = found {
- if stored_hash.verify(password)? {
- // User found and password validation succeeded.
- Some(login)
- } else {
- // Password validation failed.
- None
- }
- } else {
- // User not found.
- None
- };
-
- Ok(login)
- }
-
- async fn for_name(&mut self, name: &str) -> Result<Option<(Login, StoredHash)>, BoxedError> {
+ ) -> Result<Option<(Login, StoredHash)>, BoxedError> {
let found = sqlx::query!(
r#"
select
@@ -144,31 +105,3 @@ impl Id {
BaseId::generate("L")
}
}
-
-#[derive(Debug, sqlx::Type)]
-#[sqlx(transparent)]
-struct StoredHash(String);
-
-impl StoredHash {
- fn new(password: &str) -> Result<Self, password_hash::Error> {
- let salt = SaltString::generate(&mut OsRng);
- let argon2 = Argon2::default();
- let hash = argon2
- .hash_password(password.as_bytes(), &salt)?
- .to_string();
- Ok(Self(hash))
- }
-
- fn verify(&self, password: &str) -> Result<bool, password_hash::Error> {
- let hash = PasswordHash::new(&self.0)?;
-
- match Argon2::default().verify_password(password.as_bytes(), &hash) {
- // Successful authentication, not an error
- Ok(()) => Ok(true),
- // Unsuccessful authentication, also not an error
- Err(password_hash::errors::Error::Password) => Ok(false),
- // Password validation failed for some other reason, treat as an error
- Err(err) => Err(err),
- }
- }
-}
diff --git a/src/login/routes.rs b/src/login/routes.rs
index 9cefe38..816926e 100644
--- a/src/login/routes.rs
+++ b/src/login/routes.rs
@@ -8,10 +8,7 @@ use axum::{
use crate::{app::App, clock::RequestedAt, error::InternalError};
-use super::{
- extract::IdentityToken,
- repo::{logins::Provider as _, tokens::Provider as _},
-};
+use super::extract::IdentityToken;
pub fn router() -> Router<App> {
Router::new()
@@ -31,34 +28,7 @@ async fn on_login(
identity: IdentityToken,
Form(form): Form<LoginRequest>,
) -> Result<impl IntoResponse, InternalError> {
- let mut tx = app.db.begin().await?;
-
- // Spelling the following in the more conventional form,
- // if let Some(…) = create().await? {}
- // else if let Some(…) = validate().await? {}
- // else {}
- // pushes the specifics of whether the returned error types are Send or not
- // (they aren't) into the type of this function's generated Futures, which
- // in turn makes this function unusable as an Axum handler.
- let login = tx.logins().create(&form.name, &form.password).await?;
- let login = if login.is_some() {
- login
- } else {
- tx.logins().authenticate(&form.name, &form.password).await?
- };
-
- // If `login` is Some, then we have an identity and can issue an identity
- // token. If `login` is None, then neither creating a new login nor authenticating
- // an existing one succeeded, and we must reject the attempt.
- //
- // These properties will be transferred to `token`, as well.
- let token = if let Some(login) = login {
- Some(tx.tokens().issue(&login.id, now).await?)
- } else {
- None
- };
-
- tx.commit().await?;
+ let token = app.logins().login(&form.name, &form.password, now).await?;
let resp = if let Some(token) = token {
let identity = identity.set(&token);
@@ -91,9 +61,7 @@ async fn on_logout(
identity: IdentityToken,
) -> Result<impl IntoResponse, InternalError> {
if let Some(secret) = identity.secret() {
- let mut tx = app.db.begin().await?;
- tx.tokens().revoke(secret).await?;
- tx.commit().await?;
+ app.logins().logout(secret).await?;
}
let identity = identity.clear();