atproto_client/com_atproto_server.rs
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(any(debug_assertions, test), 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(any(debug_assertions, test), 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(any(debug_assertions, test), 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(any(debug_assertions, test), 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}