A library for ATProtocol identities.
22
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 307 lines 9.7 kB view raw
1//! AT Protocol server authentication operations. 2//! 3//! Client functions for com.atproto.server XRPC methods including 4//! session creation, refresh, and deletion with app password authentication. 5//! - **`create_app_password()`**: Create a new app password for authenticated account 6//! - **`delete_session()`**: Delete the current authentication session 7//! 8//! ## Request/Response Types 9//! 10//! - **`CreateSessionRequest`**: Parameters for creating a new session 11//! - **`AppPasswordSession`**: Response containing session data and tokens 12//! - **`RefreshSessionResponse`**: Response from session refresh operation 13//! - **`AppPasswordResponse`**: Response containing created app password details 14//! 15//! ## Authentication 16//! 17//! Session creation uses app password authentication, while session refresh requires 18//! the refresh JWT token from a previous session. App password creation requires 19//! an access JWT token from an authenticated session. 20 21use anyhow::Result; 22use atproto_identity::url::build_url; 23use serde::{Deserialize, Serialize}; 24use std::iter; 25 26use crate::{ 27 client::{Auth, post_json}, 28 errors::ClientError, 29}; 30 31/// Request to create a new authentication session. 32#[cfg_attr(debug_assertions, derive(Debug))] 33#[derive(Serialize, Deserialize, Clone)] 34pub struct CreateSessionRequest { 35 /// Handle or other identifier supported by the server for the authenticating user 36 pub identifier: String, 37 /// User password or app password 38 pub password: String, 39 /// Optional two-factor authentication token 40 #[serde(skip_serializing_if = "Option::is_none", rename = "authFactorToken")] 41 pub auth_factor_token: Option<String>, 42} 43 44/// App password session data returned from successful authentication. 45#[cfg_attr(debug_assertions, derive(Debug))] 46#[derive(Deserialize, Clone)] 47pub struct AppPasswordSession { 48 /// Distributed identifier for the authenticated account 49 pub did: String, 50 /// Handle for the authenticated account 51 pub handle: String, 52 /// Email address for the authenticated account 53 pub email: String, 54 /// JWT access token for authenticated requests 55 #[serde(rename = "accessJwt")] 56 pub access_jwt: String, 57 /// JWT refresh token for obtaining new access tokens 58 #[serde(rename = "refreshJwt")] 59 pub refresh_jwt: String, 60} 61 62/// Response from refreshing an authentication session. 63#[cfg_attr(debug_assertions, derive(Debug))] 64#[derive(Deserialize, Clone)] 65pub struct RefreshSessionResponse { 66 /// Distributed identifier for the authenticated account 67 pub did: String, 68 /// Handle for the authenticated account 69 pub handle: String, 70 /// JWT access token for authenticated requests 71 #[serde(rename = "accessJwt")] 72 pub access_jwt: String, 73 /// JWT refresh token for obtaining new access tokens 74 #[serde(rename = "refreshJwt")] 75 pub refresh_jwt: String, 76 /// Whether the account is active 77 #[serde(skip_serializing_if = "Option::is_none")] 78 pub active: Option<bool>, 79 /// Account status (e.g., "takendown", "suspended", "deactivated") 80 #[serde(skip_serializing_if = "Option::is_none")] 81 pub status: Option<String>, 82} 83 84/// Response from creating a new app password. 85#[cfg_attr(debug_assertions, derive(Debug))] 86#[derive(Deserialize, Clone)] 87pub struct AppPasswordResponse { 88 /// Name of the app password 89 pub name: String, 90 /// Generated app password string 91 pub password: String, 92 /// Creation timestamp in ISO 8601 format 93 #[serde(rename = "createdAt")] 94 pub created_at: String, 95} 96 97/// Creates a new authentication session using app password credentials. 98/// 99/// # Arguments 100/// 101/// * `http_client` - HTTP client for making requests 102/// * `base_url` - Base URL of the AT Protocol server 103/// * `identifier` - Handle or other identifier for the user 104/// * `password` - User password or app password 105/// * `auth_factor_token` - Optional two-factor authentication token 106/// 107/// # Returns 108/// 109/// The created session data including access and refresh tokens 110/// 111/// # Errors 112/// 113/// Returns errors for HTTP request failures, authentication failures, 114/// or JSON parsing failures. 115pub async fn create_session( 116 http_client: &reqwest::Client, 117 base_url: &str, 118 identifier: &str, 119 password: &str, 120 auth_factor_token: Option<&str>, 121) -> Result<AppPasswordSession> { 122 let url = build_url( 123 base_url, 124 "/xrpc/com.atproto.server.createSession", 125 iter::empty::<(&str, &str)>(), 126 )? 127 .to_string(); 128 129 let request = CreateSessionRequest { 130 identifier: identifier.to_string(), 131 password: password.to_string(), 132 auth_factor_token: auth_factor_token.map(|s| s.to_string()), 133 }; 134 135 let value = serde_json::to_value(request)?; 136 137 post_json(http_client, &url, value) 138 .await 139 .and_then(|value| serde_json::from_value(value).map_err(|err| err.into())) 140} 141 142/// Refreshes an existing authentication session using a refresh token. 143/// 144/// # Arguments 145/// 146/// * `http_client` - HTTP client for making requests 147/// * `base_url` - Base URL of the AT Protocol server 148/// * `refresh_token` - JWT refresh token from a previous session 149/// 150/// # Returns 151/// 152/// The refreshed session data with new access and refresh tokens 153/// 154/// # Errors 155/// 156/// Returns errors for HTTP request failures, authentication failures, 157/// or JSON parsing failures. 158pub async fn refresh_session( 159 http_client: &reqwest::Client, 160 base_url: &str, 161 refresh_token: &str, 162) -> Result<RefreshSessionResponse> { 163 let url = build_url( 164 base_url, 165 "/xrpc/com.atproto.server.refreshSession", 166 iter::empty::<(&str, &str)>(), 167 )? 168 .to_string(); 169 170 // Create a new client with the refresh token in Authorization header 171 let mut headers = reqwest::header::HeaderMap::new(); 172 headers.insert( 173 reqwest::header::AUTHORIZATION, 174 reqwest::header::HeaderValue::from_str(&format!("Bearer {}", refresh_token))?, 175 ); 176 177 let response = http_client.post(&url).headers(headers).send().await?; 178 179 let value = response.json::<serde_json::Value>().await?; 180 181 serde_json::from_value(value).map_err(|err| err.into()) 182} 183 184/// Creates a new app password for the authenticated account. 185/// 186/// # Arguments 187/// 188/// * `http_client` - HTTP client for making requests 189/// * `base_url` - Base URL of the AT Protocol server 190/// * `access_token` - JWT access token for authentication 191/// * `name` - Name for the app password 192/// 193/// # Returns 194/// 195/// The created app password details including the generated password 196/// 197/// # Errors 198/// 199/// Returns errors for HTTP request failures, authentication failures, 200/// or JSON parsing failures. 201pub async fn create_app_password( 202 http_client: &reqwest::Client, 203 base_url: &str, 204 access_token: &str, 205 name: &str, 206) -> Result<AppPasswordResponse> { 207 let url = build_url( 208 base_url, 209 "/xrpc/com.atproto.server.createAppPassword", 210 iter::empty::<(&str, &str)>(), 211 )? 212 .to_string(); 213 214 let request_body = serde_json::json!({ 215 "name": name 216 }); 217 218 // Create a new client with the access token in Authorization header 219 let mut headers = reqwest::header::HeaderMap::new(); 220 headers.insert( 221 reqwest::header::AUTHORIZATION, 222 reqwest::header::HeaderValue::from_str(&format!("Bearer {}", access_token))?, 223 ); 224 225 let response = http_client 226 .post(&url) 227 .headers(headers) 228 .json(&request_body) 229 .send() 230 .await?; 231 232 let value = response.json::<serde_json::Value>().await?; 233 234 serde_json::from_value(value).map_err(|err| err.into()) 235} 236 237/// Deletes the current authentication session. 238/// 239/// Terminates the authenticated session, invalidating the current access token. 240/// This operation requires app password authentication and will fail with 241/// other authentication methods. 242/// 243/// # Arguments 244/// 245/// * `http_client` - HTTP client for making requests 246/// * `auth` - Authentication method (must be AppPassword) 247/// * `base_url` - Base URL of the AT Protocol server 248/// 249/// # Returns 250/// 251/// Returns `Ok(())` on successful session deletion (HTTP 200 response) 252/// 253/// # Errors 254/// 255/// Returns `ClientError::InvalidAuthMethod` if authentication is not AppPassword, 256/// or other errors for HTTP request failures. 257pub async fn delete_session( 258 http_client: &reqwest::Client, 259 auth: &Auth, 260 base_url: &str, 261) -> Result<()> { 262 // Ensure we have AppPassword authentication 263 let app_auth = match auth { 264 Auth::AppPassword(app_auth) => app_auth, 265 _ => { 266 return Err(ClientError::InvalidAuthMethod { 267 method: "deleteSession requires AppPassword authentication".to_string(), 268 } 269 .into()); 270 } 271 }; 272 273 let url = build_url( 274 base_url, 275 "/xrpc/com.atproto.server.deleteSession", 276 iter::empty::<(&str, &str)>(), 277 )? 278 .to_string(); 279 280 // Create headers with the Bearer token 281 let mut headers = reqwest::header::HeaderMap::new(); 282 headers.insert( 283 reqwest::header::AUTHORIZATION, 284 reqwest::header::HeaderValue::from_str(&format!("Bearer {}", app_auth.access_token))?, 285 ); 286 287 // Send POST request with no body 288 let response = http_client 289 .post(&url) 290 .headers(headers) 291 .send() 292 .await 293 .map_err(|error| ClientError::HttpRequestFailed { 294 url: url.clone(), 295 error, 296 })?; 297 298 // Check for successful response (200 OK) 299 if response.status() == reqwest::StatusCode::OK { 300 Ok(()) 301 } else { 302 Err(anyhow::anyhow!( 303 "deleteSession failed: expected 200 OK, got {}", 304 response.status() 305 )) 306 } 307}