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    pub fn validate(&self, header_value: &str) -> bool {
70        if let Some(token) = header_value.strip_prefix("Bearer ") {
71            constant_time_eq(token.trim(), &self.token)
72        } else {
73            false
74        }
75    }
76
77    /// Get the raw token value.
78    pub fn token(&self) -> &str {
79        &self.token
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn validate_correct_token() {
89        let auth = BearerAuth::new("my-secret-token");
90        assert!(auth.validate("Bearer my-secret-token"));
91    }
92
93    #[test]
94    fn reject_wrong_token() {
95        let auth = BearerAuth::new("correct");
96        assert!(!auth.validate("Bearer wrong"));
97    }
98
99    #[test]
100    fn reject_missing_prefix() {
101        let auth = BearerAuth::new("token");
102        assert!(!auth.validate("token"));
103        assert!(!auth.validate("Basic token"));
104    }
105
106    #[test]
107    fn generate_produces_32_char_hex() {
108        let auth = BearerAuth::generate();
109        let token = auth.token();
110        assert_eq!(token.len(), 32);
111        assert!(token.chars().all(|c| c.is_ascii_hexdigit()));
112    }
113
114    #[test]
115    fn generate_unique() {
116        let a = BearerAuth::generate();
117        let b = BearerAuth::generate();
118        assert_ne!(a.token(), b.token());
119    }
120
121    #[test]
122    fn debug_never_prints_token() {
123        let auth = BearerAuth::new("super-secret-token");
124        let dbg = format!("{auth:?}");
125        assert!(!dbg.contains("super-secret-token"), "{dbg}");
126    }
127}