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