-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ea6b146
commit 1fafe0d
Showing
3 changed files
with
148 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,91 @@ | ||
use crate::telemetry::spawn_blocking_with_tracing; | ||
use anyhow::Context; | ||
use argon2::{Argon2, PasswordHash, PasswordVerifier}; | ||
use secrecy::{ExposeSecret, Secret}; | ||
use sqlx::PgPool; | ||
|
||
#[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 = "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 | ||
.map_err(AuthError::UnexpectedError)? | ||
{ | ||
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.") | ||
.map_err(AuthError::UnexpectedError)??; | ||
|
||
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) | ||
} | ||
|
||
#[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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters