A library for ATProtocol identities.

bug: Removed hardcoded oauth metadata field values

Signed-off-by: Nick Gerakines <nick.gerakines@gmail.com>

Changed files
+82 -34
crates
atproto-client
atproto-oauth-axum
atproto-xrpcs
src
atproto-xrpcs-helloworld
src
+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
··· 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
··· 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
··· 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
··· 246 246 } 247 247 }); 248 248 249 - println!("XRPC Hello World service started on http://0.0.0.0:{}", port); 249 + println!( 250 + "XRPC Hello World service started on http://0.0.0.0:{}", 251 + port 252 + ); 250 253 251 254 // Keep the server running 252 255 server_handle.await.unwrap();
-1
crates/atproto-xrpcs/src/errors.rs
··· 117 117 error: anyhow::Error, 118 118 }, 119 119 } 120 -