Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changelog/body-layer-credential-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
mpp: patch
---

Fixed `PaymentBodyLayer` reading credentials from `Authorization` only. It now reads the header selected by the verifier, so servers created with `requires_auth` can verify body-bound payments sent in `Payment-Authorization`.
100 changes: 99 additions & 1 deletion src/server/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,10 @@ where
}
};

// Extract the Payment credential from the header selected by the challenge.
let auth_header = parts
.headers
.get(header::AUTHORIZATION)
.get(verifier.credential_header())
.and_then(|v| v.to_str().ok())
.and_then(extract_payment_scheme)
.map(|s| s.to_string());
Expand Down Expand Up @@ -1060,6 +1061,103 @@ mod tests {
);
}

/// A body-aware verifier that, like `Mpp::requires_auth`, advertises
/// `Payment-Authorization` as the credential header.
#[derive(Clone)]
struct PaymentAuthorizationBodyVerifier(BodyAwareVerifier);

impl PaymentVerifier for PaymentAuthorizationBodyVerifier {
fn challenge(&self) -> Result<String, String> {
self.0.challenge()
}

fn challenge_with_body(&self, body: &[u8]) -> Result<String, String> {
self.0.challenge_with_body(body)
}

fn verify(
&self,
credential: &str,
) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>> {
self.0.verify(credential)
}

fn verify_with_body(
&self,
credential: &str,
body: &[u8],
) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>> {
self.0.verify_with_body(credential, body)
}

fn credential_header(&self) -> &str {
"Payment-Authorization"
}
}

#[tokio::test]
async fn test_body_service_reads_credential_from_verifier_header() {
use tower_service::Service;

let verify_body = Arc::new(std::sync::Mutex::new(None));
let layer = PaymentBodyLayer::new(PaymentAuthorizationBodyVerifier(BodyAwareVerifier {
challenge_body: Arc::new(std::sync::Mutex::new(None)),
verify_body: verify_body.clone(),
}));
let mut svc = layer.layer(BodyEchoService);

// `Authorization` carries the app's own bearer token; the Payment
// credential travels in the header the challenge advertised.
let req = Request::builder()
.uri("/premium")
.header(header::AUTHORIZATION, "Bearer app-token")
.header("Payment-Authorization", "Payment eyJmYWtlIjp0cnVlfQ")
.body(http_body_util::Full::new(Bytes::from_static(
br#"{"query":"paid"}"#,
)))
.unwrap();

let resp = svc.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.body(), br#"{"query":"paid"}"#);
assert_eq!(
resp.headers().get(PAYMENT_RECEIPT_HEADER).unwrap(),
"mock-receipt-token"
);
assert_eq!(
verify_body.lock().unwrap().as_deref(),
Some(br#"{"query":"paid"}"#.as_slice())
);
}

#[tokio::test]
async fn test_body_service_ignores_credential_in_unselected_header() {
use tower_service::Service;

let verify_body = Arc::new(std::sync::Mutex::new(None));
let layer = PaymentBodyLayer::new(PaymentAuthorizationBodyVerifier(BodyAwareVerifier {
challenge_body: Arc::new(std::sync::Mutex::new(None)),
verify_body: verify_body.clone(),
}));
let mut svc = layer.layer(BodyEchoService);

let req = Request::builder()
.uri("/premium")
.header(header::AUTHORIZATION, "Payment eyJmYWtlIjp0cnVlfQ")
.body(http_body_util::Full::new(Bytes::from_static(
br#"{"query":"paid"}"#,
)))
.unwrap();

let resp = svc.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::PAYMENT_REQUIRED);
assert_eq!(
resp.headers().get(WWW_AUTHENTICATE_HEADER).unwrap(),
"Payment id=\"body-bound\""
);
assert!(verify_body.lock().unwrap().is_none());
}

#[tokio::test]
async fn test_body_service_invalid_auth_returns_body_bound_challenge() {
use tower_service::Service;
Expand Down
Loading