A library for ATProtocol identities.

feature: atproto-client auth parameter supports multiple types of auth

Changed files
+96 -93
crates
+13
crates/atproto-client/src/client.rs
··· 32 pub access_token: String, 33 } 34 35 /// Performs an unauthenticated HTTP GET request and parses the response as JSON. 36 /// 37 /// # Arguments
··· 32 pub access_token: String, 33 } 34 35 + /// Authentication method for AT Protocol XRPC requests. 36 + /// 37 + /// Supports multiple authentication schemes including unauthenticated requests, 38 + /// DPoP (Demonstration of Proof-of-Possession) tokens, and app password bearer tokens. 39 + pub enum Auth { 40 + /// No authentication - for public endpoints that don't require authentication 41 + None, 42 + /// DPoP authentication with proof-of-possession tokens and OAuth access token 43 + DPoP(DPoPAuth), 44 + /// App password authentication using JWT bearer tokens 45 + AppPassword(AppPasswordAuth) 46 + } 47 + 48 /// Performs an unauthenticated HTTP GET request and parses the response as JSON. 49 /// 50 /// # Arguments
+13 -9
crates/atproto-client/src/com_atproto_identity.rs
··· 9 use serde::{Deserialize, de::DeserializeOwned}; 10 11 use crate::{ 12 - client::{get_dpop_json, get_json, DPoPAuth}, errors::SimpleError, url::URLBuilder 13 }; 14 15 /// Response from the com.atproto.identity.resolveHandle XRPC method. ··· 42 /// # Arguments 43 /// 44 /// * `http_client` - The HTTP client to use for the request 45 - /// * `dpop_auth` - Optional DPoP authentication credentials 46 /// * `base_url` - The base URL of the AT Protocol service 47 /// * `handle` - The handle to resolve 48 /// ··· 52 /// or an error response from the server. 53 pub async fn resolve_handle<T: DeserializeOwned>( 54 http_client: &reqwest::Client, 55 - dpop_auth: Option<&DPoPAuth>, 56 base_url: &str, 57 handle: String, 58 ) -> Result<ResolveHandleResponse> { ··· 63 64 let url = url_builder.build(); 65 66 - if let Some(dpop_auth) = dpop_auth { 67 - get_dpop_json(http_client, dpop_auth, &url) 68 .await 69 - .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 70 - } else { 71 - get_json(http_client, &url) 72 .await 73 - .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 74 } 75 }
··· 9 use serde::{Deserialize, de::DeserializeOwned}; 10 11 use crate::{ 12 + client::{get_apppassword_json, get_dpop_json, get_json, Auth}, 13 + errors::SimpleError, 14 + url::URLBuilder 15 }; 16 17 /// Response from the com.atproto.identity.resolveHandle XRPC method. ··· 44 /// # Arguments 45 /// 46 /// * `http_client` - The HTTP client to use for the request 47 + /// * `auth` - Authentication method (None, DPoP, or AppPassword) 48 /// * `base_url` - The base URL of the AT Protocol service 49 /// * `handle` - The handle to resolve 50 /// ··· 54 /// or an error response from the server. 55 pub async fn resolve_handle<T: DeserializeOwned>( 56 http_client: &reqwest::Client, 57 + auth: &Auth, 58 base_url: &str, 59 handle: String, 60 ) -> Result<ResolveHandleResponse> { ··· 65 66 let url = url_builder.build(); 67 68 + match auth { 69 + Auth::None => get_json(http_client, &url) 70 .await 71 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 72 + Auth::DPoP(dpop_auth) => get_dpop_json(http_client, dpop_auth, &url) 73 + .await 74 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 75 + Auth::AppPassword(app_auth) => get_apppassword_json(http_client, app_auth, &url) 76 .await 77 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 78 } 79 }
+60 -32
crates/atproto-client/src/com_atproto_repo.rs
··· 29 use serde::{Deserialize, Serialize, de::DeserializeOwned}; 30 31 use crate::{ 32 - client::{DPoPAuth, get_bytes, get_dpop_json, get_json, post_dpop_json}, 33 errors::SimpleError, 34 url::URLBuilder, 35 }; ··· 90 /// # Arguments 91 /// 92 /// * `http_client` - HTTP client for making requests 93 - /// * `dpop_auth` - DPoP authentication credentials 94 /// * `base_url` - Base URL of the AT Protocol server 95 /// * `repo` - Repository identifier (DID) 96 /// * `collection` - Collection NSID ··· 102 /// The record data or an error response 103 pub async fn get_record( 104 http_client: &reqwest::Client, 105 - dpop_auth: Option<&DPoPAuth>, 106 base_url: &str, 107 repo: &str, 108 collection: &str, ··· 122 123 let url = url_builder.build(); 124 125 - if let Some(dpop_auth) = dpop_auth { 126 - get_dpop_json(http_client, dpop_auth, &url) 127 .await 128 - .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 129 - } else { 130 - get_json(http_client, &url) 131 .await 132 - .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 133 } 134 } 135 ··· 196 /// # Arguments 197 /// 198 /// * `http_client` - HTTP client for making requests 199 - /// * `dpop_auth` - DPoP authentication credentials 200 /// * `base_url` - Base URL of the AT Protocol server 201 /// * `repo` - Repository identifier (DID) 202 /// * `collection` - Collection NSID to list from ··· 207 /// A paginated list of records from the collection 208 pub async fn list_records<T: DeserializeOwned>( 209 http_client: &reqwest::Client, 210 - dpop_auth: Option<&DPoPAuth>, 211 base_url: &str, 212 repo: String, 213 collection: String, ··· 234 235 let url = url_builder.build(); 236 237 - if let Some(dpop_auth) = dpop_auth { 238 - get_dpop_json(http_client, dpop_auth, &url) 239 .await 240 - .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 241 - } else { 242 - get_json(http_client, &url) 243 .await 244 - .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 245 } 246 } 247 ··· 299 /// # Arguments 300 /// 301 /// * `http_client` - HTTP client for making requests 302 - /// * `dpop_auth` - DPoP authentication credentials 303 /// * `base_url` - Base URL of the AT Protocol server 304 /// * `record` - Record creation request with content and metadata 305 /// ··· 308 /// The created record reference or an error response 309 pub async fn create_record<T: DeserializeOwned + Serialize>( 310 http_client: &reqwest::Client, 311 - dpop_auth: &DPoPAuth, 312 base_url: &str, 313 record: CreateRecordRequest<T>, 314 ) -> Result<CreateRecordResponse> { ··· 318 319 let value = serde_json::to_value(record)?; 320 321 - post_dpop_json(http_client, dpop_auth, &url, value) 322 - .await 323 - .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 324 } 325 326 /// Request to update an existing record in an AT Protocol repository. ··· 385 /// # Arguments 386 /// 387 /// * `http_client` - HTTP client for making requests 388 - /// * `dpop_auth` - DPoP authentication credentials 389 /// * `base_url` - Base URL of the AT Protocol server 390 /// * `record` - Record update request with new content and metadata 391 /// ··· 394 /// The updated record reference or an error response 395 pub async fn put_record<T: DeserializeOwned + Serialize>( 396 http_client: &reqwest::Client, 397 - dpop_auth: &DPoPAuth, 398 base_url: &str, 399 record: PutRecordRequest<T>, 400 ) -> Result<PutRecordResponse> { ··· 404 405 let value = serde_json::to_value(record)?; 406 407 - post_dpop_json(http_client, dpop_auth, &url, value) 408 - .await 409 - .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 410 } 411 412 /// Request to delete a record from an AT Protocol repository. ··· 460 /// # Arguments 461 /// 462 /// * `http_client` - HTTP client for making requests 463 - /// * `dpop_auth` - DPoP authentication credentials 464 /// * `base_url` - Base URL of the AT Protocol server 465 /// * `record` - Record deletion request with repository, collection, and key 466 /// ··· 469 /// The deletion response with commit information or an error 470 pub async fn delete_record( 471 http_client: &reqwest::Client, 472 - dpop_auth: &DPoPAuth, 473 base_url: &str, 474 record: DeleteRecordRequest, 475 ) -> Result<DeleteRecordResponse> { ··· 479 480 let value = serde_json::to_value(record)?; 481 482 - post_dpop_json(http_client, dpop_auth, &url, value) 483 - .await 484 - .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 485 }
··· 29 use serde::{Deserialize, Serialize, de::DeserializeOwned}; 30 31 use crate::{ 32 + client::{Auth, get_apppassword_json, get_bytes, get_dpop_json, get_json, post_apppassword_json, post_dpop_json, post_json}, 33 errors::SimpleError, 34 url::URLBuilder, 35 }; ··· 90 /// # Arguments 91 /// 92 /// * `http_client` - HTTP client for making requests 93 + /// * `auth` - Authentication method (None, DPoP, or AppPassword) 94 /// * `base_url` - Base URL of the AT Protocol server 95 /// * `repo` - Repository identifier (DID) 96 /// * `collection` - Collection NSID ··· 102 /// The record data or an error response 103 pub async fn get_record( 104 http_client: &reqwest::Client, 105 + auth: &Auth, 106 base_url: &str, 107 repo: &str, 108 collection: &str, ··· 122 123 let url = url_builder.build(); 124 125 + match auth { 126 + Auth::None => get_json(http_client, &url) 127 + .await 128 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 129 + Auth::DPoP(dpop_auth) => get_dpop_json(http_client, dpop_auth, &url) 130 .await 131 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 132 + Auth::AppPassword(app_auth) => get_apppassword_json(http_client, app_auth, &url) 133 .await 134 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 135 } 136 } 137 ··· 198 /// # Arguments 199 /// 200 /// * `http_client` - HTTP client for making requests 201 + /// * `auth` - Authentication method (None, DPoP, or AppPassword) 202 /// * `base_url` - Base URL of the AT Protocol server 203 /// * `repo` - Repository identifier (DID) 204 /// * `collection` - Collection NSID to list from ··· 209 /// A paginated list of records from the collection 210 pub async fn list_records<T: DeserializeOwned>( 211 http_client: &reqwest::Client, 212 + auth: &Auth, 213 base_url: &str, 214 repo: String, 215 collection: String, ··· 236 237 let url = url_builder.build(); 238 239 + match auth { 240 + Auth::None => get_json(http_client, &url) 241 .await 242 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 243 + Auth::DPoP(dpop_auth) => get_dpop_json(http_client, dpop_auth, &url) 244 .await 245 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 246 + Auth::AppPassword(app_auth) => get_apppassword_json(http_client, app_auth, &url) 247 + .await 248 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 249 } 250 } 251 ··· 303 /// # Arguments 304 /// 305 /// * `http_client` - HTTP client for making requests 306 + /// * `auth` - Authentication method (None, DPoP, or AppPassword) 307 /// * `base_url` - Base URL of the AT Protocol server 308 /// * `record` - Record creation request with content and metadata 309 /// ··· 312 /// The created record reference or an error response 313 pub async fn create_record<T: DeserializeOwned + Serialize>( 314 http_client: &reqwest::Client, 315 + auth: &Auth, 316 base_url: &str, 317 record: CreateRecordRequest<T>, 318 ) -> Result<CreateRecordResponse> { ··· 322 323 let value = serde_json::to_value(record)?; 324 325 + match auth { 326 + Auth::None => post_json(http_client, &url, value) 327 + .await 328 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 329 + Auth::DPoP(dpop_auth) => post_dpop_json(http_client, dpop_auth, &url, value) 330 + .await 331 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 332 + Auth::AppPassword(app_auth) => post_apppassword_json(http_client, app_auth, &url, value) 333 + .await 334 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 335 + } 336 } 337 338 /// Request to update an existing record in an AT Protocol repository. ··· 397 /// # Arguments 398 /// 399 /// * `http_client` - HTTP client for making requests 400 + /// * `auth` - Authentication method (None, DPoP, or AppPassword) 401 /// * `base_url` - Base URL of the AT Protocol server 402 /// * `record` - Record update request with new content and metadata 403 /// ··· 406 /// The updated record reference or an error response 407 pub async fn put_record<T: DeserializeOwned + Serialize>( 408 http_client: &reqwest::Client, 409 + auth: &Auth, 410 base_url: &str, 411 record: PutRecordRequest<T>, 412 ) -> Result<PutRecordResponse> { ··· 416 417 let value = serde_json::to_value(record)?; 418 419 + match auth { 420 + Auth::None => post_json(http_client, &url, value) 421 + .await 422 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 423 + Auth::DPoP(dpop_auth) => post_dpop_json(http_client, dpop_auth, &url, value) 424 + .await 425 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 426 + Auth::AppPassword(app_auth) => post_apppassword_json(http_client, app_auth, &url, value) 427 + .await 428 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 429 + } 430 } 431 432 /// Request to delete a record from an AT Protocol repository. ··· 480 /// # Arguments 481 /// 482 /// * `http_client` - HTTP client for making requests 483 + /// * `auth` - Authentication method (None, DPoP, or AppPassword) 484 /// * `base_url` - Base URL of the AT Protocol server 485 /// * `record` - Record deletion request with repository, collection, and key 486 /// ··· 489 /// The deletion response with commit information or an error 490 pub async fn delete_record( 491 http_client: &reqwest::Client, 492 + auth: &Auth, 493 base_url: &str, 494 record: DeleteRecordRequest, 495 ) -> Result<DeleteRecordResponse> { ··· 499 500 let value = serde_json::to_value(record)?; 501 502 + match auth { 503 + Auth::None => post_json(http_client, &url, value) 504 + .await 505 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 506 + Auth::DPoP(dpop_auth) => post_dpop_json(http_client, dpop_auth, &url, value) 507 + .await 508 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 509 + Auth::AppPassword(app_auth) => post_apppassword_json(http_client, app_auth, &url, value) 510 + .await 511 + .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())), 512 + } 513 }
+5 -49
crates/atproto-client/src/com_atproto_server.rs
··· 23 use crate::{client::post_json, url::URLBuilder}; 24 25 /// Request to create a new authentication session. 26 - #[derive(Serialize, Clone)] 27 pub struct CreateSessionRequest { 28 /// Handle or other identifier supported by the server for the authenticating user 29 pub identifier: String, ··· 34 pub auth_factor_token: Option<String>, 35 } 36 37 - impl std::fmt::Debug for CreateSessionRequest { 38 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 - f.debug_struct("CreateSessionRequest") 40 - .field("identifier", &self.identifier) 41 - .field("password", &"[REDACTED]") 42 - .field( 43 - "auth_factor_token", 44 - &self.auth_factor_token.as_ref().map(|_| "[REDACTED]"), 45 - ) 46 - .finish() 47 - } 48 - } 49 - 50 /// App password session data returned from successful authentication. 51 #[derive(Deserialize, Clone)] 52 pub struct AppPasswordSession { 53 /// Distributed identifier for the authenticated account ··· 64 pub refresh_jwt: String, 65 } 66 67 - impl std::fmt::Debug for AppPasswordSession { 68 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 69 - f.debug_struct("AppPasswordSession") 70 - .field("did", &self.did) 71 - .field("handle", &self.handle) 72 - .field("email", &self.email) 73 - .field("access_jwt", &"[REDACTED]") 74 - .field("refresh_jwt", &"[REDACTED]") 75 - .finish() 76 - } 77 - } 78 - 79 /// Response from refreshing an authentication session. 80 #[derive(Deserialize, Clone)] 81 pub struct RefreshSessionResponse { 82 /// Distributed identifier for the authenticated account ··· 97 pub status: Option<String>, 98 } 99 100 - impl std::fmt::Debug for RefreshSessionResponse { 101 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 102 - f.debug_struct("RefreshSessionResponse") 103 - .field("did", &self.did) 104 - .field("handle", &self.handle) 105 - .field("access_jwt", &"[REDACTED]") 106 - .field("refresh_jwt", &"[REDACTED]") 107 - .field("active", &self.active) 108 - .field("status", &self.status) 109 - .finish() 110 - } 111 - } 112 - 113 /// Response from creating a new app password. 114 #[derive(Deserialize, Clone)] 115 pub struct AppPasswordResponse { 116 /// Name of the app password ··· 120 /// Creation timestamp in ISO 8601 format 121 #[serde(rename = "createdAt")] 122 pub created_at: String, 123 - } 124 - 125 - impl std::fmt::Debug for AppPasswordResponse { 126 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 127 - f.debug_struct("AppPasswordResponse") 128 - .field("name", &self.name) 129 - .field("password", &"[REDACTED]") 130 - .field("created_at", &self.created_at) 131 - .finish() 132 - } 133 } 134 135 /// Creates a new authentication session using app password credentials.
··· 23 use crate::{client::post_json, url::URLBuilder}; 24 25 /// Request to create a new authentication session. 26 + #[cfg_attr(debug_assertions, derive(Debug))] 27 + #[derive(Serialize, Deserialize, Clone)] 28 pub struct CreateSessionRequest { 29 /// Handle or other identifier supported by the server for the authenticating user 30 pub identifier: String, ··· 35 pub auth_factor_token: Option<String>, 36 } 37 38 /// App password session data returned from successful authentication. 39 + #[cfg_attr(debug_assertions, derive(Debug))] 40 #[derive(Deserialize, Clone)] 41 pub struct AppPasswordSession { 42 /// Distributed identifier for the authenticated account ··· 53 pub refresh_jwt: String, 54 } 55 56 /// Response from refreshing an authentication session. 57 + #[cfg_attr(debug_assertions, derive(Debug))] 58 #[derive(Deserialize, Clone)] 59 pub struct RefreshSessionResponse { 60 /// Distributed identifier for the authenticated account ··· 75 pub status: Option<String>, 76 } 77 78 /// Response from creating a new app password. 79 + #[cfg_attr(debug_assertions, derive(Debug))] 80 #[derive(Deserialize, Clone)] 81 pub struct AppPasswordResponse { 82 /// Name of the app password ··· 86 /// Creation timestamp in ISO 8601 format 87 #[serde(rename = "createdAt")] 88 pub created_at: String, 89 } 90 91 /// Creates a new authentication session using app password credentials.
+3
crates/atproto-oauth-aip/src/workflow.rs
··· 161 pub handle: String, 162 163 /// The OAuth access token for making authenticated requests. 164 pub access_token: String, 165 166 /// The type of token (typically "Bearer"). 167 pub token_type: String, 168 169 /// The list of OAuth scopes granted to this session. ··· 175 pub pds_endpoint: String, 176 177 /// The DPoP (Demonstration of Proof-of-Possession) key in JWK format. 178 pub dpop_key: String, 179 180 /// Unix timestamp indicating when this session expires.
··· 161 pub handle: String, 162 163 /// The OAuth access token for making authenticated requests. 164 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 165 pub access_token: String, 166 167 /// The type of token (typically "Bearer"). 168 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 169 pub token_type: String, 170 171 /// The list of OAuth scopes granted to this session. ··· 177 pub pds_endpoint: String, 178 179 /// The DPoP (Demonstration of Proof-of-Possession) key in JWK format. 180 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 181 pub dpop_key: String, 182 183 /// Unix timestamp indicating when this session expires.
+1 -2
crates/atproto-record/src/bin/atproto-record-sign.rs
··· 183 &repository, 184 &collection, 185 signature_object, 186 - ) 187 - .await?; 188 189 let pretty_signed_record = serde_json::to_string_pretty(&signed_record); 190 println!("{}", pretty_signed_record.unwrap());
··· 183 &repository, 184 &collection, 185 signature_object, 186 + )?; 187 188 let pretty_signed_record = serde_json::to_string_pretty(&signed_record); 189 println!("{}", pretty_signed_record.unwrap());
+1 -1
crates/atproto-record/src/bin/atproto-record-verify.rs
··· 158 name: "key".to_string(), 159 })?; 160 161 - verify(&issuer, &key_data, record, &repository, &collection).await?; 162 163 println!("OK"); 164
··· 158 name: "key".to_string(), 159 })?; 160 161 + verify(&issuer, &key_data, record, &repository, &collection)?; 162 163 println!("OK"); 164