|
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 | +}; |
2 | 5 | use actix_web::http::header::{HeaderMap, HeaderValue};
|
3 | 6 | use actix_web::http::{header, StatusCode};
|
4 | 7 | use actix_web::{web, HttpRequest, HttpResponse, ResponseError};
|
5 | 8 | use anyhow::Context;
|
6 |
| -use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordVerifier, Version}; |
| 9 | +use argon2::{Argon2, PasswordHash, PasswordVerifier}; |
7 | 10 | use secrecy::{ExposeSecret, Secret};
|
8 |
| -use sha3::Digest; |
9 | 11 | use sqlx::PgPool;
|
10 | 12 |
|
11 | 13 | #[derive(serde::Deserialize)]
|
@@ -149,44 +151,59 @@ fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Erro
|
149 | 151 | })
|
150 | 152 | }
|
151 | 153 |
|
| 154 | +#[tracing::instrument(name = "Validate auth credentials", skip(credentials, pool))] |
152 | 155 | async fn validate_credentials(
|
153 | 156 | credentials: Credentials,
|
154 | 157 | pool: &PgPool,
|
155 | 158 | ) -> 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 | + }) |
169 | 168 | .await
|
170 |
| - .context("Failed to perform the query to validate auth credentials") |
| 169 | + .context("Failed to spawn blocking task") |
171 | 170 | .map_err(PublishError::UnexpectedError)?;
|
172 | 171 |
|
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 | +} |
181 | 174 |
|
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.") |
184 | 185 | .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 | +} |
185 | 194 |
|
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) |
192 | 209 | }
|
0 commit comments