mirror of
https://github.com/LukeMathWalker/zero-to-production.git
synced 2024-09-27 13:50:03 +00:00
90 lines
2.6 KiB
Rust
90 lines
2.6 KiB
Rust
use anyhow::Context;
|
|
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
|
use secrecy::{ExposeSecret, Secret};
|
|
use sqlx::PgPool;
|
|
|
|
use crate::telemetry::spawn_blocking_with_tracing;
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum AuthError {
|
|
#[error("Invalid credentials.")]
|
|
InvalidCredentials(#[source] anyhow::Error),
|
|
#[error(transparent)]
|
|
UnexpectedError(#[from] anyhow::Error),
|
|
}
|
|
|
|
pub struct Credentials {
|
|
pub username: String,
|
|
pub password: Secret<String>,
|
|
}
|
|
|
|
#[tracing::instrument(name = "Get stored credentials", skip(username, pool))]
|
|
async fn get_stored_credentials(
|
|
username: &str,
|
|
pool: &PgPool,
|
|
) -> Result<Option<(uuid::Uuid, Secret<String>)>, anyhow::Error> {
|
|
let row = sqlx::query!(
|
|
r#"
|
|
SELECT user_id, password_hash
|
|
FROM users
|
|
WHERE username = $1
|
|
"#,
|
|
username,
|
|
)
|
|
.fetch_optional(pool)
|
|
.await
|
|
.context("Failed to performed a query to retrieve stored credentials.")?
|
|
.map(|row| (row.user_id, Secret::new(row.password_hash)));
|
|
Ok(row)
|
|
}
|
|
|
|
#[tracing::instrument(name = "Validate credentials", skip(credentials, pool))]
|
|
pub async fn validate_credentials(
|
|
credentials: Credentials,
|
|
pool: &PgPool,
|
|
) -> Result<uuid::Uuid, AuthError> {
|
|
let mut user_id = None;
|
|
let mut expected_password_hash = Secret::new(
|
|
"$argon2id$v=19$m=15000,t=2,p=1$\
|
|
gZiV/M1gPc22ElAH/Jh1Hw$\
|
|
CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno"
|
|
.to_string(),
|
|
);
|
|
|
|
if let Some((stored_user_id, stored_password_hash)) =
|
|
get_stored_credentials(&credentials.username, pool).await?
|
|
{
|
|
user_id = Some(stored_user_id);
|
|
expected_password_hash = stored_password_hash;
|
|
}
|
|
|
|
spawn_blocking_with_tracing(move || {
|
|
verify_password_hash(expected_password_hash, credentials.password)
|
|
})
|
|
.await
|
|
.context("Failed to spawn blocking task.")??;
|
|
|
|
user_id
|
|
.ok_or_else(|| anyhow::anyhow!("Unknown username."))
|
|
.map_err(AuthError::InvalidCredentials)
|
|
}
|
|
|
|
#[tracing::instrument(
|
|
name = "Validate credentials",
|
|
skip(expected_password_hash, password_candidate)
|
|
)]
|
|
fn verify_password_hash(
|
|
expected_password_hash: Secret<String>,
|
|
password_candidate: Secret<String>,
|
|
) -> Result<(), AuthError> {
|
|
let expected_password_hash = PasswordHash::new(expected_password_hash.expose_secret())
|
|
.context("Failed to parse hash in PHC string format.")?;
|
|
|
|
Argon2::default()
|
|
.verify_password(
|
|
password_candidate.expose_secret().as_bytes(),
|
|
&expected_password_hash,
|
|
)
|
|
.context("Invalid password.")
|
|
.map_err(AuthError::InvalidCredentials)
|
|
}
|