summaryrefslogtreecommitdiff
path: root/src/login
diff options
context:
space:
mode:
Diffstat (limited to 'src/login')
-rw-r--r--src/login/app.rs22
-rw-r--r--src/login/extract.rs46
-rw-r--r--src/login/routes.rs10
-rw-r--r--src/login/routes/test/login.rs8
-rw-r--r--src/login/routes/test/logout.rs2
5 files changed, 65 insertions, 23 deletions
diff --git a/src/login/app.rs b/src/login/app.rs
index 292b95f..f7fec88 100644
--- a/src/login/app.rs
+++ b/src/login/app.rs
@@ -1,10 +1,10 @@
use chrono::TimeDelta;
use sqlx::sqlite::SqlitePool;
-use super::repo::auth::Provider as _;
+use super::{extract::IdentitySecret, repo::auth::Provider as _};
use crate::{
clock::DateTime,
- password::StoredHash,
+ password::Password,
repo::{
error::NotFound as _,
login::{Login, Provider as _},
@@ -24,9 +24,9 @@ impl<'a> Logins<'a> {
pub async fn login(
&self,
name: &str,
- password: &str,
+ password: &Password,
login_at: &DateTime,
- ) -> Result<String, LoginError> {
+ ) -> Result<IdentitySecret, LoginError> {
let mut tx = self.db.begin().await?;
let login = if let Some((login, stored_hash)) = tx.auth().for_name(name).await? {
@@ -38,7 +38,7 @@ impl<'a> Logins<'a> {
return Err(LoginError::Rejected);
}
} else {
- let password_hash = StoredHash::new(password)?;
+ let password_hash = password.hash()?;
tx.logins().create(name, &password_hash).await?
};
@@ -49,8 +49,8 @@ impl<'a> Logins<'a> {
}
#[cfg(test)]
- pub async fn create(&self, name: &str, password: &str) -> Result<Login, CreateError> {
- let password_hash = StoredHash::new(password)?;
+ pub async fn create(&self, name: &str, password: &Password) -> Result<Login, CreateError> {
+ let password_hash = password.hash()?;
let mut tx = self.db.begin().await?;
let login = tx.logins().create(name, &password_hash).await?;
@@ -59,7 +59,11 @@ impl<'a> Logins<'a> {
Ok(login)
}
- pub async fn validate(&self, secret: &str, used_at: &DateTime) -> Result<Login, ValidateError> {
+ pub async fn validate(
+ &self,
+ secret: &IdentitySecret,
+ used_at: &DateTime,
+ ) -> Result<Login, ValidateError> {
let mut tx = self.db.begin().await?;
let login = tx
.tokens()
@@ -82,7 +86,7 @@ impl<'a> Logins<'a> {
Ok(())
}
- pub async fn logout(&self, secret: &str) -> Result<(), ValidateError> {
+ pub async fn logout(&self, secret: &IdentitySecret) -> Result<(), ValidateError> {
let mut tx = self.db.begin().await?;
tx.tokens()
.revoke(secret)
diff --git a/src/login/extract.rs b/src/login/extract.rs
index 5ef454c..3b31d4c 100644
--- a/src/login/extract.rs
+++ b/src/login/extract.rs
@@ -1,3 +1,5 @@
+use std::fmt;
+
use axum::{
extract::FromRequestParts,
http::request::Parts,
@@ -7,11 +9,22 @@ use axum_extra::extract::cookie::{Cookie, CookieJar};
// 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, Debug)]
+#[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)]
@@ -26,14 +39,18 @@ impl IdentityToken {
// 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<&str> {
- self.cookies.get(IDENTITY_COOKIE).map(Cookie::value)
+ 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: &str) -> Self {
- let identity_cookie = Cookie::build((IDENTITY_COOKIE, String::from(secret)))
+ 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()
@@ -76,3 +93,22 @@ impl IntoResponseParts for IdentityToken {
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())
+ }
+}
diff --git a/src/login/routes.rs b/src/login/routes.rs
index 31a68d0..4664063 100644
--- a/src/login/routes.rs
+++ b/src/login/routes.rs
@@ -6,7 +6,9 @@ use axum::{
Router,
};
-use crate::{app::App, clock::RequestedAt, error::Internal, repo::login::Login};
+use crate::{
+ app::App, clock::RequestedAt, error::Internal, password::Password, repo::login::Login,
+};
use super::{app, extract::IdentityToken};
@@ -38,7 +40,7 @@ impl IntoResponse for Boot {
#[derive(serde::Deserialize)]
struct LoginRequest {
name: String,
- password: String,
+ password: Password,
}
async fn on_login(
@@ -52,7 +54,7 @@ async fn on_login(
.login(&request.name, &request.password, &now)
.await
.map_err(LoginError)?;
- let identity = identity.set(&token);
+ let identity = identity.set(token);
Ok((identity, StatusCode::NO_CONTENT))
}
@@ -82,7 +84,7 @@ async fn on_logout(
Json(LogoutRequest {}): Json<LogoutRequest>,
) -> Result<(IdentityToken, StatusCode), LogoutError> {
if let Some(secret) = identity.secret() {
- app.logins().logout(secret).await.map_err(LogoutError)?;
+ app.logins().logout(&secret).await.map_err(LogoutError)?;
}
let identity = identity.clear();
diff --git a/src/login/routes/test/login.rs b/src/login/routes/test/login.rs
index 719ccca..10c17d6 100644
--- a/src/login/routes/test/login.rs
+++ b/src/login/routes/test/login.rs
@@ -38,7 +38,7 @@ async fn new_identity() {
let validated_at = fixtures::now();
let validated = app
.logins()
- .validate(secret, &validated_at)
+ .validate(&secret, &validated_at)
.await
.expect("identity secret is valid");
@@ -75,7 +75,7 @@ async fn existing_identity() {
let validated_at = fixtures::now();
let validated_login = app
.logins()
- .validate(secret, &validated_at)
+ .validate(&secret, &validated_at)
.await
.expect("identity secret is valid");
@@ -122,7 +122,7 @@ async fn token_expires() {
let (identity, _) = routes::on_login(State(app.clone()), logged_in_at, identity, Json(request))
.await
.expect("logged in with valid credentials");
- let token = identity.secret().expect("logged in with valid credentials");
+ let secret = identity.secret().expect("logged in with valid credentials");
// Verify the semantics
@@ -135,7 +135,7 @@ async fn token_expires() {
let verified_at = fixtures::now();
let error = app
.logins()
- .validate(token, &verified_at)
+ .validate(&secret, &verified_at)
.await
.expect_err("validating an expired token");
diff --git a/src/login/routes/test/logout.rs b/src/login/routes/test/logout.rs
index 4c09a73..05594be 100644
--- a/src/login/routes/test/logout.rs
+++ b/src/login/routes/test/logout.rs
@@ -37,7 +37,7 @@ async fn successful() {
let error = app
.logins()
- .validate(secret, &now)
+ .validate(&secret, &now)
.await
.expect_err("secret is invalid");
match error {