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 Ok(cached.clone());
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 self.cached_credentials = Some(parsed.clone());
81 return Ok(parsed);
82 }
83 if let Ok(refreshed) = self.strategy.refresh_token(&parsed).await {
84 self.cached_credentials = Some(refreshed.clone());
85 save_credential(CREDENTIAL_ACCOUNT, &serde_json::to_string(&refreshed)?)?;
86 return Ok(refreshed);
87 }
88 }
89
90 Err(AuthError::NoActiveCredentials(format!("{:?}", self.auth_method)).into())
91 }
92
93 pub async fn apply_auth_headers(
111 &mut self,
112 mut headers: std::collections::HashMap<String, String>,
113 method: &str,
114 url: &str,
115 transport: Transport,
116 request_override: Option<&RequestCredentials>,
117 ) -> anyhow::Result<std::collections::HashMap<String, String>> {
118 let credentials = match (transport, request_override) {
119 (Transport::Http, Some(override_credentials)) => {
120 override_credentials.clone().into_credentials_map()
121 }
122 (Transport::Http, None) => {
123 anyhow::bail!(
124 "no credentials were supplied on this HTTP request; local config/env/keychain \
125 is never used as a fallback over HTTP transport"
126 );
127 }
128 (Transport::Stdio, _) => self.credentials().await?,
129 };
130 let _ = method;
136 let _ = url;
137
138 if let Some(name) = credentials.get("request_header_name") {
139 let value = credentials
140 .get("request_header_value")
141 .cloned()
142 .unwrap_or_default();
143 headers.insert(name.clone(), value);
144 } else if let Some(header) = credentials.get("authorization_header") {
145 headers.insert("Authorization".to_string(), header.clone());
146 } else if let Some(api_key) = credentials.get("api_key") {
147 headers.insert("X-Api-Key".to_string(), api_key.clone());
148 }
149 Ok(headers)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[tokio::test]
158 async fn seeded_credentials_win_over_stored_ones() {
159 let mut manager = AuthManager::new(AuthMethod::Pat);
160 let mut credentials = Credentials::new();
161 credentials.insert("token".to_string(), "abc".to_string());
167 manager.set_credentials(credentials.clone());
168
169 let resolved = manager.credentials().await.unwrap();
170 assert_eq!(resolved, credentials);
171 }
172 #[tokio::test]
173 async fn http_transport_uses_the_request_override_not_the_config_cascade() {
174 let mut manager = AuthManager::new(AuthMethod::Pat);
175 let mut stale = Credentials::new();
178 stale.insert(
179 "request_header_name".to_string(),
180 "X-Should-Not-Be-Used".to_string(),
181 );
182 manager.set_credentials(stale);
183
184 let override_credentials = RequestCredentials {
185 header_name: "X-Api-Key".to_string(),
186 value: "from-the-request".to_string(),
187 };
188 let headers = manager
189 .apply_auth_headers(
190 std::collections::HashMap::new(),
191 "GET",
192 "https://example.com",
193 Transport::Http,
194 Some(&override_credentials),
195 )
196 .await
197 .unwrap();
198
199 assert_eq!(
200 headers.get("X-Api-Key").map(String::as_str),
201 Some("from-the-request")
202 );
203 }
204
205 #[tokio::test]
206 async fn http_transport_without_an_override_errors_instead_of_falling_back() {
207 let mut manager = AuthManager::new(AuthMethod::Pat);
208 let mut stale = Credentials::new();
209 stale.insert(
210 "request_header_name".to_string(),
211 "X-Should-Not-Be-Used".to_string(),
212 );
213 manager.set_credentials(stale);
214
215 let result = manager
216 .apply_auth_headers(
217 std::collections::HashMap::new(),
218 "GET",
219 "https://example.com",
220 Transport::Http,
221 None,
222 )
223 .await;
224
225 assert!(result.is_err());
226 }
227}