Skip to main content

http_extract/
api_key.rs

1//! API-key extraction from the fixed `X-API-Key` and `Api-Key` fields.
2//!
3//! `X-API-Key` takes precedence; `Api-Key` is consulted only when it is absent.
4//! The selected value is returned unchanged, including an empty value. This
5//! module selects a text field but does not validate or authenticate it. API keys
6//! are sensitive: callers must not log or otherwise disclose returned values,
7//! and extraction errors never include them.
8
9use http::{HeaderMap, HeaderName, Request};
10
11use crate::{Error, header::extract_single_header_text};
12
13/// The preferred `X-API-Key` field name.
14pub const X_API_KEY: HeaderName = HeaderName::from_static("x-api-key");
15
16/// The fallback `Api-Key` field name.
17pub const API_KEY: HeaderName = HeaderName::from_static("api-key");
18
19/// Extract an API key from request fields.
20///
21/// `X-API-Key` takes precedence over `Api-Key`; the fallback is inspected only
22/// when `X-API-Key` is absent. If both are absent, this function returns `None`.
23/// The selected value is returned unchanged, so an empty field produces
24/// `Some("")`. A selected field that occurs more than once or is not text
25/// produces an error that does not contain its sensitive value. This function
26/// does not validate or authenticate the key, and callers must not log or echo
27/// it.
28pub fn extract_header_api_key(headers: &HeaderMap) -> Result<Option<&str>, Error> {
29    for name in [&X_API_KEY, &API_KEY] {
30        if let Some(value) = extract_single_header_text(headers, name)? {
31            return Ok(Some(value));
32        }
33    }
34    Ok(None)
35}
36
37/// Extract an API key from a complete request.
38///
39/// This reads `request.headers()` and delegates to
40/// [`extract_header_api_key`]. It therefore uses the same fixed precedence,
41/// missing and error behavior, preserves empty selected values, and performs no
42/// validation or authentication. The returned value is sensitive and must not
43/// be logged or echoed.
44pub fn extract_request_api_key<B>(request: &Request<B>) -> Result<Option<&str>, Error> {
45    extract_header_api_key(request.headers())
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn uses_x_api_key_before_api_key_fallback() {
54        let mut headers = HeaderMap::new();
55        assert_eq!(extract_header_api_key(&headers).unwrap(), None);
56
57        headers.insert("api-key", "fallback".parse().unwrap());
58        assert_eq!(extract_header_api_key(&headers).unwrap(), Some("fallback"));
59
60        headers.insert("x-api-key", "preferred".parse().unwrap());
61        assert_eq!(extract_header_api_key(&headers).unwrap(), Some("preferred"));
62    }
63
64    #[test]
65    fn preserves_empty_selected_values_without_falling_back() {
66        let mut headers = HeaderMap::new();
67        headers.insert("api-key", "fallback".parse().unwrap());
68        headers.insert("x-api-key", "".parse().unwrap());
69        assert_eq!(extract_header_api_key(&headers).unwrap(), Some(""));
70
71        headers.remove("x-api-key");
72        headers.insert("api-key", "".parse().unwrap());
73        assert_eq!(extract_header_api_key(&headers).unwrap(), Some(""));
74    }
75
76    #[test]
77    fn rejects_duplicate_and_non_text_values_without_echoing_them() {
78        let mut duplicate = HeaderMap::new();
79        duplicate.append("x-api-key", "first-secret".parse().unwrap());
80        duplicate.append("x-api-key", "second-secret".parse().unwrap());
81        let error = extract_header_api_key(&duplicate).unwrap_err();
82        assert!(matches!(error, Error::DuplicateHeader { .. }));
83        assert!(!format!("{error:?}").contains("secret"));
84
85        let mut duplicate_fallback = HeaderMap::new();
86        duplicate_fallback.append("api-key", "first-secret".parse().unwrap());
87        duplicate_fallback.append("api-key", "second-secret".parse().unwrap());
88        assert!(matches!(
89            extract_header_api_key(&duplicate_fallback),
90            Err(Error::DuplicateHeader { .. })
91        ));
92
93        let mut headers = HeaderMap::new();
94        headers.insert("x-api-key", http::HeaderValue::from_bytes(&[0xff]).unwrap());
95        assert!(matches!(
96            extract_header_api_key(&headers),
97            Err(Error::InvalidHeader { .. })
98        ));
99    }
100
101    #[test]
102    fn request_entry_point_delegates_to_headers() {
103        let request = Request::builder()
104            .header("api-key", "fallback")
105            .body(())
106            .unwrap();
107
108        assert_eq!(extract_request_api_key(&request).unwrap(), Some("fallback"));
109    }
110}