this repo has no description

bugfix with scopes in loopback flow

Orual d853091d afc9b394

Changed files
+294 -15
crates
jacquard
jacquard-oauth
+29
Cargo.lock
··· 1644 1644 checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 1645 1645 1646 1646 [[package]] 1647 + name = "gloo-storage" 1648 + version = "0.3.0" 1649 + source = "registry+https://github.com/rust-lang/crates.io-index" 1650 + checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" 1651 + dependencies = [ 1652 + "gloo-utils", 1653 + "js-sys", 1654 + "serde", 1655 + "serde_json", 1656 + "thiserror 1.0.69", 1657 + "wasm-bindgen", 1658 + "web-sys", 1659 + ] 1660 + 1661 + [[package]] 1662 + name = "gloo-utils" 1663 + version = "0.2.0" 1664 + source = "registry+https://github.com/rust-lang/crates.io-index" 1665 + checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" 1666 + dependencies = [ 1667 + "js-sys", 1668 + "serde", 1669 + "serde_json", 1670 + "wasm-bindgen", 1671 + "web-sys", 1672 + ] 1673 + 1674 + [[package]] 1647 1675 name = "group" 1648 1676 version = "0.13.0" 1649 1677 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2239 2267 "bytes", 2240 2268 "clap", 2241 2269 "getrandom 0.2.16", 2270 + "gloo-storage", 2242 2271 "http", 2243 2272 "image", 2244 2273 "jacquard-api",
+4 -13
crates/jacquard-oauth/src/loopback.rs
··· 1 1 #![cfg(feature = "loopback")] 2 2 3 3 use crate::{ 4 - atproto::AtprotoClientMetadata, 5 4 authstore::ClientAuthStore, 6 5 client::OAuthClient, 7 6 dpop::DpopExt, 8 7 error::{CallbackError, OAuthError}, 9 8 resolver::OAuthResolver, 10 - scopes::Scope, 11 9 types::{AuthorizeOptions, CallbackParams}, 12 10 }; 13 11 use jacquard_common::{IntoStatic, cowstr::ToCowStr}; ··· 122 120 local_addr.port(), 123 121 )) 124 122 .unwrap(); 125 - let client_data = crate::session::ClientData { 126 - keyset: self.registry.client_data.keyset.clone(), 127 - config: AtprotoClientMetadata::new_localhost( 128 - Some(vec![redirect.clone()]), 129 - Some(vec![ 130 - Scope::Atproto, 131 - Scope::Transition(crate::scopes::TransitionScope::Generic), 132 - ]), 133 - ), 134 - }; 135 123 124 + let mut client_data = self.registry.client_data.clone(); 125 + // Ensure the redirect URI is set correctly for the loopback server 126 + client_data.config.redirect_uris = vec![redirect]; 136 127 // Build client using store and resolver 137 128 let flow_client = OAuthClient::new_with_shared( 138 129 self.registry.store.clone(), 139 130 self.client.clone(), 140 - client_data.clone(), 131 + client_data, 141 132 ); 142 133 143 134 // Start auth and get authorization URL
+4 -1
crates/jacquard/Cargo.toml
··· 157 157 jose-jwk = { workspace = true, features = ["p256"] } 158 158 tracing = { workspace = true, optional = true } 159 159 n0-future = { workspace = true, optional = true } 160 - 160 + gloo-storage = "0.3" 161 161 162 162 [target.'cfg(not(target_family = "wasm"))'.dependencies] 163 163 jacquard-identity = { version = "0.9", path = "../jacquard-identity", features = ["cache"] } ··· 170 170 171 171 [target.'cfg(target_family = "wasm")'.dependencies] 172 172 getrandom = { version = "0.2", features = ["js"] } 173 + 174 + [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] 175 + gloo-storage = "0.3" 173 176 174 177 [dev-dependencies] 175 178 clap.workspace = true
+16 -1
crates/jacquard/src/client.rs
··· 16 16 //! - [`token`] - Token storage and persistence 17 17 //! - [`vec_update`] - Trait for fetch-modify-put patterns on array endpoints 18 18 19 + //pub mod bff_session; 19 20 /// App-password session implementation with auto-refresh 20 21 pub mod credential_session; 21 22 /// Agent error type ··· 460 461 /// App password session information from `com.atproto.server.createSession` 461 462 /// 462 463 /// Contains the access and refresh tokens along with user identity information. 463 - #[derive(Debug, Clone)] 464 + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 464 465 pub struct AtpSession { 465 466 /// Access token (JWT) used for authenticated requests 467 + #[serde(borrow)] 466 468 pub access_jwt: CowStr<'static>, 467 469 /// Refresh token (JWT) used to obtain new access tokens 468 470 pub refresh_jwt: CowStr<'static>, ··· 470 472 pub did: Did<'static>, 471 473 /// User's handle (e.g., "alice.bsky.social") 472 474 pub handle: Handle<'static>, 475 + } 476 + 477 + impl IntoStatic for AtpSession { 478 + type Output = Self; 479 + 480 + fn into_static(self) -> Self { 481 + Self { 482 + access_jwt: self.access_jwt.into_static(), 483 + refresh_jwt: self.refresh_jwt.into_static(), 484 + did: self.did.into_static(), 485 + handle: self.handle.into_static(), 486 + } 487 + } 473 488 } 474 489 475 490 #[cfg(feature = "api")]
+241
crates/jacquard/src/client/bff_session.rs
··· 1 + //! Session implementation for front-end clients that proxy to a dedicated backend 2 + //! 3 + 4 + //#[cfg(target_arch = "wasm32")] 5 + use crate::client::SessionStoreError; 6 + //#[cfg(target_arch = "wasm32")] 7 + use crate::client::{AtpSession, credential_session::SessionKey}; 8 + //#[cfg(target_arch = "wasm32")] 9 + use gloo_storage::{LocalStorage, SessionStorage, Storage}; 10 + //#[cfg(target_arch = "wasm32")] 11 + use jacquard_common::{session::SessionStore, types::string::Did}; 12 + #[cfg(target_arch = "wasm32")] 13 + use jacquard_oauth::authstore::ClientAuthStore; 14 + #[cfg(target_arch = "wasm32")] 15 + use jacquard_oauth::session::{AuthRequestData, ClientSessionData}; 16 + //#[cfg(target_arch = "wasm32")] 17 + use std::future::Future; 18 + 19 + //#[cfg(target_arch = "wasm32")] 20 + #[derive(Clone)] 21 + pub struct BrowserAuthStore; 22 + 23 + //#[cfg(target_arch = "wasm32")] 24 + impl BrowserAuthStore { 25 + pub fn new() -> Self { 26 + Self 27 + } 28 + 29 + fn session_key(did: &Did<'_>, session_id: &str) -> String { 30 + format!("session_{}_{}", did.as_ref(), session_id) 31 + } 32 + 33 + fn auth_req_key(state: &str) -> String { 34 + format!("auth_req_{}", state) 35 + } 36 + } 37 + 38 + #[cfg(target_arch = "wasm32")] 39 + impl ClientAuthStore for BrowserAuthStore { 40 + fn get_session( 41 + &self, 42 + did: &Did<'_>, 43 + session_id: &str, 44 + ) -> impl Future<Output = Result<Option<ClientSessionData<'_>>, SessionStoreError>> { 45 + let key = Self::session_key(did, session_id); 46 + async move { 47 + match LocalStorage::get::<serde_json::Value>(&key) { 48 + Ok(value) => { 49 + let data: ClientSessionData<'static> = 50 + jacquard::from_json_value::<ClientSessionData>(value).map_err(|e| { 51 + SessionStoreError::Other(format!("Deserialize error: {}", e).into()) 52 + })?; 53 + Ok(Some(data)) 54 + } 55 + Err(gloo_storage::errors::StorageError::KeyNotFound(_)) => Ok(None), 56 + Err(e) => Err(SessionStoreError::Other( 57 + format!("LocalStorage error: {}", e).into(), 58 + )), 59 + } 60 + } 61 + } 62 + 63 + fn upsert_session( 64 + &self, 65 + session: ClientSessionData<'_>, 66 + ) -> impl Future<Output = Result<(), SessionStoreError>> { 67 + async move { 68 + use jacquard::IntoStatic; 69 + 70 + let key = Self::session_key(&session.account_did, &session.session_id); 71 + let static_session = session.into_static(); 72 + 73 + let value = serde_json::to_value(&static_session) 74 + .map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?; 75 + 76 + LocalStorage::set(&key, &value).map_err(|e| { 77 + SessionStoreError::Other(format!("LocalStorage error: {}", e).into()) 78 + })?; 79 + 80 + Ok(()) 81 + } 82 + } 83 + 84 + fn delete_session( 85 + &self, 86 + did: &Did<'_>, 87 + session_id: &str, 88 + ) -> impl Future<Output = Result<(), SessionStoreError>> { 89 + let key = Self::session_key(did, session_id); 90 + async move { 91 + LocalStorage::delete(&key); 92 + Ok(()) 93 + } 94 + } 95 + 96 + fn get_auth_req_info( 97 + &self, 98 + state: &str, 99 + ) -> impl Future<Output = Result<Option<AuthRequestData<'_>>, SessionStoreError>> { 100 + let key = Self::auth_req_key(state); 101 + async move { 102 + match LocalStorage::get::<serde_json::Value>(&key) { 103 + Ok(value) => { 104 + let data: AuthRequestData<'static> = 105 + jacquard::from_json_value::<AuthRequestData>(value).map_err(|e| { 106 + SessionStoreError::Other(format!("Deserialize error: {}", e).into()) 107 + })?; 108 + Ok(Some(data)) 109 + } 110 + Err(gloo_storage::errors::StorageError::KeyNotFound(err)) => { 111 + tracing::debug!("gloo error: {}", err); 112 + Ok(None) 113 + } 114 + Err(e) => Err(SessionStoreError::Other( 115 + format!("SessionStorage error: {}", e).into(), 116 + )), 117 + } 118 + } 119 + } 120 + 121 + fn save_auth_req_info( 122 + &self, 123 + auth_req_info: &AuthRequestData<'_>, 124 + ) -> impl Future<Output = Result<(), SessionStoreError>> { 125 + async move { 126 + use jacquard::IntoStatic; 127 + 128 + let key = Self::auth_req_key(&auth_req_info.state); 129 + let static_info = auth_req_info.clone().into_static(); 130 + 131 + let value = serde_json::to_value(&static_info) 132 + .map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?; 133 + 134 + LocalStorage::set(&key, &value).map_err(|e| { 135 + SessionStoreError::Other(format!("SessionStorage error: {}", e).into()) 136 + })?; 137 + 138 + Ok(()) 139 + } 140 + } 141 + 142 + fn delete_auth_req_info( 143 + &self, 144 + state: &str, 145 + ) -> impl Future<Output = Result<(), SessionStoreError>> { 146 + let key = Self::auth_req_key(state); 147 + async move { 148 + LocalStorage::delete(&key); 149 + Ok(()) 150 + } 151 + } 152 + } 153 + 154 + #[cfg(target_arch = "wasm32")] 155 + impl SessionStore<SessionKey, AtpSession> for BrowserAuthStore { 156 + fn get(&self, key: &SessionKey) -> impl Future<Output = Option<AtpSession>> + Send { 157 + let key = Self::session_key(&key.0, &key.1); 158 + async move { 159 + match LocalStorage::get::<serde_json::Value>(&key) { 160 + Ok(value) => { 161 + let data: AtpSession = crate::from_json_value::<AtpSession>(value).ok()?; 162 + Some(data) 163 + } 164 + Err(gloo_storage::errors::StorageError::KeyNotFound(_)) => None, 165 + Err(_) => None, 166 + } 167 + } 168 + } 169 + 170 + fn set( 171 + &self, 172 + key: SessionKey, 173 + session: AtpSession, 174 + ) -> impl Future<Output = Result<(), SessionStoreError>> + Send { 175 + async move { 176 + let key = Self::session_key(&key.0, &key.1); 177 + 178 + let value = serde_json::to_value(&session) 179 + .map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?; 180 + 181 + LocalStorage::set(&key, &value).map_err(|e| { 182 + SessionStoreError::Other(format!("LocalStorage error: {}", e).into()) 183 + })?; 184 + 185 + Ok(()) 186 + } 187 + } 188 + 189 + fn del(&self, key: &SessionKey) -> impl Future<Output = Result<(), SessionStoreError>> + Send { 190 + let key = Self::session_key(&key.0, &key.1); 191 + async move { 192 + LocalStorage::delete(&key); 193 + Ok(()) 194 + } 195 + } 196 + } 197 + 198 + /// This might seem a little silly, and it sort of is, but the intended use here is for proxying requests to another client 199 + #[cfg(target_arch = "wasm32")] 200 + impl SessionStore<SessionKey, SessionKey> for BrowserAuthStore { 201 + fn get(&self, key: &SessionKey) -> impl Future<Output = Option<SessionKey>> + Send { 202 + let key = Self::session_key(&key.0, &key.1); 203 + async move { 204 + match LocalStorage::get::<serde_json::Value>(&key) { 205 + Ok(value) => { 206 + let data: SessionKey = crate::from_json_value::<SessionKey>(value).ok()?; 207 + Some(data) 208 + } 209 + Err(gloo_storage::errors::StorageError::KeyNotFound(_)) => None, 210 + Err(_) => None, 211 + } 212 + } 213 + } 214 + 215 + fn set( 216 + &self, 217 + key: SessionKey, 218 + session: SessionKey, 219 + ) -> impl Future<Output = Result<(), SessionStoreError>> + Send { 220 + async move { 221 + let key = Self::session_key(&key.0, &key.1); 222 + 223 + let value = serde_json::to_value(&session) 224 + .map_err(|e| SessionStoreError::Other(format!("Serialize error: {}", e).into()))?; 225 + 226 + LocalStorage::set(&key, &value).map_err(|e| { 227 + SessionStoreError::Other(format!("LocalStorage error: {}", e).into()) 228 + })?; 229 + 230 + Ok(()) 231 + } 232 + } 233 + 234 + fn del(&self, key: &SessionKey) -> impl Future<Output = Result<(), SessionStoreError>> + Send { 235 + let key = Self::session_key(&key.0, &key.1); 236 + async move { 237 + LocalStorage::delete(&key); 238 + Ok(()) 239 + } 240 + } 241 + }