+12
-5
crates/jacquard-identity/src/resolver.rs
+12
-5
crates/jacquard-identity/src/resolver.rs
···
228
228
let mut handle_order = vec![];
229
229
#[cfg(not(target_family = "wasm"))]
230
230
handle_order.push(HandleStep::DnsTxt);
231
+
#[cfg(not(target_family = "wasm"))]
231
232
handle_order.push(HandleStep::HttpsWellKnown);
232
233
handle_order.push(HandleStep::PdsResolveHandle);
234
+
#[cfg(target_family = "wasm")]
235
+
handle_order.push(HandleStep::HttpsWellKnown);
236
+
237
+
let mut did_order = vec![];
238
+
#[cfg(not(target_family = "wasm"))]
239
+
did_order.push(DidStep::DidWebHttps);
240
+
did_order.push(DidStep::PlcHttp);
241
+
did_order.push(DidStep::PdsResolveDid);
242
+
#[cfg(target_family = "wasm")]
243
+
did_order.push(DidStep::DidWebHttps);
233
244
234
245
Self::new()
235
246
.plc_source(PlcSource::default())
236
247
.handle_order(handle_order)
237
-
.did_order(vec![
238
-
DidStep::DidWebHttps,
239
-
DidStep::PlcHttp,
240
-
DidStep::PdsResolveDid,
241
-
])
248
+
.did_order(did_order)
242
249
.validate_doc_id(true)
243
250
.public_fallback_for_handle(true)
244
251
.build()
+59
crates/jacquard-oauth/src/atproto.rs
+59
crates/jacquard-oauth/src/atproto.rs
···
4
4
use crate::{keyset::Keyset, scopes::Scope};
5
5
use jacquard_common::CowStr;
6
6
use serde::{Deserialize, Serialize};
7
+
use smol_str::{SmolStr, ToSmolStr};
7
8
use thiserror::Error;
8
9
use url::Url;
9
10
···
85
86
#[serde(borrow)]
86
87
pub scopes: Vec<Scope<'m>>,
87
88
pub jwks_uri: Option<Url>,
89
+
pub client_name: Option<SmolStr>,
90
+
pub logo_uri: Option<Url>,
91
+
pub tos_uri: Option<Url>,
92
+
pub privacy_policy_uri: Option<Url>,
88
93
}
89
94
90
95
impl<'m> AtprotoClientMetadata<'m> {
···
103
108
grant_types,
104
109
scopes,
105
110
jwks_uri,
111
+
client_name: None,
112
+
logo_uri: None,
113
+
tos_uri: None,
114
+
privacy_policy_uri: None,
106
115
}
107
116
}
108
117
118
+
pub fn with_prod_info(
119
+
mut self,
120
+
client_name: &str,
121
+
logo_uri: Option<Url>,
122
+
tos_uri: Option<Url>,
123
+
privacy_policy_uri: Option<Url>,
124
+
) -> Self {
125
+
self.client_name = Some(client_name.to_smolstr());
126
+
self.logo_uri = logo_uri;
127
+
self.tos_uri = tos_uri;
128
+
self.privacy_policy_uri = privacy_policy_uri;
129
+
self
130
+
}
131
+
109
132
pub fn default_localhost() -> Self {
110
133
Self::new_localhost(
111
134
None,
···
155
178
grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken],
156
179
scopes: scopes.unwrap_or(vec![Scope::Atproto]),
157
180
jwks_uri: None,
181
+
client_name: None,
182
+
logo_uri: None,
183
+
tos_uri: None,
184
+
privacy_policy_uri: None,
158
185
}
159
186
}
160
187
}
···
208
235
} else {
209
236
None
210
237
},
238
+
client_name: metadata.client_name,
239
+
logo_uri: metadata.logo_uri,
240
+
tos_uri: metadata.tos_uri,
241
+
privacy_policy_uri: metadata.privacy_policy_uri,
211
242
})
212
243
}
213
244
···
247
278
jwks_uri: None,
248
279
jwks: None,
249
280
token_endpoint_auth_signing_alg: None,
281
+
tos_uri: None,
282
+
privacy_policy_uri: None,
283
+
client_name: None,
284
+
logo_uri: None,
250
285
}
251
286
);
252
287
}
···
285
320
jwks_uri: None,
286
321
jwks: None,
287
322
token_endpoint_auth_signing_alg: None,
323
+
tos_uri: None,
324
+
privacy_policy_uri: None,
325
+
client_name: None,
326
+
logo_uri: None,
288
327
}
289
328
);
290
329
}
···
317
356
jwks_uri: None,
318
357
jwks: None,
319
358
token_endpoint_auth_signing_alg: None,
359
+
tos_uri: None,
360
+
privacy_policy_uri: None,
361
+
client_name: None,
362
+
logo_uri: None,
320
363
}
321
364
);
322
365
}
···
345
388
jwks_uri: None,
346
389
jwks: None,
347
390
token_endpoint_auth_signing_alg: None,
391
+
tos_uri: None,
392
+
privacy_policy_uri: None,
393
+
client_name: None,
394
+
logo_uri: None,
348
395
}
349
396
);
350
397
}
···
373
420
jwks_uri: None,
374
421
jwks: None,
375
422
token_endpoint_auth_signing_alg: None,
423
+
tos_uri: None,
424
+
privacy_policy_uri: None,
425
+
client_name: None,
426
+
logo_uri: None,
376
427
}
377
428
);
378
429
}
···
387
438
grant_types: vec![GrantType::AuthorizationCode],
388
439
scopes: vec![Scope::Atproto],
389
440
jwks_uri: None,
441
+
client_name: None,
442
+
logo_uri: None,
443
+
tos_uri: None,
444
+
privacy_policy_uri: None,
390
445
};
391
446
{
392
447
// Non-loopback clients without a keyset should fail (must provide JWKS)
···
420
475
jwks_uri: None,
421
476
jwks: Some(keyset.public_jwks()),
422
477
token_endpoint_auth_signing_alg: Some(CowStr::new_static("ES256")),
478
+
client_name: None,
479
+
logo_uri: None,
480
+
tos_uri: None,
481
+
privacy_policy_uri: None,
423
482
}
424
483
);
425
484
}
+114
-4
crates/jacquard-oauth/src/client.rs
+114
-4
crates/jacquard-oauth/src/client.rs
···
39
39
S: ClientAuthStore,
40
40
{
41
41
pub registry: Arc<SessionRegistry<T, S>>,
42
+
pub options: RwLock<CallOptions<'static>>,
43
+
pub endpoint: RwLock<Option<Url>>,
42
44
pub client: Arc<T>,
43
45
}
44
46
···
106
108
redirect_uris = ?client_data.config.redirect_uris,
107
109
scopes = ?client_data.config.scopes,
108
110
has_keyset = client_data.keyset.is_some(),
109
-
"oauth client created"
111
+
"oauth client created:"
110
112
);
111
113
112
114
let client = Arc::new(client);
113
115
let registry = Arc::new(SessionRegistry::new(store, client.clone(), client_data));
114
-
Self { registry, client }
116
+
Self {
117
+
registry,
118
+
client,
119
+
options: RwLock::new(CallOptions::default()),
120
+
endpoint: RwLock::new(None),
121
+
}
115
122
}
116
123
117
124
pub fn new_with_shared(
···
124
131
client.clone(),
125
132
client_data,
126
133
));
127
-
Self { registry, client }
134
+
Self {
135
+
registry,
136
+
client,
137
+
options: RwLock::new(CallOptions::default()),
138
+
endpoint: RwLock::new(None),
139
+
}
128
140
}
129
141
}
130
142
···
151
163
self.registry.client_data.config.clone(),
152
164
&self.registry.client_data.keyset,
153
165
)?;
154
-
155
166
let (server_metadata, identity) = self.client.resolve_oauth(input.as_ref()).await?;
156
167
let login_hint = if identity.is_some() {
157
168
Some(input.as_ref().into())
···
163
174
client_metadata,
164
175
keyset: self.registry.client_data.keyset.clone(),
165
176
};
177
+
166
178
let auth_req_info =
167
179
par(self.client.as_ref(), login_hint, options.prompt, &metadata).await?;
180
+
168
181
// Persist state for callback handling
169
182
self.registry
170
183
.store
···
284
297
}
285
298
}
286
299
300
+
impl<T, S> HttpClient for OAuthClient<T, S>
301
+
where
302
+
S: ClientAuthStore + Send + Sync + 'static,
303
+
T: OAuthResolver + DpopExt + Send + Sync + 'static,
304
+
{
305
+
type Error = T::Error;
306
+
307
+
async fn send_http(
308
+
&self,
309
+
request: http::Request<Vec<u8>>,
310
+
) -> core::result::Result<http::Response<Vec<u8>>, Self::Error> {
311
+
self.client.send_http(request).await
312
+
}
313
+
}
314
+
315
+
impl<T, S> IdentityResolver for OAuthClient<T, S>
316
+
where
317
+
S: ClientAuthStore + Send + Sync + 'static,
318
+
T: OAuthResolver + DpopExt + Send + Sync + 'static,
319
+
{
320
+
fn options(&self) -> &ResolverOptions {
321
+
self.client.options()
322
+
}
323
+
324
+
async fn resolve_handle(
325
+
&self,
326
+
handle: &Handle<'_>,
327
+
) -> jacquard_identity::resolver::Result<Did<'static>> {
328
+
self.client.resolve_handle(handle).await
329
+
}
330
+
331
+
async fn resolve_did_doc(
332
+
&self,
333
+
did: &Did<'_>,
334
+
) -> jacquard_identity::resolver::Result<DidDocResponse> {
335
+
self.client.resolve_did_doc(did).await
336
+
}
337
+
}
338
+
339
+
impl<T, S> XrpcClient for OAuthClient<T, S>
340
+
where
341
+
S: ClientAuthStore + Send + Sync + 'static,
342
+
T: OAuthResolver + DpopExt + Send + Sync + 'static,
343
+
{
344
+
async fn base_uri(&self) -> Url {
345
+
self.endpoint.read().await.clone().unwrap_or(
346
+
Url::parse("https://public.api.bsky.app").expect("public appview should be valid url"),
347
+
)
348
+
}
349
+
350
+
async fn opts(&self) -> CallOptions<'_> {
351
+
self.options.read().await.clone()
352
+
}
353
+
354
+
async fn set_opts(&self, opts: CallOptions<'_>) {
355
+
let mut guard = self.options.write().await;
356
+
*guard = opts.into_static();
357
+
}
358
+
359
+
async fn set_base_uri(&self, url: Url) {
360
+
let mut guard = self.endpoint.write().await;
361
+
*guard = Some(url);
362
+
}
363
+
364
+
async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
365
+
where
366
+
R: XrpcRequest + Send + Sync,
367
+
<R as XrpcRequest>::Response: Send + Sync,
368
+
{
369
+
let opts = self.options.read().await.clone();
370
+
self.send_with_opts(request, opts).await
371
+
}
372
+
373
+
async fn send_with_opts<R>(
374
+
&self,
375
+
request: R,
376
+
opts: CallOptions<'_>,
377
+
) -> XrpcResult<XrpcResponse<R>>
378
+
where
379
+
R: XrpcRequest + Send + Sync,
380
+
<R as XrpcRequest>::Response: Send + Sync,
381
+
{
382
+
let base_uri = self.base_uri().await;
383
+
self.client
384
+
.xrpc(base_uri.clone())
385
+
.with_options(opts.clone())
386
+
.send(&request)
387
+
.await
388
+
}
389
+
}
390
+
287
391
pub struct OAuthSession<T, S, W = ()>
288
392
where
289
393
T: OAuthResolver,
···
377
481
.as_ref()
378
482
.map(|t| AuthorizationToken::Dpop(t.clone()))
379
483
}
484
+
485
+
pub fn to_client(&self) -> OAuthClient<T, S> {
486
+
OAuthClient::from_session(self)
487
+
}
380
488
}
381
489
impl<T, S, W> OAuthSession<T, S, W>
382
490
where
···
411
519
Self {
412
520
registry: session.registry.clone(),
413
521
client: session.client.clone(),
522
+
options: RwLock::new(CallOptions::default()),
523
+
endpoint: RwLock::new(None),
414
524
}
415
525
}
416
526
}
+10
crates/jacquard-oauth/src/request.rs
+10
crates/jacquard-oauth/src/request.rs
···
495
495
login_hint: login_hint,
496
496
prompt: prompt.map(CowStr::from),
497
497
};
498
+
499
+
#[cfg(feature = "tracing")]
500
+
tracing::debug!(
501
+
parameters = ?parameters,
502
+
"par:"
503
+
);
498
504
if metadata
499
505
.server_metadata
500
506
.pushed_authorization_request_endpoint
···
937
943
jwks_uri: None,
938
944
jwks: None,
939
945
token_endpoint_auth_signing_alg: None,
946
+
client_name: None,
947
+
privacy_policy_uri: None,
948
+
tos_uri: None,
949
+
logo_uri: None,
940
950
},
941
951
keyset: None,
942
952
}
+13
crates/jacquard-oauth/src/session.rs
+13
crates/jacquard-oauth/src/session.rs
···
237
237
pub config: AtprotoClientMetadata<'s>,
238
238
}
239
239
240
+
impl<'s> ClientData<'s> {
241
+
pub fn new(keyset: Option<Keyset>, config: AtprotoClientMetadata<'s>) -> Self {
242
+
Self { keyset, config }
243
+
}
244
+
245
+
pub fn new_public(config: AtprotoClientMetadata<'s>) -> Self {
246
+
Self {
247
+
keyset: None,
248
+
config,
249
+
}
250
+
}
251
+
}
252
+
240
253
pub struct ClientSession<'s> {
241
254
pub keyset: Option<Keyset>,
242
255
pub config: AtprotoClientMetadata<'s>,
+35
crates/jacquard-oauth/src/types.rs
+35
crates/jacquard-oauth/src/types.rs
···
12
12
pub use self::response::*;
13
13
pub use self::token::*;
14
14
use jacquard_common::CowStr;
15
+
use jacquard_common::IntoStatic;
15
16
use serde::Deserialize;
16
17
use url::Url;
17
18
···
53
54
}
54
55
}
55
56
57
+
impl<'s> AuthorizeOptions<'s> {
58
+
pub fn with_prompt(mut self, prompt: AuthorizeOptionPrompt) -> Self {
59
+
self.prompt = Some(prompt);
60
+
self
61
+
}
62
+
63
+
pub fn with_state(mut self, state: CowStr<'s>) -> Self {
64
+
self.state = Some(state);
65
+
self
66
+
}
67
+
68
+
pub fn with_redirect_uri(mut self, redirect_uri: Url) -> Self {
69
+
self.redirect_uri = Some(redirect_uri);
70
+
self
71
+
}
72
+
73
+
pub fn with_scopes(mut self, scopes: Vec<Scope<'s>>) -> Self {
74
+
self.scopes = scopes;
75
+
self
76
+
}
77
+
}
78
+
56
79
#[derive(Debug, Deserialize)]
57
80
pub struct CallbackParams<'s> {
58
81
#[serde(borrow)]
···
60
83
pub state: Option<CowStr<'s>>,
61
84
pub iss: Option<CowStr<'s>>,
62
85
}
86
+
87
+
impl IntoStatic for CallbackParams<'_> {
88
+
type Output = CallbackParams<'static>;
89
+
90
+
fn into_static(self) -> Self::Output {
91
+
CallbackParams {
92
+
code: self.code.into_static(),
93
+
state: self.state.map(|s| s.into_static()),
94
+
iss: self.iss.map(|s| s.into_static()),
95
+
}
96
+
}
97
+
}
+13
crates/jacquard-oauth/src/types/client_metadata.rs
+13
crates/jacquard-oauth/src/types/client_metadata.rs
···
1
1
use jacquard_common::{CowStr, IntoStatic};
2
2
use jose_jwk::JwkSet;
3
3
use serde::{Deserialize, Serialize};
4
+
use smol_str::SmolStr;
4
5
use url::Url;
5
6
6
7
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
···
27
28
// https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
28
29
#[serde(skip_serializing_if = "Option::is_none")]
29
30
pub token_endpoint_auth_signing_alg: Option<CowStr<'c>>,
31
+
#[serde(skip_serializing_if = "Option::is_none")]
32
+
pub client_name: Option<SmolStr>,
33
+
#[serde(skip_serializing_if = "Option::is_none")]
34
+
pub logo_uri: Option<Url>,
35
+
#[serde(skip_serializing_if = "Option::is_none")]
36
+
pub tos_uri: Option<Url>,
37
+
#[serde(skip_serializing_if = "Option::is_none")]
38
+
pub privacy_policy_uri: Option<Url>,
30
39
}
31
40
32
41
impl OAuthClientMetadata<'_> {}
···
50
59
token_endpoint_auth_signing_alg: self
51
60
.token_endpoint_auth_signing_alg
52
61
.map(|alg| alg.into_static()),
62
+
client_name: self.client_name,
63
+
logo_uri: self.logo_uri,
64
+
tos_uri: self.tos_uri,
65
+
privacy_policy_uri: self.privacy_policy_uri,
53
66
}
54
67
}
55
68
}
+93
-2
crates/jacquard/src/client.rs
+93
-2
crates/jacquard/src/client.rs
···
30
30
use core::future::Future;
31
31
pub use error::*;
32
32
#[cfg(feature = "api")]
33
+
use jacquard_api::com_atproto::repo::get_record::GetRecordOutput;
34
+
#[cfg(feature = "api")]
33
35
use jacquard_api::com_atproto::{
34
36
repo::{
35
37
create_record::CreateRecordOutput, delete_record::DeleteRecordOutput,
···
47
49
use jacquard_common::types::string::AtUri;
48
50
#[cfg(feature = "api")]
49
51
use jacquard_common::types::uri::RecordUri;
50
-
#[cfg(not(target_arch = "wasm32"))]
51
52
use jacquard_common::xrpc::XrpcResponse;
52
53
use jacquard_common::xrpc::{
53
54
CallOptions, Response, XrpcClient, XrpcError, XrpcExt, XrpcRequest, XrpcResp,
···
62
63
};
63
64
use jacquard_identity::{JacquardResolver, slingshot_resolver_default};
64
65
use jacquard_oauth::authstore::ClientAuthStore;
65
-
use jacquard_oauth::client::OAuthSession;
66
+
use jacquard_oauth::client::{OAuthClient, OAuthSession};
66
67
use jacquard_oauth::dpop::DpopExt;
67
68
use jacquard_oauth::resolver::OAuthResolver;
68
69
use serde::Serialize;
···
506
507
Self { inner }
507
508
}
508
509
510
+
pub fn inner(&self) -> &A {
511
+
&self.inner
512
+
}
513
+
509
514
/// Return the underlying session kind.
510
515
pub fn kind(&self) -> AgentKind {
511
516
self.inner.session_kind()
···
760
765
}
761
766
}
762
767
768
+
/// Untyped, freeform record fetcher.
769
+
/// Hits [https://slingshot.microcosm.blue]
770
+
fn fetch_record_slingshot(
771
+
&self,
772
+
uri: &AtUri<'_>,
773
+
) -> impl Future<Output = Result<GetRecordOutput<'static>>> {
774
+
async move {
775
+
#[cfg(feature = "tracing")]
776
+
let _span = tracing::debug_span!("fetch_record_slingshot", uri = %uri).entered();
777
+
778
+
// Make stateless XRPC call to that PDS (no auth required for public records)
779
+
use jacquard_api::com_atproto::repo::get_record::GetRecord;
780
+
let collection = uri.collection().clone().ok_or(AgentError::sub_operation(
781
+
"no collection",
782
+
ClientError::invalid_request("no collection"),
783
+
))?;
784
+
let rkey = uri.rkey().ok_or(AgentError::sub_operation(
785
+
"no rkey",
786
+
ClientError::invalid_request("no rkey"),
787
+
))?;
788
+
let request = GetRecord::new()
789
+
.repo(uri.authority().clone())
790
+
.collection(collection.clone())
791
+
.rkey(rkey.clone())
792
+
.build();
793
+
794
+
let response: Response<GetRecordResponse> = {
795
+
use url::Url;
796
+
797
+
let http_request = xrpc::build_http_request(
798
+
&Url::parse("https://slingshot.microcosm.blue")
799
+
.expect("slingshot url is valid"),
800
+
&request,
801
+
&self.opts().await,
802
+
)?;
803
+
804
+
let http_response = self
805
+
.send_http(http_request)
806
+
.await
807
+
.map_err(|e| ClientError::transport(e))?;
808
+
809
+
xrpc::process_response(http_response)
810
+
}?;
811
+
let output = response.into_output().map_err(|e| match e {
812
+
XrpcError::Auth(auth) => AgentError::from(auth),
813
+
e @ (XrpcError::Generic(_) | XrpcError::Decode(_)) => AgentError::xrpc(e),
814
+
XrpcError::Xrpc(typed) => AgentError::new(
815
+
AgentErrorKind::SubOperation {
816
+
step: "fetch record",
817
+
},
818
+
None,
819
+
)
820
+
.with_details(typed.to_string()),
821
+
})?;
822
+
Ok(output)
823
+
}
824
+
}
825
+
763
826
/// Fetches a record from the PDS. Returns an owned, parsed response.
764
827
///
765
828
/// Takes an at:// URI annotated with the collection type, which be constructed with `R::uri(uri)`
···
1165
1228
.await
1166
1229
.map(|t| t.into_static())
1167
1230
.map_err(|e| ClientError::transport(e).with_context("OAuth token refresh failed"))
1231
+
}
1232
+
}
1233
+
}
1234
+
1235
+
impl<T, S> AgentSession for OAuthClient<T, S>
1236
+
where
1237
+
S: ClientAuthStore + Send + Sync + 'static,
1238
+
T: OAuthResolver + DpopExt + Send + Sync + 'static,
1239
+
{
1240
+
fn session_kind(&self) -> AgentKind {
1241
+
AgentKind::OAuth
1242
+
}
1243
+
fn session_info(
1244
+
&self,
1245
+
) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> {
1246
+
async { None }
1247
+
}
1248
+
fn endpoint(&self) -> impl Future<Output = url::Url> {
1249
+
async { self.base_uri().await }
1250
+
}
1251
+
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> {
1252
+
async { self.set_opts(opts).await }
1253
+
}
1254
+
fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<'static>>> {
1255
+
async {
1256
+
Err(ClientError::auth(
1257
+
jacquard_common::error::AuthError::NotAuthenticated,
1258
+
))
1168
1259
}
1169
1260
}
1170
1261
}