github_mcp/auth/strategies/
basic.rs1use async_trait::async_trait;
4use base64::Engine;
5use base64::engine::general_purpose::STANDARD;
6
7use super::super::auth_strategy::{AuthConfig, AuthStrategy, Credentials};
8use super::super::errors::AuthError;
9
10#[derive(Debug, Default)]
11pub struct BasicAuthStrategy;
12
13#[async_trait]
14impl AuthStrategy for BasicAuthStrategy {
15 async fn authenticate(&self, config: &AuthConfig) -> anyhow::Result<Credentials> {
16 let username = config
17 .get("username")
18 .ok_or_else(|| AuthError::MissingCredentials("username, password".to_string()))?;
19 let password = config
20 .get("password")
21 .ok_or_else(|| AuthError::MissingCredentials("username, password".to_string()))?;
22
23 let token = STANDARD.encode(format!("{username}:{password}"));
24 let mut credentials = Credentials::new();
25 credentials.insert("username".to_string(), username.clone());
26 credentials.insert("password".to_string(), password.clone());
27 credentials.insert("authorization_header".to_string(), format!("Basic {token}"));
28 Ok(credentials)
29 }
30
31 fn validate_credentials(&self, credentials: &Credentials) -> bool {
32 credentials.contains_key("authorization_header")
33 || (credentials
34 .get("username")
35 .is_some_and(|username| !username.is_empty())
36 && credentials
37 .get("password")
38 .is_some_and(|password| !password.is_empty()))
39 }
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 fn config(username: &str, password: &str) -> AuthConfig {
47 AuthConfig::from([
48 ("username".to_string(), username.to_string()),
49 ("password".to_string(), password.to_string()),
50 ])
51 }
52
53 #[tokio::test]
54 async fn encodes_username_and_password_as_a_basic_authorization_header() {
55 let strategy = BasicAuthStrategy;
56 let credentials = strategy
57 .authenticate(&config("alice", "s3cr3t"))
58 .await
59 .unwrap();
60 assert_eq!(
61 credentials.get("authorization_header").unwrap(),
62 "Basic YWxpY2U6czNjcjN0"
63 );
64 }
65
66 #[tokio::test]
67 async fn rejects_a_config_missing_either_field() {
68 let strategy = BasicAuthStrategy;
69 assert!(strategy.authenticate(&AuthConfig::new()).await.is_err());
70 }
71
72 #[test]
73 fn validates_credentials_carrying_an_authorization_header() {
74 let strategy = BasicAuthStrategy;
75 let credentials =
76 Credentials::from([("authorization_header".to_string(), "Basic abc".to_string())]);
77 assert!(strategy.validate_credentials(&credentials));
78 assert!(strategy.validate_credentials(&config("alice", "s3cr3t")));
79 assert!(!strategy.validate_credentials(&Credentials::new()));
80 }
81}