summaryrefslogtreecommitdiff
path: root/src/login
diff options
context:
space:
mode:
Diffstat (limited to 'src/login')
-rw-r--r--src/login/app.rs17
-rw-r--r--src/login/extract.rs181
-rw-r--r--src/login/mod.rs18
-rw-r--r--src/login/password.rs58
-rw-r--r--src/login/repo/auth.rs2
-rw-r--r--src/login/routes.rs6
-rw-r--r--src/login/token/id.rs27
-rw-r--r--src/login/token/mod.rs3
-rw-r--r--src/login/types.rs2
9 files changed, 92 insertions, 222 deletions
diff --git a/src/login/app.rs b/src/login/app.rs
index 8ea0a91..60475af 100644
--- a/src/login/app.rs
+++ b/src/login/app.rs
@@ -6,18 +6,15 @@ use futures::{
};
use sqlx::sqlite::SqlitePool;
-use super::{
- broadcaster::Broadcaster, extract::IdentitySecret, repo::auth::Provider as _, token, types,
-};
+use super::{broadcaster::Broadcaster, repo::auth::Provider as _, types, Login};
use crate::{
clock::DateTime,
- password::Password,
+ event::Sequence,
+ login::Password,
repo::{
- error::NotFound as _,
- login::{Login, Provider as _},
- sequence::{Provider as _, Sequence},
- token::Provider as _,
+ error::NotFound as _, login::Provider as _, sequence::Provider as _, token::Provider as _,
},
+ token::{self, Secret},
};
pub struct Logins<'a> {
@@ -43,7 +40,7 @@ impl<'a> Logins<'a> {
name: &str,
password: &Password,
login_at: &DateTime,
- ) -> Result<IdentitySecret, LoginError> {
+ ) -> Result<Secret, LoginError> {
let mut tx = self.db.begin().await?;
let login = if let Some((login, stored_hash)) = tx.auth().for_name(name).await? {
@@ -78,7 +75,7 @@ impl<'a> Logins<'a> {
pub async fn validate(
&self,
- secret: &IdentitySecret,
+ secret: &Secret,
used_at: &DateTime,
) -> Result<(token::Id, Login), ValidateError> {
let mut tx = self.db.begin().await?;
diff --git a/src/login/extract.rs b/src/login/extract.rs
index 39dd9e4..c2d97f2 100644
--- a/src/login/extract.rs
+++ b/src/login/extract.rs
@@ -1,182 +1,15 @@
-use std::fmt;
+use axum::{extract::FromRequestParts, http::request::Parts};
-use axum::{
- extract::{FromRequestParts, State},
- http::request::Parts,
- response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
-};
-use axum_extra::extract::cookie::{Cookie, CookieJar};
-
-use crate::{
- app::App,
- clock::RequestedAt,
- error::{Internal, Unauthorized},
- login::{app::ValidateError, token},
- repo::login::Login,
-};
-
-// 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 {
- cookies: CookieJar,
-}
-
-impl fmt::Debug for IdentityToken {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("IdentityToken")
- .field(
- "identity",
- &self.cookies.get(IDENTITY_COOKIE).map(|_| "********"),
- )
- .finish()
- }
-}
-
-impl IdentityToken {
- // Creates a new, unpopulated identity token store.
- #[cfg(test)]
- pub fn new() -> Self {
- Self {
- cookies: CookieJar::new(),
- }
- }
-
- // Get the identity secret sent in the request, if any. If the identity
- // was not sent, or if it has previously been [clear]ed, then this will
- // return [None]. If the identity has previously been [set], then this
- // will return that secret, regardless of what the request originally
- // included.
- pub fn secret(&self) -> Option<IdentitySecret> {
- self.cookies
- .get(IDENTITY_COOKIE)
- .map(Cookie::value)
- .map(IdentitySecret::from)
- }
-
- // Positively set the identity secret, and ensure that it will be sent
- // back to the client when this extractor is included in a response.
- pub fn set(self, secret: impl Into<IdentitySecret>) -> Self {
- let IdentitySecret(secret) = secret.into();
- let identity_cookie = Cookie::build((IDENTITY_COOKIE, secret))
- .http_only(true)
- .path("/api/")
- .permanent()
- .build();
-
- Self {
- cookies: self.cookies.add(identity_cookie),
- }
- }
-
- // Remove the identity secret and ensure that it will be cleared when this
- // extractor is included in a response.
- pub fn clear(self) -> Self {
- Self {
- cookies: self.cookies.remove(IDENTITY_COOKIE),
- }
- }
-}
-
-const IDENTITY_COOKIE: &str = "identity";
-
-#[async_trait::async_trait]
-impl<S> FromRequestParts<S> for IdentityToken
-where
- S: Send + Sync,
-{
- type Rejection = <CookieJar as FromRequestParts<S>>::Rejection;
-
- async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
- let cookies = CookieJar::from_request_parts(parts, state).await?;
- Ok(Self { cookies })
- }
-}
-
-impl IntoResponseParts for IdentityToken {
- type Error = <CookieJar as IntoResponseParts>::Error;
-
- fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> {
- let Self { cookies } = self;
- cookies.into_response_parts(res)
- }
-}
-
-#[derive(sqlx::Type)]
-#[sqlx(transparent)]
-pub struct IdentitySecret(String);
-
-impl fmt::Debug for IdentitySecret {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_tuple("IdentityToken").field(&"********").finish()
- }
-}
-
-impl<S> From<S> for IdentitySecret
-where
- S: Into<String>,
-{
- fn from(value: S) -> Self {
- Self(value.into())
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Identity {
- pub token: token::Id,
- pub login: Login,
-}
+use super::Login;
+use crate::{app::App, token::extract::Identity};
#[async_trait::async_trait]
-impl FromRequestParts<App> for Identity {
- type Rejection = LoginError<Internal>;
+impl FromRequestParts<App> for Login {
+ type Rejection = <Identity as FromRequestParts<App>>::Rejection;
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 RequestedAt(used_at) = RequestedAt::from_request_parts(parts, state).await?;
-
- let secret = identity_token.secret().ok_or(LoginError::Unauthorized)?;
-
- let app = State::<App>::from_request_parts(parts, state).await?;
- match app.logins().validate(&secret, &used_at).await {
- Ok((token, login)) => Ok(Identity { token, login }),
- Err(ValidateError::InvalidToken) => Err(LoginError::Unauthorized),
- Err(other) => Err(other.into()),
- }
- }
-}
-
-pub enum LoginError<E> {
- Failure(E),
- Unauthorized,
-}
-
-impl<E> IntoResponse for LoginError<E>
-where
- E: IntoResponse,
-{
- fn into_response(self) -> Response {
- match self {
- Self::Unauthorized => Unauthorized.into_response(),
- Self::Failure(e) => e.into_response(),
- }
- }
-}
+ let identity = Identity::from_request_parts(parts, state).await?;
-impl<E> From<E> for LoginError<Internal>
-where
- E: Into<Internal>,
-{
- fn from(err: E) -> Self {
- Self::Failure(err.into())
+ Ok(identity.login)
}
}
diff --git a/src/login/mod.rs b/src/login/mod.rs
index 0430f4b..91c1821 100644
--- a/src/login/mod.rs
+++ b/src/login/mod.rs
@@ -2,10 +2,22 @@ pub mod app;
pub mod broadcaster;
pub mod extract;
mod id;
+pub mod password;
mod repo;
mod routes;
-pub mod token;
pub mod types;
-pub use self::id::Id;
-pub use self::routes::router;
+pub use self::{id::Id, password::Password, routes::router};
+
+// This also implements FromRequestParts (see `./extract.rs`). As a result, it
+// can be used as an extractor for endpoints that want to require login, or for
+// endpoints that need to behave differently depending on whether the client is
+// or is not logged in.
+#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
+pub struct Login {
+ pub id: Id,
+ pub name: String,
+ // The omission of the hashed password is deliberate, to minimize the
+ // chance that it ends up tangled up in debug output or in some other chunk
+ // of logic elsewhere.
+}
diff --git a/src/login/password.rs b/src/login/password.rs
new file mode 100644
index 0000000..da3930f
--- /dev/null
+++ b/src/login/password.rs
@@ -0,0 +1,58 @@
+use std::fmt;
+
+use argon2::Argon2;
+use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
+use rand_core::OsRng;
+
+#[derive(Debug, sqlx::Type)]
+#[sqlx(transparent)]
+pub struct StoredHash(String);
+
+impl StoredHash {
+ pub fn verify(&self, password: &Password) -> 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),
+ }
+ }
+}
+
+#[derive(serde::Deserialize)]
+#[serde(transparent)]
+pub struct Password(String);
+
+impl Password {
+ pub fn hash(&self) -> Result<StoredHash, password_hash::Error> {
+ let Self(password) = self;
+ let salt = SaltString::generate(&mut OsRng);
+ let argon2 = Argon2::default();
+ let hash = argon2
+ .hash_password(password.as_bytes(), &salt)?
+ .to_string();
+ Ok(StoredHash(hash))
+ }
+
+ fn as_bytes(&self) -> &[u8] {
+ let Self(value) = self;
+ value.as_bytes()
+ }
+}
+
+impl fmt::Debug for Password {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_tuple("Password").field(&"********").finish()
+ }
+}
+
+#[cfg(test)]
+impl From<String> for Password {
+ fn from(password: String) -> Self {
+ Self(password)
+ }
+}
diff --git a/src/login/repo/auth.rs b/src/login/repo/auth.rs
index 9816c5c..b299697 100644
--- a/src/login/repo/auth.rs
+++ b/src/login/repo/auth.rs
@@ -1,6 +1,6 @@
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
-use crate::{login, password::StoredHash, repo::login::Login};
+use crate::login::{self, password::StoredHash, Login};
pub trait Provider {
fn auth(&mut self) -> Auth;
diff --git a/src/login/routes.rs b/src/login/routes.rs
index ef75871..b571bd5 100644
--- a/src/login/routes.rs
+++ b/src/login/routes.rs
@@ -10,11 +10,11 @@ use crate::{
app::App,
clock::RequestedAt,
error::{Internal, Unauthorized},
- password::Password,
- repo::login::Login,
+ login::{Login, Password},
};
-use super::{app, extract::IdentityToken};
+use super::app;
+use crate::token::extract::IdentityToken;
#[cfg(test)]
mod test;
diff --git a/src/login/token/id.rs b/src/login/token/id.rs
deleted file mode 100644
index 9ef063c..0000000
--- a/src/login/token/id.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use std::fmt;
-
-use crate::id::Id as BaseId;
-
-// Stable identifier for a token. Prefixed with `T`.
-#[derive(Clone, Debug, Eq, Hash, PartialEq, sqlx::Type, serde::Deserialize, serde::Serialize)]
-#[sqlx(transparent)]
-#[serde(transparent)]
-pub struct Id(BaseId);
-
-impl From<BaseId> for Id {
- fn from(id: BaseId) -> Self {
- Self(id)
- }
-}
-
-impl Id {
- pub fn generate() -> Self {
- BaseId::generate("T")
- }
-}
-
-impl fmt::Display for Id {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
- }
-}
diff --git a/src/login/token/mod.rs b/src/login/token/mod.rs
deleted file mode 100644
index d563a88..0000000
--- a/src/login/token/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-mod id;
-
-pub use self::id::Id;
diff --git a/src/login/types.rs b/src/login/types.rs
index a210977..d53d436 100644
--- a/src/login/types.rs
+++ b/src/login/types.rs
@@ -1,4 +1,4 @@
-use crate::login::token;
+use crate::token;
#[derive(Clone, Debug)]
pub struct TokenRevoked {