+24
-16
crates/atproto-client/src/bin/atproto-client-dpop.rs
+24
-16
crates/atproto-client/src/bin/atproto-client-dpop.rs
···
38
38
);
39
39
println!();
40
40
println!("Examples:");
41
-
println!(
42
-
" # GET request (default behavior without prefix)"
43
-
);
41
+
println!(" # GET request (default behavior without prefix)");
44
42
println!(
45
43
" atproto-client-dpop alice.bsky.social dpop.pem token123 com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post"
46
44
);
47
-
println!(
48
-
" # GET request (explicit query: prefix)"
49
-
);
45
+
println!(" # GET request (explicit query: prefix)");
50
46
println!(
51
47
" atproto-client-dpop alice.bsky.social dpop.pem token123 query:com.atproto.repo.listRecords repo=alice.bsky.social collection=app.bsky.feed.post"
52
48
);
53
-
println!(
54
-
" # POST request (requires procedure: prefix and JSON file)"
55
-
);
49
+
println!(" # POST request (requires procedure: prefix and JSON file)");
56
50
println!(
57
51
" atproto-client-dpop alice.bsky.social dpop.pem token123 procedure:com.atproto.repo.createRecord data.json"
58
52
);
···
74
68
let xrpc_path_with_prefix = &args[3];
75
69
76
70
// Parse the xrpc_path prefix (optional, defaults to query:)
77
-
let (is_procedure, xrpc_path) = if let Some(path) = xrpc_path_with_prefix.strip_prefix("query:") {
71
+
let (is_procedure, xrpc_path) = if let Some(path) = xrpc_path_with_prefix.strip_prefix("query:")
72
+
{
78
73
(false, path)
79
74
} else if let Some(path) = xrpc_path_with_prefix.strip_prefix("procedure:") {
80
75
(true, path)
···
97
92
let file_path = &args[arg_index];
98
93
let file_content = std::fs::read_to_string(file_path)
99
94
.map_err(|e| anyhow::anyhow!("Failed to read file '{}': {}", file_path, e))?;
100
-
json_data = Some(serde_json::from_str(&file_content)
101
-
.map_err(|e| anyhow::anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?);
95
+
json_data = Some(serde_json::from_str(&file_content).map_err(|e| {
96
+
anyhow::anyhow!("Failed to parse JSON from file '{}': {}", file_path, e)
97
+
})?);
102
98
arg_index += 1;
103
99
}
104
100
···
114
110
eprintln!("Expected format: header=name=value");
115
111
}
116
112
} else if is_procedure {
117
-
eprintln!("Warning: Query parameters are not supported for procedure calls. Ignoring: {}", arg);
113
+
eprintln!(
114
+
"Warning: Query parameters are not supported for procedure calls. Ignoring: {}",
115
+
arg
116
+
);
118
117
} else {
119
118
query_params.insert(key.to_string(), value.to_string());
120
119
}
···
126
125
127
126
println!("Making DPoP authenticated XRPC call");
128
127
println!("Subject: {}", subject);
129
-
println!("Request Type: {}", if is_procedure { "POST (procedure)" } else { "GET (query)" });
128
+
println!(
129
+
"Request Type: {}",
130
+
if is_procedure {
131
+
"POST (procedure)"
132
+
} else {
133
+
"GET (query)"
134
+
}
135
+
);
130
136
println!("XRPC Path: {}", xrpc_path);
131
137
if !query_params.is_empty() {
132
138
println!("Query Parameters: {:?}", query_params);
···
228
234
println!("Making DPoP authenticated request...");
229
235
230
236
let response = if is_procedure {
231
-
let data = json_data.ok_or_else(|| anyhow::anyhow!("No JSON data provided for procedure call"))?;
232
-
post_dpop_json_with_headers(&http_client, &dpop_auth, &url, data, &additional_headers).await?
237
+
let data =
238
+
json_data.ok_or_else(|| anyhow::anyhow!("No JSON data provided for procedure call"))?;
239
+
post_dpop_json_with_headers(&http_client, &dpop_auth, &url, data, &additional_headers)
240
+
.await?
233
241
} else {
234
242
get_dpop_json_with_headers(&http_client, &dpop_auth, &url, &additional_headers).await?
235
243
};
+5
-1
crates/atproto-oauth-axum/src/bin/atproto-oauth-tool.rs
+5
-1
crates/atproto-oauth-axum/src/bin/atproto-oauth-tool.rs
···
200
200
}
201
201
202
202
let oauth_client_config = OAuthClientConfig {
203
-
client_uri: format!("https://{}", &external_base),
204
203
jwks_uri: format!("https://{}/.well-known/jwks.json", &external_base),
205
204
redirect_uris: format!("https://{}/oauth/callback", &external_base),
206
205
client_id: format!("https://{}/oauth/client-metadata.json", &external_base),
···
208
207
.iter()
209
208
.filter_map(|value| to_public(value).ok())
210
209
.collect(),
210
+
client_name: None,
211
+
client_uri: None,
212
+
logo_uri: None,
213
+
tos_uri: None,
214
+
policy_uri: None,
211
215
};
212
216
213
217
let mut signing_key_storage = HashMap::new();
+35
-13
crates/atproto-oauth-axum/src/handler_metadata.rs
+35
-13
crates/atproto-oauth-axum/src/handler_metadata.rs
···
8
8
9
9
use crate::state::OAuthClientConfig;
10
10
11
-
#[derive(Serialize)]
12
-
struct AuthMetadata {
11
+
// See also: https://atproto.com/specs/oauth#client-id-metadata-document
12
+
#[derive(Serialize, Default)]
13
+
struct AuthMetadata<'a> {
13
14
client_id: String,
14
15
dpop_bound_access_tokens: bool,
15
-
application_type: &'static str,
16
+
application_type: &'a str,
16
17
redirect_uris: Vec<String>,
17
-
client_uri: String,
18
-
grant_types: Vec<&'static str>,
19
-
response_types: Vec<&'static str>,
20
-
scope: &'static str,
21
-
client_name: &'static str,
22
-
token_endpoint_auth_method: &'static str,
18
+
grant_types: Vec<&'a str>,
19
+
response_types: Vec<&'a str>,
20
+
scope: &'a str,
21
+
token_endpoint_auth_method: &'a str,
23
22
jwks_uri: String,
24
-
subject_type: &'static str,
25
-
token_endpoint_auth_signing_alg: &'static str,
23
+
subject_type: &'a str,
24
+
token_endpoint_auth_signing_alg: &'a str,
25
+
26
+
#[serde(skip_serializing_if = "Option::is_none")]
27
+
#[serde(default)]
28
+
client_name: Option<String>,
29
+
30
+
#[serde(skip_serializing_if = "Option::is_none")]
31
+
#[serde(default)]
32
+
client_uri: Option<String>,
33
+
34
+
#[serde(skip_serializing_if = "Option::is_none")]
35
+
#[serde(default)]
36
+
logo_uri: Option<String>,
37
+
38
+
#[serde(skip_serializing_if = "Option::is_none")]
39
+
#[serde(default)]
40
+
tos_uri: Option<String>,
41
+
42
+
#[serde(skip_serializing_if = "Option::is_none")]
43
+
#[serde(default)]
44
+
policy_uri: Option<String>,
26
45
}
27
46
28
47
/// Handles requests for OAuth client metadata.
···
32
51
let resp = AuthMetadata {
33
52
application_type: "web",
34
53
client_id: oauth_client_config.client_id.clone(),
35
-
client_name: "Smoke Signal",
36
-
client_uri: oauth_client_config.client_uri.clone(),
37
54
dpop_bound_access_tokens: true,
38
55
grant_types: vec!["authorization_code", "refresh_token"],
39
56
jwks_uri: oauth_client_config.jwks_uri.clone(),
···
43
60
token_endpoint_auth_method: "private_key_jwt",
44
61
token_endpoint_auth_signing_alg: "ES256",
45
62
subject_type: "public",
63
+
client_name: oauth_client_config.client_name.clone(),
64
+
client_uri: oauth_client_config.client_uri.clone(),
65
+
logo_uri: oauth_client_config.logo_uri.clone(),
66
+
tos_uri: oauth_client_config.tos_uri.clone(),
67
+
policy_uri: oauth_client_config.policy_uri.clone(),
46
68
};
47
69
Json(resp)
48
70
}
+14
-2
crates/atproto-oauth-axum/src/state.rs
+14
-2
crates/atproto-oauth-axum/src/state.rs
···
13
13
/// Contains the essential configuration needed for OAuth client operations.
14
14
#[derive(Clone)]
15
15
pub struct OAuthClientConfig {
16
-
/// OAuth client URI
17
-
pub client_uri: String,
18
16
/// OAuth client identifier
19
17
pub client_id: String,
20
18
/// Allowed OAuth redirect URIs
···
23
21
pub jwks_uri: String,
24
22
/// Signing keys for JWT operations
25
23
pub signing_keys: Vec<KeyData>,
24
+
25
+
/// Optional human-readable client name
26
+
pub client_name: Option<String>,
27
+
/// Optional client website URI
28
+
pub client_uri: Option<String>,
29
+
/// Optional client logo URI
30
+
pub logo_uri: Option<String>,
31
+
/// Optional terms of service URI
32
+
pub tos_uri: Option<String>,
33
+
/// Optional privacy policy URI
34
+
pub policy_uri: Option<String>,
26
35
}
27
36
28
37
impl<S> FromRequestParts<S> for OAuthClientConfig
···
32
41
{
33
42
type Rejection = Infallible;
34
43
44
+
/// Extracts OAuth client configuration from Axum application state.
35
45
async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
36
46
let oauth_client_config = OAuthClientConfig::from_ref(state);
37
47
Ok(oauth_client_config)
···
47
57
impl std::ops::Deref for HttpClient {
48
58
type Target = reqwest::Client;
49
59
60
+
/// Provides direct access to the underlying reqwest::Client.
50
61
fn deref(&self) -> &Self::Target {
51
62
&self.0
52
63
}
···
59
70
{
60
71
type Rejection = Infallible;
61
72
73
+
/// Extracts HTTP client from Axum application state.
62
74
async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
63
75
let client = reqwest::Client::from_ref(state);
64
76
Ok(HttpClient(client))
+4
-1
crates/atproto-xrpcs-helloworld/src/main.rs
+4
-1
crates/atproto-xrpcs-helloworld/src/main.rs