A better Rust ATProto crate

import cleanup and fixing some client metadata failures due to adjusting behaviour earlier

Orual f934cf26 40c7d5ab

Changed files
+30 -39
crates
jacquard-common
src
jacquard-oauth
+1 -1
crates/jacquard-common/src/error.rs
···
RefreshFailed,
/// Request requires authentication but none was provided
-
#[error("No authentication provided")]
+
#[error("No authentication provided, but endpoint requires auth")]
NotAuthenticated,
/// Other authentication error
+18 -17
crates/jacquard-oauth/src/atproto.rs
···
Url::from_str("http://127.0.0.1/").unwrap(),
Url::from_str("http://[::1]/").unwrap(),
],
-
scope: None,
+
scope: Some(CowStr::new_static("atproto")),
grant_types: None,
token_endpoint_auth_method: Some(AuthMethod::None.into()),
dpop_bound_access_tokens: None,
···
.expect("failed to convert metadata"),
OAuthClientMetadata {
client_id: Url::from_str(
-
"http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&scope=account%3Aemail+atproto+transition%3Ageneric"
+
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&scope=account%3Aemail+atproto+transition%3Ageneric"
).unwrap(),
client_uri: None,
redirect_uris: vec![
-
Url::from_str("http://localhost/callback").unwrap(),
-
Url::from_str("http://localhost/callback").unwrap(),
+
Url::from_str("http://127.0.0.1/callback").unwrap(),
+
// TODO: fix this so that it respects IPv6
+
Url::from_str("http://127.0.0.1/callback").unwrap(),
],
-
scope: None,
+
scope: Some(CowStr::new_static("account:email atproto transition:generic")),
grant_types: None,
token_endpoint_auth_method: Some(AuthMethod::None.into()),
dpop_bound_access_tokens: None,
···
),
&None,
)
-
.expect("should coerce to localhost");
+
.expect("should coerce to 127.0.0.1");
assert_eq!(
out,
OAuthClientMetadata {
client_id: Url::from_str(
-
"http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2F"
+
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F"
)
.unwrap(),
client_uri: None,
-
redirect_uris: vec![Url::from_str("http://localhost/").unwrap()],
-
scope: None,
+
redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()],
+
scope: Some(CowStr::new_static("atproto")),
grant_types: None,
token_endpoint_auth_method: Some(AuthMethod::None.into()),
dpop_bound_access_tokens: None,
···
),
&None,
)
-
.expect("should coerce to localhost");
+
.expect("should coerce to 127.0.0.1");
assert_eq!(
out,
OAuthClientMetadata {
client_id: Url::from_str(
-
"http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2F"
+
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2F"
)
.unwrap(),
client_uri: None,
-
redirect_uris: vec![Url::from_str("http://localhost/").unwrap()],
-
scope: None,
+
redirect_uris: vec![Url::from_str("http://127.0.0.1:8000/").unwrap()],
+
scope: Some(CowStr::new_static("atproto")),
grant_types: None,
token_endpoint_auth_method: Some(AuthMethod::None.into()),
dpop_bound_access_tokens: None,
···
),
&None,
)
-
.expect("should coerce to localhost");
+
.expect("should coerce to 127.0.0.1");
assert_eq!(
out,
OAuthClientMetadata {
client_id: Url::from_str(
-
"http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2F"
+
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F"
)
.unwrap(),
client_uri: None,
-
redirect_uris: vec![Url::from_str("http://localhost/").unwrap()],
-
scope: None,
+
redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()],
+
scope: Some(CowStr::new_static("atproto")),
grant_types: None,
token_endpoint_auth_method: Some(AuthMethod::None.into()),
dpop_bound_access_tokens: None,
+11 -21
crates/jacquard-oauth/src/loopback.rs
···
scopes::Scope,
types::{AuthorizeOptions, CallbackParams},
};
-
use jacquard_common::{CowStr, IntoStatic, cowstr::ToCowStr};
+
use jacquard_common::{IntoStatic, cowstr::ToCowStr};
use rouille::Server;
-
use std::{net::SocketAddr, sync::Arc};
-
use tokio::{
-
net::TcpListener,
-
sync::{Mutex, mpsc, oneshot},
-
};
+
use std::net::SocketAddr;
+
use tokio::sync::mpsc;
use url::Url;
#[derive(Clone, Debug)]
···
opts: AuthorizeOptions<'_>,
cfg: LoopbackConfig,
) -> crate::error::Result<super::client::OAuthSession<T, S>> {
-
// 1) Bind server first to learn effective port
let port = match cfg.port {
LoopbackPort::Fixed(p) => p,
LoopbackPort::Ephemeral => 0,
};
-
// TODO: fix this to it also accepts ipv6
+
// TODO: fix this to it also accepts ipv6 and properly finds a free port
let bind_addr: SocketAddr = format!("0.0.0.0:{}", port)
.parse()
.expect("invalid loopback host/port");
let (local_addr, handle) = one_shot_server(bind_addr);
println!("Listening on {}", local_addr);
-
-
// 2) Build per-flow metadata with the actual redirect URI
+
// build redirect uri
let redirect = Url::parse(&format!(
"http://{}:{}/oauth/callback",
cfg.host,
···
),
};
-
// Build a per-flow client using shared store and resolver
+
// Build client using store and resolver
let flow_client = OAuthClient::new_with_shared(
self.registry.store.clone(),
self.client.clone(),
client_data.clone(),
);
-
// 3) Start auth (persists state) and get authorization URL
+
// Start auth and get authorization URL
let auth_url = flow_client.start_auth(input.as_ref(), opts).await?;
// Print URL for copy/paste
-
println!("Open this URL to authorize:\n{}\n", auth_url);
+
println!("To authenticate with your PDS, visit:\n{}\n", auth_url);
// Optionally open browser
if cfg.open_browser {
let _ = try_open_in_browser(&auth_url);
}
-
// 4) Await callback or timeout
+
// Await callback or timeout
let mut callback_rx = handle.callback_rx;
let cb = tokio::time::timeout(
std::time::Duration::from_millis(cfg.timeout_ms),
···
.await;
// trigger shutdown
let _ = handle.server_stop.send(());
-
if let Err(_) = cb {
-
return Err(OAuthError::Callback(CallbackError::Timeout));
-
}
-
if let Ok(Some(cb)) = cb {
-
// 5) Continue with callback flow
-
let session = flow_client.callback(cb).await?;
-
Ok(session)
+
// Handle callback and create a session
+
Ok(flow_client.callback(cb).await?)
} else {
Err(OAuthError::Callback(CallbackError::Timeout))
}