Skip to main content

toolkit_http/
security.rs

1//! HTTP security utilities.
2//!
3//! `SecurityContext` propagation over HTTP uses a single header,
4//! `Authorization: Bearer <jwt>`, carrying the original tenant-plane JWT. The
5//! token is forwarded as-is across hops and **re-validated at every hop** —
6//! there is no trusted-peer fast path (zero-trust). No binary
7//! `x-secctx-bin` encoding is used over HTTP.
8
9use http::{HeaderValue, header::AUTHORIZATION};
10use secrecy::{ExposeSecret, SecretString};
11use toolkit_security::SecurityContext;
12use toolkit_security::constants::INTERNAL_TOKEN_HEADER;
13
14/// Maximum body preview size for error messages (8KB).
15///
16/// When an HTTP request returns a non-2xx status, the response body is included
17/// in the error message for debugging. This constant limits how much of the body
18/// is read to prevent memory issues with large error responses.
19pub const ERROR_BODY_PREVIEW_LIMIT: usize = 8 * 1024;
20
21/// Attach the tenant-plane JWT from `secctx` to an outgoing request as
22/// `Authorization: Bearer <jwt>`.
23///
24/// The secret is exposed only at this transport boundary and the resulting
25/// header value is marked sensitive so it is never logged. If `secctx` carries
26/// no bearer token, or the token cannot be represented as a header value, the
27/// request is left unchanged.
28pub fn attach_bearer_http<B>(request: &mut http::Request<B>, secctx: &SecurityContext) {
29    let Some(token) = secctx.bearer_token() else {
30        return;
31    };
32    // Expose the secret only here, at the transport boundary, and never log it.
33    let Ok(mut value) = HeaderValue::from_str(&format!("Bearer {}", token.expose_secret())) else {
34        tracing::warn!(
35            "bearer token contains invalid HTTP header characters; outgoing request sent without Authorization header"
36        );
37        return;
38    };
39    value.set_sensitive(true);
40    request.headers_mut().insert(AUTHORIZATION, value);
41}
42
43/// Attach a platform-plane internal `token` to an outgoing request as the
44/// `X-ToolKit-Internal-Token` header.
45///
46/// The token is carried raw (no `Bearer` scheme) and **never** on
47/// `Authorization`, to avoid colliding with the tenant-plane user JWT.
48/// The secret is exposed only at this transport boundary
49/// and the resulting header value is marked sensitive so it is never logged. If
50/// the token cannot be represented as a header value, the request is left
51/// unchanged.
52pub fn attach_internal_token_http<B>(request: &mut http::Request<B>, token: &SecretString) {
53    // Expose the secret only here, at the transport boundary, and never log it.
54    let Ok(mut value) = HeaderValue::from_str(token.expose_secret()) else {
55        tracing::warn!(
56            "internal token contains invalid HTTP header characters; outgoing request sent without X-ToolKit-Internal-Token header"
57        );
58        return;
59    };
60    value.set_sensitive(true);
61    request.headers_mut().insert(INTERNAL_TOKEN_HEADER, value);
62}
63
64#[cfg(test)]
65#[cfg_attr(coverage_nightly, coverage(off))]
66mod tests {
67    use super::*;
68
69    fn secctx_with_token(token: &str) -> SecurityContext {
70        SecurityContext::builder()
71            .subject_id(uuid::Uuid::nil())
72            .subject_tenant_id(uuid::Uuid::nil())
73            .bearer_token(token.to_owned())
74            .build()
75            .expect("valid security context")
76    }
77
78    #[test]
79    fn attach_sets_bearer_header() {
80        let secctx = secctx_with_token("header.payload.signature");
81        let mut request = http::Request::new(());
82        attach_bearer_http(&mut request, &secctx);
83
84        let value = request.headers().get(AUTHORIZATION).expect("header set");
85        assert_eq!(value.to_str().unwrap(), "Bearer header.payload.signature");
86    }
87
88    #[test]
89    fn attach_marks_header_sensitive() {
90        let secctx = secctx_with_token("abc.def.ghi");
91        let mut request = http::Request::new(());
92        attach_bearer_http(&mut request, &secctx);
93
94        let value = request.headers().get(AUTHORIZATION).expect("header set");
95        assert!(value.is_sensitive());
96    }
97
98    #[test]
99    fn attach_noop_when_no_token() {
100        let secctx = SecurityContext::anonymous();
101        let mut request = http::Request::new(());
102        attach_bearer_http(&mut request, &secctx);
103
104        assert!(request.headers().get(AUTHORIZATION).is_none());
105    }
106
107    #[test]
108    fn internal_token_uses_dedicated_header_not_authorization() {
109        let mut request = http::Request::new(());
110        attach_internal_token_http(&mut request, &SecretString::from("sa.jwt.token"));
111
112        assert!(request.headers().get(INTERNAL_TOKEN_HEADER).is_some());
113        assert!(request.headers().get(AUTHORIZATION).is_none());
114    }
115
116    #[test]
117    fn internal_token_header_is_sensitive() {
118        let mut request = http::Request::new(());
119        attach_internal_token_http(&mut request, &SecretString::from("sa.jwt.token"));
120
121        let value = request
122            .headers()
123            .get(INTERNAL_TOKEN_HEADER)
124            .expect("header set");
125        assert!(value.is_sensitive());
126    }
127}