-
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
Showing
11 changed files
with
229 additions
and
84 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 |
---|---|---|
@@ -0,0 +1,89 @@ | ||
use crate::telemetry::spawn_blocking_with_tracing; | ||
use anyhow::Context; | ||
use argon2::{Argon2, PasswordHash, PasswordVerifier}; | ||
use secrecy::{ExposeSecret, Secret}; | ||
use sqlx::PgPool; | ||
use uuid::Uuid; | ||
|
||
#[derive(thiserror::Error, Debug)] | ||
pub enum AuthError { | ||
#[error("Invalid credentials.")] | ||
InvalidCredentials(#[source] anyhow::Error), | ||
#[error(transparent)] | ||
UnexpectedError(#[from] anyhow::Error), | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct Credentials { | ||
pub username: String, | ||
pub password: Secret<String>, | ||
} | ||
|
||
pub async fn validate_credentials( | ||
credentials: Credentials, | ||
pool: &PgPool, | ||
) -> Result<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(|| AuthError::InvalidCredentials(anyhow::anyhow!("Unknown username."))) | ||
} | ||
|
||
#[tracing::instrument(name = "Get stored credentials", skip(username, pool))] | ||
async fn get_stored_credentials( | ||
username: &str, | ||
pool: &PgPool, | ||
) -> Result<Option<(Uuid, Secret<String>)>, anyhow::Error> { | ||
let row = sqlx::query!( | ||
r#" | ||
SELECT id, password_hash | ||
FROM users | ||
WHERE username = $1 | ||
"#, | ||
username, | ||
) | ||
.fetch_optional(pool) | ||
.await | ||
.context("Failed to perform a query to retrieve user credentials.")? | ||
.map(|row| (row.id, Secret::new(row.password_hash))); | ||
|
||
Ok(row) | ||
} | ||
|
||
#[tracing::instrument( | ||
name = "Verify password hash", | ||
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) | ||
} |
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,3 +1,4 @@ | ||
pub mod authentication; | ||
pub mod bootstrap; | ||
pub mod configuration; | ||
pub mod domain; | ||
|
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 |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta http-equiv="content-type" content="text/html" charset="UTF-8"> | ||
<title>Home</title> | ||
</head> | ||
<p>Welcome to our newsletter</p> | ||
<body> | ||
|
||
</body> | ||
</html> |
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 |
---|---|---|
@@ -0,0 +1,8 @@ | ||
use actix_web::http::header::ContentType; | ||
use actix_web::HttpResponse; | ||
|
||
pub async fn home() -> HttpResponse { | ||
HttpResponse::Ok() | ||
.content_type(ContentType::html()) | ||
.body(include_str!("home.html")) | ||
} |
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 |
---|---|---|
@@ -0,0 +1,8 @@ | ||
use actix_web::http::header::ContentType; | ||
use actix_web::HttpResponse; | ||
|
||
pub async fn login_form() -> HttpResponse { | ||
HttpResponse::Ok() | ||
.content_type(ContentType::html()) | ||
.body(include_str!("login.html")) | ||
} |
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 |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta http-equiv="content-type" content="text/html" charset="UTF-8"> | ||
<title>Login</title> | ||
</head> | ||
<body> | ||
<form action="/login" method="post"> | ||
<label>Username | ||
<input | ||
type="text" | ||
placeholder="Enter Username" | ||
name="username" | ||
> | ||
</label> | ||
<label>Password | ||
<input | ||
type="password" | ||
placeholder="Enter Password" | ||
name="password" | ||
> | ||
</label> | ||
<button type="submit">Login</button> | ||
</form> | ||
</body> | ||
</html> |
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 |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pub use get::login_form; | ||
pub use post::login; | ||
|
||
mod get; | ||
mod post; |
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 |
---|---|---|
@@ -0,0 +1,63 @@ | ||
use crate::authentication::{validate_credentials, AuthError, Credentials}; | ||
use crate::routes::error_chain_fmt; | ||
use actix_web::http::StatusCode; | ||
use actix_web::{web, HttpResponse, ResponseError}; | ||
use secrecy::Secret; | ||
use sqlx::PgPool; | ||
use std::fmt::Debug; | ||
|
||
#[derive(serde::Deserialize)] | ||
pub struct FormData { | ||
pub username: String, | ||
pub password: Secret<String>, | ||
} | ||
|
||
#[tracing::instrument( | ||
skip(form, pool), | ||
fields(username = %form.username, 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, | ||
}; | ||
|
||
let user_id = validate_credentials(credentials, &pool) | ||
.await | ||
.map_err(|e| match e { | ||
AuthError::InvalidCredentials(_) => LoginError::AuthError(e.into()), | ||
AuthError::UnexpectedError(_) => LoginError::UnexpectedError(e.into()), | ||
})?; | ||
|
||
tracing::Span::current().record("user_id", tracing::field::display(&user_id)); | ||
|
||
Ok(HttpResponse::SeeOther() | ||
.insert_header(("location", "/")) | ||
.finish()) | ||
} | ||
|
||
#[derive(thiserror::Error)] | ||
pub enum LoginError { | ||
#[error("Invalid credentials.")] | ||
AuthError(#[source] anyhow::Error), | ||
#[error("Something went wrong.")] | ||
UnexpectedError(#[from] anyhow::Error), | ||
} | ||
|
||
impl 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::AuthError(_) => StatusCode::UNAUTHORIZED, | ||
LoginError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR, | ||
} | ||
} | ||
} |
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,9 +1,13 @@ | ||
pub use health_check::*; | ||
pub use home::*; | ||
pub use login::*; | ||
pub use newsletters::*; | ||
pub use subscriptions::*; | ||
pub use subscriptions_confirm::*; | ||
|
||
mod health_check; | ||
mod home; | ||
mod login; | ||
mod newsletters; | ||
mod subscriptions; | ||
mod subscriptions_confirm; |
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