From d853091d7de59e18746a78532dc28cfc017079b0 Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 15 Nov 2025 22:06:40 -0500 Subject: [PATCH] bugfix with scopes in loopback flow --- Cargo.lock | 29 +++ crates/jacquard-oauth/src/loopback.rs | 17 +- crates/jacquard/Cargo.toml | 5 +- crates/jacquard/src/client.rs | 17 +- crates/jacquard/src/client/bff_session.rs | 241 ++++++++++++++++++++++ 5 files changed, 294 insertions(+), 15 deletions(-) create mode 100644 crates/jacquard/src/client/bff_session.rs diff --git a/Cargo.lock b/Cargo.lock index 30e4d104..bdb391a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1643,6 +1643,34 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "group" version = "0.13.0" @@ -2239,6 +2267,7 @@ dependencies = [ "bytes", "clap", "getrandom 0.2.16", + "gloo-storage", "http", "image", "jacquard-api", diff --git a/crates/jacquard-oauth/src/loopback.rs b/crates/jacquard-oauth/src/loopback.rs index bd80fd64..afd81a7d 100644 --- a/crates/jacquard-oauth/src/loopback.rs +++ b/crates/jacquard-oauth/src/loopback.rs @@ -1,13 +1,11 @@ #![cfg(feature = "loopback")] use crate::{ - atproto::AtprotoClientMetadata, authstore::ClientAuthStore, client::OAuthClient, dpop::DpopExt, error::{CallbackError, OAuthError}, resolver::OAuthResolver, - scopes::Scope, types::{AuthorizeOptions, CallbackParams}, }; use jacquard_common::{IntoStatic, cowstr::ToCowStr}; @@ -122,22 +120,15 @@ where local_addr.port(), )) .unwrap(); - let client_data = crate::session::ClientData { - keyset: self.registry.client_data.keyset.clone(), - config: AtprotoClientMetadata::new_localhost( - Some(vec![redirect.clone()]), - Some(vec![ - Scope::Atproto, - Scope::Transition(crate::scopes::TransitionScope::Generic), - ]), - ), - }; + let mut client_data = self.registry.client_data.clone(); + // Ensure the redirect URI is set correctly for the loopback server + client_data.config.redirect_uris = vec![redirect]; // Build client using store and resolver let flow_client = OAuthClient::new_with_shared( self.registry.store.clone(), self.client.clone(), - client_data.clone(), + client_data, ); // Start auth and get authorization URL diff --git a/crates/jacquard/Cargo.toml b/crates/jacquard/Cargo.toml index 00aac604..f919ca07 100644 --- a/crates/jacquard/Cargo.toml +++ b/crates/jacquard/Cargo.toml @@ -157,7 +157,7 @@ webpage.workspace = true jose-jwk = { workspace = true, features = ["p256"] } tracing = { workspace = true, optional = true } n0-future = { workspace = true, optional = true } - +gloo-storage = "0.3" [target.'cfg(not(target_family = "wasm"))'.dependencies] jacquard-identity = { version = "0.9", path = "../jacquard-identity", features = ["cache"] } @@ -171,6 +171,9 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread", "fs"] } [target.'cfg(target_family = "wasm")'.dependencies] getrandom = { version = "0.2", features = ["js"] } +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +gloo-storage = "0.3" + [dev-dependencies] clap.workspace = true miette = { workspace = true, features = ["fancy"] } diff --git a/crates/jacquard/src/client.rs b/crates/jacquard/src/client.rs index cfffa8d4..688abe25 100644 --- a/crates/jacquard/src/client.rs +++ b/crates/jacquard/src/client.rs @@ -16,6 +16,7 @@ //! - [`token`] - Token storage and persistence //! - [`vec_update`] - Trait for fetch-modify-put patterns on array endpoints +//pub mod bff_session; /// App-password session implementation with auto-refresh pub mod credential_session; /// Agent error type @@ -460,9 +461,10 @@ impl Default for MemoryCredentialSession { /// App password session information from `com.atproto.server.createSession` /// /// Contains the access and refresh tokens along with user identity information. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AtpSession { /// Access token (JWT) used for authenticated requests + #[serde(borrow)] pub access_jwt: CowStr<'static>, /// Refresh token (JWT) used to obtain new access tokens pub refresh_jwt: CowStr<'static>, @@ -472,6 +474,19 @@ pub struct AtpSession { pub handle: Handle<'static>, } +impl IntoStatic for AtpSession { + type Output = Self; + + fn into_static(self) -> Self { + Self { + access_jwt: self.access_jwt.into_static(), + refresh_jwt: self.refresh_jwt.into_static(), + did: self.did.into_static(), + handle: self.handle.into_static(), + } + } +} + #[cfg(feature = "api")] impl From> for AtpSession { fn from(output: CreateSessionOutput<'_>) -> Self { diff --git a/crates/jacquard/src/client/bff_session.rs b/crates/jacquard/src/client/bff_session.rs new file mode 100644 index 00000000..f655bd2b --- /dev/null +++ b/crates/jacquard/src/client/bff_session.rs @@ -0,0 +1,241 @@ +//! Session implementation for front-end clients that proxy to a dedicated backend +//! + +//#[cfg(target_arch = "wasm32")] +use crate::client::SessionStoreError; +//#[cfg(target_arch = "wasm32")] +use crate::client::{AtpSession, credential_session::SessionKey}; +//#[cfg(target_arch = "wasm32")] +use gloo_storage::{LocalStorage, SessionStorage, Storage}; +//#[cfg(target_arch = "wasm32")] +use jacquard_common::{session::SessionStore, types::string::Did}; +#[cfg(target_arch = "wasm32")] +use jacquard_oauth::authstore::ClientAuthStore; +#[cfg(target_arch = "wasm32")] +use jacquard_oauth::session::{AuthRequestData, ClientSessionData}; +//#[cfg(target_arch = "wasm32")] +use std::future::Future; + +//#[cfg(target_arch = "wasm32")] +#[derive(Clone)] +pub struct BrowserAuthStore; + +//#[cfg(target_arch = "wasm32")] +impl BrowserAuthStore { + pub fn new() -> Self { + Self + } + + fn session_key(did: &Did<'_>, session_id: &str) -> String { + format!("session_{}_{}", did.as_ref(), session_id) + } + + fn auth_req_key(state: &str) -> String { + format!("auth_req_{}", state) + } +} + +#[cfg(target_arch = "wasm32")] +impl ClientAuthStore for BrowserAuthStore { + fn get_session( + &self, + did: &Did<'_>, + session_id: &str, + ) -> impl Future>, SessionStoreError>> { + let key = Self::session_key(did, session_id); + async move { + match LocalStorage::get::(&key) { + Ok(value) => { + let data: ClientSessionData<'static> = + jacquard::from_json_value::(value).map_err(|e| { + SessionStoreError::Other(format!("Deserialize error: {}", e).into()) + })?; + Ok(Some(data)) + } + Err(gloo_storage::errors::StorageError::KeyNotFound(_)) => Ok(None), + Err(e) => Err(SessionStoreError::Other( + format!("LocalStorage error: {}", e).into(), + )), + } + } + } + + fn upsert_session( + &self, + session: ClientSessionData<'_>, + ) -> impl Future> { + async move { + use jacquard::IntoStatic; + + let key = Self::session_key(&session.account_did, &session.session_id); + let static_session = session.into_static(); + + let value = serde_json::to_value(&static_session) + .map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?; + + LocalStorage::set(&key, &value).map_err(|e| { + SessionStoreError::Other(format!("LocalStorage error: {}", e).into()) + })?; + + Ok(()) + } + } + + fn delete_session( + &self, + did: &Did<'_>, + session_id: &str, + ) -> impl Future> { + let key = Self::session_key(did, session_id); + async move { + LocalStorage::delete(&key); + Ok(()) + } + } + + fn get_auth_req_info( + &self, + state: &str, + ) -> impl Future>, SessionStoreError>> { + let key = Self::auth_req_key(state); + async move { + match LocalStorage::get::(&key) { + Ok(value) => { + let data: AuthRequestData<'static> = + jacquard::from_json_value::(value).map_err(|e| { + SessionStoreError::Other(format!("Deserialize error: {}", e).into()) + })?; + Ok(Some(data)) + } + Err(gloo_storage::errors::StorageError::KeyNotFound(err)) => { + tracing::debug!("gloo error: {}", err); + Ok(None) + } + Err(e) => Err(SessionStoreError::Other( + format!("SessionStorage error: {}", e).into(), + )), + } + } + } + + fn save_auth_req_info( + &self, + auth_req_info: &AuthRequestData<'_>, + ) -> impl Future> { + async move { + use jacquard::IntoStatic; + + let key = Self::auth_req_key(&auth_req_info.state); + let static_info = auth_req_info.clone().into_static(); + + let value = serde_json::to_value(&static_info) + .map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?; + + LocalStorage::set(&key, &value).map_err(|e| { + SessionStoreError::Other(format!("SessionStorage error: {}", e).into()) + })?; + + Ok(()) + } + } + + fn delete_auth_req_info( + &self, + state: &str, + ) -> impl Future> { + let key = Self::auth_req_key(state); + async move { + LocalStorage::delete(&key); + Ok(()) + } + } +} + +#[cfg(target_arch = "wasm32")] +impl SessionStore for BrowserAuthStore { + fn get(&self, key: &SessionKey) -> impl Future> + Send { + let key = Self::session_key(&key.0, &key.1); + async move { + match LocalStorage::get::(&key) { + Ok(value) => { + let data: AtpSession = crate::from_json_value::(value).ok()?; + Some(data) + } + Err(gloo_storage::errors::StorageError::KeyNotFound(_)) => None, + Err(_) => None, + } + } + } + + fn set( + &self, + key: SessionKey, + session: AtpSession, + ) -> impl Future> + Send { + async move { + let key = Self::session_key(&key.0, &key.1); + + let value = serde_json::to_value(&session) + .map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?; + + LocalStorage::set(&key, &value).map_err(|e| { + SessionStoreError::Other(format!("LocalStorage error: {}", e).into()) + })?; + + Ok(()) + } + } + + fn del(&self, key: &SessionKey) -> impl Future> + Send { + let key = Self::session_key(&key.0, &key.1); + async move { + LocalStorage::delete(&key); + Ok(()) + } + } +} + +/// This might seem a little silly, and it sort of is, but the intended use here is for proxying requests to another client +#[cfg(target_arch = "wasm32")] +impl SessionStore for BrowserAuthStore { + fn get(&self, key: &SessionKey) -> impl Future> + Send { + let key = Self::session_key(&key.0, &key.1); + async move { + match LocalStorage::get::(&key) { + Ok(value) => { + let data: SessionKey = crate::from_json_value::(value).ok()?; + Some(data) + } + Err(gloo_storage::errors::StorageError::KeyNotFound(_)) => None, + Err(_) => None, + } + } + } + + fn set( + &self, + key: SessionKey, + session: SessionKey, + ) -> impl Future> + Send { + async move { + let key = Self::session_key(&key.0, &key.1); + + let value = serde_json::to_value(&session) + .map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?; + + LocalStorage::set(&key, &value).map_err(|e| { + SessionStoreError::Other(format!("LocalStorage error: {}", e).into()) + })?; + + Ok(()) + } + } + + fn del(&self, key: &SessionKey) -> impl Future> + Send { + let key = Self::session_key(&key.0, &key.1); + async move { + LocalStorage::delete(&key); + Ok(()) + } + } +} -- 2.51.2