Skip to content

Commit

Permalink
10.6.4
Browse files Browse the repository at this point in the history
  • Loading branch information
josemoura212 committed Jun 26, 2024
1 parent ea6b146 commit 1fafe0d
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 86 deletions.
84 changes: 84 additions & 0 deletions src/authentication.rs
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)
}
60 changes: 56 additions & 4 deletions src/routes/login/post.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
use crate::authentication::AuthError;
use crate::routes::error_chain_fmt;
use actix_web::http::header::LOCATION;
use actix_web::http::StatusCode;
use actix_web::web;
use actix_web::HttpResponse;
use actix_web::ResponseError;
use secrecy::Secret;
use sqlx::PgPool;

use crate::authentication::validate_credentials;
use crate::authentication::Credentials;

#[allow(unused)]
#[derive(serde::Deserialize)]
Expand All @@ -10,8 +18,52 @@ pub struct FormData {
password: Secret<String>,
}

pub async fn login(_form: web::Form<FormData>) -> HttpResponse {
HttpResponse::SeeOther()
.insert_header((LOCATION, "/"))
.finish()
#[tracing::instrument(
skip(form, pool),
fields(username=tracing::field::Empty, user_id=tracing::field::Empty)
)]
pub async fn login(
form: web::Form<FormData>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, LoginError> {
let credentials = Credentials {
username: form.0.username,
password: form.0.password,
};
tracing::Span::current().record("username", &tracing::field::display(&credentials.username));
match validate_credentials(credentials, &pool).await {
Ok(user_id) => {
tracing::Span::current().record("user_id", &tracing::field::display(&user_id));
Ok(HttpResponse::SeeOther()
.insert_header((LOCATION, "/"))
.finish())
}
Err(e) => match e {
AuthError::InvalidCredentials(_) => Err(LoginError::AuthError(e.into())),
_ => Err(LoginError::UnexpectedError(e.into())),
},
}
}

#[derive(thiserror::Error)]
pub enum LoginError {
#[error("Authentication failed")]
AuthError(#[source] anyhow::Error),
#[error("Something went wrong")]
UnexpectedError(#[from] anyhow::Error),
}

impl std::fmt::Debug for LoginError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}

impl ResponseError for LoginError {
fn status_code(&self) -> StatusCode {
match self {
LoginError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
LoginError::AuthError(_) => StatusCode::UNAUTHORIZED,
}
}
}
90 changes: 8 additions & 82 deletions src/routes/newsletters.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
use crate::authentication::{validate_credentials, AuthError, Credentials};
use crate::domain::SubscriberEmail;
use crate::email_client::EmailClient;
use crate::routes::error_chain_fmt;
use crate::telemetry::spawn_blocking_with_tracing;
use actix_web::http::{
header::{HeaderMap, HeaderValue},
StatusCode,
};
use actix_web::{web, HttpRequest, HttpResponse, ResponseError};
use anyhow::Context;
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use base64::Engine;
use secrecy::{ExposeSecret, Secret};
use secrecy::Secret;
use sqlx::PgPool;

#[derive(serde::Deserialize)]
Expand Down Expand Up @@ -57,11 +56,6 @@ impl ResponseError for PublishError {
}
}

struct Credentials {
username: String,
password: Secret<String>,
}

fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Error> {
// The header value, if present, must be a valid UTF8 string
let header_value = headers
Expand Down Expand Up @@ -94,79 +88,6 @@ fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Erro
})
}

#[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))]
async fn validate_credentials(
credentials: Credentials,
pool: &PgPool,
) -> Result<uuid::Uuid, PublishError> {
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(PublishError::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(PublishError::UnexpectedError)??;

user_id.ok_or_else(|| PublishError::AuthError(anyhow::anyhow!("Unknown username.")))
}

#[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<(), PublishError> {
let expected_password_hash = PasswordHash::new(expected_password_hash.expose_secret())
.context("Failed to parse hash in PHC string format.")
.map_err(PublishError::UnexpectedError)?;

Argon2::default()
.verify_password(
password_candidate.expose_secret().as_bytes(),
&expected_password_hash,
)
.context("Invalid password.")
.map_err(PublishError::AuthError)
}

#[tracing::instrument(
name = "Publish a newsletter issue",
skip(body, pool, email_client, request),
Expand All @@ -180,7 +101,12 @@ pub async fn publish_newsletter(
) -> Result<HttpResponse, PublishError> {
let credentials = basic_authentication(request.headers()).map_err(PublishError::AuthError)?;
tracing::Span::current().record("username", &tracing::field::display(&credentials.username));
let user_id = validate_credentials(credentials, &pool).await?;
let user_id = validate_credentials(credentials, &pool)
.await
.map_err(|e| match e {
AuthError::InvalidCredentials(_) => PublishError::AuthError(e.into()),
_ => PublishError::UnexpectedError(e.into()),
})?;
tracing::Span::current().record("user_id", &tracing::field::display(&user_id));

let subscribers = get_confirmed_subscribers(&pool).await?;
Expand Down

0 comments on commit 1fafe0d

Please sign in to comment.