mirror of
https://github.com/LukeMathWalker/zero-to-production.git
synced 2024-12-18 14:06:37 +00:00
Add secrecy
This commit is contained in:
parent
cb01855668
commit
93ed0c4150
5 changed files with 51 additions and 17 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -1762,6 +1762,16 @@ dependencies = [
|
||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "secrecy"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -2746,6 +2756,7 @@ dependencies = [
|
||||||
"quickcheck_macros",
|
"quickcheck_macros",
|
||||||
"rand 0.8.4",
|
"rand 0.8.4",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"secrecy",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-aux",
|
"serde-aux",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -2763,6 +2774,12 @@ dependencies = [
|
||||||
"wiremock",
|
"wiremock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
version = "0.9.1+zstd.1.5.1"
|
version = "0.9.1+zstd.1.5.1"
|
||||||
|
|
|
@ -36,6 +36,7 @@ tracing-actix-web = "0.5.0-beta.6"
|
||||||
anyhow = "1.0.40"
|
anyhow = "1.0.40"
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
argon2 = { version = "0.3", features = ["std"] }
|
argon2 = { version = "0.3", features = ["std"] }
|
||||||
|
secrecy = { version = "0.8", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
once_cell = "1.7.2"
|
once_cell = "1.7.2"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::domain::SubscriberEmail;
|
use crate::domain::SubscriberEmail;
|
||||||
|
use secrecy::{ExposeSecret, Secret};
|
||||||
use serde_aux::field_attributes::deserialize_number_from_string;
|
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||||
use sqlx::postgres::{PgConnectOptions, PgSslMode};
|
use sqlx::postgres::{PgConnectOptions, PgSslMode};
|
||||||
use sqlx::ConnectOptions;
|
use sqlx::ConnectOptions;
|
||||||
|
@ -22,7 +23,7 @@ pub struct ApplicationSettings {
|
||||||
#[derive(serde::Deserialize, Clone)]
|
#[derive(serde::Deserialize, Clone)]
|
||||||
pub struct DatabaseSettings {
|
pub struct DatabaseSettings {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: Secret<String>,
|
||||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
@ -40,7 +41,7 @@ impl DatabaseSettings {
|
||||||
PgConnectOptions::new()
|
PgConnectOptions::new()
|
||||||
.host(&self.host)
|
.host(&self.host)
|
||||||
.username(&self.username)
|
.username(&self.username)
|
||||||
.password(&self.password)
|
.password(&self.password.expose_secret())
|
||||||
.port(self.port)
|
.port(self.port)
|
||||||
.ssl_mode(ssl_mode)
|
.ssl_mode(ssl_mode)
|
||||||
}
|
}
|
||||||
|
@ -56,7 +57,8 @@ impl DatabaseSettings {
|
||||||
pub struct EmailClientSettings {
|
pub struct EmailClientSettings {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub sender_email: String,
|
pub sender_email: String,
|
||||||
pub authorization_token: String,
|
pub authorization_token: Secret<String>,
|
||||||
|
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||||
pub timeout_milliseconds: u64,
|
pub timeout_milliseconds: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
use crate::domain::SubscriberEmail;
|
use crate::domain::SubscriberEmail;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use secrecy::{ExposeSecret, Secret};
|
||||||
|
|
||||||
pub struct EmailClient {
|
pub struct EmailClient {
|
||||||
http_client: Client,
|
http_client: Client,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
sender: SubscriberEmail,
|
sender: SubscriberEmail,
|
||||||
authorization_token: String,
|
authorization_token: Secret<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmailClient {
|
impl EmailClient {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
base_url: String,
|
base_url: String,
|
||||||
sender: SubscriberEmail,
|
sender: SubscriberEmail,
|
||||||
authorization_token: String,
|
authorization_token: Secret<String>,
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let http_client = Client::builder().timeout(timeout).build().unwrap();
|
let http_client = Client::builder().timeout(timeout).build().unwrap();
|
||||||
|
@ -41,7 +42,10 @@ impl EmailClient {
|
||||||
};
|
};
|
||||||
self.http_client
|
self.http_client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.header("X-Postmark-Server-Token", &self.authorization_token)
|
.header(
|
||||||
|
"X-Postmark-Server-Token",
|
||||||
|
self.authorization_token.expose_secret(),
|
||||||
|
)
|
||||||
.json(&request_body)
|
.json(&request_body)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
|
@ -68,6 +72,7 @@ mod tests {
|
||||||
use fake::faker::internet::en::SafeEmail;
|
use fake::faker::internet::en::SafeEmail;
|
||||||
use fake::faker::lorem::en::{Paragraph, Sentence};
|
use fake::faker::lorem::en::{Paragraph, Sentence};
|
||||||
use fake::{Fake, Faker};
|
use fake::{Fake, Faker};
|
||||||
|
use secrecy::Secret;
|
||||||
use wiremock::matchers::{any, header, header_exists, method, path};
|
use wiremock::matchers::{any, header, header_exists, method, path};
|
||||||
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
|
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
|
||||||
|
|
||||||
|
@ -108,7 +113,7 @@ mod tests {
|
||||||
EmailClient::new(
|
EmailClient::new(
|
||||||
base_url,
|
base_url,
|
||||||
email(),
|
email(),
|
||||||
Faker.fake(),
|
Secret::new(Faker.fake()),
|
||||||
std::time::Duration::from_millis(200),
|
std::time::Duration::from_millis(200),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use actix_web::http::{
|
||||||
use actix_web::{web, HttpResponse, ResponseError};
|
use actix_web::{web, HttpResponse, ResponseError};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||||
|
use secrecy::{ExposeSecret, Secret};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
|
@ -57,7 +58,7 @@ impl ResponseError for PublishError {
|
||||||
|
|
||||||
struct Credentials {
|
struct Credentials {
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: Secret<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Error> {
|
fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Error> {
|
||||||
|
@ -85,14 +86,17 @@ fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Erro
|
||||||
.ok_or_else(|| anyhow::anyhow!("A password must be provided in 'Basic' auth."))?
|
.ok_or_else(|| anyhow::anyhow!("A password must be provided in 'Basic' auth."))?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
Ok(Credentials { username, password })
|
Ok(Credentials {
|
||||||
|
username,
|
||||||
|
password: Secret::new(password),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(name = "Get stored credentials", skip(username, pool))]
|
#[tracing::instrument(name = "Get stored credentials", skip(username, pool))]
|
||||||
async fn get_stored_credentials(
|
async fn get_stored_credentials(
|
||||||
username: &str,
|
username: &str,
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
) -> Result<Option<(uuid::Uuid, String)>, anyhow::Error> {
|
) -> Result<Option<(uuid::Uuid, Secret<String>)>, anyhow::Error> {
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT user_id, password_hash
|
SELECT user_id, password_hash
|
||||||
|
@ -104,7 +108,7 @@ async fn get_stored_credentials(
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await
|
.await
|
||||||
.context("Failed to performed a query to retrieve stored credentials.")?
|
.context("Failed to performed a query to retrieve stored credentials.")?
|
||||||
.map(|row| (row.user_id, row.password_hash));
|
.map(|row| (row.user_id, Secret::new(row.password_hash)));
|
||||||
Ok(row)
|
Ok(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,10 +118,12 @@ async fn validate_credentials(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
) -> Result<uuid::Uuid, PublishError> {
|
) -> Result<uuid::Uuid, PublishError> {
|
||||||
let mut user_id = None;
|
let mut user_id = None;
|
||||||
let mut expected_password_hash = "$argon2id$v=19$m=15000,t=2,p=1$\
|
let mut expected_password_hash = Secret::new(
|
||||||
|
"$argon2id$v=19$m=15000,t=2,p=1$\
|
||||||
gZiV/M1gPc22ElAH/Jh1Hw$\
|
gZiV/M1gPc22ElAH/Jh1Hw$\
|
||||||
CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno"
|
CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno"
|
||||||
.to_string();
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some((stored_user_id, stored_password_hash)) =
|
if let Some((stored_user_id, stored_password_hash)) =
|
||||||
get_stored_credentials(&credentials.username, pool)
|
get_stored_credentials(&credentials.username, pool)
|
||||||
|
@ -143,15 +149,18 @@ async fn validate_credentials(
|
||||||
skip(expected_password_hash, password_candidate)
|
skip(expected_password_hash, password_candidate)
|
||||||
)]
|
)]
|
||||||
fn verify_password_hash(
|
fn verify_password_hash(
|
||||||
expected_password_hash: String,
|
expected_password_hash: Secret<String>,
|
||||||
password_candidate: String,
|
password_candidate: Secret<String>,
|
||||||
) -> Result<(), PublishError> {
|
) -> Result<(), PublishError> {
|
||||||
let expected_password_hash = PasswordHash::new(&expected_password_hash)
|
let expected_password_hash = PasswordHash::new(expected_password_hash.expose_secret())
|
||||||
.context("Failed to parse hash in PHC string format.")
|
.context("Failed to parse hash in PHC string format.")
|
||||||
.map_err(PublishError::UnexpectedError)?;
|
.map_err(PublishError::UnexpectedError)?;
|
||||||
|
|
||||||
Argon2::default()
|
Argon2::default()
|
||||||
.verify_password(password_candidate.as_bytes(), &expected_password_hash)
|
.verify_password(
|
||||||
|
password_candidate.expose_secret().as_bytes(),
|
||||||
|
&expected_password_hash,
|
||||||
|
)
|
||||||
.context("Invalid password.")
|
.context("Invalid password.")
|
||||||
.map_err(PublishError::AuthError)
|
.map_err(PublishError::AuthError)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue