github_mcp/auth/
auth_manager.rs1use crate::core::config_schema::{AuthMethod, Transport};
4use crate::core::credential_storage::{load_credential, save_credential};
5
6use super::auth_strategy::{AuthConfig, AuthStrategy, Credentials};
7use super::errors::AuthError;
8use super::request_credentials::RequestCredentials;
9use super::strategies::api_key::ApiKeyStrategy;
10use super::strategies::basic::BasicAuthStrategy;
11use super::strategies::pat::PatAuthStrategy;
12use super::strategies::stub::StubAuthStrategy;
13
14const CREDENTIAL_ACCOUNT: &str = "active-credentials";
15
16fn strategy_for(auth_method: AuthMethod) -> Box<dyn AuthStrategy> {
17 match auth_method {
18 AuthMethod::Basic => Box::new(BasicAuthStrategy),
19 AuthMethod::ApiKey => Box::new(ApiKeyStrategy),
20 AuthMethod::Pat => Box::new(PatAuthStrategy),
21 #[allow(unreachable_patterns)]
22 _ => Box::new(StubAuthStrategy),
23 }
24}
25
26pub fn header_location_for(auth_method: AuthMethod) -> (&'static str, &'static str) {
30 match auth_method {
31 AuthMethod::Pat => ("header", "Authorization"),
32 AuthMethod::ApiKey => ("header", "Authorization"),
33 AuthMethod::Basic => ("header", "Authorization"),
34 }
35}
36
37pub struct AuthManager {
41 auth_method: AuthMethod,
42 strategy: Box<dyn AuthStrategy>,
43 cached_credentials: Option<Credentials>,
44}
45
46impl AuthManager {
47 pub fn new(auth_method: AuthMethod) -> Self {
48 Self {
49 strategy: strategy_for(auth_method),
50 auth_method,
51 cached_credentials: None,
52 }
53 }
54
55 pub async fn login(&mut self, config: &AuthConfig) -> anyhow::Result<Credentials> {
56 let credentials = self.strategy.authenticate(config).await?;
57 self.cached_credentials = Some(credentials.clone());
58 save_credential(CREDENTIAL_ACCOUNT, &serde_json::to_string(&credentials)?)?;
59 Ok(credentials)
60 }
61
62 pub fn set_credentials(&mut self, credentials: Credentials) {
67 self.cached_credentials = Some(credentials);
68 }
69
70 pub async fn credentials(&mut self) -> anyhow::Result<Credentials> {
71 if let Some(cached) = &self.cached_credentials
72 && self.strategy.validate_credentials(cached)
73 {
74 return self.normalize_credentials(cached).await;
75 }
76
77 if let Some(stored) = load_credential(CREDENTIAL_ACCOUNT)? {
78 let parsed: Credentials = serde_json::from_str(&stored)?;
79 if self.strategy.validate_credentials(&parsed) {
80 let normalized = self.normalize_credentials(&parsed).await?;
81 self.cached_credentials = Some(normalized.clone());
82 save_credential(CREDENTIAL_ACCOUNT, &serde_json::to_string(&normalized)?)?;
83 return Ok(normalized);
84 }
85 if let Ok(refreshed) = self.strategy.refresh_token(&parsed).await {
86 let normalized = self.normalize_credentials(&refreshed).await?;
87 self.cached_credentials = Some(normalized.clone());
88 save_credential(CREDENTIAL_ACCOUNT, &serde_json::to_string(&normalized)?)?;
89 return Ok(normalized);
90 }
91 }
92
93 Err(AuthError::NoActiveCredentials(format!("{:?}", self.auth_method)).into())
94 }
95
96 async fn normalize_credentials(
97 &self,
98 credentials: &Credentials,
99 ) -> anyhow::Result<Credentials> {
100 if credentials.contains_key("authorization_header")
101 || credentials.contains_key("api_key")
102 || credentials.contains_key("request_header_name")
103 {
104 return Ok(credentials.clone());
105 }
106
107 self.strategy.authenticate(credentials).await
108 }
109
110 pub async fn apply_auth_headers(
128 &mut self,
129 mut headers: std::collections::HashMap<String, String>,
130 method: &str,
131 url: &str,
132 transport: Transport,
133 request_override: Option<&RequestCredentials>,
134 ) -> anyhow::Result<std::collections::HashMap<String, String>> {
135 let credentials = match (transport, request_override) {
136 (Transport::Http, Some(override_credentials)) => {
137 override_credentials.clone().into_credentials_map()
138 }
139 (Transport::Http, None) => {
140 anyhow::bail!(
141 "no credentials were supplied on this HTTP request; local config/env/keychain \
142 is never used as a fallback over HTTP transport"
143 );
144 }
145 (Transport::Stdio, _) => self.credentials().await?,
146 };
147 let _ = method;
153 let _ = url;
154
155 if let Some(name) = credentials.get("request_header_name") {
156 let value = credentials
157 .get("request_header_value")
158 .cloned()
159 .unwrap_or_default();
160 headers.insert(name.clone(), value);
161 } else if let Some(header) = credentials.get("authorization_header") {
162 headers.insert("Authorization".to_string(), header.clone());
163 } else if let Some(api_key) = credentials.get("api_key") {
164 headers.insert("X-Api-Key".to_string(), api_key.clone());
165 }
166 Ok(headers)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[tokio::test]
175 async fn seeded_credentials_win_over_stored_ones() {
176 let mut manager = AuthManager::new(AuthMethod::Pat);
177 let mut credentials = Credentials::new();
178 credentials.insert("token".to_string(), "abc".to_string());
184 manager.set_credentials(credentials.clone());
185
186 let resolved = manager.credentials().await.unwrap();
187 assert_eq!(resolved.get("token"), credentials.get("token"));
188 assert_eq!(
189 resolved.get("authorization_header").map(String::as_str),
190 Some("Bearer abc")
191 );
192 }
193
194 #[tokio::test]
195 async fn pat_credentials_saved_by_setup_are_normalized_before_use() {
196 let mut manager = AuthManager::new(AuthMethod::Pat);
197 let mut credentials = Credentials::new();
198 credentials.insert("token".to_string(), "abc".to_string());
199 manager.set_credentials(credentials);
200
201 let headers = manager
202 .apply_auth_headers(
203 std::collections::HashMap::new(),
204 "GET",
205 "https://api.github.com/user",
206 Transport::Stdio,
207 None,
208 )
209 .await
210 .unwrap();
211
212 assert_eq!(
213 headers.get("Authorization").map(String::as_str),
214 Some("Bearer abc")
215 );
216 }
217 #[tokio::test]
218 async fn http_transport_uses_the_request_override_not_the_config_cascade() {
219 let mut manager = AuthManager::new(AuthMethod::Pat);
220 let mut stale = Credentials::new();
223 stale.insert(
224 "request_header_name".to_string(),
225 "X-Should-Not-Be-Used".to_string(),
226 );
227 manager.set_credentials(stale);
228
229 let override_credentials = RequestCredentials {
230 header_name: "X-Api-Key".to_string(),
231 value: "from-the-request".to_string(),
232 };
233 let headers = manager
234 .apply_auth_headers(
235 std::collections::HashMap::new(),
236 "GET",
237 "https://example.com",
238 Transport::Http,
239 Some(&override_credentials),
240 )
241 .await
242 .unwrap();
243
244 assert_eq!(
245 headers.get("X-Api-Key").map(String::as_str),
246 Some("from-the-request")
247 );
248 }
249
250 #[tokio::test]
251 async fn http_transport_without_an_override_errors_instead_of_falling_back() {
252 let mut manager = AuthManager::new(AuthMethod::Pat);
253 let mut stale = Credentials::new();
254 stale.insert(
255 "request_header_name".to_string(),
256 "X-Should-Not-Be-Used".to_string(),
257 );
258 manager.set_credentials(stale);
259
260 let result = manager
261 .apply_auth_headers(
262 std::collections::HashMap::new(),
263 "GET",
264 "https://example.com",
265 Transport::Http,
266 None,
267 )
268 .await;
269
270 assert!(result.is_err());
271 }
272}