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