Skip to main content

llm_kernel/mcp/
auth.rs

1//! Bearer token authentication for MCP servers.
2
3/// Constant-time-ish string comparison for bearer tokens.
4///
5/// Not true constant-time (uses length-based early exit), but avoids
6/// the obvious timing leak of `==` on short strings.
7fn constant_time_eq(a: &str, b: &str) -> bool {
8    if a.len() != b.len() {
9        return false;
10    }
11    a.bytes()
12        .zip(b.bytes())
13        .fold(0, |acc, (x, y)| acc | (x ^ y))
14        == 0
15}
16
17/// Bearer token authenticator for MCP HTTP transport.
18pub struct BearerAuth {
19    token: String,
20}
21
22/// Deriving `Debug` would print the token into logs and panic messages.
23impl std::fmt::Debug for BearerAuth {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("BearerAuth")
26            .field("token", &"<redacted>")
27            .finish()
28    }
29}
30
31impl BearerAuth {
32    /// Create a new bearer auth with the given token.
33    pub fn new(token: impl Into<String>) -> Self {
34        Self {
35            token: token.into(),
36        }
37    }
38
39    /// Generate a random bearer token (128 bits of OS entropy, hex-encoded).
40    ///
41    /// Uses the OS CSPRNG via `getrandom`. If the OS entropy source is
42    /// unavailable the call fails rather than falling back to a guessable
43    /// token — see [`BearerAuth::try_generate`].
44    ///
45    /// # Panics
46    ///
47    /// Panics if the OS entropy source is unavailable. Use
48    /// [`BearerAuth::try_generate`] to handle that case, or
49    /// [`BearerAuth::new`] to supply an externally generated token.
50    pub fn generate() -> Self {
51        Self::try_generate().expect("OS entropy source unavailable")
52    }
53
54    /// Fallible [`BearerAuth::generate`] — never falls back to a weak token.
55    pub fn try_generate() -> crate::error::Result<Self> {
56        let mut bytes = [0u8; 16];
57        getrandom::fill(&mut bytes).map_err(|e| {
58            crate::error::KernelError::Config(format!("failed to read OS entropy for token: {e}"))
59        })?;
60        let mut token = String::with_capacity(32);
61        for b in bytes {
62            use std::fmt::Write;
63            let _ = write!(token, "{b:02x}");
64        }
65        Ok(Self { token })
66    }
67
68    /// Validate a bearer token from an Authorization header.
69    ///
70    /// The auth scheme is matched case-insensitively, as RFC 7235 requires
71    /// (`bearer x` and `Bearer x` are equivalent).
72    pub fn validate(&self, header_value: &str) -> bool {
73        match header_value.split_once(' ') {
74            Some((scheme, token)) if scheme.eq_ignore_ascii_case("Bearer") => {
75                constant_time_eq(token.trim(), &self.token)
76            }
77            _ => false,
78        }
79    }
80
81    /// Get the raw token value.
82    pub fn token(&self) -> &str {
83        &self.token
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn validate_correct_token() {
93        let auth = BearerAuth::new("my-secret-token");
94        assert!(auth.validate("Bearer my-secret-token"));
95    }
96
97    #[test]
98    fn reject_wrong_token() {
99        let auth = BearerAuth::new("correct");
100        assert!(!auth.validate("Bearer wrong"));
101    }
102
103    #[test]
104    fn reject_missing_prefix() {
105        let auth = BearerAuth::new("token");
106        assert!(!auth.validate("token"));
107        assert!(!auth.validate("Basic token"));
108    }
109
110    #[test]
111    fn scheme_match_is_case_insensitive() {
112        let auth = BearerAuth::new("token");
113        assert!(auth.validate("bearer token"));
114        assert!(auth.validate("BEARER token"));
115        assert!(auth.validate("Bearer token"));
116    }
117
118    #[test]
119    fn generate_produces_32_char_hex() {
120        let auth = BearerAuth::generate();
121        let token = auth.token();
122        assert_eq!(token.len(), 32);
123        assert!(token.chars().all(|c| c.is_ascii_hexdigit()));
124    }
125
126    #[test]
127    fn generate_unique() {
128        let a = BearerAuth::generate();
129        let b = BearerAuth::generate();
130        assert_ne!(a.token(), b.token());
131    }
132
133    #[test]
134    fn debug_never_prints_token() {
135        let auth = BearerAuth::new("super-secret-token");
136        let dbg = format!("{auth:?}");
137        assert!(!dbg.contains("super-secret-token"), "{dbg}");
138    }
139}