+1
-1
crates/jacquard-common/src/error.rs
+1
-1
crates/jacquard-common/src/error.rs
+18
-17
crates/jacquard-oauth/src/atproto.rs
+18
-17
crates/jacquard-oauth/src/atproto.rs
···
232
232
Url::from_str("http://127.0.0.1/").unwrap(),
233
233
Url::from_str("http://[::1]/").unwrap(),
234
234
],
235
-
scope: None,
235
+
scope: Some(CowStr::new_static("atproto")),
236
236
grant_types: None,
237
237
token_endpoint_auth_method: Some(AuthMethod::None.into()),
238
238
dpop_bound_access_tokens: None,
···
262
262
.expect("failed to convert metadata"),
263
263
OAuthClientMetadata {
264
264
client_id: Url::from_str(
265
-
"http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&scope=account%3Aemail+atproto+transition%3Ageneric"
265
+
"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"
266
266
).unwrap(),
267
267
client_uri: None,
268
268
redirect_uris: vec![
269
-
Url::from_str("http://localhost/callback").unwrap(),
270
-
Url::from_str("http://localhost/callback").unwrap(),
269
+
Url::from_str("http://127.0.0.1/callback").unwrap(),
270
+
// TODO: fix this so that it respects IPv6
271
+
Url::from_str("http://127.0.0.1/callback").unwrap(),
271
272
],
272
-
scope: None,
273
+
scope: Some(CowStr::new_static("account:email atproto transition:generic")),
273
274
grant_types: None,
274
275
token_endpoint_auth_method: Some(AuthMethod::None.into()),
275
276
dpop_bound_access_tokens: None,
···
291
292
),
292
293
&None,
293
294
)
294
-
.expect("should coerce to localhost");
295
+
.expect("should coerce to 127.0.0.1");
295
296
assert_eq!(
296
297
out,
297
298
OAuthClientMetadata {
298
299
client_id: Url::from_str(
299
-
"http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2F"
300
+
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F"
300
301
)
301
302
.unwrap(),
302
303
client_uri: None,
303
-
redirect_uris: vec![Url::from_str("http://localhost/").unwrap()],
304
-
scope: None,
304
+
redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()],
305
+
scope: Some(CowStr::new_static("atproto")),
305
306
grant_types: None,
306
307
token_endpoint_auth_method: Some(AuthMethod::None.into()),
307
308
dpop_bound_access_tokens: None,
···
319
320
),
320
321
&None,
321
322
)
322
-
.expect("should coerce to localhost");
323
+
.expect("should coerce to 127.0.0.1");
323
324
assert_eq!(
324
325
out,
325
326
OAuthClientMetadata {
326
327
client_id: Url::from_str(
327
-
"http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2F"
328
+
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2F"
328
329
)
329
330
.unwrap(),
330
331
client_uri: None,
331
-
redirect_uris: vec![Url::from_str("http://localhost/").unwrap()],
332
-
scope: None,
332
+
redirect_uris: vec![Url::from_str("http://127.0.0.1:8000/").unwrap()],
333
+
scope: Some(CowStr::new_static("atproto")),
333
334
grant_types: None,
334
335
token_endpoint_auth_method: Some(AuthMethod::None.into()),
335
336
dpop_bound_access_tokens: None,
···
347
348
),
348
349
&None,
349
350
)
350
-
.expect("should coerce to localhost");
351
+
.expect("should coerce to 127.0.0.1");
351
352
assert_eq!(
352
353
out,
353
354
OAuthClientMetadata {
354
355
client_id: Url::from_str(
355
-
"http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2F"
356
+
"http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F"
356
357
)
357
358
.unwrap(),
358
359
client_uri: None,
359
-
redirect_uris: vec![Url::from_str("http://localhost/").unwrap()],
360
-
scope: None,
360
+
redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()],
361
+
scope: Some(CowStr::new_static("atproto")),
361
362
grant_types: None,
362
363
token_endpoint_auth_method: Some(AuthMethod::None.into()),
363
364
dpop_bound_access_tokens: None,
+11
-21
crates/jacquard-oauth/src/loopback.rs
+11
-21
crates/jacquard-oauth/src/loopback.rs
···
10
10
scopes::Scope,
11
11
types::{AuthorizeOptions, CallbackParams},
12
12
};
13
-
use jacquard_common::{CowStr, IntoStatic, cowstr::ToCowStr};
13
+
use jacquard_common::{IntoStatic, cowstr::ToCowStr};
14
14
use rouille::Server;
15
-
use std::{net::SocketAddr, sync::Arc};
16
-
use tokio::{
17
-
net::TcpListener,
18
-
sync::{Mutex, mpsc, oneshot},
19
-
};
15
+
use std::net::SocketAddr;
16
+
use tokio::sync::mpsc;
20
17
use url::Url;
21
18
22
19
#[derive(Clone, Debug)]
···
108
105
opts: AuthorizeOptions<'_>,
109
106
cfg: LoopbackConfig,
110
107
) -> crate::error::Result<super::client::OAuthSession<T, S>> {
111
-
// 1) Bind server first to learn effective port
112
108
let port = match cfg.port {
113
109
LoopbackPort::Fixed(p) => p,
114
110
LoopbackPort::Ephemeral => 0,
115
111
};
116
-
// TODO: fix this to it also accepts ipv6
112
+
// TODO: fix this to it also accepts ipv6 and properly finds a free port
117
113
let bind_addr: SocketAddr = format!("0.0.0.0:{}", port)
118
114
.parse()
119
115
.expect("invalid loopback host/port");
120
116
let (local_addr, handle) = one_shot_server(bind_addr);
121
117
println!("Listening on {}", local_addr);
122
-
123
-
// 2) Build per-flow metadata with the actual redirect URI
118
+
// build redirect uri
124
119
let redirect = Url::parse(&format!(
125
120
"http://{}:{}/oauth/callback",
126
121
cfg.host,
···
138
133
),
139
134
};
140
135
141
-
// Build a per-flow client using shared store and resolver
136
+
// Build client using store and resolver
142
137
let flow_client = OAuthClient::new_with_shared(
143
138
self.registry.store.clone(),
144
139
self.client.clone(),
145
140
client_data.clone(),
146
141
);
147
142
148
-
// 3) Start auth (persists state) and get authorization URL
143
+
// Start auth and get authorization URL
149
144
let auth_url = flow_client.start_auth(input.as_ref(), opts).await?;
150
145
// Print URL for copy/paste
151
-
println!("Open this URL to authorize:\n{}\n", auth_url);
146
+
println!("To authenticate with your PDS, visit:\n{}\n", auth_url);
152
147
// Optionally open browser
153
148
if cfg.open_browser {
154
149
let _ = try_open_in_browser(&auth_url);
155
150
}
156
151
157
-
// 4) Await callback or timeout
152
+
// Await callback or timeout
158
153
let mut callback_rx = handle.callback_rx;
159
154
let cb = tokio::time::timeout(
160
155
std::time::Duration::from_millis(cfg.timeout_ms),
···
163
158
.await;
164
159
// trigger shutdown
165
160
let _ = handle.server_stop.send(());
166
-
if let Err(_) = cb {
167
-
return Err(OAuthError::Callback(CallbackError::Timeout));
168
-
}
169
-
170
161
if let Ok(Some(cb)) = cb {
171
-
// 5) Continue with callback flow
172
-
let session = flow_client.callback(cb).await?;
173
-
Ok(session)
162
+
// Handle callback and create a session
163
+
Ok(flow_client.callback(cb).await?)
174
164
} else {
175
165
Err(OAuthError::Callback(CallbackError::Timeout))
176
166
}