systemprompt_security/extraction/
token.rs1use axum::http::HeaderMap;
7use std::error::Error;
8use std::fmt;
9
10use super::cookie::{CookieExtractionError, CookieExtractor};
11
12const DEFAULT_MCP_HEADER_NAME: &str = "x-mcp-proxy-auth";
13const BEARER_PREFIX: &str = "Bearer ";
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ExtractionMethod {
17 AuthorizationHeader,
18 McpProxyHeader,
19 Cookie,
20}
21
22impl fmt::Display for ExtractionMethod {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::AuthorizationHeader => write!(f, "Authorization header"),
26 Self::McpProxyHeader => write!(f, "MCP proxy header"),
27 Self::Cookie => write!(f, "Cookie"),
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
33pub struct TokenExtractor {
34 fallback_chain: Vec<ExtractionMethod>,
35 cookie_name: String,
36 mcp_header_name: String,
37}
38
39impl TokenExtractor {
40 #[must_use]
41 pub fn new(fallback_chain: Vec<ExtractionMethod>) -> Self {
42 Self {
43 fallback_chain,
44 cookie_name: CookieExtractor::DEFAULT_COOKIE_NAME.to_owned(),
45 mcp_header_name: DEFAULT_MCP_HEADER_NAME.to_owned(),
46 }
47 }
48
49 #[must_use]
50 pub fn with_cookie_name(mut self, name: String) -> Self {
51 self.cookie_name = name;
52 self
53 }
54
55 #[must_use]
56 pub fn with_mcp_header_name(mut self, name: String) -> Self {
57 self.mcp_header_name = name;
58 self
59 }
60
61 #[must_use]
62 pub fn standard() -> Self {
63 Self::new(vec![
64 ExtractionMethod::AuthorizationHeader,
65 ExtractionMethod::McpProxyHeader,
66 ExtractionMethod::Cookie,
67 ])
68 }
69
70 #[must_use]
71 pub fn browser_only() -> Self {
72 Self::new(vec![
73 ExtractionMethod::AuthorizationHeader,
74 ExtractionMethod::Cookie,
75 ])
76 }
77
78 #[must_use]
79 pub fn api_only() -> Self {
80 Self::new(vec![ExtractionMethod::AuthorizationHeader])
81 }
82
83 #[must_use]
84 pub fn chain(&self) -> &[ExtractionMethod] {
85 &self.fallback_chain
86 }
87
88 pub fn extract(&self, headers: &HeaderMap) -> Result<String, TokenExtractionError> {
89 for method in &self.fallback_chain {
90 match method {
91 ExtractionMethod::AuthorizationHeader => {
92 if let Ok(token) = Self::extract_from_authorization(headers) {
93 return Ok(token);
94 }
95 },
96 ExtractionMethod::McpProxyHeader => {
97 if let Ok(token) = self.extract_from_mcp_proxy(headers) {
98 return Ok(token);
99 }
100 },
101 ExtractionMethod::Cookie => {
102 if let Ok(token) = self.extract_from_cookie(headers) {
103 return Ok(token);
104 }
105 },
106 }
107 }
108
109 Err(TokenExtractionError::NoTokenFound)
110 }
111
112 pub fn extract_from_authorization(headers: &HeaderMap) -> Result<String, TokenExtractionError> {
113 let auth_headers = headers.get_all("authorization");
114
115 if auth_headers.iter().count() == 0 {
116 return Err(TokenExtractionError::MissingAuthorizationHeader);
117 }
118
119 for auth_value in &auth_headers {
120 let Ok(auth_header) = auth_value.to_str().map_err(|e| {
121 tracing::debug!(error = %e, "Authorization header contains non-ASCII characters");
122 e
123 }) else {
124 continue;
125 };
126
127 if let Some(token) = auth_header.strip_prefix(BEARER_PREFIX) {
128 let token = token.trim();
129 if !token.is_empty() {
130 return Ok(token.to_owned());
131 }
132 }
133 }
134
135 Err(TokenExtractionError::InvalidAuthorizationFormat)
136 }
137
138 pub fn extract_from_mcp_proxy(
139 &self,
140 headers: &HeaderMap,
141 ) -> Result<String, TokenExtractionError> {
142 let header_value = headers
143 .get(&self.mcp_header_name)
144 .ok_or(TokenExtractionError::MissingMcpProxyHeader)?;
145
146 let auth_header = header_value
147 .to_str()
148 .map_err(|_e| TokenExtractionError::InvalidMcpProxyFormat)?;
149
150 auth_header
151 .strip_prefix(BEARER_PREFIX)
152 .ok_or(TokenExtractionError::InvalidMcpProxyFormat)
153 .map(str::to_owned)
154 }
155
156 pub fn extract_from_cookie(&self, headers: &HeaderMap) -> Result<String, TokenExtractionError> {
157 CookieExtractor::new(self.cookie_name.clone())
158 .extract(headers)
159 .map_err(|err| match err {
160 CookieExtractionError::MissingCookie => TokenExtractionError::MissingCookie,
161 CookieExtractionError::InvalidCookieFormat => {
162 TokenExtractionError::InvalidCookieFormat
163 },
164 CookieExtractionError::TokenNotFoundInCookie => {
165 TokenExtractionError::TokenNotFoundInCookie
166 },
167 })
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum TokenExtractionError {
173 NoTokenFound,
174 MissingAuthorizationHeader,
175 InvalidAuthorizationFormat,
176 MissingMcpProxyHeader,
177 InvalidMcpProxyFormat,
178 MissingCookie,
179 InvalidCookieFormat,
180 TokenNotFoundInCookie,
181}
182
183impl fmt::Display for TokenExtractionError {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 match self {
186 Self::NoTokenFound => write!(f, "No token found in request"),
187 Self::MissingAuthorizationHeader => {
188 write!(f, "Missing Authorization header")
189 },
190 Self::InvalidAuthorizationFormat => {
191 write!(
192 f,
193 "Invalid Authorization header format (expected 'Bearer <token>')"
194 )
195 },
196 Self::MissingMcpProxyHeader => {
197 write!(f, "Missing MCP proxy authorization header")
198 },
199 Self::InvalidMcpProxyFormat => {
200 write!(
201 f,
202 "Invalid MCP proxy header format (expected 'Bearer <token>')"
203 )
204 },
205 Self::MissingCookie => write!(f, "Missing cookie header"),
206 Self::InvalidCookieFormat => write!(f, "Invalid cookie format"),
207 Self::TokenNotFoundInCookie => write!(f, "Token not found in cookies"),
208 }
209 }
210}
211
212impl Error for TokenExtractionError {}