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.
18#[derive(Debug)]
19pub struct BearerAuth {
20    token: String,
21}
22
23impl BearerAuth {
24    /// Create a new bearer auth with the given token.
25    pub fn new(token: impl Into<String>) -> Self {
26        Self {
27            token: token.into(),
28        }
29    }
30
31    /// Generate a random bearer token.
32    ///
33    /// # Security Note
34    ///
35    /// Uses xorshift PRNG seeded from system time + atomic counter.
36    /// This is **not** cryptographically secure — tokens are predictable
37    /// if the seed can be guessed. Suitable for local MCP server auth
38    /// (localhost-only transport). For production/remote servers, use
39    /// externally generated tokens via [`BearerAuth::new`].
40    pub fn generate() -> Self {
41        use std::sync::atomic::{AtomicU64, Ordering};
42        use std::time::{SystemTime, UNIX_EPOCH};
43
44        static COUNTER: AtomicU64 = AtomicU64::new(0);
45        let time = SystemTime::now()
46            .duration_since(UNIX_EPOCH)
47            .unwrap_or_default()
48            .as_nanos() as u64;
49        let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
50        // Simple xorshift for a hex token — no external rand dep.
51        let mut s = time.wrapping_add(counter).wrapping_add(0x9e3779b97f4a7c15);
52        let mut token = String::with_capacity(32);
53        for _ in 0..32 {
54            s ^= s << 13;
55            s ^= s >> 7;
56            s ^= s << 17;
57            let nibble = (s & 0xF) as u8;
58            token.push(if nibble < 10 {
59                b'0' + nibble
60            } else {
61                b'a' + nibble - 10
62            } as char);
63        }
64        Self { token }
65    }
66
67    /// Validate a bearer token from an Authorization header.
68    pub fn validate(&self, header_value: &str) -> bool {
69        if let Some(token) = header_value.strip_prefix("Bearer ") {
70            constant_time_eq(token.trim(), &self.token)
71        } else {
72            false
73        }
74    }
75
76    /// Get the raw token value.
77    pub fn token(&self) -> &str {
78        &self.token
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn validate_correct_token() {
88        let auth = BearerAuth::new("my-secret-token");
89        assert!(auth.validate("Bearer my-secret-token"));
90    }
91
92    #[test]
93    fn reject_wrong_token() {
94        let auth = BearerAuth::new("correct");
95        assert!(!auth.validate("Bearer wrong"));
96    }
97
98    #[test]
99    fn reject_missing_prefix() {
100        let auth = BearerAuth::new("token");
101        assert!(!auth.validate("token"));
102        assert!(!auth.validate("Basic token"));
103    }
104
105    #[test]
106    fn generate_produces_32_char_hex() {
107        let auth = BearerAuth::generate();
108        let token = auth.token();
109        assert_eq!(token.len(), 32);
110        assert!(token.chars().all(|c| c.is_ascii_hexdigit()));
111    }
112
113    #[test]
114    fn generate_unique() {
115        let a = BearerAuth::generate();
116        let b = BearerAuth::generate();
117        assert_ne!(a.token(), b.token());
118    }
119}