1use chio_http_core::{AuthMethod, CallerIdentity};
4
5pub type IdentityExtractor = fn(&http::HeaderMap) -> CallerIdentity;
7
8pub fn extract_identity(headers: &http::HeaderMap) -> CallerIdentity {
16 if let Some(auth) = headers.get(http::header::AUTHORIZATION) {
18 if let Ok(auth_str) = auth.to_str() {
19 if let Some(token) = auth_str
20 .strip_prefix("Bearer ")
21 .and_then(valid_secret_value)
22 {
23 let token_hash = chio_http_core::sha256_hex(token.as_bytes());
24 let subject = format!("bearer:{}", &token_hash[..16]);
25 return CallerIdentity {
26 subject,
27 auth_method: AuthMethod::Bearer { token_hash },
28 verified: false,
29 tenant: None,
30 agent_id: None,
31 };
32 }
33 }
34 }
35
36 for key_header in &["x-api-key", "X-Api-Key", "X-API-Key"] {
38 if let Some(key_value) = headers.get(*key_header) {
39 if let Ok(key_str) = key_value.to_str().map(valid_secret_value) {
40 let Some(key_str) = key_str else {
41 continue;
42 };
43 let key_hash = chio_http_core::sha256_hex(key_str.as_bytes());
44 let subject = format!("apikey:{}", &key_hash[..16]);
45 return CallerIdentity {
46 subject,
47 auth_method: AuthMethod::ApiKey {
48 key_name: key_header.to_string(),
49 key_hash,
50 },
51 verified: false,
52 tenant: None,
53 agent_id: None,
54 };
55 }
56 }
57 }
58
59 if let Some(cookie) = headers.get(http::header::COOKIE) {
61 if let Ok(cookie_str) = cookie.to_str() {
62 if let Some(first) = cookie_str.split(';').next() {
63 let parts: Vec<&str> = first.splitn(2, '=').collect();
64 if parts.len() == 2 {
65 let cookie_name = parts[0].trim().to_string();
66 let cookie_value = parts[1].trim();
67 if !cookie_value.is_empty() {
68 let cookie_hash = chio_http_core::sha256_hex(cookie_value.as_bytes());
69 let subject = format!("cookie:{}", &cookie_hash[..16]);
70 return CallerIdentity {
71 subject,
72 auth_method: AuthMethod::Cookie {
73 cookie_name,
74 cookie_hash,
75 },
76 verified: false,
77 tenant: None,
78 agent_id: None,
79 };
80 }
81 }
82 }
83 }
84 }
85
86 CallerIdentity::anonymous()
88}
89
90fn valid_secret_value(value: &str) -> Option<&str> {
91 if value.trim().is_empty() || value.trim() != value {
92 None
93 } else {
94 Some(value)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn extract_bearer() {
104 let mut headers = http::HeaderMap::new();
105 headers.insert(
106 http::header::AUTHORIZATION,
107 http::HeaderValue::from_static("Bearer my-test-token"),
108 );
109 let caller = extract_identity(&headers);
110 assert!(caller.subject.starts_with("bearer:"));
111 assert!(matches!(caller.auth_method, AuthMethod::Bearer { .. }));
112 assert!(!caller.verified);
113 }
114
115 #[test]
116 fn extract_api_key() {
117 let mut headers = http::HeaderMap::new();
118 headers.insert("x-api-key", http::HeaderValue::from_static("secret-key"));
119 let caller = extract_identity(&headers);
120 assert!(caller.subject.starts_with("apikey:"));
121 assert!(matches!(caller.auth_method, AuthMethod::ApiKey { .. }));
122 }
123
124 #[test]
125 fn extract_anonymous() {
126 let headers = http::HeaderMap::new();
127 let caller = extract_identity(&headers);
128 assert_eq!(caller.subject, "anonymous");
129 assert!(matches!(caller.auth_method, AuthMethod::Anonymous));
130 }
131
132 #[test]
133 fn extract_cookie_identity() {
134 let mut headers = http::HeaderMap::new();
135 headers.insert(
136 http::header::COOKIE,
137 http::HeaderValue::from_static("session_id=abc123xyz"),
138 );
139 let caller = extract_identity(&headers);
140 assert!(caller.subject.starts_with("cookie:"));
141 match &caller.auth_method {
142 AuthMethod::Cookie {
143 cookie_name,
144 cookie_hash,
145 } => {
146 assert_eq!(cookie_name, "session_id");
147 assert_eq!(cookie_hash.len(), 64); }
149 other => panic!("expected Cookie, got {other:?}"),
150 }
151 }
152
153 #[test]
154 fn bearer_takes_precedence_over_cookie() {
155 let mut headers = http::HeaderMap::new();
156 headers.insert(
157 http::header::AUTHORIZATION,
158 http::HeaderValue::from_static("Bearer my-token"),
159 );
160 headers.insert(
161 http::header::COOKIE,
162 http::HeaderValue::from_static("session_id=abc123"),
163 );
164 let caller = extract_identity(&headers);
165 assert!(caller.subject.starts_with("bearer:"));
167 }
168
169 #[test]
170 fn non_bearer_authorization_falls_through() {
171 let mut headers = http::HeaderMap::new();
172 headers.insert(
173 http::header::AUTHORIZATION,
174 http::HeaderValue::from_static("Basic dXNlcjpwYXNz"),
175 );
176 let caller = extract_identity(&headers);
177 assert_eq!(caller.subject, "anonymous");
179 }
180
181 #[test]
182 fn blank_bearer_token_falls_through() {
183 let mut headers = http::HeaderMap::new();
184 headers.insert(
185 http::header::AUTHORIZATION,
186 http::HeaderValue::from_static("Bearer "),
187 );
188 let caller = extract_identity(&headers);
189 assert_eq!(caller.subject, "anonymous");
190 assert!(matches!(caller.auth_method, AuthMethod::Anonymous));
191 }
192
193 #[test]
194 fn blank_api_key_falls_through() {
195 let mut headers = http::HeaderMap::new();
196 headers.insert("x-api-key", http::HeaderValue::from_static(" "));
197 let caller = extract_identity(&headers);
198 assert_eq!(caller.subject, "anonymous");
199 assert!(matches!(caller.auth_method, AuthMethod::Anonymous));
200 }
201
202 #[test]
203 fn empty_cookie_value_falls_through() {
204 let mut headers = http::HeaderMap::new();
205 headers.insert(
206 http::header::COOKIE,
207 http::HeaderValue::from_static("session_id="),
208 );
209 let caller = extract_identity(&headers);
210 assert_eq!(caller.subject, "anonymous");
212 }
213}