Skip to content

Commit 7658346

Browse files
committed
Chore: Improved error tracing and logging
1 parent 41af00c commit 7658346

File tree

5 files changed

+73
-96
lines changed

5 files changed

+73
-96
lines changed

Cargo.lock

Lines changed: 6 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ rand = { version = "0.8", features = ["std_rng"] }
3434
thiserror = "1"
3535
anyhow = "1"
3636
base64 = "0.13.0"
37-
sha3 = "0.9"
3837
argon2 = { version = "0.3", features = ["std"] }
3938

4039
[dev-dependencies]

src/routes/newsletters.rs

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
use crate::{domain::SubscriberEmail, email_client::EmailClient, routes::error_chain_fmt};
1+
use crate::{
2+
domain::SubscriberEmail, email_client::EmailClient, routes::error_chain_fmt,
3+
telemetry::spawn_blocking_with_tracing,
4+
};
25
use actix_web::http::header::{HeaderMap, HeaderValue};
36
use actix_web::http::{header, StatusCode};
47
use actix_web::{web, HttpRequest, HttpResponse, ResponseError};
58
use anyhow::Context;
6-
use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordVerifier, Version};
9+
use argon2::{Argon2, PasswordHash, PasswordVerifier};
710
use secrecy::{ExposeSecret, Secret};
8-
use sha3::Digest;
911
use sqlx::PgPool;
1012

1113
#[derive(serde::Deserialize)]
@@ -149,44 +151,59 @@ fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Erro
149151
})
150152
}
151153

154+
#[tracing::instrument(name = "Validate auth credentials", skip(credentials, pool))]
152155
async fn validate_credentials(
153156
credentials: Credentials,
154157
pool: &PgPool,
155158
) -> Result<uuid::Uuid, PublishError> {
156-
let hasher = Argon2::new(
157-
Algorithm::Argon2id,
158-
Version::V0x13,
159-
Params::new(15000, 2, 1, None)
160-
.context("Failed to build Argon2 params")
161-
.map_err(PublishError::UnexpectedError)?,
162-
);
163-
164-
let row: Option<_> = sqlx::query!(
165-
r#"SELECT user_id, password_hash, salt FROM users WHERE username = $1"#,
166-
credentials.username,
167-
)
168-
.fetch_optional(pool)
159+
let (user_id, expected_password_hash) = get_stored_credentials(&credentials.username, pool)
160+
.await
161+
.map_err(PublishError::UnexpectedError)?
162+
.ok_or_else(|| PublishError::AuthError(anyhow::anyhow!("Unknown username")))?;
163+
164+
let current_span = tracing::Span::current();
165+
tokio::task::spawn_blocking(move || {
166+
current_span.in_scope(|| verify_password_hash(expected_password_hash, credentials.password))
167+
})
169168
.await
170-
.context("Failed to perform the query to validate auth credentials")
169+
.context("Failed to spawn blocking task")
171170
.map_err(PublishError::UnexpectedError)?;
172171

173-
let (expected_password_hash, user_id, salt) = match row {
174-
Some(row) => (row.password_hash, row.user_id, row.salt),
175-
None => {
176-
return Err(PublishError::AuthError(anyhow::anyhow!(
177-
"Invalid username or password"
178-
)))
179-
}
180-
};
172+
Ok(user_id)
173+
}
181174

182-
let expected_password_hash = PasswordHash::new(&expected_password_hash)
183-
.context("Failed to parse hash in PHC string format")
175+
#[tracing::instrument(
176+
name = "Verify Password Hash",
177+
skip(expected_password_hash, password_candidate)
178+
)]
179+
async fn verify_password_hash(
180+
expected_password_hash: Secret<String>,
181+
password_candidate: Secret<String>,
182+
) -> Result<(), PublishError> {
183+
let expected_password_hash = PasswordHash::new(expected_password_hash.expose_secret())
184+
.context("Failed to parse hash in PHC string format.")
184185
.map_err(PublishError::UnexpectedError)?;
186+
Argon2::default()
187+
.verify_password(
188+
password_candidate.expose_secret().as_bytes(),
189+
&expected_password_hash,
190+
)
191+
.context("Invalid password.")
192+
.map_err(PublishError::AuthError)
193+
}
185194

186-
Argon2::default().verify_password(
187-
credentials.password.expose_secret().as_bytes(),
188-
&expected_password_hash,
189-
).context("Failed to verify password").map_err(PublishError::UnexpectedError)?;
190-
191-
Ok(user_id)
195+
#[tracing::instrument(name = "Get stored credentials", skip(username, pool))]
196+
async fn get_stored_credentials(
197+
username: &str,
198+
pool: &PgPool,
199+
) -> Result<Option<(uuid::Uuid, Secret<String>)>, anyhow::Error> {
200+
let row = sqlx::query!(
201+
r#"SELECT user_id, password_hash FROM users WHERE username = $1"#,
202+
username,
203+
)
204+
.fetch_optional(pool)
205+
.await
206+
.context("Failed to perform the query to validate auth credentials")?
207+
.map(|row| (row.user_id, Secret::new(row.password_hash)));
208+
Ok(row)
192209
}

src/telemetry.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use tokio::task::JoinHandle;
12
use tracing::subscriber::set_global_default;
23
use tracing::Subscriber;
34
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
@@ -26,3 +27,12 @@ pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
2627
LogTracer::init().expect("Failed to set logger");
2728
set_global_default(subscriber).expect("Failed to set subscriber");
2829
}
30+
31+
pub fn spawn_blocking_with_tracing<F, R>(f: F) -> JoinHandle<R>
32+
where
33+
F: FnOnce() -> R + Send + 'static,
34+
R: Send + 'static,
35+
{
36+
let current_span = tracing::Span::current();
37+
tokio::task::spawn_blocking(move || current_span.in_scope(f))
38+
}

tests/api/helpers.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
use argon2::password_hash::SaltString;
2+
use argon2::{Argon2, PasswordHasher};
13
use once_cell::sync::Lazy;
24
use robust_rust::{
35
configuration::{get_configuration, DatabaseSettings},
46
startup::{get_connection_pool, Application},
57
telemetry::{get_subscriber, init_subscriber},
68
};
7-
use sha3::Digest;
89
use sqlx::{Connection, Executor, PgConnection, PgPool};
910
use uuid::Uuid;
1011
use wiremock::MockServer;
@@ -94,8 +95,11 @@ impl TestUser {
9495
}
9596

9697
async fn store(&self, pool: &PgPool) {
97-
let password_harsh = sha3::Sha3_256::digest(self.password.as_bytes());
98-
let password_hash = format!("{:x}", password_harsh);
98+
let salt = SaltString::generate(&mut rand::thread_rng());
99+
let password_hash = Argon2::default()
100+
.hash_password(self.password.as_bytes(), salt.as_ref())
101+
.unwrap()
102+
.to_string();
99103

100104
sqlx::query!(
101105
r#"

0 commit comments

Comments
 (0)