1use std::{str::FromStr, sync::Arc};
2
3use chrono::TimeDelta;
4
5use crate::{
6 atproto::{AtprotoClientMetadata, atproto_client_metadata},
7 authstore::ClientAuthStore,
8 dpop::DpopExt,
9 keyset::Keyset,
10 request::{OAuthMetadata, refresh},
11 resolver::OAuthResolver,
12 scopes::Scopes,
13 types::TokenSet,
14};
15
16use dashmap::DashMap;
17use jacquard_common::{
18 IntoStatic,
19 bos::{BosStr, DefaultStr},
20 deps::fluent_uri::Uri,
21 http_client::HttpClient,
22 session::{SessionKey, SessionStoreError},
23 types::{did::Did, string::Datetime},
24};
25use jose_jwk::Key;
26use mini_moka::sync::Cache as MokaCache;
27use serde::{Deserialize, Serialize};
28use smol_str::{SmolStr, ToSmolStr, format_smolstr};
29use tokio::sync::Mutex;
30
31pub trait DpopDataSource {
39 fn key(&self) -> &Key;
41 fn authserver_nonce(&self) -> Option<&str>;
43 fn set_authserver_nonce(&mut self, nonce: SmolStr);
45 fn host_nonce(&self) -> Option<&str>;
47 fn set_host_nonce(&mut self, nonce: SmolStr);
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(bound(
54 serialize = "S: serde::Serialize + BosStr + Ord",
55 deserialize = "S: serde::Deserialize<'de> + BosStr + AsRef<str>"
56))]
57pub struct ClientSessionData<S: BosStr = DefaultStr> {
58 pub account_did: Did<S>,
61
62 pub session_id: S,
66
67 pub host_url: Uri<String>,
69
70 pub authserver_url: S,
72
73 pub authserver_token_endpoint: S,
75
76 #[serde(skip_serializing_if = "std::option::Option::is_none")]
78 pub authserver_revocation_endpoint: Option<S>,
79
80 pub scopes: Scopes<S>,
82
83 #[serde(flatten)]
85 pub dpop_data: DpopClientData,
86
87 #[serde(flatten)]
89 pub token_set: TokenSet<S>,
90
91 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub resolved_scopes: Option<Vec<crate::scopes::Scope<smol_str::SmolStr>>>,
98}
99
100impl<S: BosStr + Ord + IntoStatic + AsRef<str>> IntoStatic for ClientSessionData<S>
101where
102 S::Output: BosStr + Ord + AsRef<str>,
103{
104 type Output = ClientSessionData<S::Output>;
105
106 fn into_static(self) -> Self::Output {
107 let resolved_scopes = self.resolved_scopes;
108
109 ClientSessionData {
110 authserver_url: self.authserver_url.into_static(),
111 authserver_token_endpoint: self.authserver_token_endpoint.into_static(),
112 authserver_revocation_endpoint: self
113 .authserver_revocation_endpoint
114 .map(IntoStatic::into_static),
115 scopes: self.scopes.into_static(),
116 dpop_data: self.dpop_data,
117 token_set: self.token_set.into_static(),
118 account_did: self.account_did.into_static(),
119 session_id: self.session_id.into_static(),
120 host_url: self.host_url.clone(),
121 resolved_scopes,
122 }
123 }
124}
125
126impl<S: BosStr + Ord + AsRef<str>> ClientSessionData<S> {
127 pub fn update_with_tokens(&mut self, token_set: &TokenSet<S>)
136 where
137 S: FromStr + Clone + From<SmolStr> + AsRef<str>,
138 S::Err: std::fmt::Debug,
139 {
140 if let Some(scope_str) = token_set.scope.as_ref() {
141 let scopes_smol = Scopes::new(scope_str.as_ref().to_smolstr())
143 .expect("server returned invalid scopes in token refresh");
144 self.scopes = scopes_smol.convert();
145 }
146 self.token_set = token_set.clone();
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
155pub struct DpopClientData {
156 pub dpop_key: Key,
158 pub dpop_authserver_nonce: SmolStr,
160 pub dpop_host_nonce: SmolStr,
162}
163
164impl DpopDataSource for DpopClientData {
165 fn key(&self) -> &Key {
166 &self.dpop_key
167 }
168
169 fn authserver_nonce(&self) -> Option<&str> {
170 Some(self.dpop_authserver_nonce.as_ref())
171 }
172
173 fn host_nonce(&self) -> Option<&str> {
174 Some(self.dpop_host_nonce.as_ref())
175 }
176
177 fn set_authserver_nonce(&mut self, nonce: SmolStr) {
178 self.dpop_authserver_nonce = nonce;
179 }
180
181 fn set_host_nonce(&mut self, nonce: SmolStr) {
182 self.dpop_host_nonce = nonce;
183 }
184}
185
186#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(bound(
193 serialize = "S: serde::Serialize + BosStr + Ord",
194 deserialize = "S: serde::Deserialize<'de> + BosStr + AsRef<str>"
195))]
196pub struct AuthRequestData<S: BosStr = DefaultStr> {
197 pub state: S,
200
201 pub authserver_url: S,
203
204 #[serde(skip_serializing_if = "std::option::Option::is_none")]
207 pub account_did: Option<Did<S>>,
208
209 pub scopes: Scopes<S>,
211
212 pub request_uri: S,
214
215 pub authserver_token_endpoint: S,
217
218 #[serde(skip_serializing_if = "std::option::Option::is_none")]
220 pub authserver_revocation_endpoint: Option<S>,
221
222 pub pkce_verifier: S,
225
226 #[serde(flatten)]
228 pub dpop_data: DpopReqData,
229}
230
231impl<S: BosStr + Ord + IntoStatic + AsRef<str>> IntoStatic for AuthRequestData<S>
232where
233 S::Output: BosStr + Ord + AsRef<str>,
234{
235 type Output = AuthRequestData<S::Output>;
236
237 fn into_static(self) -> AuthRequestData<S::Output> {
238 AuthRequestData {
239 request_uri: self.request_uri.into_static(),
240 authserver_token_endpoint: self.authserver_token_endpoint.into_static(),
241 authserver_revocation_endpoint: self
242 .authserver_revocation_endpoint
243 .map(|s| s.into_static()),
244 pkce_verifier: self.pkce_verifier.into_static(),
245 dpop_data: self.dpop_data,
246 state: self.state.into_static(),
247 authserver_url: self.authserver_url.into_static(),
248 account_did: self.account_did.into_static(),
249 scopes: self.scopes.into_static(),
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
259pub struct DpopReqData {
260 pub dpop_key: Key,
262 pub dpop_authserver_nonce: Option<SmolStr>,
264}
265
266impl DpopDataSource for DpopReqData {
267 fn key(&self) -> &Key {
268 &self.dpop_key
269 }
270
271 fn authserver_nonce(&self) -> Option<&str> {
272 self.dpop_authserver_nonce.as_ref().map(|n| n.as_ref())
273 }
274
275 fn host_nonce(&self) -> Option<&str> {
276 None
277 }
278
279 fn set_authserver_nonce(&mut self, nonce: SmolStr) {
280 self.dpop_authserver_nonce = Some(nonce);
281 }
282
283 fn set_host_nonce(&mut self, _nonce: SmolStr) {}
284}
285
286#[derive(Clone, Debug)]
291pub struct ClientData<S>
292where
293 S: BosStr + FromStr + Ord,
294 <S as FromStr>::Err: core::fmt::Debug,
295{
296 pub keyset: Option<Keyset>,
299 pub config: AtprotoClientMetadata<S>,
301}
302
303impl<S> IntoStatic for ClientData<S>
304where
305 S: BosStr + FromStr + Ord + IntoStatic,
306 S::Output: BosStr + Ord + FromStr,
307 <S as FromStr>::Err: core::fmt::Debug,
308 <S::Output as FromStr>::Err: core::fmt::Debug,
309{
310 type Output = ClientData<S::Output>;
311 fn into_static(self) -> ClientData<S::Output> {
312 ClientData {
313 keyset: self.keyset,
314 config: self.config.into_static(),
315 }
316 }
317}
318
319impl<S: BosStr + FromStr + Ord> ClientData<S>
320where
321 <S as FromStr>::Err: core::fmt::Debug,
322{
323 pub fn new(keyset: Option<Keyset>, config: AtprotoClientMetadata<S>) -> Self {
325 Self { keyset, config }
326 }
327
328 pub fn new_public(config: AtprotoClientMetadata<S>) -> Self {
333 Self {
334 keyset: None,
335 config,
336 }
337 }
338}
339
340pub struct ClientSession<S: BosStr = DefaultStr>
346where
347 S: FromStr + Ord,
348 <S as FromStr>::Err: core::fmt::Debug,
349{
350 pub keyset: Option<Keyset>,
352 pub config: AtprotoClientMetadata<S>,
354 pub session_data: ClientSessionData<S>,
356}
357
358impl<S: BosStr> ClientSession<S>
359where
360 S: FromStr + Ord + Clone,
361 <S as FromStr>::Err: core::fmt::Debug,
362{
363 pub fn new(
365 ClientData { keyset, config }: ClientData<S>,
366 session_data: ClientSessionData<S>,
367 ) -> Self {
368 Self {
369 keyset,
370 config,
371 session_data,
372 }
373 }
374
375 pub async fn metadata<T: HttpClient + OAuthResolver + Send + Sync>(
377 &self,
378 client: &T,
379 ) -> Result<OAuthMetadata<S>, Error>
380 where
381 S: IntoStatic,
382 {
383 Ok(OAuthMetadata {
384 server_metadata: client
385 .get_authorization_server_metadata(self.session_data.authserver_url.as_ref())
386 .await
387 .map_err(|e| Error::ServerAgent(crate::request::RequestError::resolver(e)))?,
388 client_metadata: atproto_client_metadata(&self.config, &self.keyset).unwrap(),
389 keyset: self.keyset.clone(),
390 })
391 }
392}
393
394#[derive(thiserror::Error, Debug, miette::Diagnostic)]
396#[non_exhaustive]
397pub enum Error {
398 #[error(transparent)]
400 #[diagnostic(code(jacquard_oauth::session::request))]
401 ServerAgent(#[from] crate::request::RequestError),
402 #[error(transparent)]
404 #[diagnostic(code(jacquard_oauth::session::storage))]
405 Store(#[from] SessionStoreError),
406 #[error("session does not exist")]
408 #[diagnostic(code(jacquard_oauth::session::not_found))]
409 SessionNotFound,
410 #[error("session refresh failed permanently")]
413 #[diagnostic(
414 code(jacquard_oauth::session::refresh_failed),
415 help("the session has been cleared - user must re-authenticate")
416 )]
417 RefreshFailed(#[source] crate::request::RequestError),
418}
419
420impl Error {
421 pub fn is_permanent(&self) -> bool {
424 match self {
425 Error::RefreshFailed(_) => true,
426 Error::SessionNotFound => true,
427 Error::ServerAgent(e) => e.is_permanent(),
428 Error::Store(_) => false,
429 }
430 }
431}
432
433pub struct SessionRegistry<T, S, Str>
440where
441 T: OAuthResolver,
442 S: ClientAuthStore,
443 Str: BosStr + FromStr + Ord,
444 <Str as FromStr>::Err: core::fmt::Debug,
445{
446 pub store: Arc<S>,
448 pub client: Arc<T>,
450 pub client_data: ClientData<Str>,
452 pending: DashMap<SmolStr, Arc<Mutex<()>>>,
454 #[allow(dead_code)]
459 pub(crate) session_cache:
460 std::sync::OnceLock<MokaCache<SessionKey, crate::client::OAuthSession<T, S>>>,
461}
462
463impl<T, S, Str> SessionRegistry<T, S, Str>
464where
465 S: ClientAuthStore,
466 T: OAuthResolver,
467 Str: BosStr + FromStr + Ord,
468 <Str as FromStr>::Err: core::fmt::Debug,
469{
470 pub fn new(store: S, client: Arc<T>, client_data: ClientData<Str>) -> Self {
472 let store = Arc::new(store);
473 Self {
474 store,
475 client,
476 client_data,
477 pending: DashMap::new(),
478 session_cache: std::sync::OnceLock::new(),
479 }
480 }
481
482 pub fn new_shared(store: Arc<S>, client: Arc<T>, client_data: ClientData<Str>) -> Self {
487 Self {
488 store,
489 client,
490 client_data,
491 pending: DashMap::new(),
492 session_cache: std::sync::OnceLock::new(),
493 }
494 }
495}
496
497impl<T, S, Str> SessionRegistry<T, S, Str>
498where
499 S: ClientAuthStore + Send + Sync + 'static,
500 T: OAuthResolver + DpopExt + Send + Sync + 'static,
501 Str: BosStr + FromStr + Ord + Clone,
502 <Str as FromStr>::Err: core::fmt::Debug,
503{
504 pub(crate) fn cache(&self) -> &MokaCache<SessionKey, crate::client::OAuthSession<T, S>> {
505 self.session_cache.get_or_init(|| {
506 MokaCache::builder()
507 .time_to_live(std::time::Duration::from_secs(30))
508 .max_capacity(64)
509 .build()
510 })
511 }
512
513 async fn get_refreshed<D: BosStr + Send + Sync>(
514 &self,
515 did: &Did<D>,
516 session_id: &str,
517 ) -> Result<ClientSessionData, Error> {
518 let key = format_smolstr!("{}_{}", did, session_id);
519 let lock = self
520 .pending
521 .entry(key)
522 .or_insert_with(|| Arc::new(Mutex::new(())))
523 .clone();
524 let _guard = lock.lock().await;
525
526 let session = self
527 .store
528 .get_session(did, session_id)
529 .await?
530 .ok_or(Error::SessionNotFound)?;
531
532 const EXPIRY_BUFFER_SECS: i64 = 60;
536 if let Some(expires_at) = &session.token_set.expires_at {
537 let now_with_buffer = Datetime::now()
538 .as_ref()
539 .checked_add_signed(TimeDelta::seconds(EXPIRY_BUFFER_SECS))
540 .map(Datetime::new)
541 .unwrap_or_else(Datetime::now);
542 if expires_at > &now_with_buffer {
543 return Ok(session);
544 }
545 }
546 let metadata =
547 OAuthMetadata::new(self.client.as_ref(), &self.client_data, &session).await?;
548 match refresh(self.client.as_ref(), session, &metadata).await {
549 Ok(refreshed) => {
550 self.store.upsert_session(refreshed.clone()).await?;
551 Ok(refreshed)
552 }
553 Err(e) if e.is_permanent() => {
554 let _ = self.store.delete_session(did, session_id).await;
556 Err(Error::RefreshFailed(e))
557 }
558 Err(e) => Err(Error::ServerAgent(e)),
559 }
560 }
561 pub async fn get<D: BosStr + Send + Sync>(
567 &self,
568 did: &Did<D>,
569 session_id: &str,
570 refresh: bool,
571 ) -> Result<ClientSessionData, Error> {
572 if refresh {
573 self.get_refreshed(did, session_id).await
574 } else {
575 self.store
577 .get_session(did, session_id)
578 .await?
579 .ok_or(Error::SessionNotFound)
580 }
581 }
582 pub async fn set(&self, value: ClientSessionData) -> Result<(), Error> {
584 self.store.upsert_session(value).await?;
585 Ok(())
586 }
587 pub async fn del<D: BosStr + Send + Sync>(
589 &self,
590 did: &Did<D>,
591 session_id: &str,
592 ) -> Result<(), Error> {
593 self.store.delete_session(did, session_id).await?;
594 Ok(())
595 }
596}