zeph_core/bootstrap/
oauth.rs1use std::sync::Arc;
7
8use rmcp::transport::auth::{AuthError, CredentialStore, StoredCredentials};
9use tokio::sync::RwLock;
10
11use crate::vault::AgeVaultProvider;
12use crate::vault::VaultProvider as _;
13
14pub struct VaultCredentialStore {
22 vault_key: String,
23 vault: Arc<RwLock<AgeVaultProvider>>,
24}
25
26impl VaultCredentialStore {
27 pub fn new(server_id: &str, vault: Arc<RwLock<AgeVaultProvider>>) -> Self {
31 let normalized = server_id.to_uppercase().replace('-', "_");
32 Self {
33 vault_key: format!("ZEPH_MCP_OAUTH_{normalized}"),
34 vault,
35 }
36 }
37
38 #[must_use]
40 pub fn vault_key(&self) -> &str {
41 &self.vault_key
42 }
43}
44
45#[async_trait::async_trait]
46impl CredentialStore for VaultCredentialStore {
47 async fn load(&self) -> Result<Option<StoredCredentials>, AuthError> {
48 let guard = self.vault.read().await;
49 let value = guard
50 .get_secret(&self.vault_key)
51 .await
52 .map_err(|e| AuthError::InternalError(format!("vault read: {e}")))?;
53 match value {
54 None => Ok(None),
55 Some(json) => {
56 let creds: StoredCredentials = serde_json::from_str(&json)
57 .map_err(|e| AuthError::InternalError(format!("vault deserialize: {e}")))?;
58 Ok(Some(creds))
59 }
60 }
61 }
62
63 async fn save(&self, credentials: StoredCredentials) -> Result<(), AuthError> {
64 let json = serde_json::to_string(&credentials)
65 .map_err(|e| AuthError::InternalError(format!("vault serialize: {e}")))?;
66 let vault = Arc::clone(&self.vault);
67 let key = self.vault_key.clone();
68 tokio::task::spawn_blocking(move || {
69 let mut guard = vault.blocking_write();
70 guard.set_secret_mut(key, json);
71 guard
72 .save()
73 .map_err(|e| AuthError::InternalError(format!("vault save: {e}")))
74 })
75 .await
76 .map_err(|e| AuthError::InternalError(format!("spawn_blocking: {e}")))?
77 }
78
79 async fn clear(&self) -> Result<(), AuthError> {
80 let vault = Arc::clone(&self.vault);
81 let key = self.vault_key.clone();
82 tokio::task::spawn_blocking(move || {
83 let mut guard = vault.blocking_write();
84 guard.remove_secret_mut(&key);
85 guard
86 .save()
87 .map_err(|e| AuthError::InternalError(format!("vault clear: {e}")))
88 })
89 .await
90 .map_err(|e| AuthError::InternalError(format!("spawn_blocking: {e}")))?
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 #[test]
97 fn vault_key_normalization_hyphen() {
98 let key = format!(
99 "ZEPH_MCP_OAUTH_{}",
100 "my-server".to_uppercase().replace('-', "_")
101 );
102 assert_eq!(key, "ZEPH_MCP_OAUTH_MY_SERVER");
103 }
104
105 #[test]
106 fn vault_key_collision_documented() {
107 let a = "my-app".to_uppercase().replace('-', "_");
109 let b = "my_app".to_uppercase().replace('-', "_");
110 assert_eq!(
111 a, b,
112 "vault key collision exists for hyphens vs underscores"
113 );
114 }
115}