Skip to main content

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}