use crate::config::{WEBHOOK_HEADER_NAME, WEBHOOK_SECRET};
use anyhow::Context;
use axum::{
body::Body,
extract::{Request, State},
http::StatusCode,
middleware::Next,
response::Response,
};
use hmac::Mac;
use tracing::debug;
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
#[derive(Clone, Debug)]
pub struct HmacConfig {
pub secret: String,
pub header_name: String,
}
impl Default for HmacConfig {
fn default() -> Self {
Self {
secret: WEBHOOK_SECRET.to_string(),
header_name: WEBHOOK_HEADER_NAME.to_string(),
}
}
}
impl HmacConfig {
pub fn new(secret: String, header_name: String) -> Self {
Self {
secret,
header_name,
}
}
}
pub async fn verify_hmac_middleware(
State(config): State<HmacConfig>,
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let (parts, body) = req.into_parts();
let signature = parts
.headers
.get(&config.header_name)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| {
tracing::error!("Missing HMAC signature header: {}", config.header_name);
StatusCode::BAD_REQUEST
})?;
let payload = axum::body::to_bytes(body, usize::MAX).await.map_err(|e| {
tracing::error!("Failed to read request body: {}", e);
StatusCode::BAD_REQUEST
})?;
match verify_hmac_sha256(signature, &payload, &config.secret) {
Ok(_) => {
debug!("HMAC signature verified successfully");
let new_body = Body::from(payload);
let req = Request::from_parts(parts, new_body);
Ok(next.run(req).await)
}
Err(e) => {
tracing::error!("HMAC verification failed: {}", e);
Err(StatusCode::UNAUTHORIZED)
}
}
}
fn verify_hmac_sha256(signature: &str, payload: &[u8], secret: &str) -> anyhow::Result<()> {
let signature_hex = signature
.strip_prefix("sha256=")
.context("Signature must start with 'sha256='")?;
let expected_signature =
hex::decode(signature_hex).context("Failed to decode hex signature")?;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.map_err(|_| anyhow::anyhow!("Invalid secret key for HMAC"))?;
mac.update(payload);
match mac.verify_slice(&expected_signature) {
Ok(_) => Ok(()),
Err(_) => Err(anyhow::anyhow!("HMAC signature verification failed")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_hmac_sha256_valid() {
let secret = "test-secret";
let payload = b"test payload";
let signature = "sha256=a04b3cf265807f4b1d80b2ed5c3e0914c9b3b8d7b8f6b8c8d1e8b8f8c8d8e8f8";
let result = verify_hmac_sha256(signature, payload, secret);
assert!(result.is_err());
}
#[test]
fn test_verify_hmac_sha256_invalid_prefix() {
let secret = "test-secret";
let payload = b"test payload";
let signature = "md5=invalid";
let result = verify_hmac_sha256(signature, payload, secret);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("sha256="));
}
#[test]
fn test_verify_hmac_sha256_invalid_hex() {
let secret = "test-secret";
let payload = b"test payload";
let signature = "sha256=invalid-hex";
let result = verify_hmac_sha256(signature, payload, secret);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("decode hex"));
}
}