···11//! Error types for XRPC client operations
2233-use crate::types::xrpc::EncodeError;
33+use crate::xrpc::EncodeError;
44use bytes::Bytes;
5566/// Client error type wrapping all possible error conditions
+2
crates/jacquard-common/src/lib.rs
···2121pub mod session;
2222/// Baseline fundamental AT Protocol data types.
2323pub mod types;
2424+/// XRPC protocol types and traits
2525+pub mod xrpc;
24262527/// Authorization token types for XRPC requests.
2628#[derive(Debug, Clone)]
+2-4
crates/jacquard-common/src/types.rs
···88pub mod cid;
99/// Repository collection trait for records
1010pub mod collection;
1111+/// Crypto helpers for keys (Multikey decoding, conversions)
1212+pub mod crypto;
1113/// AT Protocol datetime string type
1214pub mod datetime;
1315/// Decentralized Identifier (DID) types and validation
1416pub mod did;
1517/// DID Document types and helpers
1618pub mod did_doc;
1717-/// Crypto helpers for keys (Multikey decoding, conversions)
1818-pub mod crypto;
1919/// AT Protocol handle types and validation
2020pub mod handle;
2121/// AT Protocol identifier types (handle or DID)
···3838pub mod uri;
3939/// Generic data value types for lexicon data model
4040pub mod value;
4141-/// XRPC protocol types and traits
4242-pub mod xrpc;
43414442/// Trait for a constant string literal type
4543pub trait Literal: Clone + Copy + PartialEq + Eq + Send + Sync + 'static {
···590590 }
591591592592 // IdentityResolver methods won't be called in these tests; provide stubs.
593593- #[async_trait::async_trait]
594593 impl IdentityResolver for MockClient {
595594 fn options(&self) -> &jacquard_identity::resolver::ResolverOptions {
596595 use std::sync::LazyLock;
+113-96
crates/jacquard-oauth/src/resolver.rs
···115115 Uri(#[from] url::ParseError),
116116}
117117118118-#[async_trait::async_trait]
119118pub trait OAuthResolver: IdentityResolver + HttpClient {
120120- async fn verify_issuer(
119119+ fn verify_issuer(
121120 &self,
122121 server_metadata: &OAuthAuthorizationServerMetadata<'_>,
123122 sub: &Did<'_>,
124124- ) -> Result<Url, ResolverError> {
125125- let (metadata, identity) = self.resolve_from_identity(sub).await?;
126126- if !issuer_equivalent(&metadata.issuer, &server_metadata.issuer) {
127127- return Err(ResolverError::AuthorizationServerMetadata(
128128- "issuer mismatch".to_string(),
129129- ));
123123+ ) -> impl std::future::Future<Output = Result<Url, ResolverError>> {
124124+ async {
125125+ let (metadata, identity) = self.resolve_from_identity(sub).await?;
126126+ if !issuer_equivalent(&metadata.issuer, &server_metadata.issuer) {
127127+ return Err(ResolverError::AuthorizationServerMetadata(
128128+ "issuer mismatch".to_string(),
129129+ ));
130130+ }
131131+ Ok(identity
132132+ .pds_endpoint()
133133+ .ok_or(ResolverError::DidDocument(format!("{:?}", identity).into()))?)
130134 }
131131- Ok(identity
132132- .pds_endpoint()
133133- .ok_or(ResolverError::DidDocument(format!("{:?}", identity).into()))?)
134135 }
135135- async fn resolve_oauth(
136136+ fn resolve_oauth(
136137 &self,
137138 input: &str,
138138- ) -> Result<
139139- (
140140- OAuthAuthorizationServerMetadata<'static>,
141141- Option<DidDocument<'static>>,
142142- ),
143143- ResolverError,
139139+ ) -> impl Future<
140140+ Output = Result<
141141+ (
142142+ OAuthAuthorizationServerMetadata<'static>,
143143+ Option<DidDocument<'static>>,
144144+ ),
145145+ ResolverError,
146146+ >,
144147 > {
145148 // Allow using an entryway, or PDS url, directly as login input (e.g.
146149 // when the user forgot their handle, or when the handle does not
147150 // resolve to a DID)
148148- Ok(if input.starts_with("https://") {
149149- let url = Url::parse(input).map_err(|_| ResolverError::NotFound)?;
150150- (self.resolve_from_service(&url).await?, None)
151151- } else {
152152- let (metadata, identity) = self.resolve_from_identity(input).await?;
153153- (metadata, Some(identity))
154154- })
151151+ async {
152152+ Ok(if input.starts_with("https://") {
153153+ let url = Url::parse(input).map_err(|_| ResolverError::NotFound)?;
154154+ (self.resolve_from_service(&url).await?, None)
155155+ } else {
156156+ let (metadata, identity) = self.resolve_from_identity(input).await?;
157157+ (metadata, Some(identity))
158158+ })
159159+ }
155160 }
156156- async fn resolve_from_service(
161161+ fn resolve_from_service(
157162 &self,
158163 input: &Url,
159159- ) -> Result<OAuthAuthorizationServerMetadata<'static>, ResolverError> {
160160- // Assume first that input is a PDS URL (as required by ATPROTO)
161161- if let Ok(metadata) = self.get_resource_server_metadata(input).await {
162162- return Ok(metadata);
164164+ ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>, ResolverError>>
165165+ {
166166+ async {
167167+ // Assume first that input is a PDS URL (as required by ATPROTO)
168168+ if let Ok(metadata) = self.get_resource_server_metadata(input).await {
169169+ return Ok(metadata);
170170+ }
171171+ // Fallback to trying to fetch as an issuer (Entryway)
172172+ self.get_authorization_server_metadata(input).await
163173 }
164164- // Fallback to trying to fetch as an issuer (Entryway)
165165- self.get_authorization_server_metadata(input).await
166174 }
167167- async fn resolve_from_identity(
175175+ fn resolve_from_identity(
168176 &self,
169177 input: &str,
170170- ) -> Result<
171171- (
172172- OAuthAuthorizationServerMetadata<'static>,
173173- DidDocument<'static>,
174174- ),
175175- ResolverError,
178178+ ) -> impl Future<
179179+ Output = Result<
180180+ (
181181+ OAuthAuthorizationServerMetadata<'static>,
182182+ DidDocument<'static>,
183183+ ),
184184+ ResolverError,
185185+ >,
176186 > {
177177- let actor = AtIdentifier::new(input)
178178- .map_err(|e| ResolverError::AtIdentifier(format!("{:?}", e)))?;
179179- let identity = self.resolve_ident_owned(&actor).await?;
180180- if let Some(pds) = &identity.pds_endpoint() {
181181- let metadata = self.get_resource_server_metadata(pds).await?;
182182- Ok((metadata, identity))
183183- } else {
184184- Err(ResolverError::DidDocument(format!("Did doc lacking pds")))
187187+ async {
188188+ let actor = AtIdentifier::new(input)
189189+ .map_err(|e| ResolverError::AtIdentifier(format!("{:?}", e)))?;
190190+ let identity = self.resolve_ident_owned(&actor).await?;
191191+ if let Some(pds) = &identity.pds_endpoint() {
192192+ let metadata = self.get_resource_server_metadata(pds).await?;
193193+ Ok((metadata, identity))
194194+ } else {
195195+ Err(ResolverError::DidDocument(format!("Did doc lacking pds")))
196196+ }
185197 }
186198 }
187187- async fn get_authorization_server_metadata(
199199+ fn get_authorization_server_metadata(
188200 &self,
189201 issuer: &Url,
190190- ) -> Result<OAuthAuthorizationServerMetadata<'static>, ResolverError> {
191191- let mut md = resolve_authorization_server(self, issuer).await?;
192192- // Normalize issuer string to the input URL representation to avoid slash quirks
193193- md.issuer = jacquard_common::CowStr::from(issuer.as_str()).into_static();
194194- Ok(md)
202202+ ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>, ResolverError>>
203203+ {
204204+ async {
205205+ let mut md = resolve_authorization_server(self, issuer).await?;
206206+ // Normalize issuer string to the input URL representation to avoid slash quirks
207207+ md.issuer = jacquard_common::CowStr::from(issuer.as_str()).into_static();
208208+ Ok(md)
209209+ }
195210 }
196196- async fn get_resource_server_metadata(
211211+ fn get_resource_server_metadata(
197212 &self,
198213 pds: &Url,
199199- ) -> Result<OAuthAuthorizationServerMetadata<'static>, ResolverError> {
200200- let rs_metadata = resolve_protected_resource_info(self, pds).await?;
201201- // ATPROTO requires one, and only one, authorization server entry
202202- // > That document MUST contain a single item in the authorization_servers array.
203203- // https://github.com/bluesky-social/proposals/tree/main/0004-oauth#server-metadata
204204- let issuer = match &rs_metadata.authorization_servers {
205205- Some(servers) if !servers.is_empty() => {
206206- if servers.len() > 1 {
214214+ ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>, ResolverError>>
215215+ {
216216+ async move {
217217+ let rs_metadata = resolve_protected_resource_info(self, pds).await?;
218218+ // ATPROTO requires one, and only one, authorization server entry
219219+ // > That document MUST contain a single item in the authorization_servers array.
220220+ // https://github.com/bluesky-social/proposals/tree/main/0004-oauth#server-metadata
221221+ let issuer = match &rs_metadata.authorization_servers {
222222+ Some(servers) if !servers.is_empty() => {
223223+ if servers.len() > 1 {
224224+ return Err(ResolverError::ProtectedResourceMetadata(format!(
225225+ "unable to determine authorization server for PDS: {pds}"
226226+ )));
227227+ }
228228+ &servers[0]
229229+ }
230230+ _ => {
207231 return Err(ResolverError::ProtectedResourceMetadata(format!(
208208- "unable to determine authorization server for PDS: {pds}"
232232+ "no authorization server found for PDS: {pds}"
233233+ )));
234234+ }
235235+ };
236236+ let as_metadata = self.get_authorization_server_metadata(issuer).await?;
237237+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#name-authorization-server-metada
238238+ if let Some(protected_resources) = &as_metadata.protected_resources {
239239+ let resource_url = rs_metadata
240240+ .resource
241241+ .strip_suffix('/')
242242+ .unwrap_or(rs_metadata.resource.as_str());
243243+ if !protected_resources.contains(&CowStr::Borrowed(resource_url)) {
244244+ return Err(ResolverError::AuthorizationServerMetadata(format!(
245245+ "pds {pds}, resource {0} not protected by issuer: {issuer}, protected resources: {1:?}",
246246+ rs_metadata.resource, protected_resources
209247 )));
210248 }
211211- &servers[0]
212212- }
213213- _ => {
214214- return Err(ResolverError::ProtectedResourceMetadata(format!(
215215- "no authorization server found for PDS: {pds}"
216216- )));
217249 }
218218- };
219219- let as_metadata = self.get_authorization_server_metadata(issuer).await?;
220220- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-08#name-authorization-server-metada
221221- if let Some(protected_resources) = &as_metadata.protected_resources {
222222- let resource_url = rs_metadata
223223- .resource
224224- .strip_suffix('/')
225225- .unwrap_or(rs_metadata.resource.as_str());
226226- if !protected_resources.contains(&CowStr::Borrowed(resource_url)) {
227227- return Err(ResolverError::AuthorizationServerMetadata(format!(
228228- "pds {pds}, resource {0} not protected by issuer: {issuer}, protected resources: {1:?}",
229229- rs_metadata.resource, protected_resources
230230- )));
231231- }
232232- }
233250234234- // TODO: atproot specific validation?
235235- // https://github.com/bluesky-social/proposals/tree/main/0004-oauth#server-metadata
236236- //
237237- // eg.
238238- // https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html
239239- // if as_metadata.client_id_metadata_document_supported != Some(true) {
240240- // return Err(Error::AuthorizationServerMetadata(format!(
241241- // "authorization server does not support client_id_metadata_document: {issuer}"
242242- // )));
243243- // }
251251+ // TODO: atproot specific validation?
252252+ // https://github.com/bluesky-social/proposals/tree/main/0004-oauth#server-metadata
253253+ //
254254+ // eg.
255255+ // https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html
256256+ // if as_metadata.client_id_metadata_document_supported != Some(true) {
257257+ // return Err(Error::AuthorizationServerMetadata(format!(
258258+ // "authorization server does not support client_id_metadata_document: {issuer}"
259259+ // )));
260260+ // }
244261245245- Ok(as_metadata)
262262+ Ok(as_metadata)
263263+ }
246264 }
247265}
248266···316334 }
317335}
318336319319-#[async_trait::async_trait]
320337impl OAuthResolver for jacquard_identity::JacquardResolver {}
321338322339#[cfg(test)]