1use std::env;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum AuthMode {
10 None,
12 Required,
14 Optional,
16}
17
18#[derive(Debug, Clone)]
20pub struct TokenAuth {
21 mode: AuthMode,
22 token: Option<String>,
23}
24
25impl TokenAuth {
26 pub fn from_env() -> Self {
31 let mode = match env::var("AGENTIC_AUTH_MODE")
32 .unwrap_or_default()
33 .to_lowercase()
34 .as_str()
35 {
36 "required" => AuthMode::Required,
37 "optional" => AuthMode::Optional,
38 _ => AuthMode::None,
39 };
40
41 let token = env::var("AGENTIC_AUTH_TOKEN")
42 .ok()
43 .filter(|t| !t.is_empty());
44
45 if mode == AuthMode::Required && token.is_none() {
46 eprintln!(
47 "WARNING: AGENTIC_AUTH_MODE=required but AGENTIC_AUTH_TOKEN is not set. \
48 All requests will be rejected."
49 );
50 }
51
52 Self { mode, token }
53 }
54
55 pub fn new(mode: AuthMode, token: Option<String>) -> Self {
57 Self { mode, token }
58 }
59
60 pub fn validate(&self, provided: Option<&str>) -> Result<(), AuthError> {
62 match self.mode {
63 AuthMode::None => Ok(()),
64 AuthMode::Required => {
65 let provided = provided.ok_or(AuthError::TokenMissing)?;
66 let expected = self
67 .token
68 .as_deref()
69 .ok_or(AuthError::ServerMisconfigured)?;
70 if constant_time_eq(provided.as_bytes(), expected.as_bytes()) {
71 Ok(())
72 } else {
73 Err(AuthError::TokenInvalid)
74 }
75 }
76 AuthMode::Optional => {
77 if let Some(provided) = provided {
78 let expected = self
79 .token
80 .as_deref()
81 .ok_or(AuthError::ServerMisconfigured)?;
82 if constant_time_eq(provided.as_bytes(), expected.as_bytes()) {
83 Ok(())
84 } else {
85 Err(AuthError::TokenInvalid)
86 }
87 } else {
88 Ok(())
90 }
91 }
92 }
93 }
94
95 pub fn mode(&self) -> &AuthMode {
96 &self.mode
97 }
98}
99
100#[derive(Debug, PartialEq, Eq)]
101pub enum AuthError {
102 TokenMissing,
104 TokenInvalid,
106 ServerMisconfigured,
108}
109
110impl std::fmt::Display for AuthError {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 match self {
113 AuthError::TokenMissing => write!(f, "authentication token required"),
114 AuthError::TokenInvalid => write!(f, "invalid authentication token"),
115 AuthError::ServerMisconfigured => {
116 write!(f, "server requires auth but no token is configured")
117 }
118 }
119 }
120}
121
122impl std::error::Error for AuthError {}
123
124fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
128 if a.len() != b.len() {
129 let mut _acc: u8 = 1;
132 for byte in a {
133 _acc |= byte;
134 }
135 return false;
136 }
137
138 let mut diff: u8 = 0;
139 for (x, y) in a.iter().zip(b.iter()) {
140 diff |= x ^ y;
141 }
142 diff == 0
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn none_mode_always_passes() {
151 let auth = TokenAuth::new(AuthMode::None, None);
152 assert!(auth.validate(None).is_ok());
153 assert!(auth.validate(Some("anything")).is_ok());
154 }
155
156 #[test]
157 fn required_mode_needs_token() {
158 let auth = TokenAuth::new(AuthMode::Required, Some("secret123".to_string()));
159 assert_eq!(auth.validate(None), Err(AuthError::TokenMissing));
160 assert_eq!(auth.validate(Some("wrong")), Err(AuthError::TokenInvalid));
161 assert!(auth.validate(Some("secret123")).is_ok());
162 }
163
164 #[test]
165 fn required_mode_no_server_token() {
166 let auth = TokenAuth::new(AuthMode::Required, None);
167 assert_eq!(
168 auth.validate(Some("anything")),
169 Err(AuthError::ServerMisconfigured)
170 );
171 }
172
173 #[test]
174 fn optional_mode_works() {
175 let auth = TokenAuth::new(AuthMode::Optional, Some("secret".to_string()));
176 assert!(auth.validate(None).is_ok()); assert!(auth.validate(Some("secret")).is_ok());
178 assert_eq!(auth.validate(Some("wrong")), Err(AuthError::TokenInvalid));
179 }
180
181 #[test]
182 fn constant_time_eq_works() {
183 assert!(constant_time_eq(b"hello", b"hello"));
184 assert!(!constant_time_eq(b"hello", b"world"));
185 assert!(!constant_time_eq(b"hello", b"hell"));
186 assert!(!constant_time_eq(b"", b"x"));
187 assert!(constant_time_eq(b"", b""));
188 }
189
190 #[test]
191 fn constant_time_eq_different_lengths() {
192 assert!(!constant_time_eq(b"short", b"longer-string"));
193 assert!(!constant_time_eq(b"longer-string", b"short"));
194 }
195}