Skip to content

Commit

Permalink
Ch10: Add HMAC Tag to Query Parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
Taha Afzal committed Aug 29, 2024
1 parent e687df8 commit 092586b
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 39 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ base64 = "0.21"
argon2 = { version = "0.4", features = ["std"] }
urlencoding = "2"
htmlescape = "0.3"
hmac = { version = "0.12", features = ["std"] }
sha2 = "0.10"

# We need the optional `derive` feature to use `serde`'s procedural macros:
# `#[derive(Serialize)]` and `#[derive(Deserialize)]`.
Expand Down
17 changes: 17 additions & 0 deletions Notes/Ch6-10.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@
- [Naive Approach](#naive-approach)
- [Query Parameters](#query-parameters)
- [Cross-Site Scripting (XSS)](#cross-site-scripting-xss)
- [Message Authentication Codes (MACs)](#message-authentication-codes-macs)
- [Add An HMAC Tag To Protect Query Parameters](#add-an-hmac-tag-to-protect-query-parameters)

# Ch 6 - Reject Invalid Subscribers #1
* Our input validation for `/POST` is limited: we just ensure that both the `name` and the `email` fields are provided, even if they are empty.
Expand Down Expand Up @@ -1225,3 +1227,18 @@ TEST_LOG=true cargo test --quiet --release newsletters_are_delivered | grep "VER
6. convert `/` to `&#x2F`
* HTML prevents insertion of further HTML elements by escaping the characters required to define them.
* But just escaping characters is not enough.

#### Message Authentication Codes (MACs)
* **Message authentication** guarantees that the message has not been modified in transit
(**integrity**) and it allows you to verify the identity of the sender (**data origin authentication**).
* A tag is added to the message allowing verifiers to check its integrity and origin. HMAC are a well-known family of MACs - **h**ash-based **m**essage **a**uthentication **c**odes.
* HMACs are built around a secret and a hash function.
* The secret is prepended to the message and the resulting string is fed into the hash function. The resulting hash is then concatenated to the secret and hashed again - the output is the message tag.

#### Add An HMAC Tag To Protect Query Parameters
* The Rust Crypto organization provides an implementation of HMAC, the `hmac` crate.
* We need to get access to a secret - but it won't be possible from within `ResponseError` since we only have access to the error type `LoginError` that we are trying to convert into an HTTP response.
* In particular, we don't have access to the application state, which is where we would be storing the secret.
* To get around it, we can move the `hmac_tag` code to the `match_credentials` but we would not be propogating the error context upstream to the middleware chain.
* We can ger around it by using `actix_web::errorr::InternalError`.
* We can now inject the secret used by our HMACs into the application state but `Secret<String>` as the type injected into the application state has risk of conflict -- another middleware or service registring another `Secret<String>` against the application state, overriding our HMAC secret so we can create a wrapper to get around the issue.
3 changes: 3 additions & 0 deletions configurations/base.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
application:
port: 8000
// Corresponds to `APP_APPLICATION__HMAC_SECRET` environment variable
// on DigitalOcean for production
hmac_secret: "long-and-vert-secret-random-key-needed-to-verify-message-integrity"
database:
host: 127.0.0.1
port: 5432
Expand Down
3 changes: 3 additions & 0 deletions spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ services:
- key: APP_DATABASE__DATABASE_NAME
scope: RUN_TIME
value: ${newsletter.DATABASE}
- key: APP_APPLICATION__HMAC_SECRET
scope: RUN_TIME
value: ${APP_APPLICATION__HMAC_SECRET}

databases:
- engine: PG # Postgres
Expand Down
1 change: 1 addition & 0 deletions src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct ApplicationSettings {
#[serde(deserialize_with = "deserialize_number_from_string")]
pub port: u16,
pub base_url: String,
pub hmac_secret: Secret<String>,
}

#[derive(serde::Deserialize, Clone)]
Expand Down
2 changes: 1 addition & 1 deletion src/routes/login_form/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod get;
mod post;
pub mod post;

pub use get::*;
// Not publishing everything since
Expand Down
80 changes: 43 additions & 37 deletions src/routes/login_form/post.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use actix_web::{web, HttpResponse, ResponseError};
use reqwest::{header::LOCATION, StatusCode};
use secrecy::Secret;
use actix_web::{error::InternalError, web, HttpResponse};
use hmac::{Hmac, Mac};
use reqwest::header::LOCATION;
use secrecy::{ExposeSecret, Secret};
use sqlx::PgPool;

use crate::{
authentication::{validate_credentials, AuthError, Credentials},
routes::{error_chain_fmt, home_route},
};

#[derive(Clone)]
pub struct HmacSecret(pub Secret<String>);

use super::login_route;

#[derive(serde::Deserialize)]
Expand All @@ -30,49 +34,51 @@ impl std::fmt::Debug for LoginError {
}
}

impl ResponseError for LoginError {
fn status_code(&self) -> StatusCode {
match self {
LoginError::AuthError(_) => StatusCode::UNAUTHORIZED,
LoginError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}

fn error_response(&self) -> HttpResponse {
let encoded_error = urlencoding::Encoded::new(self.to_string());

HttpResponse::SeeOther()
.insert_header((
LOCATION,
format!("{}?error={}", login_route(), encoded_error),
))
.finish()
}
}

#[tracing::instrument(
skip(form, pool),
fields(email=tracing::field::Empty, user_id=tracing::field::Empty)
skip(form, pool, secret),
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> {
secret: web::Data<HmacSecret>,
) -> Result<HttpResponse, InternalError<LoginError>> {
let credentials = Credentials {
email: form.0.email,
password: form.0.password,
};
tracing::Span::current().record("email", &tracing::field::display(&credentials.email));

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));
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, home_route()))
.finish())
}
Err(error) => {
let error = match error {
AuthError::InvalidCredentials(_) => LoginError::AuthError(error.into()),
AuthError::UnexpectedError(_) => LoginError::UnexpectedError(error.into()),
};

let query_string = format!("error={}", urlencoding::Encoded::new(error.to_string()));
let hmac_tag = {
let mut mac =
Hmac::<sha2::Sha256>::new_from_slice(secret.0.expose_secret().as_bytes())
.unwrap();
mac.update(query_string.as_bytes());

mac.finalize().into_bytes()
};

Ok(HttpResponse::SeeOther()
.insert_header((LOCATION, home_route()))
.finish())
let response = HttpResponse::SeeOther()
.insert_header((
LOCATION,
format!("{}?{}&tag={:x}", login_route(), query_string, hmac_tag),
))
.finish();

Err(InternalError::from_response(error, response))
}
}
}
8 changes: 7 additions & 1 deletion src/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ use crate::{
email_client::{subscriptions_confirm_route, EmailClient},
routes::{
confirm, health_check, health_check_route, home, home_route, login, login_form,
login_route, publish_newsletter, publish_newsletter_route, subscribe, subscriptions_route,
login_route, post::HmacSecret, publish_newsletter, publish_newsletter_route, subscribe,
subscriptions_route,
},
};

use actix_web::{dev::Server, web, App, HttpServer};
use secrecy::Secret;
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use std::net::TcpListener;
Expand Down Expand Up @@ -54,6 +56,7 @@ impl Application {
connection_pool,
email_client,
configuration.application.base_url,
configuration.application.hmac_secret,
)?;

Ok(Self { server, port })
Expand All @@ -77,10 +80,12 @@ pub fn run(
connection_pool: PgPool,
email_client: EmailClient,
base_url: String,
hmac_secret: Secret<String>,
) -> Result<Server, std::io::Error> {
let connection_pool = web::Data::new(connection_pool);
let email_client = web::Data::new(email_client);
let base_url = web::Data::new(base_url);
let hmac_secret = web::Data::new(HmacSecret(hmac_secret));

let server = HttpServer::new(move || {
App::new()
Expand All @@ -98,6 +103,7 @@ pub fn run(
.app_data(connection_pool.clone())
.app_data(email_client.clone())
.app_data(base_url.clone())
.app_data(hmac_secret.clone())
})
.listen(listener)?
.run();
Expand Down

0 comments on commit 092586b

Please sign in to comment.