1use std::future::Future;
21use std::pin::Pin;
22
23use oauth2::{CsrfToken, PkceCodeVerifier, TokenResponse as _};
24use rmcp::transport::auth::{
25 AuthError, CredentialStore, StateStore, StoredAuthorizationState, StoredCredentials,
26};
27use rmcp_client as rmcp;
28
29use crate::sqlite::SqliteStore;
30use crate::types::{UpstreamOauthCredentialRow, UpstreamOauthStateRow};
31use crate::upstream::encryption::{self, EncryptionKey};
32
33fn credential_aad(upstream_name: &str, subject: &str, client_id: &str) -> Vec<u8> {
34 format!("upstream={upstream_name}\0subject={subject}\0client_id={client_id}").into_bytes()
35}
36
37fn now_unix() -> i64 {
38 std::time::SystemTime::now()
39 .duration_since(std::time::UNIX_EPOCH)
40 .unwrap_or_default()
41 .as_secs() as i64
42}
43
44pub struct SqliteCredentialStore {
50 store: SqliteStore,
51 key: EncryptionKey,
52 upstream_name: String,
53 subject: String,
54}
55
56impl SqliteCredentialStore {
57 pub fn new(
58 store: SqliteStore,
59 key: EncryptionKey,
60 upstream_name: impl Into<String>,
61 subject: impl Into<String>,
62 ) -> Self {
63 Self {
64 store,
65 key,
66 upstream_name: upstream_name.into(),
67 subject: subject.into(),
68 }
69 }
70}
71
72impl CredentialStore for SqliteCredentialStore {
73 fn load<'life0, 'async_trait>(
74 &'life0 self,
75 ) -> Pin<
76 Box<
77 dyn Future<Output = Result<Option<StoredCredentials>, AuthError>> + Send + 'async_trait,
78 >,
79 >
80 where
81 'life0: 'async_trait,
82 Self: 'async_trait,
83 {
84 Box::pin(async move {
85 let row = self
86 .store
87 .find_upstream_oauth_credentials(&self.upstream_name, &self.subject)
88 .await
89 .map_err(|e| AuthError::InternalError(e.to_string()))?;
90
91 let Some(row) = row else {
92 return Ok(None);
93 };
94
95 let aad = credential_aad(&row.upstream_name, &row.subject, &row.client_id);
96 let plaintext =
97 encryption::open_with_aad(&self.key, &row.token_blob, &row.token_blob_nonce, &aad)
98 .map_err(|_| AuthError::AuthorizationRequired)?;
99
100 let creds: StoredCredentials = serde_json::from_slice(&plaintext)
101 .map_err(|e| AuthError::InternalError(format!("deserialize credentials: {e}")))?;
102
103 Ok(Some(creds))
104 })
105 }
106
107 fn save<'life0, 'async_trait>(
108 &'life0 self,
109 credentials: StoredCredentials,
110 ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'async_trait>>
111 where
112 'life0: 'async_trait,
113 Self: 'async_trait,
114 {
115 Box::pin(async move {
116 let token_received_at = credentials
117 .token_received_at
118 .map(|t| t as i64)
119 .unwrap_or_else(now_unix);
120
121 let (access_token_expires_at, refresh_token_present) =
122 if let Some(ref token) = credentials.token_response {
123 let expires_in = token.expires_in().map(|d| d.as_secs()).unwrap_or(3600) as i64;
124 (
125 token_received_at + expires_in,
126 token.refresh_token().is_some(),
127 )
128 } else {
129 (0, false)
130 };
131
132 let granted_scopes_json = serde_json::to_string(&credentials.granted_scopes)
133 .map_err(|e| AuthError::InternalError(format!("serialize scopes: {e}")))?;
134
135 let plaintext = serde_json::to_vec(&credentials)
136 .map_err(|e| AuthError::InternalError(format!("serialize credentials: {e}")))?;
137
138 let aad = credential_aad(&self.upstream_name, &self.subject, &credentials.client_id);
139 let (token_blob, token_blob_nonce) =
140 encryption::seal_with_aad(&self.key, &plaintext, &aad)
141 .map_err(|e| AuthError::InternalError(format!("encrypt credentials: {e}")))?;
142
143 let row = UpstreamOauthCredentialRow {
144 upstream_name: self.upstream_name.clone(),
145 subject: self.subject.clone(),
146 client_id: credentials.client_id.clone(),
147 granted_scopes_json,
148 token_blob,
149 token_blob_nonce,
150 token_received_at,
151 access_token_expires_at,
152 refresh_token_present,
153 };
154
155 self.store
156 .upsert_upstream_oauth_credentials(row)
157 .await
158 .map_err(|e| AuthError::InternalError(e.to_string()))
159 })
160 }
161
162 fn clear<'life0, 'async_trait>(
163 &'life0 self,
164 ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'async_trait>>
165 where
166 'life0: 'async_trait,
167 Self: 'async_trait,
168 {
169 Box::pin(async move {
170 self.store
171 .delete_upstream_oauth_credentials(&self.upstream_name, &self.subject)
172 .await
173 .map_err(|e| AuthError::InternalError(e.to_string()))
174 })
175 }
176}
177
178pub struct SqliteStateStore {
183 store: SqliteStore,
184 upstream_name: String,
185 subject: String,
186}
187
188impl SqliteStateStore {
189 pub fn new(
190 store: SqliteStore,
191 upstream_name: impl Into<String>,
192 subject: impl Into<String>,
193 ) -> Self {
194 Self {
195 store,
196 upstream_name: upstream_name.into(),
197 subject: subject.into(),
198 }
199 }
200}
201
202const STATE_TTL_SECS: i64 = 300;
204
205impl StateStore for SqliteStateStore {
206 fn save<'life0, 'life1, 'async_trait>(
207 &'life0 self,
208 csrf_token: &'life1 str,
209 state: StoredAuthorizationState,
210 ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'async_trait>>
211 where
212 'life0: 'async_trait,
213 'life1: 'async_trait,
214 Self: 'async_trait,
215 {
216 Box::pin(async move {
217 let now = now_unix();
218 let row = UpstreamOauthStateRow {
219 upstream_name: self.upstream_name.clone(),
220 subject: self.subject.clone(),
221 csrf_token: csrf_token.to_string(),
222 pkce_verifier: state.pkce_verifier,
223 created_at: now,
224 expires_at: now + STATE_TTL_SECS,
225 };
226 self.store
227 .save_upstream_oauth_state(row)
228 .await
229 .map_err(|e| AuthError::InternalError(e.to_string()))
230 })
231 }
232
233 fn load<'life0, 'life1, 'async_trait>(
234 &'life0 self,
235 csrf_token: &'life1 str,
236 ) -> Pin<
237 Box<
238 dyn Future<Output = Result<Option<StoredAuthorizationState>, AuthError>>
239 + Send
240 + 'async_trait,
241 >,
242 >
243 where
244 'life0: 'async_trait,
245 'life1: 'async_trait,
246 Self: 'async_trait,
247 {
248 Box::pin(async move {
249 let now = now_unix();
250 let row = self
251 .store
252 .take_upstream_oauth_state(&self.upstream_name, &self.subject, csrf_token, now)
253 .await
254 .map_err(|e| AuthError::InternalError(e.to_string()))?;
255
256 Ok(row.map(|r| {
257 StoredAuthorizationState::new(
258 &PkceCodeVerifier::new(r.pkce_verifier),
259 &CsrfToken::new(r.csrf_token),
260 )
261 }))
262 })
263 }
264
265 fn delete<'life0, 'life1, 'async_trait>(
266 &'life0 self,
267 _csrf_token: &'life1 str,
268 ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'async_trait>>
269 where
270 'life0: 'async_trait,
271 'life1: 'async_trait,
272 Self: 'async_trait,
273 {
274 Box::pin(async move { Ok(()) })
277 }
278}
279
280#[cfg(test)]
289mod tests {
290 use oauth2::{CsrfToken, PkceCodeVerifier};
291 use rmcp_client::transport::auth::{StateStore, StoredAuthorizationState};
292
293 use super::SqliteStateStore;
294
295 async fn temp_store() -> crate::sqlite::SqliteStore {
297 let path = tempfile::tempdir()
298 .expect("tempdir")
299 .keep()
300 .join("upstream_oauth_test.db");
301 crate::sqlite::SqliteStore::open(path)
302 .await
303 .expect("open test store")
304 }
305
306 fn make_state_store(
307 store: crate::sqlite::SqliteStore,
308 upstream: &str,
309 subject: &str,
310 ) -> SqliteStateStore {
311 SqliteStateStore::new(store, upstream, subject)
312 }
313
314 fn sample_stored_state(csrf: &str) -> StoredAuthorizationState {
315 StoredAuthorizationState::new(
316 &PkceCodeVerifier::new("verifier-value".to_string()),
317 &CsrfToken::new(csrf.to_string()),
318 )
319 }
320
321 #[tokio::test]
323 async fn unknown_state_returns_none() {
324 let store = make_state_store(temp_store().await, "acme", "alice");
325 let result = store
326 .load("nonexistent-csrf")
327 .await
328 .expect("load should not error");
329 assert!(result.is_none(), "unknown state token must return None");
330 }
331
332 #[tokio::test]
338 async fn replayed_state_is_rejected() {
339 let sqlite = temp_store().await;
340 let store = make_state_store(sqlite, "acme", "alice");
341 let csrf = "csrf-replay-test";
342
343 store
345 .save(csrf, sample_stored_state(csrf))
346 .await
347 .expect("save should succeed");
348
349 let first = store.load(csrf).await.expect("first load should not error");
351 assert!(first.is_some(), "first load must return the stored state");
352
353 let second = store
355 .load(csrf)
356 .await
357 .expect("second load should not error");
358 assert!(
359 second.is_none(),
360 "replayed (already-consumed) state token must return None"
361 );
362 }
363
364 #[tokio::test]
370 async fn expired_state_is_rejected() {
371 use crate::types::UpstreamOauthStateRow;
372
373 let sqlite = temp_store().await;
374
375 let now = std::time::SystemTime::now()
377 .duration_since(std::time::UNIX_EPOCH)
378 .unwrap_or_default()
379 .as_secs() as i64;
380 let row = UpstreamOauthStateRow {
381 upstream_name: "acme".to_string(),
382 subject: "alice".to_string(),
383 csrf_token: "csrf-expired".to_string(),
384 pkce_verifier: "verifier".to_string(),
385 created_at: now - 400,
386 expires_at: now - 1, };
388 sqlite
389 .save_upstream_oauth_state(row)
390 .await
391 .expect("save expired row");
392
393 let store = make_state_store(sqlite, "acme", "alice");
395 let result = store
396 .load("csrf-expired")
397 .await
398 .expect("load should not error on expired state");
399 assert!(
400 result.is_none(),
401 "expired state token must return None, not the stored state"
402 );
403 }
404}