Skip to main content

origin_mcp/
auth.rs

1use std::net::IpAddr;
2use subtle::ConstantTimeEq;
3
4pub fn verify_token(provided: &str, expected: &str) -> bool {
5    if expected.is_empty() {
6        return false;
7    }
8    // Fixed-size buffer comparison to eliminate length timing oracle.
9    // Tokens are base64url-encoded 256-bit values (~43 chars).
10    const BUF_SIZE: usize = 136; // 128 content + 8 length
11    let pb = provided.as_bytes();
12    let eb = expected.as_bytes();
13    if pb.len() > 128 || eb.len() > 128 {
14        return false;
15    }
16    let mut p = [0u8; BUF_SIZE];
17    let mut e = [0u8; BUF_SIZE];
18    p[..pb.len()].copy_from_slice(pb);
19    e[..eb.len()].copy_from_slice(eb);
20    p[128..].copy_from_slice(&(pb.len() as u64).to_le_bytes());
21    e[128..].copy_from_slice(&(eb.len() as u64).to_le_bytes());
22    p.ct_eq(&e).into()
23}
24
25pub fn is_loopback(addr: &IpAddr) -> bool {
26    addr.is_loopback()
27}
28
29pub fn is_origin_allowed(origin: &str, allowed: &[String]) -> bool {
30    allowed.iter().any(|a| a == "*" || a == origin)
31}
32
33pub fn extract_bearer_token(header_value: &str) -> Option<&str> {
34    header_value.strip_prefix("Bearer ")
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn test_verify_token_matching() {
43        assert!(verify_token("abc123", "abc123"));
44    }
45
46    #[test]
47    fn test_verify_token_different() {
48        assert!(!verify_token("abc123", "xyz789"));
49    }
50
51    #[test]
52    fn test_verify_token_different_lengths() {
53        assert!(!verify_token("short", "much-longer-token"));
54    }
55
56    #[test]
57    fn test_verify_token_empty_expected_always_false() {
58        assert!(!verify_token("", ""));
59        assert!(!verify_token("", "notempty"));
60        assert!(!verify_token("anything", ""));
61    }
62
63    #[test]
64    fn test_loopback_ipv4() {
65        let addr: IpAddr = "127.0.0.1".parse().unwrap();
66        assert!(is_loopback(&addr));
67    }
68
69    #[test]
70    fn test_loopback_ipv6() {
71        let addr: IpAddr = "::1".parse().unwrap();
72        assert!(is_loopback(&addr));
73    }
74
75    #[test]
76    fn test_not_loopback_0000() {
77        let addr: IpAddr = "0.0.0.0".parse().unwrap();
78        assert!(!is_loopback(&addr));
79    }
80
81    #[test]
82    fn test_not_loopback_ipv6_unspecified() {
83        let addr: IpAddr = "::".parse().unwrap();
84        assert!(!is_loopback(&addr));
85    }
86
87    #[test]
88    fn test_not_loopback_lan() {
89        let addr: IpAddr = "192.168.1.1".parse().unwrap();
90        assert!(!is_loopback(&addr));
91    }
92
93    #[test]
94    fn test_origin_allowed_exact_match() {
95        let allowed = vec!["https://claude.ai".into(), "https://chatgpt.com".into()];
96        assert!(is_origin_allowed("https://claude.ai", &allowed));
97    }
98
99    #[test]
100    fn test_origin_not_allowed() {
101        let allowed = vec!["https://claude.ai".into()];
102        assert!(!is_origin_allowed("https://evil.com", &allowed));
103    }
104
105    #[test]
106    fn test_origin_wildcard_allows_all() {
107        let allowed = vec!["*".into()];
108        assert!(is_origin_allowed("https://anything.com", &allowed));
109    }
110
111    #[test]
112    fn test_origin_empty_list_denies_all() {
113        let allowed: Vec<String> = vec![];
114        assert!(!is_origin_allowed("https://claude.ai", &allowed));
115    }
116
117    #[test]
118    fn test_extract_bearer_valid() {
119        assert_eq!(
120            extract_bearer_token("Bearer mytoken123"),
121            Some("mytoken123")
122        );
123    }
124
125    #[test]
126    fn test_extract_bearer_no_prefix() {
127        assert_eq!(extract_bearer_token("mytoken123"), None);
128    }
129
130    #[test]
131    fn test_extract_bearer_wrong_scheme() {
132        assert_eq!(extract_bearer_token("Basic abc123"), None);
133    }
134
135    #[test]
136    fn test_extract_bearer_case_sensitive() {
137        assert_eq!(extract_bearer_token("bearer mytoken"), None);
138    }
139
140    #[test]
141    fn test_extract_bearer_empty_token() {
142        assert_eq!(extract_bearer_token("Bearer "), Some(""));
143    }
144}